1/* 2 File: MBCBoardWin.mm 3 Contains: Manage the board window 4 Copyright: © 2002-2012 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 "MBCBoardWin.h" 47#import "MBCBoardView.h" 48#import "MBCPlayer.h" 49#import "MBCEngine.h" 50#import "MBCDocument.h" 51#import "MBCGameInfo.h" 52#import "MBCMoveAnimation.h" 53#import "MBCBoardAnimation.h" 54#import "MBCInteractivePlayer.h" 55#import "MBCRemotePlayer.h" 56#import "MBCUserDefaults.h" 57#import "MBCController.h" 58 59@implementation MBCBoardWin 60 61@synthesize gameView, gameNewSheet, logContainer, logView, board, engine, interactive; 62@synthesize gameInfo, remote, logViewRightEdgeConstraint, dialogController; 63@synthesize primarySynth, alternateSynth, primaryLocalization, alternateLocalization; 64 65- (void)removeChessObservers 66{ 67 if (![fObservers count]) 68 return; 69 70 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 71 72 [fObservers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 73 [notificationCenter removeObserver:obj]; 74 }]; 75 76 [notificationCenter removeObserver:self name:MBCWhiteMoveNotification object:nil]; 77 [notificationCenter removeObserver:self name:MBCBlackMoveNotification object:nil]; 78 [notificationCenter removeObserver:self name:MBCGameEndNotification object:nil]; 79 [notificationCenter removeObserver:self name:MBCEndMoveNotification object:nil]; 80 81 MBCDocument * document = [self document]; 82 [document removeObserver:self forKeyPath:kMBCDefaultVoice]; 83 [document removeObserver:self forKeyPath:kMBCAlternateVoice]; 84 [document removeObserver:self forKeyPath:kMBCBoardStyle]; 85 [document removeObserver:self forKeyPath:kMBCPieceStyle]; 86 [document removeObserver:self forKeyPath:kMBCListenForMoves]; 87 88 [fObservers removeAllObjects]; 89} 90 91- (void)dealloc 92{ 93 [fCurAnimation cancel]; 94 [self removeChessObservers]; 95 [fObservers release]; 96 [primaryLocalization release]; 97 [alternateLocalization release]; 98 [super dealloc]; 99} 100 101- (void)endAnimation 102{ 103 fCurAnimation = nil; 104} 105 106- (void)windowDidLoad 107{ 108 [super windowDidLoad]; 109 110 if (!fObservers) 111 fObservers = [[NSMutableArray alloc] init]; 112 113 MBCDocument * document = [self document]; 114 [document setBoard:board]; 115 [engine setDocument:document]; 116 [interactive setDocument:document]; 117 [gameInfo setDocument:document]; 118 [remote setDocument:document]; 119 120 [self removeChessObservers]; 121 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 122 [fObservers addObject: 123 [notificationCenter 124 addObserverForName:MBCGameLoadNotification object:document 125 queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { 126 NSDictionary * dict = [note userInfo]; 127 NSString * fen = [dict objectForKey:@"Position"]; 128 NSString * holding = [dict objectForKey:@"Holding"]; 129 NSString * moves = [dict objectForKey:@"Moves"]; 130 131 if (fen || moves) 132 [engine setGame:[document variant] fen:fen holding:holding moves:moves]; 133 }]]; 134 [fObservers addObject: 135 [notificationCenter 136 addObserverForName:MBCGameStartNotification object:document 137 queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { 138 MBCVariant variant = [document variant]; 139 140 [gameView startGame:variant playing:[document humanSide]]; 141 [engine setSearchTime:[document integerForKey:kMBCSearchTime]]; 142 [engine startGame:variant playing:[document engineSide]]; 143 [interactive startGame:variant playing:[document humanSide]]; 144 [gameInfo startGame:variant playing:[document humanSide]]; 145 if (document.match) 146 [remote startGame:variant playing:[document remoteSide]]; 147 }]]; 148 [fObservers addObject: 149 [notificationCenter 150 addObserverForName:MBCTakebackNotification object:document 151 queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { 152 [gameView unselectPiece]; 153 [gameView hideMoves]; 154 [board undoMoves:2]; 155 }]]; 156 [notificationCenter 157 addObserver:self 158 selector:@selector(executeMove:) 159 name:MBCWhiteMoveNotification 160 object:document]; 161 [notificationCenter 162 addObserver:self 163 selector:@selector(executeMove:) 164 name:MBCBlackMoveNotification 165 object:document]; 166 [notificationCenter 167 addObserver:self 168 selector:@selector(gameEnded:) 169 name:MBCGameEndNotification 170 object:document]; 171 [notificationCenter 172 addObserver:self 173 selector:@selector(commitMove:) 174 name:MBCEndMoveNotification 175 object:document]; 176 [document addObserver:self forKeyPath:kMBCDefaultVoice options:NSKeyValueObservingOptionNew context:nil]; 177 [document addObserver:self forKeyPath:kMBCAlternateVoice options:NSKeyValueObservingOptionNew context:nil]; 178 [document addObserver:self forKeyPath:kMBCBoardStyle options:NSKeyValueObservingOptionNew context:nil]; 179 [document addObserver:self forKeyPath:kMBCPieceStyle options:NSKeyValueObservingOptionNew context:nil]; 180 [document addObserver:self forKeyPath:kMBCListenForMoves options:NSKeyValueObservingOptionNew context:nil]; 181 182 gameView->fElevation = [document floatForKey:kMBCBoardAngle]; 183 gameView->fAzimuth = [document floatForKey:kMBCBoardSpin]; 184 185 [gameView setStyleForBoard:[document objectForKey:kMBCBoardStyle] pieces:[document objectForKey:kMBCPieceStyle]]; 186 187 [self setShouldCascadeWindows:NO]; 188 NSWindow * window = [self window]; 189 if ([[self document] match]) 190 [window setFrameAutosaveName:[NSString stringWithFormat:@"Match %@\n", document.match.matchID]]; 191 if (![document boolForKey:kMBCShowGameLog]) 192 [self hideLogContainer:self]; 193 [window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; 194 [window makeFirstResponder:gameView]; 195 [window makeKeyAndOrderFront:self]; 196 [fObservers addObject: 197 [notificationCenter 198 addObserverForName:NSWindowWillCloseNotification object:window 199 queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { 200 // 201 // Due to a plethora of mutual observers, circular references prevent 202 // proper deallocation unless we remove all of observers first. 203 // 204 [fCurAnimation endState]; 205 [board removeChessObservers]; 206 [engine removeChessObservers]; 207 [engine shutdown]; 208 [gameInfo removeChessObservers]; 209 [gameInfo setDocument:nil]; 210 [interactive removeChessObservers]; 211 [interactive removeController]; 212 [remote removeChessObservers]; 213 [self removeChessObservers]; 214 }]]; 215 if ([document needNewGameSheet]) { 216 usleep(500000); 217 [self showNewGameSheet]; 218 } 219} 220 221- (void)windowDidBecomeMain:(NSNotification *)notification 222{ 223 if ([self listenForMoves]) 224 [interactive allowedToListen:YES]; 225 [gameView setNeedsDisplay:YES]; 226} 227 228- (void)windowDidResignMain:(NSNotification *)notification 229{ 230 if ([self listenForMoves]) 231 [interactive allowedToListen:NO]; 232} 233 234- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 235{ 236 if ([keyPath isEqual:kMBCDefaultVoice]) { 237 [primarySynth release]; primarySynth = nil; 238 [primaryLocalization release]; primaryLocalization = nil; 239 } else if ([keyPath isEqual:kMBCAlternateVoice]) { 240 [alternateSynth release]; alternateSynth = nil; 241 [alternateLocalization release]; alternateLocalization = nil; 242 } else if ([keyPath isEqual:kMBCListenForMoves]) { 243 [interactive allowedToListen:[self listenForMoves]]; 244 } else { 245 [gameView setStyleForBoard:[[self document] objectForKey:kMBCBoardStyle] 246 pieces:[[self document] objectForKey:kMBCPieceStyle]]; 247 } 248} 249 250- (BOOL)validateMenuItem:(NSMenuItem *)menuItem 251{ 252 if ([menuItem action] == @selector(takeback:)) { 253 return [[self document] canTakeback]; 254 } else if ([menuItem action] == @selector(toggleLogView:)) { 255 BOOL logViewVisible = ! [logView isHiddenOrHasHiddenAncestor]; 256 [menuItem setState:(logViewVisible ? NSOnState : NSOffState)]; 257 return YES; 258 } else 259 return YES; 260} 261 262- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName 263{ 264 return [gameInfo gameTitle]; 265} 266 267- (IBAction)takeback:(id)sender 268{ 269 if ([[self document] match]) { 270 [gameInfo willChangeValueForKey:@"gameTitle"]; 271 [[self document] offerTakeback]; 272 [gameInfo didChangeValueForKey:@"gameTitle"]; 273 } else { 274 [engine takeback]; 275 } 276} 277 278typedef void (^MBCAlertCallback)(NSInteger returnCode); 279 280- (void) endAlertSheet:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo 281{ 282 MBCAlertCallback callback = (MBCAlertCallback)contextInfo; 283 callback(returnCode); 284 Block_release(callback); 285} 286 287- (void) requestTakeback 288{ 289 NSAlert * alertSheet = 290 [NSAlert alertWithMessageText:NSLocalizedString(@"takeback_request_text", @"Opp wants takeback") 291 defaultButton:NSLocalizedString(@"takeback_request_yes", @"OK") 292 alternateButton:NSLocalizedString(@"takeback_request_no", @"No") 293 otherButton:nil informativeTextWithFormat:@""]; 294 for (NSButton * button in [alertSheet buttons]) 295 [button setKeyEquivalent:@""]; 296 [alertSheet beginSheetModalForWindow:[self window] modalDelegate:self 297 didEndSelector:@selector(endAlertSheet:returnCode:contextInfo:) 298 contextInfo:Block_copy( 299 ^(NSInteger returnCode) { 300 MBCController * controller = (MBCController *)[NSApp delegate]; 301 if (returnCode == NSAlertDefaultReturn) { 302 [engine takeback]; 303 [controller setValue:100.0 forAchievement:@"AppleChess_Merciful"]; 304 [[self document] allowTakeback:YES]; 305 } else { 306 [controller setValue:100.0 forAchievement:@"AppleChess_Cry_me_a_River"]; 307 [[self document] allowTakeback:NO]; 308 } 309 })]; 310} 311 312- (void) requestDraw 313{ 314 NSAlert * alertSheet = 315 [NSAlert alertWithMessageText:NSLocalizedString(@"draw_request_text", @"Opp wants draw") 316 defaultButton:NSLocalizedString(@"draw_request_yes", @"OK") 317 alternateButton:NSLocalizedString(@"draw_request_no", @"No") 318 otherButton:nil informativeTextWithFormat:@""]; 319 for (NSButton * button in [alertSheet buttons]) 320 [button setKeyEquivalent:@""]; 321 [alertSheet beginSheetModalForWindow:[self window] modalDelegate:self 322 didEndSelector:@selector(endAlertSheet:returnCode:contextInfo:) 323 contextInfo:Block_copy( 324 ^(NSInteger returnCode) { 325 MBCController * controller = (MBCController *)[NSApp delegate]; 326 if (returnCode == NSAlertDefaultReturn) { 327 [[NSNotificationCenter defaultCenter] 328 postNotificationName:MBCGameEndNotification 329 object:[self document] userInfo:[MBCMove moveWithCommand:kCmdDraw]]; 330 } else { 331 [controller setValue:100.0 forAchievement:@"AppleChess_Not_So_Fast"]; 332 } 333 })]; 334} 335 336- (void)handleRemoteResponse:(NSString *)response 337{ 338 [gameInfo willChangeValueForKey:@"gameTitle"]; 339 if ([response isEqual:@"Takeback"]) { 340 [engine takeback]; 341 } else if ([response isEqual:@"NoTakeback"]) { 342 NSAlert * alertSheet = 343 [NSAlert alertWithMessageText:NSLocalizedString(@"takeback_refused", @"Opp refused") 344 defaultButton:NSLocalizedString(@"takeback_refused_ok", @"OK") 345 alternateButton:nil otherButton:nil 346 informativeTextWithFormat:@""]; 347 [alertSheet beginSheetModalForWindow:[self window] modalDelegate:self 348 didEndSelector:@selector(endAlertSheet:returnCode:contextInfo:) 349 contextInfo:Block_copy(^(NSInteger returnCode) {})]; 350 } 351 [gameInfo didChangeValueForKey:@"gameTitle"]; 352} 353 354- (void) showNewGameSheet 355{ 356 if ([[self document] invitees]) 357 [self runMatchmakerPanel]; 358 else 359 [NSApp beginSheet:gameNewSheet modalForWindow:[self window] 360 modalDelegate:nil didEndSelector:nil contextInfo:nil]; 361} 362 363uint32_t sAttributesForSides[] = { 364 0xFFFF0000, 365 0x0000FFFF, 366 0xFFFFFFFF 367}; 368 369- (void) runMatchmakerPanel 370{ 371 NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; 372 GKMatchRequest *matchRequest = [[[GKMatchRequest alloc] init] autorelease]; 373 matchRequest.minPlayers = 2; 374 matchRequest.maxPlayers = 2; 375 matchRequest.playerGroup = [defaults integerForKey:kMBCNewGameVariant]; 376 matchRequest.playerAttributes = sAttributesForSides[[defaults integerForKey:kMBCNewGameSides]]; 377 matchRequest.playersToInvite = nil; 378 379 GKTurnBasedMatchmakerViewController *shadkhan = [[[GKTurnBasedMatchmakerViewController alloc] initWithMatchRequest:matchRequest] autorelease]; 380 shadkhan.turnBasedMatchmakerDelegate = self; 381 shadkhan.showExistingMatches = YES; 382 [dialogController presentViewController:shadkhan]; 383} 384 385- (IBAction)startNewGame:(id)sender 386{ 387 [(NSUserDefaultsController *)[NSUserDefaultsController sharedUserDefaultsController] save:self]; 388 NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; 389 [defaults setInteger:[self searchTime] forKey:kMBCSearchTime]; 390 [NSApp endSheet:gameNewSheet]; 391 [gameNewSheet orderOut:self]; 392 if ([defaults integerForKey:kMBCNewGamePlayers] == kHumanVsGameCenter) { 393 [self runMatchmakerPanel]; 394 } else { 395 NSError * error; 396 [[self document] initWithType:@"com.apple.chess.game" error:&error]; 397 [[self document] setEphemeral:NO]; // Explicitly opened, so not ephemeral 398 } 399 [self willChangeValueForKey:@"hideSpeakMoves"]; 400 [self willChangeValueForKey:@"hideSpeakHumanMoves"]; 401 [self willChangeValueForKey:@"hideEngineProperties"]; 402 [self willChangeValueForKey:@"hideRemoteProperties"]; 403 [self didChangeValueForKey:@"hideSpeakMoves"]; 404 [self didChangeValueForKey:@"hideSpeakHumanMoves"]; 405 [self didChangeValueForKey:@"hideEngineProperties"]; 406 [self didChangeValueForKey:@"hideRemoteProperties"]; 407} 408 409- (IBAction)cancelNewGame:(id)sender 410{ 411 [[NSUserDefaultsController sharedUserDefaultsController] revert:self]; 412 [NSApp endSheet:gameNewSheet]; 413 [gameNewSheet orderOut:self]; 414 [self close]; 415} 416 417- (IBAction)resign:(id)sender 418{ 419 [[self document] resign]; 420} 421 422- (IBAction) showHint:(id)sender 423{ 424 [gameView showMoveAsHint:[engine lastPonder]]; 425 [interactive announceHint:[engine lastPonder]]; 426} 427 428- (IBAction) showLastMove:(id)sender 429{ 430 [gameView showMoveAsLast:[board lastMove]]; 431 [interactive announceLastMove:[board lastMove]]; 432} 433 434// Called when our animate-out animation is done 435// While the logView is not visible by virtue of it being outside the window, 436// hiding it ensures it won't interact with (for example) the key view loop. 437- (void)hideLogContainer:(id)now { 438 if (now) 439 [logViewRightEdgeConstraint setConstant:NSWidth([logContainer frame])]; 440 [logContainer setHidden:YES]; 441} 442 443- (IBAction) toggleLogView:(id)sender 444{ 445 // Make sure that another animation isn't going to hide this at some point in the future. 446 [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideLogContainer:) object:nil]; 447 MBCDocument * doc = [self document]; 448 BOOL currentlyShowing = [doc boolForKey:kMBCShowGameLog]; 449 [doc setValue:[NSNumber numberWithBool:!currentlyShowing] forKey:kMBCShowGameLog]; 450 if (!currentlyShowing) { 451 // We want to make it visible immediately so the user can see it animate it. 452 [logContainer setHidden:NO]; 453 [[logViewRightEdgeConstraint animator] setConstant:0]; 454 } else { 455 [[logViewRightEdgeConstraint animator] setConstant:NSWidth([logContainer frame])]; 456 // We want to keep it visible up until the end, so the user can see the animation. 457 [self performSelector:@selector(hideLogContainer:) withObject:nil afterDelay:[[NSAnimationContext currentContext] duration]]; 458 } 459} 460 461- (void) adjustLogView 462{ 463 // 464 // Show or hide game log if necessary if window was reused 465 // 466 MBCDocument * document = [self document]; 467 NSWindow * window = [self window]; 468 if ([document needNewGameSheet]) 469 [self showNewGameSheet]; 470 else if ([document boolForKey:kMBCShowGameLog] == [logContainer isHidden]) { 471 [document setValue:[NSNumber numberWithBool:![logContainer isHidden]] forKey:kMBCShowGameLog]; 472 [self toggleLogView:self]; 473 } 474 if ([document match]) 475 [window setFrameAutosaveName:[NSString stringWithFormat:@"Match %@\n", document.match.matchID]]; 476} 477 478- (void) gameEnded:(NSNotification *)notification 479{ 480 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 481 482 [board makeMove:move]; 483 484 BOOL weWon = NO; 485 if (move->fCommand == kCmdWhiteWins && SideIncludesWhite([[self document] humanSide])) 486 weWon = YES; 487 if (move->fCommand == kCmdBlackWins && SideIncludesBlack([[self document] humanSide])) 488 weWon = YES; 489 if (weWon) { 490 MBCController * controller = (MBCController *)[NSApp delegate]; 491 if ([[self document] engineSide] != kNeitherSide && [[self document] integerForKey:kMBCMinSearchTime] >= 0) 492 [controller setValue:100.0 forAchievement:@"AppleChess_Luddite"]; 493 if ([[self document] remoteSide] != kNeitherSide) { 494 [controller setValue:100.0 forAchievement:@"AppleChess_King_of_the_Cloud"]; 495 NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; 496 NSDictionary * victories = [defaults objectForKey:kMBCGCVictories]; 497 if ([victories count] < 10) { 498 NSMutableDictionary * v = victories 499 ? [victories mutableCopy] : [[NSMutableDictionary alloc] init]; 500 for (GKTurnBasedParticipant * p in [[self document] match].participants) 501 if (![p.playerID isEqual:[controller localPlayer].playerID]) 502 [v setObject:[NSNumber numberWithBool:YES] forKey:p.playerID]; 503 victories = [v autorelease]; 504 } 505 [defaults setObject:victories forKey:kMBCGCVictories]; 506 if ([victories count] == 10) 507 [controller setValue:100.0 forAchievement:@"AppleChess_Battle_Royal"]; 508 } 509 if ([[self document] variant] == kVarSuicide || [[self document] variant] == kVarLosers) 510 if ([[self board] numMoves] < 39) 511 [controller setValue:100.0 forAchievement:@"AppleChess_Lightning_Loser"]; 512 } 513} 514 515- (void)updateAchievementsForMove:(MBCMove *)move 516{ 517 BOOL humanMove; 518 MBCVariant variant = [[self document] variant]; 519 BOOL notAntiChess = variant == kVarNormal || variant == kVarCrazyhouse; 520 MBCController * controller = (MBCController *)[NSApp delegate]; 521 MBCPieceCode ourColor = Color(move->fPiece); 522 MBCPieceCode oppColor = MBCPieceCode(Opposite(ourColor)); 523 524 if (ourColor == kWhitePiece) 525 humanMove = SideIncludesWhite([[self document] humanSide]); 526 else 527 humanMove = SideIncludesBlack([[self document] humanSide]); 528 529 if (humanMove && notAntiChess) { 530 MBCPieces * curPos = [[self board] curPos]; 531 if (move->fCheck) 532 [controller setValue:100.0 forAchievement:@"AppleChess_Checker"]; 533 if (move->fEnPassant) 534 [controller setValue:100.0 forAchievement:@"AppleChess_Sidestepped"]; 535 if (move->fPromotion) { 536 if (Piece(move->fPromotion) == QUEEN) 537 [controller setValue:100.0 forAchievement:@"AppleChess_Promotional_Value"]; 538 else 539 [controller setValue:100.0 forAchievement:@"AppleChess_Promotional_Discount"]; 540 } 541 if (move->fCommand == kCmdMove && Piece(move->fPiece) == PAWN 542 && (labs((int)Row(move->fFromSquare)-(int)Row(move->fToSquare)) == 2) 543 ) 544 [controller setValue:100.0 forAchievement:@"AppleChess_One_Step_Beyond"]; 545 if (move->fVictim && variant == kVarNormal) { 546 if (Piece(move->fVictim) == PAWN || Promoted(move->fVictim)) 547 if (curPos->fInHand[ourColor+PAWN] == 5) 548 [controller setValue:100.0 forAchievement:@"AppleChess_Pawnbroker"]; 549 if (Piece(move->fVictim) == KNIGHT) 550 if (curPos->fInHand[ourColor+KNIGHT] == 2) 551 [controller setValue:100.0 forAchievement:@"AppleChess_Pikeman"]; 552 if (curPos->NoPieces(oppColor)) 553 [controller setValue:100.0 forAchievement:@"AppleChess_Take_no_Prisoners"]; 554 } 555 if (move->fCheckMate) { 556 if ([[self board] numMoves] < 19) 557 [controller setValue:100.0 forAchievement:@"AppleChess_Blitz"]; 558 if (variant == kVarNormal) { 559 int materialBalance = 560 (curPos->fInHand[ourColor+QUEEN] -curPos->fInHand[oppColor+QUEEN]) * 9 561 + (curPos->fInHand[ourColor+ROOK] -curPos->fInHand[oppColor+ROOK]) * 5 562 + (curPos->fInHand[ourColor+KNIGHT]-curPos->fInHand[oppColor+KNIGHT])* 3 563 + (curPos->fInHand[ourColor+BISHOP]-curPos->fInHand[oppColor+BISHOP])* 3 564 + (curPos->fInHand[ourColor+PAWN] -curPos->fInHand[oppColor+PAWN]) * 1; 565 if (materialBalance <= -9) 566 [controller setValue:100.0 forAchievement:@"AppleChess_Last_Ditch_Effort"]; 567 } else if (variant == kVarCrazyhouse) { 568 if (move->fCommand == kCmdDrop) 569 [controller setValue:100.0 forAchievement:@"AppleChess_Aerial_Attack"]; 570 } 571 } 572 if (move->fCastling != kNoCastle) { 573 NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; 574 int sides = [defaults integerForKey:kMBCCastleSides] | move->fCastling; 575 [defaults setInteger:sides forKey:kMBCCastleSides]; 576 if (sides == (kCastleKingside|kCastleQueenside)) 577 [controller setValue:100.0 forAchievement:@"AppleChess_Duck_and_Cover"]; 578 } 579 } 580} 581 582- (void) executeMove:(NSNotification *)notification 583{ 584 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 585 586 [board makeMove:move]; 587 [gameView unselectPiece]; 588 [gameView hideMoves]; 589 [[self document] updateChangeCount:NSChangeDone]; 590 [self updateAchievementsForMove:move]; 591 592 if (move->fAnimate) 593 fCurAnimation = [MBCMoveAnimation moveAnimation:move board:board view:gameView]; 594 else 595 [[NSNotificationQueue defaultQueue] 596 enqueueNotification: 597 [NSNotification 598 notificationWithName:MBCEndMoveNotification 599 object:[self document] userInfo:(id)move] 600 postingStyle: NSPostWhenIdle]; 601 602 if ([[self document] engineSide] == kNeitherSide) 603 if (MBCMoveCode cmd = [[self board] outcome]) 604 [[NSNotificationQueue defaultQueue] 605 enqueueNotification: 606 [NSNotification 607 notificationWithName:MBCGameEndNotification 608 object:[self document] 609 userInfo:[MBCMove moveWithCommand:cmd]] 610 postingStyle: NSPostWhenIdle]; 611} 612 613- (void) commitMove:(NSNotification *)notification 614{ 615 [board commitMove]; 616 [gameView hideMoves]; 617 [[self document] updateChangeCount:NSChangeDone]; 618 619 if ([[self document] humanSide] == kBothSides 620 && [gameView facing] != kNeitherSide 621 ) { 622 // 623 // Rotate board 624 // 625 fCurAnimation = [MBCBoardAnimation boardAnimation:gameView]; 626 } 627} 628 629- (BOOL)listenForMoves 630{ 631 return [[self document] boolForKey:kMBCListenForMoves]; 632} 633 634- (BOOL)speakMoves 635{ 636 return [[self document] boolForKey:kMBCSpeakMoves]; 637} 638 639- (BOOL)speakHumanMoves 640{ 641 return [[self document] boolForKey:kMBCSpeakHumanMoves]; 642} 643 644- (NSString *)speakOpponentTitle 645{ 646 if ([[self document] match]) 647 return NSLocalizedString(@"gc_opponent", @"Speak Opponent Moves"); 648 else 649 return NSLocalizedString(@"engine_opponent", @"Speak Computer Moves"); 650} 651 652- (IBAction) updatePlayers:(id)sender 653{ 654 [self willChangeValueForKey:@"hideEngineStrength"]; 655 [self willChangeValueForKey:@"hideNewGameSides"]; 656 [self didChangeValueForKey:@"hideEngineStrength"]; 657 [self didChangeValueForKey:@"hideNewGameSides"]; 658} 659 660- (BOOL) hideEngineStrength 661{ 662 NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults]; 663 664 switch ([userDefaults integerForKey:kMBCNewGamePlayers]) { 665 case kHumanVsHuman: 666 case kHumanVsGameCenter: 667 return YES; 668 default: 669 return NO; 670 } 671} 672 673- (BOOL) hideNewGameSides 674{ 675 NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults]; 676 677 switch ([userDefaults integerForKey:kMBCNewGamePlayers]) { 678 case kHumanVsGameCenter: 679 return NO; 680 default: 681 return YES; 682 } 683} 684 685- (BOOL)hideSpeakMoves 686{ 687 return [[self document] humanSide] == kBothSides; 688} 689 690- (BOOL)hideSpeakHumanMoves 691{ 692 return [[self document] engineSide] == kBothSides; 693} 694 695- (BOOL)hideEngineProperties 696{ 697 return [[self document] engineSide] == kNeitherSide; 698} 699 700- (BOOL)hideRemoteProperties 701{ 702 return [[self document] remoteSide] == kNeitherSide; 703} 704 705- (NSString *)voiceIDForKey:(NSString *)key 706{ 707 NSString * voiceID = [[self document] objectForKey:key]; 708 709 return [voiceID length] ? voiceID : nil; 710} 711 712- (NSSpeechSynthesizer *)copySpeechSynthesizerForKey:(NSString *)key 713{ 714 return [[NSSpeechSynthesizer alloc] initWithVoice:[self voiceIDForKey:key]]; 715} 716 717- (NSSpeechSynthesizer *)primarySynth 718{ 719 if (!primarySynth) 720 primarySynth = [self copySpeechSynthesizerForKey:kMBCDefaultVoice]; 721 return primarySynth; 722} 723 724- (NSSpeechSynthesizer *)alternateSynth 725{ 726 if (!alternateSynth) 727 alternateSynth = [self copySpeechSynthesizerForKey:kMBCAlternateVoice]; 728 return alternateSynth; 729} 730 731- (NSDictionary *)copyLocalizationForKey:(NSString *)key 732{ 733 NSString * voice = [self voiceIDForKey:key]; 734 NSString * localeID = [[NSSpeechSynthesizer attributesForVoice:voice] 735 valueForKey:NSVoiceLocaleIdentifier]; 736 if (!localeID) 737 return nil; 738 739 NSLocale * locale = [[[NSLocale alloc] initWithLocaleIdentifier:localeID] autorelease]; 740 NSBundle * mainBundle = [NSBundle mainBundle]; 741 NSArray * preferred = [NSBundle preferredLocalizationsFromArray:[mainBundle localizations] 742 forPreferences:[NSArray arrayWithObject:localeID]]; 743 if (!preferred) 744 return nil; 745 746 for (NSString * tryLocale in preferred) 747 if (NSURL * url = [mainBundle URLForResource:@"Spoken" withExtension:@"strings" 748 subdirectory:nil localization:tryLocale] 749 ) 750 return [[NSDictionary alloc] initWithObjectsAndKeys: 751 [NSDictionary dictionaryWithContentsOfURL:url], @"strings", 752 locale, @"locale", nil]; 753 return nil; 754} 755 756- (NSDictionary *)primaryLocalization 757{ 758 if (!primaryLocalization) 759 primaryLocalization = [self copyLocalizationForKey:kMBCDefaultVoice]; 760 return primaryLocalization; 761} 762 763- (NSDictionary *)alternateLocalization 764{ 765 if (!alternateLocalization) 766 alternateLocalization = [self copyLocalizationForKey:kMBCAlternateVoice]; 767 return alternateLocalization; 768} 769 770- (NSString *)engineStrengthForTime:(int)time 771{ 772 switch (time) { 773 case -3: 774 return NSLocalizedString(@"fixed_depth_mode", @"Computer thinks 1 move ahead"); 775 case -2: 776 case -1: 777 return [NSString localizedStringWithFormat:NSLocalizedString(@"fixed_depths_mode", @"Computer thinks %d moves ahead"), 4+time]; 778 case 0: 779 return NSLocalizedString(@"fixed_time_mode", @"Computer thinks 1 second per move"); 780 default: 781 return [NSString localizedStringWithFormat:NSLocalizedString(@"fixed_times_mode", @"Computer thinks %d seconds per move"), [MBCEngine secondsForTime:time]]; 782 } 783} 784 785- (int)searchTime 786{ 787 return [[self document] integerForKey:kMBCSearchTime]; 788} 789 790- (NSString *)engineStrength 791{ 792 return [self engineStrengthForTime:[self searchTime]]; 793} 794 795+ (NSSet *) keyPathsForValuesAffectingEngineStrength 796{ 797 return [NSSet setWithObject:@"document.MBCSearchTime"]; 798} 799 800- (IBAction)showPreferences:(id)sender 801{ 802 [gameInfo editPreferencesForWindow:[self window]]; 803} 804 805- (void)setAngle:(float)angle spin:(float)spin 806{ 807 [[self document] setObject:[NSNumber numberWithFloat:angle] forKey:kMBCBoardAngle]; 808 [[self document] setObject:[NSNumber numberWithFloat:spin] forKey:kMBCBoardSpin]; 809} 810 811- (IBAction) profileDraw:(id)sender 812{ 813 timeval startTime; 814 gettimeofday(&startTime, NULL); 815 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 816 timeval endTime; 817 [gameView profileDraw]; 818 gettimeofday(&endTime, NULL); 819 double elapsed = endTime.tv_sec-startTime.tv_sec 820 +0.000001*(endTime.tv_usec-startTime.tv_usec); 821 NSLog(@"Profiling took %4.2fs, %4.0fms per frame", 822 elapsed, elapsed*10.0); 823 }); 824} 825 826#pragma mark - 827#pragma mark GKTurnBasedMatchmakerViewControllerDelegate 828// The user has cancelled 829- (void)turnBasedMatchmakerViewControllerWasCancelled:(GKTurnBasedMatchmakerViewController *)vc 830{ 831 [dialogController dismiss:vc]; 832 [NSApp stopModal]; 833 if ([[self document] invitees]) 834 [self close]; 835 else 836 [self showNewGameSheet]; 837} 838 839// Matchmaking has failed with an error 840- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)vc didFailWithError:(NSError *)error 841{ 842 [self turnBasedMatchmakerViewControllerWasCancelled:vc]; 843} 844 845// A turned-based match has been found, the game should start 846- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)vc didFindMatch:(GKTurnBasedMatch *)match { 847 [dialogController dismiss:vc]; 848 [NSApp stopModal]; 849 [[NSApp delegate] startNewOnlineGame:match withDocument:[self document]]; 850} 851 852// Called when a users chooses to quit a match and that player has the current turn. The developer should call playerQuitInTurnWithOutcome:nextPlayer:matchData:completionHandler: on the match passing in appropriate values. They can also update matchOutcome for other players as appropriate. 853- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)vc playerQuitForMatch:(GKTurnBasedMatch *)match { 854 for (GKTurnBasedParticipant * participant in[match participants]) 855 if ([[participant playerID] isEqual:[[(MBCController *)[NSApp delegate] localPlayer] playerID]]) 856 [participant setMatchOutcome:GKTurnBasedMatchOutcomeQuit]; 857 else 858 [participant setMatchOutcome:GKTurnBasedMatchOutcomeWon]; 859 860 [match endMatchInTurnWithMatchData:[NSData data] completionHandler:^(NSError *error) { 861 }]; 862} 863 864#pragma mark - 865#pragma mark GKAchievementViewControllerDelegate 866 867- (IBAction)showAchievements:(id)sender 868{ 869 fAchievements = [[GKAchievementViewController alloc] init]; 870 fAchievements.achievementDelegate = self; 871 [dialogController presentViewController:fAchievements]; 872} 873 874- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)vc 875{ 876 if (fAchievements) { 877 [dialogController dismiss:vc]; 878 fAchievements = nil; 879 } 880} 881 882 883@end 884