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