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