1/* 2 File: MBCInteractivePlayer.mm 3 Contains: An agent representing a local human player 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 "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 performSelectorOnMainThread:@selector(updateNeedMouse:) 176 withObject:self waitUntilDone:NO]; 177} 178 179- (void) updateNeedMouse:(id)arg 180{ 181 BOOL wantMouse; 182 183 if (fLastSide == kBlackSide) 184 wantMouse = fSide == kWhiteSide || fSide == kBothSides; 185 else 186 wantMouse = fSide == kBlackSide || fSide == kBothSides; 187 188 if (wantMouse && [fDocument gameDone]) 189 wantMouse = NO; 190 191 [[fController gameView] wantMouse:wantMouse]; 192 [[NSApp delegate] updateApplicationBadge]; 193 194 if ([fController listenForMoves]) { 195 // 196 // Work with speech recognition 197 // 198 if (wantMouse) { 199 if (fStartingSR) { 200 ; // Current starting, will update later 201 } else if (!fRecSystem) { 202 static dispatch_once_t sInitOnce; 203 static dispatch_queue_t sInitQueue; 204 dispatch_once(&sInitOnce, ^{ 205 sInitQueue = dispatch_queue_create("InitSR", DISPATCH_QUEUE_SERIAL); 206 AEInstallEventHandler(kAESpeechSuite, kAESpeechDone, 207 NewAEEventHandlerUPP(HandleSpeechDoneAppleEvent), 208 NULL, false); 209 }); 210 fStartingSR = true; 211 dispatch_async(sInitQueue, ^{ 212 [self initSR]; 213 }); 214 } else { 215 if (!fSpeechHelp) { 216 [self makeSpeechHelp]; 217 SRSetProperty(fRecognizer, kSRCommandsDisplayCFPropListRef, 218 [fSpeechHelp bytes], [fSpeechHelp length]); 219 } 220 221 SRStopListening(fRecognizer); 222 MBCMoveCollector * moves = [MBCMoveCollector new]; 223 MBCMoveGenerator generateMoves(moves, fVariant, 0); 224 generateMoves.Generate(fLastSide==kBlackSide, 225 *[[fController board] curPos]); 226 [fLanguageModel buildLanguageModel:fModel 227 fromMoves:[moves collection] 228 takeback:[[fController board] canUndo]]; 229 SRSetLanguageModel(fRecognizer, fModel); 230 SRStartListening(fRecognizer); 231 [moves release]; 232 } 233 } else if (fRecSystem) 234 SRStopListening(fRecognizer); 235 } else if (fRecSystem && !fStartingSR) { 236 // 237 // Time to take the recognition system down 238 // 239 SRStopListening(fRecognizer); 240 [fLanguageModel release]; 241 fLanguageModel = nil; 242 SRReleaseObject(fRecognizer); 243 SRCloseRecognitionSystem(fRecSystem); 244 fRecSystem = 0; 245 } 246} 247 248- (void)allowedToListen:(BOOL)allowed 249{ 250 [self updateNeedMouse:self]; 251 if (fRecSystem && !allowed) 252 SRStopListening(fRecognizer); 253} 254 255- (void) removeChessObservers 256{ 257 if (!fHasObservers) 258 return; 259 260 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 261 [notificationCenter removeObserver:self name:MBCWhiteMoveNotification object:nil]; 262 [notificationCenter removeObserver:self name:MBCBlackMoveNotification object:nil]; 263 [notificationCenter removeObserver:self name:MBCIllegalMoveNotification object:nil]; 264 [notificationCenter removeObserver:self name:MBCTakebackNotification object:nil]; 265 [notificationCenter removeObserver:self name:MBCGameEndNotification object:nil]; 266 [fDocument removeObserver:self forKeyPath:@"Result"]; 267 268 fHasObservers = NO; 269} 270 271- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 272{ 273 [self updateNeedMouse:self]; 274} 275 276- (void)dealloc 277{ 278 [self removeChessObservers]; 279 [fSpeechHelp release]; 280 [fLanguageModel release]; 281 [super dealloc]; 282} 283 284- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay 285{ 286 fVariant = variant; 287 fLastSide = 288 ([[fController board] numMoves] & 1) 289 ? kWhiteSide : kBlackSide; 290 291 [self removeChessObservers]; 292 NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter]; 293 switch (fSide = sideToPlay) { 294 case kWhiteSide: 295 [notificationCenter 296 addObserver:self 297 selector:@selector(humanMoved:) 298 name:MBCWhiteMoveNotification 299 object:fDocument]; 300 [notificationCenter 301 addObserver:self 302 selector:@selector(opponentMoved:) 303 name:MBCBlackMoveNotification 304 object:fDocument]; 305 break; 306 case kBlackSide: 307 [notificationCenter 308 addObserver:self 309 selector:@selector(opponentMoved:) 310 name:MBCWhiteMoveNotification 311 object:fDocument]; 312 [notificationCenter 313 addObserver:self 314 selector:@selector(humanMoved:) 315 name:MBCBlackMoveNotification 316 object:fDocument]; 317 break; 318 case kBothSides: 319 [notificationCenter 320 addObserver:self 321 selector:@selector(humanMoved:) 322 name:MBCWhiteMoveNotification 323 object:fDocument]; 324 [notificationCenter 325 addObserver:self 326 selector:@selector(humanMoved:) 327 name:MBCBlackMoveNotification 328 object:fDocument]; 329 break; 330 case kNeitherSide: 331 [notificationCenter 332 addObserver:self 333 selector:@selector(opponentMoved:) 334 name:MBCWhiteMoveNotification 335 object:fDocument]; 336 [notificationCenter 337 addObserver:self 338 selector:@selector(opponentMoved:) 339 name:MBCBlackMoveNotification 340 object:fDocument]; 341 break; 342 } 343 [notificationCenter 344 addObserver:self 345 selector:@selector(reject:) 346 name:MBCIllegalMoveNotification 347 object:fDocument]; 348 [notificationCenter 349 addObserver:self 350 selector:@selector(takeback:) 351 name:MBCTakebackNotification 352 object:fDocument]; 353 [notificationCenter 354 addObserver:self 355 selector:@selector(gameEnded:) 356 name:MBCGameEndNotification 357 object:fDocument]; 358 [fDocument addObserver:self forKeyPath:@"Result" options:NSKeyValueObservingOptionNew context:nil]; 359 fHasObservers = YES; 360 361 [self updateNeedMouse:self]; 362} 363 364- (void) reject:(NSNotification *)n 365{ 366 NSBeep(); 367 [[fController gameView] unselectPiece]; 368} 369 370- (void) takeback:(NSNotification *)n 371{ 372 dispatch_async(dispatch_get_main_queue(), ^{ 373 [self updateNeedMouse:self]; 374 }); 375} 376 377- (void) switchSides:(NSNotification *)n 378{ 379 fLastSide = fLastSide==kBlackSide ? kWhiteSide : kBlackSide; 380 381 [self updateNeedMouse:self]; 382} 383 384- (BOOL)useAlternateSynthForMove:(MBCMove *)move 385{ 386 if (fSide == kBothSides || fSide == kNeitherSide) 387 return [[fController board] sideOfMove:move] == kBlackSide; 388 else 389 return fSide == [[fController board] sideOfMove:move]; 390} 391 392- (NSString *)stringFromMove:(MBCMove *)move 393{ 394 NSDictionary * localization = [self useAlternateSynthForMove:move] 395 ? [fController alternateLocalization] 396 : [fController primaryLocalization]; 397 398 return [[fController board] stringFromMove:move withLocalization:localization]; 399} 400 401- (NSString *)stringForCheck:(MBCMove *)move 402{ 403 NSDictionary * localization = [self useAlternateSynthForMove:move] 404 ? [fController alternateLocalization] 405 : [fController primaryLocalization]; 406 407 return LOC(@"check", @"Check!"); 408} 409 410- (void) speakMove:(MBCMove *)move text:(NSString *)text check:(BOOL)check 411{ 412 NSSpeechSynthesizer * synth = [self useAlternateSynthForMove:move] 413 ? [fController alternateSynth] 414 : [fController primarySynth]; 415 416 if (!check || (move->fCheck && !move->fCheckMate)) 417 SpeakStringWhenReady(synth, text); 418 if (!check && move->fCheck) 419 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200*NSEC_PER_MSEC), 420 dispatch_get_main_queue(), ^{ 421 [self speakMove:move text:[self stringForCheck:move] check:YES]; 422 }); 423 } 424 425- (void) speakMove:(NSNotification *)notification 426{ 427 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 428 429 NSString * text = [self stringFromMove:move]; 430 431 [self speakMove:move text:text check:NO]; 432} 433 434- (void) gameEnded:(NSNotification *)notification 435{ 436 MBCSide humanSide = [fDocument humanSide]; 437 BOOL wasHumanMove; 438 if (humanSide == kBothSides) { 439 wasHumanMove = YES; 440 } else if (humanSide == kNeitherSide) { 441 wasHumanMove = NO; 442 } else { 443 MBCMove * move = reinterpret_cast<MBCMove *>([notification userInfo]); 444 wasHumanMove = [[fController board] sideOfMove:move] == humanSide; 445 } 446 if (wasHumanMove ? [fController speakHumanMoves] : [fController speakMoves]) 447 if (![fDocument gameDone]) // Game was not previously finished 448 [self speakMove:notification]; 449} 450 451- (void) speakMove:(MBCMove *) move withWrapper:(NSString *)wrapper 452{ 453 if (move && ([fController speakHumanMoves] || [fController speakMoves])) { 454 NSString * text = [self stringFromMove:move]; 455 NSString * wrapped = 456 [NSString stringWithFormat:wrapper, text]; 457 458 [self speakMove:move text:wrapped check:NO]; 459 } 460} 461 462- (void) announceHint:(MBCMove *) move 463{ 464 if (!move) 465 return; 466 467 NSDictionary * localization = [self useAlternateSynthForMove:move] 468 ? [fController alternateLocalization] 469 : [fController primaryLocalization]; 470 471 [self speakMove:move withWrapper:LOC(@"suggest_fmt", @"I would suggest \"%@\"")]; 472} 473 474- (void) announceLastMove:(MBCMove *) move 475{ 476 if (!move) 477 return; 478 479 NSDictionary * localization = [self useAlternateSynthForMove:move] 480 ? [fController alternateLocalization] 481 : [fController primaryLocalization]; 482 483 [self speakMove:move withWrapper:LOC(@"last_move_fmt", @"The last move was \"%@\"")]; 484} 485 486- (void) opponentMoved:(NSNotification *)notification 487{ 488 dispatch_async(dispatch_get_main_queue(), ^{ 489 if ([fController speakMoves]) 490 [self speakMove:notification]; 491 [self switchSides:notification]; 492 }); 493} 494 495- (void) humanMoved:(NSNotification *)notification 496{ 497 dispatch_async(dispatch_get_main_queue(), ^{ 498 if ([fController speakHumanMoves]) 499 [self speakMove:notification]; 500 [self switchSides:notification]; 501 }); 502} 503 504- (void) startSelection:(MBCSquare)square 505{ 506 MBCPiece piece; 507 508 if (square > kInHandSquare) { 509 piece = square-kInHandSquare; 510 if (fVariant!=kVarCrazyhouse || ![[fController board] curInHand:piece]) 511 return; 512 } else if (square == kWhitePromoSquare || square == kBlackPromoSquare) 513 return; 514 else 515 piece = [[fController board] oldContents:square]; 516 517 if (!piece) 518 return; 519 520 if (Color(piece) == (fLastSide==kBlackSide ? kWhitePiece : kBlackPiece)) { 521 fFromSquare = square; 522 [[fController gameView] selectPiece:piece at:square]; 523 } 524} 525 526- (void) endSelection:(MBCSquare)square animate:(BOOL)animate 527{ 528 if (fFromSquare == square) { 529 [[fController gameView] clickPiece]; 530 531 return; 532 } else if (square > kSyntheticSquare) { 533 [[fController gameView] unselectPiece]; 534 535 return; 536 } 537 538 MBCMove * move = [MBCMove moveWithCommand:kCmdMove]; 539 540 if (fFromSquare > kInHandSquare) { 541 move->fCommand = kCmdDrop; 542 move->fPiece = fFromSquare-kInHandSquare; 543 } else { 544 move->fFromSquare = fFromSquare; 545 } 546 move->fToSquare = square; 547 move->fAnimate = animate; 548 549 // 550 // Fill in promotion info 551 // 552 [[fController board] tryPromotion:move]; 553 554 [[NSNotificationCenter defaultCenter] 555 postNotificationName: 556 (fLastSide==kBlackSide 557 ? MBCUncheckedWhiteMoveNotification 558 : MBCUncheckedBlackMoveNotification) 559 object:fDocument userInfo:(id)move]; 560} 561 562- (void) recognized:(SRRecognitionResult)result 563{ 564 if (MBCMove * move = [fLanguageModel recognizedMove:result]) { 565 if (move->fCommand == kCmdUndo) { 566 [fController takeback:self]; 567 } else { 568 // 569 // Fill in promotion info if missing 570 // 571 [[fController board] tryPromotion:move]; 572 573 NSString * notification; 574 if (fLastSide==kBlackSide) 575 notification = MBCUncheckedWhiteMoveNotification; 576 else 577 notification = MBCUncheckedBlackMoveNotification; 578 [[NSNotificationCenter defaultCenter] 579 postNotificationName:notification 580 object:fDocument userInfo:(id)move]; 581 } 582 } 583} 584 585- (void) removeController 586{ 587 // 588 // Avoid crashes from delayed methods 589 // 590 fController = nil; 591} 592 593@end 594 595// Local Variables: 596// mode:ObjC 597// End: 598 599