1/* 2 File: MBCInteractivePlayer.mm 3 Contains: An agent representing a local human player 4 Copyright: © 2002-2014 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 "MBCInteractivePlayer.h" 47#import "MBCEngine.h" 48#import "MBCBoardView.h" 49#import "MBCBoardWin.h" 50#import "MBCLanguageModel.h" 51#import "MBCController.h" 52#import "MBCDocument.h" 53 54#import <ApplicationServices/ApplicationServices.h> 55#include <dispatch/dispatch.h> 56// 57// Private selector to set the help text in the speech feedback window 58// 59#ifndef kSRCommandsDisplayCFPropListRef 60#define kSRCommandsDisplayCFPropListRef 'cdpl' 61#endif 62 63pascal OSErr HandleSpeechDoneAppleEvent (const AppleEvent *theAEevt, AppleEvent* reply, SRefCon refcon) 64{ 65 long actualSize; 66 DescType actualType; 67 OSErr status = 0; 68 OSErr recStatus = 0; 69 SRRecognitionResult recResult = 0; 70 SRRecognizer recognizer; 71 72 status = AEGetParamPtr(theAEevt,keySRSpeechStatus,typeSInt16, 73 &actualType, (Ptr)&recStatus, sizeof(status), &actualSize); 74 if (!status) 75 status = recStatus; 76 77 if (!status) 78 status = AEGetParamPtr(theAEevt,keySRRecognizer, 79 typeSRRecognizer, &actualType, 80 (Ptr)&recognizer, 81 sizeof(SRRecognizer), &actualSize); 82 if (!status) 83 status = AEGetParamPtr(theAEevt,keySRSpeechResult, 84 typeSRSpeechResult, &actualType, 85 (Ptr)&recResult, 86 sizeof(SRRecognitionResult), &actualSize); 87 if (!status) { 88 Size sz = sizeof(refcon); 89 status = SRGetProperty(recognizer, kSRRefCon, &refcon, &sz); 90 } 91 if (!status) { 92 [reinterpret_cast<MBCInteractivePlayer *>(refcon) 93 recognized:recResult]; 94 SRReleaseObject(recResult); 95 } 96 97 return status; 98} 99 100void SpeakStringWhenReady(NSSpeechSynthesizer * synth, NSString * text) 101{ 102 static NSSpeechSynthesizer * sLastSynth; 103 static NSMutableArray * sSynthQueue; 104 105 if (synth) { 106 if (!sSynthQueue) 107 sSynthQueue = [[NSMutableArray alloc] initWithCapacity:1]; 108 [sSynthQueue addObject:[NSArray arrayWithObjects:synth, text, nil]]; 109 } 110 if (sLastSynth) { 111 if ([sLastSynth isSpeaking]) { 112 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, [sSynthQueue count] ? 100*NSEC_PER_MSEC : NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 113 SpeakStringWhenReady(nil, nil); 114 }); 115 return; 116 } else { 117 [sLastSynth release]; 118 sLastSynth = nil; 119 } 120 } 121 if ([sSynthQueue count]) { 122 NSArray * job = [sSynthQueue objectAtIndex:0]; 123 124 sLastSynth = [[job objectAtIndex:0] retain]; 125 [sLastSynth startSpeakingString:[job objectAtIndex:1]]; 126 [sSynthQueue removeObjectAtIndex:0]; 127 } 128} 129 130@implementation MBCInteractivePlayer 131 132- (void) makeSpeechHelp 133{ 134 NSPropertyListFormat format; 135 136 NSString * path = 137 [[NSBundle mainBundle] pathForResource: @"SpeechHelp" ofType: @"plist"]; 138 NSData * help = 139 [NSData dataWithContentsOfFile:path]; 140 NSMutableDictionary * prop = 141 [NSPropertyListSerialization 142 propertyListFromData: help 143 mutabilityOption: NSPropertyListMutableContainers 144 format: &format 145 errorDescription:nil]; 146 ProcessSerialNumber psn; 147 GetCurrentProcess(&psn); 148 [prop setObject:[NSNumber numberWithLong:psn.highLongOfPSN] 149 forKey:@"ProcessPSNHigh"]; 150 [prop setObject:[NSNumber numberWithLong:psn.lowLongOfPSN] 151 forKey:@"ProcessPSNLow"]; 152 fSpeechHelp = 153 [[NSPropertyListSerialization 154 dataFromPropertyList:prop 155 format: NSPropertyListXMLFormat_v1_0 156 errorDescription:nil] 157 retain]; 158} 159 160- (void) initSR 161{ 162 if (SROpenRecognitionSystem(&fRecSystem, kSRDefaultRecognitionSystemID)) 163 return; 164 SRNewRecognizer(fRecSystem, &fRecognizer, kSRDefaultSpeechSource); 165 SRSetProperty(fRecognizer, kSRRefCon, &self, sizeof(self)); 166 short modes = kSRHasFeedbackHasListenModes; 167 SRSetProperty(fRecognizer, kSRFeedbackAndListeningModes, &modes, sizeof(short)); 168 SRNewLanguageModel(fRecSystem, &fModel, "<moves>", 7); 169 fLanguageModel = 170 [[MBCLanguageModel alloc] initWithRecognitionSystem:fRecSystem]; 171 if (fSpeechHelp) 172 SRSetProperty(fRecognizer, kSRCommandsDisplayCFPropListRef, 173 [fSpeechHelp bytes], [fSpeechHelp length]); 174 fStartingSR = false; 175 [self updateNeedMouse:self]; 176} 177 178- (void) updateNeedMouse:(id)arg 179{ 180 // 181 // Avoid multiple updates for same board position 182 // 183 fPendingMouseUpdate = YES; 184 dispatch_async(dispatch_get_main_queue(), ^{ 185 if (fPendingMouseUpdate) { 186 fPendingMouseUpdate = NO; 187 [self doUpdateNeedMouse]; 188 } 189 }); 190} 191 192- (void) doUpdateNeedMouse 193{ 194 BOOL wantMouse; 195 196 if (fLastSide == kBlackSide) 197 wantMouse = fSide == kWhiteSide || fSide == kBothSides; 198 else 199 wantMouse = fSide == kBlackSide || fSide == kBothSides; 200 201 if (wantMouse && [fDocument gameDone]) 202 wantMouse = NO; 203 204 [[fController gameView] wantMouse:wantMouse]; 205 [[NSApp delegate] updateApplicationBadge]; 206 207 if ([fController listenForMoves]) { 208 // 209 // Work with speech recognition 210 // 211 if (wantMouse) { 212 if (fStartingSR) { 213 ; // Current starting, will update later 214 } else if (!fRecSystem) { 215 static dispatch_once_t sInitOnce; 216 static dispatch_queue_t sInitQueue; 217 dispatch_once(&sInitOnce, ^{ 218 sInitQueue = dispatch_queue_create("InitSR", DISPATCH_QUEUE_SERIAL); 219 AEInstallEventHandler(kAESpeechSuite, kAESpeechDone, 220 NewAEEventHandlerUPP(HandleSpeechDoneAppleEvent), 221 NULL, false); 222 }); 223 fStartingSR = true; 224 dispatch_async(sInitQueue, ^{ 225 [self initSR]; 226 }); 227 } else { 228 if (!fSpeechHelp) { 229 [self makeSpeechHelp]; 230 SRSetProperty(fRecognizer, kSRCommandsDisplayCFPropListRef, 231 [fSpeechHelp bytes], [fSpeechHelp length]); 232 } 233 234 SRStopListening(fRecognizer); 235 MBCMoveCollector * moves = [MBCMoveCollector new]; 236 MBCMoveGenerator generateMoves(moves, fVariant, 0); 237 generateMoves.Generate(fLastSide==kBlackSide, 238 *[[fController board] curPos]); 239 [fLanguageModel buildLanguageModel:fModel 240 fromMoves:[moves collection] 241 takeback:[[fController board] canUndo]]; 242 SRSetLanguageModel(fRecognizer, fModel); 243 SRStartListening(fRecognizer); 244 [moves release]; 245 } 246 } else if (fRecSystem) 247 SRStopListening(fRecognizer); 248 } else if (fRecSystem && !fStartingSR) { 249 // 250 // Time to take the recognition system down 251 // 252 SRStopListening(fRecognizer); 253 [fLanguageModel release]; 254 fLanguageModel = nil; 255 SRReleaseObject(fRecognizer); 256 SRCloseRecognitionSystem(fRecSystem); 257 fRecSystem = 0; 258 } 259} 260 261- (void)allowedToListen:(BOOL)allowed 262{ 263 [self updateNeedMouse:self]; 264 if (fRecSystem && !allowed) 265 SRStopListening(fRecognizer); 266} 267 268- (void) removeChessObservers 269{ 270 if (!fHasObservers) 271 return; 272 273 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 274 [notificationCenter removeObserver:self name:MBCWhiteMoveNotification object:nil]; 275 [notificationCenter removeObserver:self name:MBCBlackMoveNotification object:nil]; 276 [notificationCenter removeObserver:self name:MBCIllegalMoveNotification object:nil]; 277 [notificationCenter removeObserver:self name:MBCTakebackNotification object:nil]; 278 [notificationCenter removeObserver:self name:MBCGameEndNotification object:nil]; 279 [fDocument removeObserver:self forKeyPath:@"Result"]; 280 281 fHasObservers = NO; 282} 283 284- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 285{ 286 [self updateNeedMouse:self]; 287} 288 289- (void)dealloc 290{ 291 [self removeChessObservers]; 292 [fSpeechHelp release]; 293 [fLanguageModel release]; 294 [super dealloc]; 295} 296 297- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay 298{ 299 fVariant = variant; 300 fLastSide = 301 ([[fController board] numMoves] & 1) 302 ? kWhiteSide : kBlackSide; 303 304 [self removeChessObservers]; 305 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 306 switch (fSide = sideToPlay) { 307 case kWhiteSide: 308 [notificationCenter 309 addObserver:self 310 selector:@selector(humanMoved:) 311 name:MBCWhiteMoveNotification 312 object:fDocument]; 313 [notificationCenter 314 addObserver:self 315 selector:@selector(opponentMoved:) 316 name:MBCBlackMoveNotification 317 object:fDocument]; 318 break; 319 case kBlackSide: 320 [notificationCenter 321 addObserver:self 322 selector:@selector(opponentMoved:) 323 name:MBCWhiteMoveNotification 324 object:fDocument]; 325 [notificationCenter 326 addObserver:self 327 selector:@selector(humanMoved:) 328 name:MBCBlackMoveNotification 329 object:fDocument]; 330 break; 331 case kBothSides: 332 [notificationCenter 333 addObserver:self 334 selector:@selector(humanMoved:) 335 name:MBCWhiteMoveNotification 336 object:fDocument]; 337 [notificationCenter 338 addObserver:self 339 selector:@selector(humanMoved:) 340 name:MBCBlackMoveNotification 341 object:fDocument]; 342 break; 343 case kNeitherSide: 344 [notificationCenter 345 addObserver:self 346 selector:@selector(opponentMoved:) 347 name:MBCWhiteMoveNotification 348 object:fDocument]; 349 [notificationCenter 350 addObserver:self 351 selector:@selector(opponentMoved:) 352 name:MBCBlackMoveNotification 353 object:fDocument]; 354 break; 355 } 356 [notificationCenter 357 addObserver:self 358 selector:@selector(reject:) 359 name:MBCIllegalMoveNotification 360 object:fDocument]; 361 [notificationCenter 362 addObserver:self 363 selector:@selector(takeback:) 364 name:MBCTakebackNotification 365 object:fDocument]; 366 [notificationCenter 367 addObserver:self 368 selector:@selector(gameEnded:) 369 name:MBCGameEndNotification 370 object:fDocument]; 371 [fDocument addObserver:self forKeyPath:@"Result" options:NSKeyValueObservingOptionNew context:nil]; 372 fHasObservers = YES; 373 374 [self updateNeedMouse:self]; 375} 376 377- (void) reject:(NSNotification *)n 378{ 379 NSBeep(); 380 [[fController gameView] unselectPiece]; 381} 382 383- (void) takeback:(NSNotification *)n 384{ 385 [self updateNeedMouse:self]; 386} 387 388- (void) switchSides:(NSNotification *)n 389{ 390 fLastSide = fLastSide==kBlackSide ? kWhiteSide : kBlackSide; 391 392 [self updateNeedMouse:self]; 393} 394 395- (BOOL)useAlternateSynthForMove:(MBCMove *)move 396{ 397 if (fSide == kBothSides || fSide == kNeitherSide) 398 return [[fController board] sideOfMove:move] == kBlackSide; 399 else 400 return fSide == [[fController board] sideOfMove:move]; 401} 402 403- (NSString *)stringFromMove:(MBCMove *)move 404{ 405 NSDictionary * localization = [self useAlternateSynthForMove:move] 406 ? [fController alternateLocalization] 407 : [fController primaryLocalization]; 408 409 return [[fController board] stringFromMove:move withLocalization:localization]; 410} 411 412- (NSString *)stringForCheck:(MBCMove *)move 413{ 414 NSDictionary * localization = [self useAlternateSynthForMove:move] 415 ? [fController alternateLocalization] 416 : [fController primaryLocalization]; 417 418 return LOC(@"check", @"Check!"); 419} 420 421- (void) speakMove:(MBCMove *)move text:(NSString *)text check:(BOOL)check 422{ 423 NSSpeechSynthesizer * synth = [self useAlternateSynthForMove:move] 424 ? [fController alternateSynth] 425 : [fController primarySynth]; 426 427 if (!check || (move->fCheck && !move->fCheckMate)) 428 SpeakStringWhenReady(synth, text); 429 if (!check && move->fCheck) 430 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200*NSEC_PER_MSEC), 431 dispatch_get_main_queue(), ^{ 432 [self speakMove:move text:[self stringForCheck:move] check:YES]; 433 }); 434 } 435 436- (void) speakMove:(NSNotification *)notification 437{ 438 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 439 440 NSString * text = [self stringFromMove:move]; 441 442 [self speakMove:move text:text check:NO]; 443} 444 445- (void) gameEnded:(NSNotification *)notification 446{ 447 MBCSide humanSide = [fDocument humanSide]; 448 BOOL wasHumanMove; 449 if (humanSide == kBothSides) { 450 wasHumanMove = YES; 451 } else if (humanSide == kNeitherSide) { 452 wasHumanMove = NO; 453 } else { 454 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 455 wasHumanMove = [[fController board] sideOfMove:move] == humanSide; 456 } 457 if (wasHumanMove ? [fController speakHumanMoves] : [fController speakMoves]) 458 if (![fDocument gameDone]) // Game was not previously finished 459 [self speakMove:notification]; 460} 461 462- (void) speakMove:(MBCMove *) move withWrapper:(NSString *)wrapper 463{ 464 if (move && ([fController speakHumanMoves] || [fController speakMoves])) { 465 NSString * text = [self stringFromMove:move]; 466 NSString * wrapped = 467 [NSString stringWithFormat:wrapper, text]; 468 469 [self speakMove:move text:wrapped check:NO]; 470 } 471} 472 473- (void) announceHint:(MBCMove *) move 474{ 475 if (!move) 476 return; 477 478 NSDictionary * localization = [self useAlternateSynthForMove:move] 479 ? [fController alternateLocalization] 480 : [fController primaryLocalization]; 481 482 [self speakMove:move withWrapper:LOC(@"suggest_fmt", @"I would suggest \"%@\"")]; 483} 484 485- (void) announceLastMove:(MBCMove *) move 486{ 487 if (!move) 488 return; 489 490 NSDictionary * localization = [self useAlternateSynthForMove:move] 491 ? [fController alternateLocalization] 492 : [fController primaryLocalization]; 493 494 [self speakMove:move withWrapper:LOC(@"last_move_fmt", @"The last move was \"%@\"")]; 495} 496 497- (void) opponentMoved:(NSNotification *)notification 498{ 499 dispatch_async(dispatch_get_main_queue(), ^{ 500 if ([fController speakMoves]) 501 [self speakMove:notification]; 502 [self switchSides:notification]; 503 }); 504} 505 506- (void) humanMoved:(NSNotification *)notification 507{ 508 dispatch_async(dispatch_get_main_queue(), ^{ 509 if ([fController speakHumanMoves]) 510 [self speakMove:notification]; 511 [self switchSides:notification]; 512 }); 513} 514 515- (void) startSelection:(MBCSquare)square 516{ 517 MBCPiece piece; 518 519 if (square > kInHandSquare) { 520 piece = square-kInHandSquare; 521 if (fVariant!=kVarCrazyhouse || ![[fController board] curInHand:piece]) 522 return; 523 } else if (square == kWhitePromoSquare || square == kBlackPromoSquare) 524 return; 525 else 526 piece = [[fController board] oldContents:square]; 527 528 if (!piece) 529 return; 530 531 if (Color(piece) == (fLastSide==kBlackSide ? kWhitePiece : kBlackPiece)) { 532 fFromSquare = square; 533 [[fController gameView] selectPiece:piece at:square]; 534 } 535} 536 537- (void) endSelection:(MBCSquare)square animate:(BOOL)animate 538{ 539 if (fFromSquare == square) { 540 [[fController gameView] clickPiece]; 541 542 return; 543 } else if (square > kSyntheticSquare) { 544 [[fController gameView] unselectPiece]; 545 546 return; 547 } 548 549 MBCMove * move = [MBCMove moveWithCommand:kCmdMove]; 550 551 if (fFromSquare > kInHandSquare) { 552 move->fCommand = kCmdDrop; 553 move->fPiece = fFromSquare-kInHandSquare; 554 } else { 555 move->fFromSquare = fFromSquare; 556 } 557 move->fToSquare = square; 558 move->fAnimate = animate; 559 560 // 561 // Fill in promotion info 562 // 563 [[fController board] tryPromotion:move]; 564 565 [[NSNotificationCenter defaultCenter] 566 postNotificationName: 567 (fLastSide==kBlackSide 568 ? MBCUncheckedWhiteMoveNotification 569 : MBCUncheckedBlackMoveNotification) 570 object:fDocument userInfo:(id)move]; 571} 572 573- (void) recognized:(SRRecognitionResult)result 574{ 575 if (MBCMove * move = [fLanguageModel recognizedMove:result]) { 576 if (move->fCommand == kCmdUndo) { 577 [fController takeback:self]; 578 } else { 579 // 580 // Fill in promotion info if missing 581 // 582 [[fController board] tryPromotion:move]; 583 584 NSString * notification; 585 if (fLastSide==kBlackSide) 586 notification = MBCUncheckedWhiteMoveNotification; 587 else 588 notification = MBCUncheckedBlackMoveNotification; 589 [[NSNotificationCenter defaultCenter] 590 postNotificationName:notification 591 object:fDocument userInfo:(id)move]; 592 } 593 } 594} 595 596- (void) removeController 597{ 598 // 599 // Avoid crashes from delayed methods 600 // 601 fController = nil; 602} 603 604@end 605 606// Local Variables: 607// mode:ObjC 608// End: 609 610