MBCDocument.mm   [plain text]

/*
File: MBCDocument.mm
Contains: Document representing a Chess game
Copyright: © 2003-2013 by Apple Inc., all rights reserved.

IMPORTANT: This Apple software is supplied to you by Apple Computer,
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.

In consideration of your agreement to abide by the following terms,
and subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the
Apple Software, with or without modifications, in source and/or binary
forms; provided that if you redistribute the Apple Software in its
entirety and without modifications, you must retain this notice and
the following text and disclaimers in all such redistributions of the
Apple Software. Neither the name, trademarks, service marks or logos
of Apple Inc. may be used to endorse or promote products
derived from the Apple Software without specific prior written
permission from Apple. Except as expressly stated in this notice, no
other rights or licenses, express or implied, are granted by Apple
herein, including but not limited to any patent rights that may be
infringed by your derivative works or by other works in which the
Apple Software may be incorporated.

The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.

IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE,
HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING
NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "MBCDocument.h"
#import "MBCController.h"
#import "MBCPlayer.h"
#import "MBCEngine.h"
#import "MBCGameInfo.h"
#import "MBCUserDefaults.h"
#import "MBCBoardWin.h"

NSString * const sProperties[] = {
kMBCBoardStyle, kMBCPieceStyle,
kMBCDefaultVoice, kMBCAlternateVoice,
kMBCSpeakMoves, kMBCSpeakHumanMoves, kMBCListenForMoves,
kMBCSearchTime, kMBCBoardAngle, kMBCBoardSpin, kMBCShowGameLog,
NULL
};

static void MBCEndTurn(GKTurnBasedMatch *match, NSData * matchData)
{
GKTurnBasedParticipant * opponent;

for (opponent in [match participants])
if (opponent != [match currentParticipant])
break;

[match endTurnWithNextParticipants:[[NSArray alloc] initWithObjects:opponent, nil]
turnTimeout:86400
matchData:matchData
completionHandler:^(NSError *error) {
if(error) {

}
}];

}

@implementation MBCDocument

@synthesize board, variant, players, match, properties, offerDraw, ephemeral, needNewGameSheet,
disallowSubstitutes, invitees;

+ (BOOL) autosavesInPlace
{
return YES;
}

- (BOOL) boolForKey:(NSString *)key;
{
return [[properties valueForKey:key] boolValue];
}

- (NSInteger) integerForKey:(NSString *)key
{
return [[properties valueForKey:key] intValue];
}

- (float) floatForKey:(NSString *)key
{
return [[properties valueForKey:key] floatValue];
}

- (id) objectForKey:(NSString *)key
{
id result = [properties valueForKey:key];
if (!result && ([key isEqual:@"White"] || [key isEqual:@"Black"]))
result = NSLocalizedString(@"gc_automatch", @"GameCenter Automatch");
return result;
}

- (BOOL) brandNewGame
{
return ![properties valueForKey:@"White"] && ![properties valueForKey:@"Black"];
}

- (id) valueForUndefinedKey:(NSString *)key
{
return [self objectForKey:key];
}

- (void) setObject:(id)value forKey:(NSString *)key
{
[self setValue:value forUndefinedKey:key];
}

- (void) setValue:(id)value forUndefinedKey:(NSString *)key
{
if (value == [NSNull null])
value = nil;
[self willChangeValueForKey:key];
[[NSUserDefaults standardUserDefaults] setObject:value forKey:key];
[properties setValue:value forKey:key];
[self didChangeValueForKey:key];
[self updateChangeCount:NSChangeDone];
}

- (void)updateSearchTime
{
id curSearchTime = [properties valueForKey:kMBCSearchTime];
id minSearchTime = [properties valueForKey:kMBCMinSearchTime];

if (minSearchTime && [minSearchTime intValue] > [curSearchTime intValue])
[properties setValue:curSearchTime forKey:kMBCMinSearchTime];
[[[[self windowControllers] objectAtIndex:0] engine] setSearchTime:[curSearchTime intValue]];
}

+ (BOOL)coinFlip
{
static BOOL sLastFlip = arc4random() & 1;

return sLastFlip = !sLastFlip;
}

+ (BOOL)processNewMatch:(GKTurnBasedMatch *)match variant:(MBCVariant)variant side:(MBCSideCode)side document:(MBCDocument *)doc
{
NSPropertyListFormat format;
NSMutableDictionary * gameData = !match.matchData ? nil : [NSPropertyListSerialization propertyListWithData:match.matchData options:0 format:&format error:nil];
MBCController * controller = (MBCController *)[NSApp delegate];
NSString * localPlayerID = controller.localPlayer.playerID;
if (!gameData) {
//
// Brand new game, pick a side
//
NSString * playerIDKey;
switch (side) {
case kPlayWhite:
playerIDKey = @"WhitePlayerID";
break;
case kPlayBlack:
playerIDKey = @"BlackPlayerID";
break;
default:
playerIDKey = [self coinFlip] ? @"WhitePlayerID" : @"BlackPlayerID";
break;
}
gameData = [NSMutableDictionary dictionaryWithObjectsAndKeys:
localPlayerID, playerIDKey,
gVariantName[variant], @"Variant",
kMBCHumanPlayer, @"WhiteType", kMBCHumanPlayer, @"BlackType",
nil];
if ([playerIDKey isEqual:@"BlackPlayerID"]) {
//
// We picked black, it's our opponent's turn
//
MBCEndTurn(match, [NSPropertyListSerialization dataWithPropertyList:gameData
format:NSPropertyListXMLFormat_v1_0
options:0
error:nil]);
}
} else if (![localPlayerID isEqual:[gameData objectForKey:@"WhitePlayerID"]] && ![localPlayerID isEqual:[gameData objectForKey:@"BlackPlayerID"]]) {
//
// We're the second participant, pick the side that hasn't been picked yet
//
BOOL playingWhite = ![gameData objectForKey:@"WhitePlayerID"];

// gameData here is not nil, which means we should be in a game already. We need to pick a side to this game.
[gameData removeObjectForKey:@"PeerID"];
[gameData setObject:localPlayerID forKey:(playingWhite ? @"WhitePlayerID" : @"BlackPlayerID")];
}

BOOL createDoc = !doc;

if (createDoc) {
doc = [[MBCDocument alloc] initWithMatch:match game:gameData];
[doc makeWindowControllers];
[doc showWindows];
[doc autorelease];
}

return YES;
}

- (id)init
{
//
// If we have any implicitly opened document that did not get used yet, we reuse one
//
if (![self disallowSubstitutes])
for (MBCDocument * doc in [[NSDocumentController sharedDocumentController] documents])
if ([doc ephemeral]) {
[doc close];

break;
}
if (self = [super init]) {
[self setEphemeral:YES];
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
properties = [[NSMutableDictionary alloc] init];
for (int i = 0; sProperties[i]; ++i)
[properties setValue:[defaults objectForKey:sProperties[i]]
forKey:sProperties[i]];
}
return self;
}

- (void)dealloc
{
[super dealloc];
}

- (id)initForNewGameSheet:(NSArray *)guests
{
if (self = [self init]) {
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
if (![(MBCController *)[NSApp delegate] localPlayer] && (MBCPlayers)[defaults integerForKey:kMBCNewGamePlayers] == kHumanVsGameCenter)
[defaults setInteger:kHumanVsComputer forKey:kMBCNewGamePlayers];
[self setEphemeral:NO];
[self setNeedNewGameSheet:YES];
[self setDisallowSubstitutes:YES];
[self setInvitees:guests];
[[NSDocumentController sharedDocumentController] addDocument:self];
}

return self;
}

- (id)initWithType:(NSString *)typeName error:(NSError **)outError
{
if (self = [super initWithType:typeName error:outError]) {
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
players = (MBCPlayers)[defaults integerForKey:kMBCNewGamePlayers];
switch (players) {
case kHumanVsHuman:
case kHumanVsComputer:
//
// Always accept these
//
break;
case kComputerVsHuman:
case kComputerVsComputer:
//
// Accept it for explicit creation, but reset default
//
[defaults setInteger:kHumanVsComputer forKey:kMBCNewGamePlayers];
break;
default:
//
// Never create a GameCenter game for an "untitled" action
//
players = kHumanVsComputer;
break;
}
variant = (MBCVariant)[defaults integerForKey:kMBCNewGameVariant];
if (players == kHumanVsComputer || players == kComputerVsHuman)
[properties setValue:[properties valueForKey:kMBCSearchTime] forKey:kMBCMinSearchTime];
[[NSNotificationQueue defaultQueue]
enqueueNotification:[NSNotification notificationWithName:MBCGameStartNotification
object:self]
postingStyle:NSPostWhenIdle];
[self updateChangeCount:NSChangeCleared];
}
return self;
}

- (void)duplicateDocument:(id)sender
{
[self setEphemeral:NO];
[super duplicateDocument:sender];
}

- (void)updateChangeCount:(NSDocumentChangeType)change
{
if (!board || [board numMoves] || [self remoteSide] != kNeitherSide) {
[self setEphemeral:NO];

[super updateChangeCount:change];
}
}

- (void)close
{
//
// Delay disposing document a little while, because main window with active bindings is autoreleased
//
[self retain];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_MSEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self release];
});
[super close];
}

- (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)sel contextInfo:(void *)contextInfo
{
//
// Never ask to save GameCenter games (we autosave them, though)
//
if (match) {
NSInvocation * invoc =
[NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:sel]];
[invoc setTarget:delegate];
[invoc setSelector:sel];
[invoc setArgument:&self atIndex:2];
BOOL shouldClose = YES;
[invoc setArgument:&shouldClose atIndex:3];
[invoc setArgument:&contextInfo atIndex:4];
[invoc invoke];
} else {
[super canCloseDocumentWithDelegate:delegate shouldCloseSelector:sel contextInfo:contextInfo];
}
}

- (void)postMatchOutcomeNotification
{
MBCMoveCode cmd = kCmdNull;
NSString * localPlayerID = [(MBCController *)[NSApp delegate] localPlayer].playerID;
for (GKTurnBasedParticipant * p in match.participants)
if ([p.player.playerID isEqual:localPlayerID]) {
if (p.matchOutcome == GKTurnBasedMatchOutcomeTied)
cmd = kCmdDraw;
else if (p.matchOutcome == GKTurnBasedMatchOutcomeWon)
cmd = localWhite ? kCmdWhiteWins : kCmdBlackWins;
else
cmd = localWhite ? kCmdBlackWins : kCmdWhiteWins;
break;
}
[[NSNotificationQueue defaultQueue]
enqueueNotification:[NSNotification notificationWithName:MBCGameEndNotification
object:self
userInfo:(id)[MBCMove moveWithCommand:cmd]]
postingStyle:NSPostWhenIdle];
}

- (BOOL)checkForEndOfGame
{
bool firstIsDone = [[match.participants objectAtIndex:0] matchOutcome] != GKTurnBasedMatchOutcomeNone;
bool secondIsDone = [[match.participants objectAtIndex:1] matchOutcome] != GKTurnBasedMatchOutcomeNone;

if (firstIsDone != secondIsDone) {
GKTurnBasedMatchOutcome outcome;
if ([[match.participants objectAtIndex:secondIsDone] matchOutcome] == GKTurnBasedMatchOutcomeWon)
outcome = GKTurnBasedMatchOutcomeLost;
else
outcome = GKTurnBasedMatchOutcomeWon;
[[match.participants objectAtIndex:firstIsDone] setMatchOutcome:outcome];
}
if ((firstIsDone || secondIsDone) && match.status != GKTurnBasedMatchStatusEnded
&& [match.currentParticipant.player.playerID isEqual:[(MBCController *)[NSApp delegate] localPlayer].playerID]
)
[match endMatchInTurnWithMatchData:[self matchData] completionHandler:^(NSError *error) {}];

return firstIsDone || secondIsDone;
}

- (void) updateMatchForEndOfGame:(MBCMoveCode)cmd
{
if ([match.currentParticipant.player.playerID isEqual:[(MBCController *)[NSApp delegate] localPlayer].playerID]) {
//
// Participant whose turn it is updates the match
//
GKTurnBasedMatchOutcome whiteOutcome;
GKTurnBasedMatchOutcome blackOutcome;

if (cmd == kCmdDraw) {
whiteOutcome = GKTurnBasedMatchOutcomeTied;
blackOutcome = GKTurnBasedMatchOutcomeTied;
} else if (cmd == kCmdWhiteWins) {
whiteOutcome = GKTurnBasedMatchOutcomeWon;
blackOutcome = GKTurnBasedMatchOutcomeLost;
} else {
whiteOutcome = GKTurnBasedMatchOutcomeLost;
blackOutcome = GKTurnBasedMatchOutcomeWon;
}
for (GKTurnBasedParticipant * participant in match.participants)
if ([participant.player.playerID isEqual:[properties objectForKey:@"WhitePlayerID"]])
[participant setMatchOutcome:whiteOutcome];
else
[participant setMatchOutcome:blackOutcome];
[match endMatchInTurnWithMatchData:[self matchData] completionHandler:^(NSError *error) {}];
}
}

- (id)initWithMatch:(GKTurnBasedMatch *)gkMatch game:(NSDictionary *)gameData
{
if (self = [self init]) {
[self setDisallowSubstitutes:YES];
[self setInvitees:nil];
[self setMatch:gkMatch];
[self loadGame:gameData];
localWhite = [[(MBCController *)[NSApp delegate] localPlayer].playerID isEqual:[properties objectForKey:@"WhitePlayerID"]];
NSDictionary * localProps = [properties objectForKey:(localWhite ? @"WhiteProperties" : @"BlackProperties")];
if (localProps)
[properties addEntriesFromDictionary:localProps];
[self updateChangeCount:NSChangeCleared];
[[NSDocumentController sharedDocumentController] addDocument:self];
}
return self;
}

- (NSString *)windowNibName
{
return @"Board";
}

- (void)makeWindowControllers
{
if ([[self windowControllers] count]) {
//
// Reused document
//
[[[self windowControllers] objectAtIndex:0] adjustLogView];
} else {
MBCBoardWin * windowController = [[MBCBoardWin alloc] initWithWindowNibName:[self windowNibName]];
[self addWindowController:windowController];
[windowController release];
}
}

- (MBCSide) humanSide;
{
if (!match)
return gHumanSide[players];
else
return localWhite ? kWhiteSide : kBlackSide;
}

- (BOOL) nontrivialHumanTurn
{
//
// Dock badge is rather distracting, save this for important situations.
//
if ([self gameDone])
return NO;
if ([self engineSide] != kNeitherSide && [[properties valueForKey:kMBCSearchTime] intValue] < 5)
return NO; // Only report for engine search > 30s
switch ([self humanSide]) {
case kBothSides:
return NO; // Duh! We know it's a human's turn
case kWhiteSide:
return [board numMoves] && !([board numMoves] & 1);
case kBlackSide:
return ([board numMoves] & 1);
case kNeitherSide:
return NO;
}
}

- (BOOL) gameDone
{
if (NSString * result = [properties objectForKey:@"Result"])
return ![result isEqual:@"*"];
else
return NO;
}

- (MBCSide) engineSide;
{
return gEngineSide[players];
}

- (MBCSide) remoteSide
{
if (!match)
return kNeitherSide;
else
return localWhite ? kBlackSide : kWhiteSide;
}

- (NSString *)nonLocalPlayerID
{
if (!match)
return nil;
NSString * localPlayerID = [(MBCController *)[NSApp delegate] localPlayer].playerID;
for (GKTurnBasedParticipant * p in match.participants)
if (![localPlayerID isEqual:p.player.playerID])
return p.player.playerID;
return nil;
}

- (BOOL) loadGame:(NSDictionary *)dict
{
[self setEphemeral:NO];

NSString * v = [dict objectForKey:@"Variant"];
NSString * moves = [dict objectForKey:@"Moves"];
if (!moves) {
[dict setValue:@"" forKey:@"Moves"];
}

for (variant = kVarNormal; gVariantName[variant] && ![v isEqual:gVariantName[variant]]; )
variant = static_cast<MBCVariant>(variant+1);
if (!gVariantName[variant])
variant = kVarNormal;

NSString * whiteType = [dict objectForKey:@"WhiteType"];
NSString * blackType = [dict objectForKey:@"BlackType"];

if ([whiteType isEqual:kMBCHumanPlayer])
if ([blackType isEqual:kMBCHumanPlayer])
players = kHumanVsHuman;
else
players = kHumanVsComputer;
else if ([blackType isEqual:kMBCHumanPlayer])
players = kComputerVsHuman;
else
players = kComputerVsComputer;
properties = [dict mutableCopy];
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
for (int i = 0; sProperties[i]; ++i)
if (![properties valueForKey:sProperties[i]])
[properties setValue:[defaults objectForKey:sProperties[i]]
forKey:sProperties[i]];

[[NSNotificationQueue defaultQueue]
enqueueNotification:[NSNotification notificationWithName:MBCGameLoadNotification
object:self userInfo:dict]
postingStyle:NSPostWhenIdle];
[[NSNotificationQueue defaultQueue]
enqueueNotification:[NSNotification notificationWithName:MBCGameStartNotification
object:self]
postingStyle:NSPostWhenIdle];

if (match)
dispatch_async(dispatch_get_main_queue(), ^{
[self updateMatchForRemoteMove];
});

return YES;
}

- (BOOL)readFromData:(NSData *)docData ofType:(NSString *)docType error:(NSError **)outError
{
NSPropertyListFormat format;
NSDictionary * gameData =
[NSPropertyListSerialization propertyListWithData:docData options:0 format:&format error:outError];
if (gameData == nil) {
if (outError) {
*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:nil];
}
return NO;
}
if (NSString * matchID = [gameData objectForKey:@"MatchID"]) {

MBCController *appDelegate = (MBCController *)[[NSApplication sharedApplication] delegate];
[appDelegate loadMatch:matchID];
if (outError)
*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:nil];

return NO;
}

[self loadGame:gameData];
[self updateChangeCount:NSChangeCleared];

return YES;
}

- (NSArray *)writableTypesForSaveOperation:(NSSaveOperationType)saveOperation
{
//
// Don't filter out PGN, even though we're not an editor for that type
//
return [[self class] writableTypes];
}

- (void) parseName:(NSString *)fullName intoFirst:(NSString **)firstName
last:(NSString **)lastName
{
//
// Get name as UTF8. If the name is longer than 99 bytes, the last
// character might be bad if it's non-ASCII. What sane person would
// have such a long name anyway?
//
char n[100];
if (fullName == nil) {
fullName = @"";
}

strlcpy(n, [fullName UTF8String], 100);

char * first = n+strspn(n, " \t"); // Beginning of first name
char * last;
char * nb1 = NULL; // Beginning of last word
char * ne1 = NULL; // End of last word
char * nb2 = NULL; // Beginning of last but one word
char * ne2 = NULL; // End of last but two word
char * ne3 = NULL; // End of last but three word

nb1 = first;
ne1 = nb1+strcspn(nb1, " \t");

for (char * n; (n = ne1+strspn(ne1, " \t")) && *n; ) {
ne3 = ne2;
nb2 = nb1;
ne2 = ne1;
nb1 = n;
ne1 = nb1+strcspn(nb1, " \t");
}

if (ne3 && *nb2 >= 'a' && *nb2 <= 'z') {
//
// Name has at least 3 words and last but one is
// lowercase, as in Ludwig van Beethoven
//
last = nb2;
ne3 = 0;
} else if (ne2) {
//
// Name has at least two words. If 3 or more, last but one is
// uppercase as in John Wayne Miller
//
last = nb1;
ne2 = 0;
} else { // Name is single word
last = ne1;
ne1 = 0;
}
*firstName = [NSString stringWithUTF8String:first];
*lastName = [NSString stringWithUTF8String:last];
}

- (NSString *)pgnHeader
{
NSString * wf;
NSString * wl;
NSString * bf;
NSString * bl;
[self parseName:[properties objectForKey:@"White"] intoFirst:&wf last:&wl];
[self parseName:[properties objectForKey:@"Black"] intoFirst:&bf last:&bl];
NSString * humanw =
[NSString stringWithFormat:@"%@, %@", wl, wf];
NSString * humanb =
[NSString stringWithFormat:@"%@, %@", bl, bf];
NSString * engine =
[NSString stringWithFormat:@"Apple Chess %@",
[[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleVersion"]];
NSString * white;
NSString * black;
switch ([self engineSide]) {
case kBlackSide:
white = humanw;
black = engine;
break;
case kWhiteSide:
white = engine;
black = humanb;
break;
case kNeitherSide:
white = humanw;
black = humanb;

break;
case kBothSides:
white = engine;
black = engine;
break;
}

//
// PGN uses a standard format that is NOT localized
//
NSString * format =
[NSString stringWithUTF8String:
"[Event \"%@\"]\n"
"[Site \"%@, %@\"]\n"
"[Date \"%@\"]\n"
"[Round \"-\"]\n"
"[White \"%@\"]\n"
"[Black \"%@\"]\n"
"[Result \"%@\"]\n"
"[Time \"%@\"]\n"];
return [NSString stringWithFormat:format,
[properties objectForKey:@"Event"],
[properties objectForKey:@"City"],
[properties objectForKey:@"Country"],
[properties objectForKey:@"StartDate"],
white, black,
[properties objectForKey:@"Result"],
[properties objectForKey:@"StartTime"]];
}

- (NSString *)pgnResult
{
return [properties objectForKey:@"Result"];
}

- (void) saveMovesToFile:(FILE *)f
{
NSString * header = [self pgnHeader];
NSData * encoded = [header dataUsingEncoding:NSISOLatin1StringEncoding
allowLossyConversion:YES];
fwrite([encoded bytes], 1, [encoded length], f);

//
// Add variant tag (nonstandard, but used by xboard & Co.)
//
if (variant != kVarNormal)
fprintf(f, "[Variant \"%s\"]\n", [gVariantName[variant] UTF8String]);
//
// Mark nonhuman players
//
MBCSide engineSide = gEngineSide[players];

if (SideIncludesWhite(engineSide))
fprintf(f, "[WhiteType: \"program\"]\n");
if (SideIncludesBlack(engineSide))
fprintf(f, "[BlackType: \"program\"]\n");

[board saveMovesTo:f];

fputc('\n', f);
fputs([[self pgnResult] UTF8String], f);
fputc('\n', f);
}

- (BOOL) saveMovesTo:(NSString *)fileName
{
FILE * f = fopen([fileName fileSystemRepresentation], "w");

[self saveMovesToFile:f];

fclose(f);

return YES;
}

- (BOOL)writeToURL:(NSURL *)fileURL ofType:(NSString *)docType error:(NSError **)outError
{
BOOL res;

if ([docType isEqualToString:@"com.apple.chess.pgn"])
res = [self saveMovesTo:[fileURL path]];
else
res = [super writeToURL:fileURL ofType:docType error:outError];

return res;
}

- (void)setFileURL:(NSURL *)url
{
//
// Never pick PGN representation as autosave
//
if ([[url pathExtension] isEqualToString:@"game"])
[super setFileURL:url];
}

- (void)performActivityWithSynchronousWaiting:(BOOL)waitSynchronously usingBlock:(void (^)(void (^)()))block
{
// If achievement controller is showing and we have to put up e.g. a save dialog,
// dismiss it.
//
NSArray * windowControllers = [self windowControllers];
if ([windowControllers count])
[[windowControllers objectAtIndex:0] gameCenterViewControllerDidFinish:[windowControllers objectAtIndex:0]];
[super performActivityWithSynchronousWaiting:waitSynchronously usingBlock:block];
}

- (NSDocument *)duplicateAndReturnError:(NSError **)outError
{
if (match) {
if (outError)
*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnsupportedSchemeError userInfo:
[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"cant_duplicate_gc", @"Cannot duplicate GameCenter games"), NSLocalizedDescriptionKey, nil]];
return nil;
}
return [super duplicateAndReturnError:outError];
}

- (NSMutableDictionary *) saveGameToDict
{
MBCSide humanSide = gHumanSide[players];
NSString * whiteType = SideIncludesWhite(humanSide) ? kMBCHumanPlayer : kMBCEnginePlayer;
NSString * blackType = SideIncludesBlack(humanSide) ? kMBCHumanPlayer : kMBCEnginePlayer;
NSMutableDictionary * dict = [[properties mutableCopy] autorelease];
[dict addEntriesFromDictionary:
[NSDictionary dictionaryWithObjectsAndKeys:
gVariantName[variant], @"Variant",
whiteType, @"WhiteType",
blackType, @"BlackType",
[board fen], @"Position",
[board holding], @"Holding",
[board moves], @"Moves",
nil]];

return dict;
}

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
NSMutableDictionary * game = [self saveGameToDict];
if (match)
[game setObject:match.matchID forKey:@"MatchID"];
return [NSPropertyListSerialization
dataFromPropertyList:game
format: NSPropertyListXMLFormat_v1_0
errorDescription:nil];
}

- (NSData *)matchData
{
NSMutableDictionary * game = [self saveGameToDict];
NSArray * keys = [game allKeys];
NSMutableDictionary * local= [NSMutableDictionary dictionary];
for (NSString * key in keys)
if ([[key substringToIndex:3] isEqual:@"MBC"]) {
[local setObject:[game objectForKey:key] forKey:key];
[game removeObjectForKey:key];
}
[game setObject:local forKey:(localWhite ? @"WhiteProperties" : @"BlackProperties")];

return [NSPropertyListSerialization dataWithPropertyList:game
format:NSPropertyListXMLFormat_v1_0
options:0
error:nil];
}

+ (NSURL *)casualGameSaveLocation
{
NSString * savePath = [[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:@"Casual.game"];

return [NSURL fileURLWithPath:savePath];
}

- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
BOOL loaded = [super readFromURL:absoluteURL ofType:typeName error:outError];
if (loaded && [absoluteURL isEqual:[MBCDocument casualGameSaveLocation]]) // Upgrade legacy autosave
[self setFileURL:nil];

return loaded;
}

- (BOOL) canTakeback
{
return [board canUndo] && players != kComputerVsComputer
&& (!match || [match.currentParticipant.player.playerID isEqual:[(MBCController *)[NSApp delegate] localPlayer].playerID]);
}

- (void) setBoard:(MBCBoard *)b
{
board = b;
[board setDocument:self];
}

- (void) updateMatchForLocalMove
{
if (offerDraw) {
if (![properties objectForKey:@"Request"])
[properties setObject:@"Draw" forKey:@"Request"];
[self setOfferDraw:NO];
}
MBCEndTurn(match, [self matchData]);
}

- (void) updateMatchForRemoteMove
{
if ([self checkForEndOfGame]) {
[self postMatchOutcomeNotification];
} else {
NSPropertyListFormat format;
NSDictionary * gameData =
[NSPropertyListSerialization propertyListWithData:match.matchData options:0 format:&format error:nil];
if (![properties objectForKey:@"BlackPlayerID"] && [gameData objectForKey:@"BlackPlayerID"])
[properties setObject:[gameData objectForKey:@"BlackPlayerID"] forKey:@"BlackPlayerID"];
if (![properties objectForKey:@"WhitePlayerID"] && [gameData objectForKey:@"WhitePlayerID"])
[properties setObject:[gameData objectForKey:@"WhitePlayerID"] forKey:@"WhitePlayerID"];
if ([[gameData objectForKey:@"Request"] isEqual:@"Takeback"]) {
[[[self windowControllers] objectAtIndex:0] requestTakeback];
} else if (NSString * response = [gameData objectForKey:@"Response"]) {
[properties removeObjectForKey:@"Request"];
[[[self windowControllers] objectAtIndex:0] handleRemoteResponse:response];
} else if ([gameData objectForKey:@"Moves"]) {
NSArray * moves = [[gameData objectForKey:@"Moves"] componentsSeparatedByString:@"\n"];
if ([moves count]-1 > [board numMoves]) {
MBCMove * lastMove = [MBCMove moveFromEngineMove:[moves objectAtIndex:[moves count]-2]];
[[NSNotificationCenter defaultCenter]
postNotificationName:(localWhite ? MBCUncheckedBlackMoveNotification : MBCUncheckedWhiteMoveNotification)
object:self userInfo:(id)lastMove];
}
if ([[gameData objectForKey:@"Request"] isEqual:@"Draw"]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200*NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
[[[self windowControllers] objectAtIndex:0] requestDraw];
});
}
}
}
[properties removeObjectForKey:@"Request"];
[properties removeObjectForKey:@"Response"];
}

- (void) offerTakeback
{
[properties setObject:@"Takeback" forKey:@"Request"];
[self updateMatchForLocalMove];
}

- (void) allowTakeback:(BOOL)allow
{
[properties setObject:(allow ? @"Takeback" : @"NoTakeback") forKey:@"Response"];
[self updateMatchForLocalMove];
}

- (void) resign
{
NSString * localPlayerID = [(MBCController *)[NSApp delegate] localPlayer].playerID;
BOOL wasOurTurn = NO;
for (GKTurnBasedParticipant * p in match.participants)
if ([p.player.playerID isEqual:localPlayerID]) {
[p setMatchOutcome:GKTurnBasedMatchOutcomeLost];
wasOurTurn = match.currentParticipant == p;
break;
}

[self updateMatchForRemoteMove];

if (!wasOurTurn)
[match participantQuitOutOfTurnWithOutcome:GKTurnBasedMatchOutcomeLost
withCompletionHandler:^(NSError *error){}];
}

@end

void
MBCAbort(NSString * message, MBCDocument * doc)
{
fprintf(stderr, "%s in game %p\n", [message UTF8String], doc);
abort();
}

// Local Variables:
// mode:ObjC
// End: