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