1/*
2	File:		MBCDocument.mm
3	Contains:	Document representing a Chess game
4	Copyright:	© 2003-2013 by Apple Inc., all rights reserved.
5
6	IMPORTANT: This Apple software is supplied to you by Apple Computer,
7	Inc.  ("Apple") in consideration of your agreement to the following
8	terms, and your use, installation, modification or redistribution of
9	this Apple software constitutes acceptance of these terms.  If you do
10	not agree with these terms, please do not use, install, modify or
11	redistribute this Apple software.
12
13	In consideration of your agreement to abide by the following terms,
14	and subject to these terms, Apple grants you a personal, non-exclusive
15	license, under Apple's copyrights in this original Apple software (the
16	"Apple Software"), to use, reproduce, modify and redistribute the
17	Apple Software, with or without modifications, in source and/or binary
18	forms; provided that if you redistribute the Apple Software in its
19	entirety and without modifications, you must retain this notice and
20	the following text and disclaimers in all such redistributions of the
21	Apple Software.  Neither the name, trademarks, service marks or logos
22	of Apple Inc. may be used to endorse or promote products
23	derived from the Apple Software without specific prior written
24	permission from Apple.  Except as expressly stated in this notice, no
25	other rights or licenses, express or implied, are granted by Apple
26	herein, including but not limited to any patent rights that may be
27	infringed by your derivative works or by other works in which the
28	Apple Software may be incorporated.
29
30	The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
31	MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
32	THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND
33	FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
34	USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
35
36	IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
37	INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
39	PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
40	REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE,
41	HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING
42	NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
43	ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44*/
45
46#import "MBCDocument.h"
47#import "MBCController.h"
48#import "MBCPlayer.h"
49#import "MBCEngine.h"
50#import "MBCGameInfo.h"
51#import "MBCUserDefaults.h"
52#import "MBCBoardWin.h"
53
54NSString * const sProperties[] = {
55    kMBCBoardStyle, kMBCPieceStyle,
56    kMBCDefaultVoice, kMBCAlternateVoice,
57    kMBCSpeakMoves, kMBCSpeakHumanMoves, kMBCListenForMoves,
58    kMBCSearchTime, kMBCBoardAngle, kMBCBoardSpin, kMBCShowGameLog,
59    NULL
60};
61
62
63static void MBCEndTurn(GKTurnBasedMatch *match, NSData * matchData)
64{
65    GKTurnBasedParticipant * opponent;
66
67    for (opponent in [match participants])
68        if (opponent != [match currentParticipant])
69            break;
70
71    [match endTurnWithNextParticipant:opponent matchData:matchData
72                    completionHandler:^(NSError *error) {
73                    }];
74}
75
76@implementation MBCDocument
77
78@synthesize board, variant, players, match, properties, offerDraw, ephemeral, needNewGameSheet,
79    disallowSubstitutes, invitees;
80
81+ (BOOL) autosavesInPlace
82{
83    return YES;
84}
85
86- (BOOL) boolForKey:(NSString *)key;
87{
88    return [[properties valueForKey:key] boolValue];
89}
90
91- (NSInteger) integerForKey:(NSString *)key
92{
93    return [[properties valueForKey:key] intValue];
94}
95
96- (float) floatForKey:(NSString *)key
97{
98    return [[properties valueForKey:key] floatValue];
99}
100
101- (id) objectForKey:(NSString *)key
102{
103    id result = [properties valueForKey:key];
104    if (!result && ([key isEqual:@"White"] || [key isEqual:@"Black"]))
105        result = NSLocalizedString(@"gc_automatch", @"GameCenter Automatch");
106    return result;
107}
108
109- (BOOL) brandNewGame
110{
111    return ![properties valueForKey:@"White"] && ![properties valueForKey:@"Black"];
112}
113
114- (id) valueForUndefinedKey:(NSString *)key
115{
116    return [self objectForKey:key];
117}
118
119- (void) setObject:(id)value forKey:(NSString *)key
120{
121    [self setValue:value forUndefinedKey:key];
122}
123
124- (void) setValue:(id)value forUndefinedKey:(NSString *)key
125{
126    if (value == [NSNull null])
127        value = nil;
128    [self willChangeValueForKey:key];
129    [[NSUserDefaults standardUserDefaults] setObject:value forKey:key];
130    [properties setValue:value forKey:key];
131    [self didChangeValueForKey:key];
132    [self updateChangeCount:NSChangeDone];
133}
134
135- (void)updateSearchTime
136{
137    id curSearchTime = [properties valueForKey:kMBCSearchTime];
138    id minSearchTime = [properties valueForKey:kMBCMinSearchTime];
139
140    if (minSearchTime && [minSearchTime intValue] > [curSearchTime intValue])
141        [properties setValue:curSearchTime forKey:kMBCMinSearchTime];
142    [[[[self windowControllers] objectAtIndex:0] engine] setSearchTime:[curSearchTime intValue]];
143}
144
145+ (BOOL)coinFlip
146{
147    static BOOL sLastFlip = arc4random() & 1;
148
149    return sLastFlip = !sLastFlip;
150}
151
152+ (BOOL)processNewMatch:(GKTurnBasedMatch *)match variant:(MBCVariant)variant side:(MBCSideCode)side
153    document:(MBCDocument *)doc
154{
155    NSPropertyListFormat    format;
156    NSDictionary *          gameData        = !match.matchData ? nil :
157        [NSPropertyListSerialization propertyListWithData:match.matchData options:0 format:&format error:nil];
158    MBCController *         controller      = [NSApp delegate];
159    NSString *              localPlayerID   = controller.localPlayer.playerID;
160    if (!gameData) {
161        //
162        // Brand new game, pick a side
163        //
164        NSString *       playerIDKey;
165        switch (side) {
166        case kPlayWhite:
167            playerIDKey = @"WhitePlayerID";
168            break;
169        case kPlayBlack:
170            playerIDKey = @"BlackPlayerID";
171            break;
172        default:
173            playerIDKey = [self coinFlip] ? @"WhitePlayerID" : @"BlackPlayerID";
174            break;
175        }
176        gameData = [NSDictionary dictionaryWithObjectsAndKeys:
177                    localPlayerID, playerIDKey,
178                    gVariantName[variant], @"Variant",
179                    kMBCHumanPlayer, @"WhiteType", kMBCHumanPlayer, @"BlackType",
180                    nil];
181        if ([playerIDKey isEqual:@"BlackPlayerID"]) {
182            //
183            // We picked black, it's our opponent's turn
184            //
185            MBCEndTurn(match, [NSPropertyListSerialization
186                               dataFromPropertyList:gameData
187                                            format:NSPropertyListXMLFormat_v1_0
188                                    errorDescription:nil]);
189        } else {
190            //
191            // Push our choices to the server
192            //
193            [match endTurnWithNextParticipant:match.currentParticipant
194                                    matchData:[NSPropertyListSerialization
195                                               dataFromPropertyList:gameData
196                                               format:NSPropertyListXMLFormat_v1_0
197                                               errorDescription:nil]
198                            completionHandler:^(NSError *error) {}];
199        }
200    } else if (![localPlayerID isEqual:[gameData objectForKey:@"WhitePlayerID"]]
201           &&  ![localPlayerID isEqual:[gameData objectForKey:@"BlackPlayerID"]]
202    ) {
203        //
204        // We're the second participant, pick the side that hasn't been picked yet
205        //
206        BOOL       playingWhite = ![gameData objectForKey:@"WhitePlayerID"];
207        NSMutableDictionary * newGameData = [gameData mutableCopy];
208        [newGameData removeObjectForKey:@"PeerID"]; // Legacy key
209        [newGameData setObject:localPlayerID forKey:(playingWhite ? @"WhitePlayerID" : @"BlackPlayerID")];
210        gameData = newGameData;
211    }
212    BOOL createDoc = !doc;
213    if (createDoc)
214        doc = [[MBCDocument alloc] init];
215    [doc initWithMatch:match game:gameData];
216    if (createDoc) {
217        [doc makeWindowControllers];
218        [doc showWindows];
219        [doc autorelease];
220    }
221
222    return YES;
223}
224
225- (id)init
226{
227    //
228    // If we have any implicitly opened document that did not get used yet, we reuse one
229    //
230    if (![self disallowSubstitutes])
231        for (MBCDocument * doc in [[NSDocumentController sharedDocumentController] documents])
232            if ([doc ephemeral]) {
233                [doc close];
234
235                break;
236            }
237    if (self = [super init]) {
238        [self setEphemeral:YES];
239        NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
240        properties  = [[NSMutableDictionary alloc] init];
241        for (int i = 0; sProperties[i]; ++i)
242            [properties setValue:[defaults objectForKey:sProperties[i]]
243                          forKey:sProperties[i]];
244    }
245    return self;
246}
247
248- (void)dealloc
249{
250    [properties release];
251    [super dealloc];
252}
253
254- (id)initForNewGameSheet:(NSArray *)guests
255{
256    if (self = [self init]) {
257        NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
258        if (![[NSApp delegate] localPlayer] && (MBCPlayers)[defaults integerForKey:kMBCNewGamePlayers] == kHumanVsGameCenter)
259            [defaults setInteger:kHumanVsComputer forKey:kMBCNewGamePlayers];
260        [self setEphemeral:NO];
261        [self setNeedNewGameSheet:YES];
262        [self setDisallowSubstitutes:YES];
263        [self setInvitees:guests];
264        [[NSDocumentController sharedDocumentController] addDocument:self];
265    }
266
267    return self;
268}
269
270- (id)initWithType:(NSString *)typeName error:(NSError **)outError
271{
272    if (self = [super initWithType:typeName error:outError]) {
273        NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
274        players     = (MBCPlayers)[defaults integerForKey:kMBCNewGamePlayers];
275        switch (players) {
276        case kHumanVsHuman:
277        case kHumanVsComputer:
278            //
279            // Always accept these
280            //
281            break;
282        case kComputerVsHuman:
283        case kComputerVsComputer:
284            //
285            // Accept it for explicit creation, but reset default
286            //
287            [defaults setInteger:kHumanVsComputer forKey:kMBCNewGamePlayers];
288            break;
289        default:
290            //
291            // Never create a GameCenter game for an "untitled" action
292            //
293            players = kHumanVsComputer;
294            break;
295        }
296        variant     = (MBCVariant)[defaults integerForKey:kMBCNewGameVariant];
297        if (players == kHumanVsComputer || players == kComputerVsHuman)
298            [properties setValue:[properties valueForKey:kMBCSearchTime] forKey:kMBCMinSearchTime];
299        [[NSNotificationQueue defaultQueue]
300         enqueueNotification:[NSNotification notificationWithName:MBCGameStartNotification
301                                                           object:self]
302                postingStyle:NSPostWhenIdle];
303        [self updateChangeCount:NSChangeCleared];
304    }
305    return self;
306}
307
308- (void)duplicateDocument:(id)sender
309{
310    [self setEphemeral:NO];
311    [super duplicateDocument:sender];
312}
313
314- (void)updateChangeCount:(NSDocumentChangeType)change
315{
316    if (!board || [board numMoves] || [self remoteSide] != kNeitherSide) {
317        [self setEphemeral:NO];
318
319        [super updateChangeCount:change];
320    }
321}
322
323- (void)close
324{
325    //
326    // Delay disposing document a little while, because main window with active bindings is autoreleased
327    //
328    [self retain];
329    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_MSEC);
330    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
331        [self release];
332    });
333    [super close];
334}
335
336- (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)sel contextInfo:(void *)contextInfo
337{
338    //
339    // Never ask to save GameCenter games (we autosave them, though)
340    //
341    if (match) {
342        NSInvocation * invoc =
343            [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:sel]];
344        [invoc setTarget:delegate];
345        [invoc setSelector:sel];
346        [invoc setArgument:&self atIndex:2];
347        BOOL shouldClose = YES;
348        [invoc setArgument:&shouldClose atIndex:3];
349        [invoc setArgument:&contextInfo atIndex:4];
350        [invoc invoke];
351    } else {
352        [super canCloseDocumentWithDelegate:delegate shouldCloseSelector:sel contextInfo:contextInfo];
353    }
354}
355
356- (void)postMatchOutcomeNotification
357{
358    MBCMoveCode cmd;
359    NSString *  localPlayerID = [[NSApp delegate] localPlayer].playerID;
360    for (GKTurnBasedParticipant * p in match.participants)
361        if ([p.playerID isEqual:localPlayerID]) {
362            if (p.matchOutcome == GKTurnBasedMatchOutcomeTied)
363                cmd = kCmdDraw;
364            else if (p.matchOutcome == GKTurnBasedMatchOutcomeWon)
365                cmd = localWhite ? kCmdWhiteWins : kCmdBlackWins;
366            else
367                cmd = localWhite ? kCmdBlackWins : kCmdWhiteWins;
368            break;
369        }
370    [[NSNotificationQueue defaultQueue]
371     enqueueNotification:[NSNotification notificationWithName:MBCGameEndNotification
372                                                       object:self
373                                                     userInfo:(id)[MBCMove moveWithCommand:cmd]]
374     postingStyle:NSPostWhenIdle];
375}
376
377- (BOOL)checkForEndOfGame
378{
379    bool firstIsDone = [[match.participants objectAtIndex:0] matchOutcome] != GKTurnBasedMatchOutcomeNone;
380    bool secondIsDone = [[match.participants objectAtIndex:1] matchOutcome] != GKTurnBasedMatchOutcomeNone;
381
382    if (firstIsDone != secondIsDone) {
383        GKTurnBasedMatchOutcome outcome;
384        if ([[match.participants objectAtIndex:secondIsDone] matchOutcome] == GKTurnBasedMatchOutcomeWon)
385            outcome = GKTurnBasedMatchOutcomeLost;
386        else
387            outcome = GKTurnBasedMatchOutcomeWon;
388        [[match.participants objectAtIndex:firstIsDone] setMatchOutcome:outcome];
389    }
390    if ((firstIsDone || secondIsDone) && match.status != GKTurnBasedMatchStatusEnded
391        && [match.currentParticipant.playerID isEqual:[[NSApp delegate] localPlayer].playerID]
392    )
393        [match endMatchInTurnWithMatchData:[self matchData] completionHandler:^(NSError *error) {}];
394
395    return firstIsDone || secondIsDone;
396}
397
398- (void) updateMatchForEndOfGame:(MBCMoveCode)cmd
399{
400    if ([match.currentParticipant.playerID isEqual:[[NSApp delegate] localPlayer].playerID]) {
401        //
402        // Participant whose turn it is updates the match
403        //
404        GKTurnBasedMatchOutcome whiteOutcome;
405        GKTurnBasedMatchOutcome blackOutcome;
406
407        if (cmd == kCmdDraw) {
408            whiteOutcome    = GKTurnBasedMatchOutcomeTied;
409            blackOutcome    = GKTurnBasedMatchOutcomeTied;
410        } else if (cmd == kCmdWhiteWins) {
411            whiteOutcome    = GKTurnBasedMatchOutcomeWon;
412            blackOutcome    = GKTurnBasedMatchOutcomeLost;
413        } else {
414            whiteOutcome    = GKTurnBasedMatchOutcomeLost;
415            blackOutcome    = GKTurnBasedMatchOutcomeWon;
416        }
417        for (GKTurnBasedParticipant * participant in match.participants)
418            if ([participant.playerID isEqual:[properties objectForKey:@"WhitePlayerID"]])
419                [participant setMatchOutcome:whiteOutcome];
420            else
421                [participant setMatchOutcome:blackOutcome];
422        [match endMatchInTurnWithMatchData:[self matchData] completionHandler:^(NSError *error) {}];
423    }
424}
425
426- (id)initWithMatch:(GKTurnBasedMatch *)gkMatch game:(NSDictionary *)gameData
427{
428    if (self = [self init]) {
429        [self setDisallowSubstitutes:YES];
430        [self setInvitees:nil];
431        [self setMatch:gkMatch];
432        [self loadGame:gameData];
433        localWhite = [[[NSApp delegate] localPlayer].playerID isEqual:[properties objectForKey:@"WhitePlayerID"]];
434        NSDictionary * localProps = [properties objectForKey:(localWhite ? @"WhiteProperties" : @"BlackProperties")];
435        if (localProps)
436            [properties addEntriesFromDictionary:localProps];
437        [self updateChangeCount:NSChangeCleared];
438        [[NSDocumentController sharedDocumentController] addDocument:self];
439    }
440    return self;
441}
442
443- (NSString *)windowNibName
444{
445    return @"Board";
446}
447
448- (void)makeWindowControllers
449{
450    if ([[self windowControllers] count]) {
451        //
452        // Reused document
453        //
454        [[[self windowControllers] objectAtIndex:0] adjustLogView];
455    } else {
456        MBCBoardWin * windowController = [[MBCBoardWin alloc] initWithWindowNibName:[self windowNibName]];
457        [self addWindowController:windowController];
458        [windowController release];
459    }
460}
461
462- (MBCSide) humanSide;
463{
464    if (!match)
465        return gHumanSide[players];
466    else
467        return localWhite ? kWhiteSide : kBlackSide;
468}
469
470- (BOOL) nontrivialHumanTurn
471{
472    //
473    //    Dock badge is rather distracting, save this for important situations.
474    //
475    if ([self gameDone])
476        return NO;
477    if ([self engineSide] != kNeitherSide && [[properties valueForKey:kMBCSearchTime] intValue] < 5)
478        return NO;  // Only report for engine search > 30s
479    switch ([self humanSide]) {
480    case kBothSides:
481        return NO;  // Duh! We know it's a human's turn
482    case kWhiteSide:
483        return [board numMoves] && !([board numMoves] & 1);
484    case kBlackSide:
485        return ([board numMoves] & 1);
486    case kNeitherSide:
487        return NO;
488    }
489}
490
491- (BOOL) gameDone
492{
493    if (NSString * result = [properties objectForKey:@"Result"])
494        return ![result isEqual:@"*"];
495    else
496        return NO;
497}
498
499- (MBCSide) engineSide;
500{
501    return gEngineSide[players];
502}
503
504- (MBCSide) remoteSide
505{
506    if (!match)
507        return kNeitherSide;
508    else
509        return localWhite ? kBlackSide : kWhiteSide;
510}
511
512- (NSString *)nonLocalPlayerID
513{
514    if (!match)
515        return nil;
516    NSString * localPlayerID = [[NSApp delegate] localPlayer].playerID;
517    for (GKTurnBasedParticipant * p in match.participants)
518        if (![localPlayerID isEqual:p.playerID])
519            return p.playerID;
520    return nil;
521}
522
523- (BOOL) loadGame:(NSDictionary *)dict
524{
525    [self setEphemeral:NO];
526
527	NSString * v = [dict objectForKey:@"Variant"];
528
529	for (variant = kVarNormal; gVariantName[variant] && ![v isEqual:gVariantName[variant]]; )
530		variant = static_cast<MBCVariant>(variant+1);
531	if (!gVariantName[variant])
532		variant = kVarNormal;
533
534    NSString * whiteType = [dict objectForKey:@"WhiteType"];
535    NSString * blackType = [dict objectForKey:@"BlackType"];
536
537    if ([whiteType isEqual:kMBCHumanPlayer])
538        if ([blackType isEqual:kMBCHumanPlayer])
539            players = kHumanVsHuman;
540        else
541            players = kHumanVsComputer;
542    else if ([blackType isEqual:kMBCHumanPlayer])
543        players = kComputerVsHuman;
544    else
545        players = kComputerVsComputer;
546    properties = [dict mutableCopy];
547    NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
548    for (int i = 0; sProperties[i]; ++i)
549        if (![properties valueForKey:sProperties[i]])
550            [properties setValue:[defaults objectForKey:sProperties[i]]
551                          forKey:sProperties[i]];
552
553    [[NSNotificationQueue defaultQueue]
554     enqueueNotification:[NSNotification notificationWithName:MBCGameLoadNotification
555                                                       object:self userInfo:dict]
556     postingStyle:NSPostWhenIdle];
557    [[NSNotificationQueue defaultQueue]
558     enqueueNotification:[NSNotification notificationWithName:MBCGameStartNotification
559                                                       object:self]
560     postingStyle:NSPostWhenIdle];
561
562    if (match)
563        dispatch_async(dispatch_get_main_queue(), ^{
564            [self updateMatchForRemoteMove];
565        });
566
567	return YES;
568}
569
570- (BOOL)readFromData:(NSData *)docData ofType:(NSString *)docType error:(NSError **)outError
571{
572	NSPropertyListFormat	format;
573	NSDictionary *			gameData =
574		[NSPropertyListSerialization propertyListWithData:docData options:0 format:&format error:outError];
575    if (NSString * matchID = [gameData objectForKey:@"MatchID"]) {
576        [[NSApp delegate] loadMatch:matchID];
577        if (outError)
578            *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:nil];
579
580        return NO;
581    }
582
583	[self loadGame:gameData];
584    [self updateChangeCount:NSChangeCleared];
585
586	return YES;
587}
588
589- (NSArray *)writableTypesForSaveOperation:(NSSaveOperationType)saveOperation
590{
591	//
592	// Don't filter out PGN, even though we're not an editor for that type
593	//
594	return [[self class] writableTypes];
595}
596
597- (void) parseName:(NSString *)fullName intoFirst:(NSString **)firstName
598			  last:(NSString **)lastName
599{
600	//
601	// Get name as UTF8. If the name is longer than 99 bytes, the last
602	// character might be bad if it's non-ASCII. What sane person would
603	// have such a long name anyway?
604	//
605	char	   n[100];
606	strlcpy(n, [fullName UTF8String], 100);
607
608	char * first 	= n+strspn(n, " \t"); 	// Beginning of first name
609	char * last;
610	char * nb1  	= NULL;			 		// Beginning of last word
611	char * ne1  	= NULL;			 		// End of last word
612	char * nb2  	= NULL;			 		// Beginning of last but one word
613	char * ne2  	= NULL;			 		// End of last but two word
614	char * ne3  	= NULL;            		// End of last but three word
615
616	nb1	  = first;
617	ne1   = nb1+strcspn(nb1, " \t");
618
619	for (char * n; (n = ne1+strspn(ne1, " \t")) && *n; ) {
620		ne3	= ne2;
621		nb2 = nb1;
622		ne2 = ne1;
623		nb1	= n;
624		ne1 = nb1+strcspn(nb1, " \t");
625	}
626
627	if (ne3 && *nb2 >= 'a' && *nb2 <= 'z') {
628		//
629		// Name has at least 3 words and last but one is
630		// lowercase, as in Ludwig van Beethoven
631		//
632		last = nb2;
633		*ne3 = 0;
634	} else if (ne2) {
635		//
636		// Name has at least two words. If 3 or more, last but one is
637		// uppercase as in John Wayne Miller
638		//
639		last = nb1;
640		*ne2 = 0;
641	} else {		// Name is single word
642		last = ne1;
643		*ne1 = 0;
644	}
645	*firstName	= [NSString stringWithUTF8String:first];
646	*lastName	= [NSString stringWithUTF8String:last];
647}
648
649- (NSString *)pgnHeader
650{
651	NSString * 		wf;
652	NSString *		wl;
653	NSString *		bf;
654	NSString * 		bl;
655	[self parseName:[properties objectForKey:@"White"] intoFirst:&wf last:&wl];
656	[self parseName:[properties objectForKey:@"Black"] intoFirst:&bf last:&bl];
657	NSString *		humanw	=
658    [NSString stringWithFormat:@"%@, %@", wl, wf];
659	NSString *		humanb	=
660    [NSString stringWithFormat:@"%@, %@", bl, bf];
661	NSString * 		engine 		=
662    [NSString stringWithFormat:@"Apple Chess %@",
663     [[NSBundle mainBundle]
664      objectForInfoDictionaryKey:@"CFBundleVersion"]];
665	NSString * white;
666	NSString * black;
667	switch ([self engineSide]) {
668    case kBlackSide:
669        white 	= humanw;
670        black 	= engine;
671        break;
672    case kWhiteSide:
673        white 	= engine;
674        black	= humanb;
675        break;
676    case kNeitherSide:
677        white	= humanw;
678        black	= humanb;
679
680        break;
681    case kBothSides:
682        white	= engine;
683        black	= engine;
684        break;
685	}
686
687	//
688	// PGN uses a standard format that is NOT localized
689	//
690	NSString * format =
691    [NSString stringWithUTF8String:
692     "[Event \"%@\"]\n"
693     "[Site \"%@, %@\"]\n"
694     "[Date \"%@\"]\n"
695     "[Round \"-\"]\n"
696     "[White \"%@\"]\n"
697     "[Black \"%@\"]\n"
698     "[Result \"%@\"]\n"
699     "[Time \"%@\"]\n"];
700	return [NSString stringWithFormat:format,
701            [properties objectForKey:@"Event"],
702            [properties objectForKey:@"City"],
703            [properties objectForKey:@"Country"],
704            [properties objectForKey:@"StartDate"],
705            white, black,
706            [properties objectForKey:@"Result"],
707            [properties objectForKey:@"StartTime"]];
708}
709
710- (NSString *)pgnResult
711{
712	return [properties objectForKey:@"Result"];
713}
714
715- (void) saveMovesToFile:(FILE *)f
716{
717	NSString *  header = [self pgnHeader];
718	NSData *	encoded = [header dataUsingEncoding:NSISOLatin1StringEncoding
719                            allowLossyConversion:YES];
720	fwrite([encoded bytes], 1, [encoded length], f);
721
722	//
723	// Add variant tag (nonstandard, but used by xboard & Co.)
724	//
725	if (variant != kVarNormal)
726		fprintf(f, "[Variant \"%s\"]\n", [gVariantName[variant] UTF8String]);
727	//
728	// Mark nonhuman players
729	//
730    MBCSide engineSide = gEngineSide[players];
731
732	if (SideIncludesWhite(engineSide))
733		fprintf(f, "[WhiteType: \"program\"]\n");
734	if (SideIncludesBlack(engineSide))
735		fprintf(f, "[BlackType: \"program\"]\n");
736
737	[board saveMovesTo:f];
738
739	fputc('\n', f);
740	fputs([[self pgnResult] UTF8String], f);
741	fputc('\n', f);
742}
743
744- (BOOL) saveMovesTo:(NSString *)fileName
745{
746	FILE * f = fopen([fileName fileSystemRepresentation], "w");
747
748    [self saveMovesToFile:f];
749
750	fclose(f);
751
752	return YES;
753}
754
755- (BOOL)writeToURL:(NSURL *)fileURL ofType:(NSString *)docType error:(NSError **)outError
756{
757	BOOL res;
758
759	if ([docType isEqualToString:@"com.apple.chess.pgn"])
760		res = [self saveMovesTo:[fileURL path]];
761	else
762		res = [super writeToURL:fileURL ofType:docType error:outError];
763
764	return res;
765}
766
767- (void)setFileURL:(NSURL *)url
768{
769    //
770    // Never pick PGN representation as autosave
771    //
772    if ([[url pathExtension] isEqualToString:@"game"])
773        [super setFileURL:url];
774}
775
776- (void)performActivityWithSynchronousWaiting:(BOOL)waitSynchronously usingBlock:(void (^)(void (^)()))block
777{
778    //      If achievement controller is showing and we have to put up e.g. a save dialog,
779    //      dismiss it.
780    //
781    NSArray * windowControllers = [self windowControllers];
782    if ([windowControllers count])
783        [[windowControllers objectAtIndex:0] achievementViewControllerDidFinish:nil];
784    [super performActivityWithSynchronousWaiting:waitSynchronously usingBlock:block];
785}
786
787- (NSDocument *)duplicateAndReturnError:(NSError **)outError
788{
789    if (match) {
790        if (outError)
791            *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnsupportedSchemeError userInfo:
792                         [NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"cant_duplicate_gc", @"Cannot duplicate GameCenter games"), NSLocalizedDescriptionKey, nil]];
793        return nil;
794    }
795    return [super duplicateAndReturnError:outError];
796}
797
798- (NSMutableDictionary *) saveGameToDict
799{
800    MBCSide     humanSide   = gHumanSide[players];
801    NSString *  whiteType   = SideIncludesWhite(humanSide) ? kMBCHumanPlayer : kMBCEnginePlayer;
802    NSString *  blackType   = SideIncludesBlack(humanSide) ? kMBCHumanPlayer : kMBCEnginePlayer;
803	NSMutableDictionary * dict = [properties mutableCopy];
804    [dict addEntriesFromDictionary:
805     [NSDictionary dictionaryWithObjectsAndKeys:
806      gVariantName[variant], @"Variant",
807      whiteType, @"WhiteType",
808      blackType, @"BlackType",
809      [board fen], @"Position",
810      [board holding], @"Holding",
811      [board moves], @"Moves",
812      nil]];
813
814	return [dict autorelease];
815}
816
817- (NSData *)dataRepresentationOfType:(NSString *)aType
818{
819    NSMutableDictionary * game = [self saveGameToDict];
820    if (match)
821        [game setObject:match.matchID forKey:@"MatchID"];
822	return [NSPropertyListSerialization
823			   dataFromPropertyList:game
824			   format: NSPropertyListXMLFormat_v1_0
825			   errorDescription:nil];
826}
827
828- (NSData *)matchData
829{
830    NSMutableDictionary * game = [self saveGameToDict];
831    NSArray * keys             = [game allKeys];
832    NSMutableDictionary * local= [NSMutableDictionary dictionary];
833    for (NSString * key in keys)
834        if ([[key substringToIndex:3] isEqual:@"MBC"]) {
835            [local setObject:[game objectForKey:key] forKey:key];
836            [game removeObjectForKey:key];
837        }
838    [game setObject:local forKey:(localWhite ? @"WhiteProperties" : @"BlackProperties")];
839
840    return [NSPropertyListSerialization
841            dataFromPropertyList:game
842            format: NSPropertyListXMLFormat_v1_0
843            errorDescription:nil];
844}
845
846+ (NSURL *)casualGameSaveLocation
847{
848	NSString *	savePath = [[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:@"Casual.game"];
849
850	return [NSURL fileURLWithPath:savePath];
851}
852
853- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
854{
855	BOOL loaded = [super readFromURL:absoluteURL ofType:typeName error:outError];
856	if (loaded && [absoluteURL isEqual:[MBCDocument casualGameSaveLocation]]) // Upgrade legacy autosave
857		[self setFileURL:nil];
858
859	return loaded;
860}
861
862- (BOOL) canTakeback
863{
864    return [board canUndo] && players != kComputerVsComputer
865        && (!match || [match.currentParticipant.playerID isEqual:[[NSApp delegate] localPlayer].playerID]);
866}
867
868- (void) setBoard:(MBCBoard *)b
869{
870    board = b;
871    [board setDocument:self];
872}
873
874- (void) updateMatchForLocalMove
875{
876    if (offerDraw) {
877        if (![properties objectForKey:@"Request"])
878            [properties setObject:@"Draw" forKey:@"Request"];
879        [self setOfferDraw:NO];
880    }
881    MBCEndTurn(match, [self matchData]);
882}
883
884- (void) updateMatchForRemoteMove
885{
886    if ([self checkForEndOfGame]) {
887        [self postMatchOutcomeNotification];
888    } else {
889        NSPropertyListFormat	format;
890        NSDictionary *			gameData =
891        [NSPropertyListSerialization propertyListWithData:match.matchData options:0 format:&format error:nil];
892        if (![properties objectForKey:@"BlackPlayerID"] && [gameData objectForKey:@"BlackPlayerID"])
893            [properties setObject:[gameData objectForKey:@"BlackPlayerID"] forKey:@"BlackPlayerID"];
894        if (![properties objectForKey:@"WhitePlayerID"] && [gameData objectForKey:@"WhitePlayerID"])
895            [properties setObject:[gameData objectForKey:@"WhitePlayerID"] forKey:@"WhitePlayerID"];
896        if ([[gameData objectForKey:@"Request"] isEqual:@"Takeback"]) {
897            [[[self windowControllers] objectAtIndex:0] requestTakeback];
898        } else if (NSString * response = [gameData objectForKey:@"Response"]) {
899            [properties removeObjectForKey:@"Request"];
900            [[[self windowControllers] objectAtIndex:0] handleRemoteResponse:response];
901        } else if ([gameData objectForKey:@"Moves"]) {
902            NSArray *             moves = [[gameData objectForKey:@"Moves"] componentsSeparatedByString:@"\n"];
903            if ([moves count]-1 > [board numMoves]) {
904                MBCMove *             lastMove = [MBCMove moveFromEngineMove:[moves objectAtIndex:[moves count]-2]];
905                [[NSNotificationCenter defaultCenter]
906                 postNotificationName:(localWhite ? MBCUncheckedBlackMoveNotification : MBCUncheckedWhiteMoveNotification)
907                 object:self userInfo:(id)lastMove];
908            }
909            if ([[gameData objectForKey:@"Request"] isEqual:@"Draw"]) {
910                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200*NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
911                    [[[self windowControllers] objectAtIndex:0] requestDraw];
912                });
913            }
914        }
915    }
916    [properties removeObjectForKey:@"Request"];
917    [properties removeObjectForKey:@"Response"];
918}
919
920- (void) offerTakeback
921{
922    [properties setObject:@"Takeback" forKey:@"Request"];
923    [self updateMatchForLocalMove];
924}
925
926- (void) allowTakeback:(BOOL)allow
927{
928    [properties setObject:(allow ? @"Takeback" : @"NoTakeback") forKey:@"Response"];
929    [self updateMatchForLocalMove];
930}
931
932- (void) resign
933{
934    NSString *  localPlayerID = [[NSApp delegate] localPlayer].playerID;
935    BOOL        wasOurTurn;
936    for (GKTurnBasedParticipant * p in match.participants)
937        if ([p.playerID isEqual:localPlayerID]) {
938            [p setMatchOutcome:GKTurnBasedMatchOutcomeLost];
939            wasOurTurn = match.currentParticipant == p;
940            break;
941        }
942
943    [self updateMatchForRemoteMove];
944
945    if (!wasOurTurn)
946       [match participantQuitOutOfTurnWithOutcome:GKTurnBasedMatchOutcomeLost
947                            withCompletionHandler:^(NSError *error){}];
948}
949
950@end
951
952void
953MBCAbort(NSString * message, MBCDocument * doc)
954{
955    fprintf(stderr, "%s in game %p\n", [message UTF8String], doc);
956    abort();
957}
958
959
960// Local Variables:
961// mode:ObjC
962// End:
963