1/* 2 File: MBCGameInfo.mm 3 Contains: Managing information about the current game 4 Copyright: © 2003-2011 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 "MBCGameInfo.h" 47#import "MBCController.h" 48#import "MBCPlayer.h" 49#import "MBCDocument.h" 50#import "MBCUserDefaults.h" 51 52#import <GameKit/GameKit.h> 53 54 55#include <sys/types.h> 56#include <regex.h> 57#include <algorithm> 58 59NSArray * sVoices; 60 61@implementation MBCGameInfo 62 63@synthesize document = fDocument; 64@synthesize moveList = fMoveList; 65@synthesize whiteEditable, blackEditable; 66 67NSString * kMBCShowMoveInTitle = @"MBCShowMoveInTitle"; 68// 69// Obsolete: Parsing the human name serves no purpose and carries some risk 70// 71NSString * kMBCHumanFirst = @"MBCHumanFirst"; 72NSString * kMBCHumanLast = @"MBCHumanLast"; 73 74+ (void)initialize 75{ 76 NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults]; 77 NSString * humanName; 78 79 // 80 // Deal with legacy user name default representation 81 // 82 if ([userDefaults stringForKey:kMBCHumanFirst]) { 83 humanName = [NSString stringWithFormat:@"%@ %@", 84 [userDefaults stringForKey:kMBCHumanFirst], 85 [userDefaults stringForKey:kMBCHumanLast]]; 86 [userDefaults removeObjectForKey:kMBCHumanFirst]; 87 [userDefaults removeObjectForKey:kMBCHumanLast]; 88 } else 89 humanName = NSFullUserName(); 90 91 // 92 // Get the city we might be in. 93 // 94 NSString * city = @"?"; 95 NSString * country = @"?"; 96 97 NSString * event = 98 [NSLocalizedString(@"casual_game", @"Casual Game") retain]; 99 100 NSDictionary * defaults = 101 [NSDictionary 102 dictionaryWithObjectsAndKeys: 103 humanName, kMBCHumanName, 104 city, kMBCGameCity, 105 country, kMBCGameCountry, 106 event, kMBCGameEvent, 107 nil]; 108 [userDefaults registerDefaults: defaults]; 109 sVoices = [[NSSpeechSynthesizer availableVoices] retain]; 110} 111 112const int kNumFixedMenuItems = 2; 113 114- (void)loadVoiceMenu:(id)menu 115{ 116 for (NSString * voiceIdentifier in sVoices) 117 [menu addItemWithTitle:[[NSSpeechSynthesizer attributesForVoice:voiceIdentifier] objectForKey:NSVoiceName]]; 118} 119 120- (NSString *)voiceAtIndex:(NSUInteger)menuIndex 121{ 122 if (menuIndex < kNumFixedMenuItems) 123 return nil; 124 else 125 return [sVoices objectAtIndex:menuIndex-kNumFixedMenuItems]; 126} 127 128- (NSUInteger)indexForVoice:(NSString *)voiceId 129{ 130 return [voiceId length] ? [sVoices indexOfObject:voiceId]+kNumFixedMenuItems : 0; 131} 132 133- (NSString *) localizedStyleName:(NSString *)name 134{ 135 NSString * loc = NSLocalizedString(name, @""); 136 137 return loc; 138} 139 140- (NSString *) unlocalizedStyleName:(NSString *)name 141{ 142 NSString * unloc = [fStyleLocMap objectForKey:name]; 143 144 return unloc ? unloc : name; 145} 146 147- (void)awakeFromNib 148{ 149 [self loadVoiceMenu:fPrimaryVoiceMenu]; 150 [self loadVoiceMenu:fAlternateVoiceMenu]; 151 152 fStyleLocMap = [[NSMutableDictionary alloc] init]; 153 [fBoardStyle removeAllItems]; 154 [fPieceStyle removeAllItems]; 155 156 NSFileManager * fileManager = [NSFileManager defaultManager]; 157 NSString * stylePath = 158 [[[NSBundle mainBundle] resourcePath] 159 stringByAppendingPathComponent:@"Styles"]; 160 NSEnumerator * styles = 161 [[fileManager contentsOfDirectoryAtPath:stylePath error:nil] objectEnumerator]; 162 while (NSString * style = [styles nextObject]) { 163 NSString * locStyle = [self localizedStyleName:style]; 164 [fStyleLocMap setObject:style forKey:locStyle]; 165 NSString * s = [stylePath stringByAppendingPathComponent:style]; 166 if ([fileManager fileExistsAtPath: 167 [s stringByAppendingPathComponent:@"Board.plist"]] 168 ) 169 [fBoardStyle addItemWithTitle:locStyle]; 170 if ([fileManager fileExistsAtPath: 171 [s stringByAppendingPathComponent:@"Piece.plist"]] 172 ) 173 [fPieceStyle addItemWithTitle:locStyle]; 174 } 175} 176 177- (void) setPlayerAlias:(NSString *)playerID forKey:(NSString *)key 178{ 179 if (!playerID) 180 playerID = [fDocument nonLocalPlayerID]; 181 if (playerID) 182 [GKPlayer loadPlayersForIdentifiers:[NSArray arrayWithObject:playerID] 183 withCompletionHandler:^(NSArray *players, NSError *error) { 184 if (!error) { 185 [fDocument setObject:[[players objectAtIndex:0] alias] forKey:key]; 186 [self updateMoves:nil]; 187 } 188 }]; 189} 190 191- (void) removeChessObservers 192{ 193 if (!fHasObservers) 194 return; 195 196 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 197 [notificationCenter removeObserver:self name:MBCEndMoveNotification object:nil]; 198 [notificationCenter removeObserver:self name:MBCTakebackNotification object:nil]; 199 [notificationCenter removeObserver:self name:MBCIllegalMoveNotification object:nil]; 200 [notificationCenter removeObserver:self name:MBCGameEndNotification object:nil]; 201 202 fHasObservers = NO; 203} 204 205- (void)dealloc 206{ 207 [self removeChessObservers]; 208 [super dealloc]; 209} 210 211- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay 212{ 213 [self removeChessObservers]; 214 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 215 [notificationCenter 216 addObserver:self 217 selector:@selector(updateMoves:) 218 name:MBCEndMoveNotification 219 object:fDocument]; 220 [notificationCenter 221 addObserver:self 222 selector:@selector(takeback:) 223 name:MBCTakebackNotification 224 object:fDocument]; 225 [notificationCenter 226 addObserver:self 227 selector:@selector(updateMoves:) 228 name:MBCIllegalMoveNotification 229 object:fDocument]; 230 [notificationCenter 231 addObserver:self 232 selector:@selector(gameEnd:) 233 name:MBCGameEndNotification 234 object:fDocument]; 235 fHasObservers = YES; 236 237 // 238 // Fill in missing properties 239 // 240 NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; 241 NSString * human = [defaults stringForKey:kMBCHumanName]; 242 NSString * engine = NSLocalizedString(@"engine_player", @"Computer"); 243 NSMutableDictionary * props = fDocument.properties; 244 BOOL gameCenter = [fDocument remoteSide] != kNeitherSide; 245 246 [self setWhiteEditable: !gameCenter && SideIncludesWhite(sideToPlay)]; 247 [self setBlackEditable: !gameCenter && SideIncludesBlack(sideToPlay)]; 248 249 if (![props objectForKey:@"White"]) 250 if (gameCenter) 251 [self setPlayerAlias:[props objectForKey:@"WhitePlayerID"] forKey:@"White"]; 252 else if (SideIncludesWhite(sideToPlay)) 253 [fDocument setObject:human forKey:@"White"]; 254 else 255 [fDocument setObject:engine forKey:@"White"]; 256 if (![props objectForKey:@"Black"]) 257 if (gameCenter) 258 [self setPlayerAlias:[props objectForKey:@"BlackPlayerID"] forKey:@"Black"]; 259 else if (SideIncludesBlack(sideToPlay)) 260 [fDocument setObject:human forKey:@"Black"]; 261 else 262 [fDocument setObject:engine forKey:@"Black"]; 263 264 NSDate * now = [NSDate date]; 265 if (![props objectForKey:@"StartDate"]) 266 [fDocument setObject:[now descriptionWithCalendarFormat:@"%Y.%m.%d" timeZone:nil locale:nil] 267 forKey:@"StartDate"]; 268 if (![props objectForKey:@"StartTime"]) 269 [fDocument setObject:[now descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil] 270 forKey:@"StartTime"]; 271 if (![props objectForKey:@"Result"]) 272 [fDocument setObject:@"*" forKey:@"Result"]; 273 if (![props objectForKey:@"City"]) 274 if (gameCenter) 275 [fDocument setObject:NSLocalizedString(@"cloud_city", @"Game Center") forKey:@"City"]; 276 else 277 [fDocument setObject:[defaults stringForKey:kMBCGameCity] forKey:@"City"]; 278 if (![props objectForKey:@"Country"]) 279 if (gameCenter) 280 [fDocument setObject:NSLocalizedString(@"cloud_country", @"The Cloud") forKey:@"Country"]; 281 else 282 [fDocument setObject:[defaults stringForKey:kMBCGameCountry] forKey:@"Country"]; 283 if (![props objectForKey:@"Event"]) 284 [fDocument setObject:[defaults stringForKey:kMBCGameEvent] forKey:@"Event"]; 285 286 fRows = 0; 287 288 [self updateMoves:nil]; 289} 290 291- (void) updateMoves:(NSNotification *)notification 292{ 293 [self willChangeValueForKey:@"gameTitle"]; 294 NSDictionary * props = fDocument.properties; 295 if (![props objectForKey:@"White"]) 296 [self setPlayerAlias:[props objectForKey:@"WhitePlayerID"] forKey:@"White"]; 297 if (![props objectForKey:@"Black"]) 298 [self setPlayerAlias:[props objectForKey:@"BlackPlayerID"] forKey:@"Black"]; 299 [fMoveList reloadData]; 300 [fMoveList setNeedsDisplay:YES]; 301 302 int rows = [self numberOfRowsInTableView:fMoveList]; 303 if (rows != fRows) { 304 fRows = rows; 305 [fMoveList scrollRowToVisible:rows-1]; 306 } 307 [self didChangeValueForKey:@"gameTitle"]; 308} 309 310- (void) takeback:(NSNotification *)notification 311{ 312 [fDocument setObject:@"*" forKey:@"Result"]; 313 314 [self updateMoves:notification]; 315} 316 317- (void) gameEnd:(NSNotification *)notification 318{ 319 dispatch_async(dispatch_get_main_queue(), ^{ 320 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 321 322 switch (move->fCommand) { 323 case kCmdWhiteWins: 324 [fDocument setObject:@"1-0" forKey:@"Result"]; 325 break; 326 case kCmdBlackWins: 327 [fDocument setObject:@"0-1" forKey:@"Result"]; 328 break; 329 case kCmdDraw: 330 [fDocument setObject:@"1/2-1/2" forKey:@"Result"]; 331 break; 332 default: 333 return; 334 } 335 [self updateMoves:notification]; 336 }); 337} 338 339- (NSString *)outcome 340{ 341 NSString * result = [fDocument objectForKey:@"Result"]; 342 if ([result isEqual:@"1-0"]) 343 return NSLocalizedString(@"white_win_msg", @"White wins"); 344 if ([result isEqual:@"0-1"]) 345 return NSLocalizedString(@"black_win_msg", @"Black wins"); 346 else if ([result isEqual:@"1/2-1/2"]) 347 return NSLocalizedString(@"draw_msg", @"Draw"); 348 349 return nil; 350} 351 352- (void)editProperties:(NSWindow *)sheet modalForWindow:(NSWindow *)window 353{ 354 fEditedProperties = [[NSMutableDictionary alloc] init]; 355 356 [NSApp beginSheet:sheet 357 modalForWindow:window 358 modalDelegate:self 359 didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) 360 contextInfo:nil]; 361} 362 363- (IBAction) editInfo:(id)sender 364{ 365 [self editProperties:fEditSheet modalForWindow:[sender window]]; 366} 367 368- (void)editPreferencesForWindow:(NSWindow *)window 369{ 370 [fPrimaryVoiceMenu selectItemAtIndex:[self indexForVoice:[fDocument objectForKey:kMBCDefaultVoice]]]; 371 [fAlternateVoiceMenu selectItemAtIndex:[self indexForVoice:[fDocument objectForKey:kMBCAlternateVoice]]]; 372 [fBoardStyle selectItemWithTitle:[self localizedStyleName:[fDocument objectForKey:kMBCBoardStyle]]]; 373 [fPieceStyle selectItemWithTitle:[self localizedStyleName:[fDocument objectForKey:kMBCPieceStyle]]]; 374 375 [self editProperties:fPrefsSheet modalForWindow:window]; 376} 377 378- (id)valueForUndefinedKey:(NSString *)key 379{ 380 return [fDocument objectForKey:key]; 381} 382 383- (void)setValue:(id)value forUndefinedKey:(NSString *)key 384{ 385 if (![fEditedProperties objectForKey:key]) { 386 id oldValue = [fDocument objectForKey:key]; 387 if (!oldValue) 388 oldValue = [NSNull null]; 389 [fEditedProperties setObject:oldValue forKey:key]; 390 } 391 392 [fDocument setValue:value forKey:key]; 393} 394 395+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key 396{ 397 if (isupper([key characterAtIndex:0])) 398 return [NSSet setWithObject:[@"document." stringByAppendingString:key]]; 399 else 400 return [NSSet set]; 401} 402 403- (void) didEndSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)ctx 404{ 405 [sheet orderOut:self]; 406} 407 408- (IBAction) cancelProperties:(id)sender 409{ 410 // 411 // Restore all the values that were changed 412 // 413 [NSApp endSheet:[sender window]]; 414 for (NSString * prop in fEditedProperties) 415 [fDocument setObject:[fEditedProperties objectForKey:prop] forKey:prop]; 416 [fEditedProperties release]; 417} 418 419- (IBAction) updateProperties:(id)sender 420{ 421 [self willChangeValueForKey:@"gameTitle"]; 422 NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; 423 424 // 425 // Update defaults 426 // 427 for (NSString * edited in fEditedProperties) { 428 id val = [fDocument objectForKey:edited]; 429 if ([edited isEqual:@"White"] 430 || ([edited isEqual:@"Black"] && ![fEditedProperties objectForKey:@"White"]) 431 ) 432 [defaults setObject:val forKey:kMBCHumanName]; 433 else if ([edited isEqual:@"City"] ||[edited isEqual:@"Country"] || [edited isEqual:@"Event"]) 434 [defaults setObject:val forKey:[@"MBCGame" stringByAppendingString:edited]]; 435 else if ([[edited substringToIndex:3] isEqual:@"MBC"]) 436 [defaults setObject:val forKey:edited]; 437 } 438 if ([fEditedProperties objectForKey:kMBCSearchTime]) 439 [fDocument updateSearchTime]; 440 441 [NSApp endSheet:[sender window]]; 442 [fEditedProperties release]; 443 [self didChangeValueForKey:@"gameTitle"]; 444} 445 446- (IBAction) updateVoices:(id)sender; 447{ 448 NSString * voice = [self voiceAtIndex:[sender indexOfSelectedItem]]; 449 NSString * pvoice = voice ? voice : @""; 450 if ([sender tag]) 451 [self setValue:pvoice forKey:kMBCAlternateVoice]; 452 else 453 [self setValue:pvoice forKey:kMBCDefaultVoice]; 454 455 NSSpeechSynthesizer * selectedSynth = [[NSSpeechSynthesizer alloc] initWithVoice:voice]; 456 NSString * demoText = 457 [[NSSpeechSynthesizer attributesForVoice:[selectedSynth voice]] 458 objectForKey:NSVoiceDemoText]; 459 if (demoText) 460 [selectedSynth startSpeakingString:demoText]; 461} 462 463- (IBAction) updateStyles:(id)sender; 464{ 465 NSString * boardStyle = 466 [self unlocalizedStyleName:[fBoardStyle titleOfSelectedItem]]; 467 NSString * pieceStyle = 468 [self unlocalizedStyleName:[fPieceStyle titleOfSelectedItem]]; 469 470 [self setValue:boardStyle forKey:kMBCBoardStyle]; 471 [self setValue:pieceStyle forKey:kMBCPieceStyle]; 472} 473 474// 475// If there is no document anymore, fBoard can't be trusted either 476// 477- (void)setDocument:(MBCDocument *)document 478{ 479 fDocument = document; 480 if (!fDocument) 481 fBoard = nil; 482} 483 484- (int)numberOfRowsInTableView:(NSTableView *)aTableView 485{ 486 return ([fBoard numMoves]+1) / 2; 487} 488 489- (id)tableView:(NSTableView *)v objectValueForTableColumn:(NSTableColumn *)col row:(int)row 490{ 491 NSString * ident = [col identifier]; 492 if ([ident isEqual:@"Move"]) { 493 return [NSNumber numberWithInt:row+1]; 494 } else { 495 NSArray * identComp = [ident componentsSeparatedByString:@"."]; 496 return [[fBoard move: row*2+[[identComp objectAtIndex:0] isEqual:@"Black"]] 497 valueForKey:[identComp objectAtIndex:1]]; 498 } 499 return nil; 500} 501 502- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex 503{ 504 return NO; /* Disallow all selections */ 505} 506 507- (NSString *)describeMove:(int)move 508{ 509 NSDictionary * localization = nil; 510 511 if (NSURL * url = [[NSBundle mainBundle] URLForResource:@"Spoken" withExtension:@"strings" 512 subdirectory:nil] 513 ) 514 localization = [NSDictionary dictionaryWithContentsOfURL:url]; 515 NSString * nr_fmt = 516 NSLocalizedStringFromTable(@"move_table_nr", @"Spoken", @"Move %d"); 517 NSString * text = [NSString localizedStringWithFormat:nr_fmt, move]; 518 int white = (move-1)*2; 519 if (white < [fBoard numMoves]) 520 text = [text stringByAppendingFormat:@"\n\n%@", 521 [fBoard extStringFromMove:[fBoard move:white] 522 withLocalization:localization]]; 523 int black = move*2 - 1; 524 if (black < [fBoard numMoves]) 525 text = [text stringByAppendingFormat:@"\n\n%@", 526 [fBoard extStringFromMove:[fBoard move:black] 527 withLocalization:localization]]; 528 529 return text; 530} 531 532- (NSString *)gameTitle 533{ 534 if (!fDocument || [fDocument brandNewGame]) 535 return @""; 536 537 NSString * move; 538 int numMoves = [fBoard numMoves]; 539 540 if (numMoves && [[NSUserDefaults standardUserDefaults] boolForKey:kMBCShowMoveInTitle]) { 541 NSNumber * moveNum = [NSNumber numberWithInt:(numMoves+1)/2]; 542 NSString * moveStr = [NSNumberFormatter localizedStringFromNumber:moveNum 543 numberStyle:NSNumberFormatterDecimalStyle]; 544 move = [NSString localizedStringWithFormat:NSLocalizedString(@"title_move_line_fmt", @"%@. %@%@"), 545 moveStr, numMoves&1 ? @"":@"\xE2\x80\xA6 ", 546 [[fBoard lastMove] localizedText]]; 547 } else if ([[fDocument objectForKey:@"Request"] isEqual:@"Takeback"]) { 548 move = NSLocalizedString(@"takeback_msg", @"Takeback requested"); 549 } else if (!numMoves) { 550 move = NSLocalizedString(@"new_msg", @"New Game"); 551 } else if (numMoves & 1) { 552 move = NSLocalizedString(@"black_move_msg", @"Black to move"); 553 } else { 554 move = NSLocalizedString(@"white_move_msg", @"White to move"); 555 } 556 557 if (NSString * outcome = [self outcome]) 558 move = outcome; 559 NSString * title = 560 [NSString localizedStringWithFormat:NSLocalizedString(@"game_title_fmt", @"%@ - %@ (%@)"), 561 [fDocument objectForKey:@"White"], [fDocument objectForKey:@"Black"], move]; 562 563 return title; 564} 565 566@end 567 568// Local Variables: 569// mode:ObjC 570// End: 571