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