1/*
2	File:		MBCEngine.mm
3	Contains:	An agent representing the sjeng chess engine
4	Copyright:	© 2002-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 "MBCEngine.h"
47#import "MBCEngineCommands.h"
48#import "MBCController.h"
49#import "MBCUserDefaults.h"
50
51#include <unistd.h>
52#include <algorithm>
53
54//
55// Paradoxically enough, moving as quickly as possible is
56// not necessarily desirable. Users tend to get frustrated
57// once they realize how little time their Mac really spends
58// to crush them at low levels. In the interest of promoting
59// harmonious Human - Machine relations, we enforce minimum
60// response times.
61//
62const NSTimeInterval kInteractiveDelay	= 2.0;
63const NSTimeInterval kAutomaticDelay	= 4.0;
64
65using std::max;
66
67@implementation MBCEngine
68
69- (id) init
70{
71	fEngineEnabled	= false;
72	fSetPosition	= false;
73	fTakeback		= false;
74	fNeedsGo		= false;
75	fLastMove	 	= nil;
76	fLastPonder	 	= nil;
77	fLastEngineMove	= nil;
78	fDontMoveBefore	= [NSDate timeIntervalSinceReferenceDate];
79	fMainRunLoop = [NSRunLoop currentRunLoop];
80	fEngineMoves = [[NSPort port] retain];
81	[fEngineMoves setDelegate:self];
82	fMove = [[NSPortMessage alloc] initWithSendPort: fEngineMoves
83								   receivePort: fEngineMoves
84								   components: [NSArray array]];
85	[self enableEngineMoves:YES];
86	fEngineTask 	= [[NSTask alloc] init];
87	fToEnginePipe	= [[NSPipe alloc] init];
88	fFromEnginePipe = [[NSPipe alloc] init];
89	[fEngineTask setStandardInput:fToEnginePipe];
90	[fEngineTask setStandardOutput:fFromEnginePipe];
91	[fEngineTask setLaunchPath:
92					 [[NSBundle mainBundle] pathForResource:@"sjeng"
93											ofType:@"ChessEngine"]];
94	[fEngineTask setArguments: [NSArray arrayWithObject:@"sjeng (Chess Engine)"]];
95	[self performSelector:@selector(launchEngine:) withObject:nil afterDelay:0.001];
96	fToEngine		= [fToEnginePipe fileHandleForWriting];
97	fFromEngine		= [fFromEnginePipe fileHandleForReading];
98	[NSThread detachNewThreadSelector:@selector(runEngine:) toTarget:self
99			  withObject:nil];
100	[self writeToEngine:@"xboard\nconfirm_moves\n"];
101
102	return self;
103}
104
105
106- (void)setLogging:(BOOL)logging
107{
108	if ((fIsLogging = logging) && !fEngineLogFile) {
109		NSFileManager * mgr		= [NSFileManager defaultManager];
110		NSURL *			libLog	=
111        [[mgr URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask
112            appropriateForURL:nil create:YES error:nil] URLByAppendingPathComponent:@"Logs"];
113		NSString*		logDir	= [libLog path];
114		[mgr createDirectoryAtPath:logDir withIntermediateDirectories:YES attributes:nil error:nil];
115		NSString * log	= [logDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d.log",
116                           [[NSDate date] descriptionWithCalendarFormat:@"Chess %Y-%m-%d %H%M" timeZone:nil locale:nil],
117                           [fEngineTask processIdentifier]]];
118		creat([log fileSystemRepresentation], 0666);
119		fEngineLogFile = [[NSFileHandle fileHandleForWritingAtPath:log] retain];
120	}
121}
122
123- (BOOL)isLogging
124{
125    return fIsLogging;
126}
127
128- (void) writeLog:(NSString *)text
129{
130	[fEngineLogFile writeData:[text dataUsingEncoding:NSASCIIStringEncoding]];
131}
132
133- (void) logToEngine:(NSString *)text
134{
135	if (fIsLogging) {
136		NSString * decorated =
137        [NSString stringWithFormat:@">>> %@\n",
138         [[text componentsSeparatedByString:@"\n"]
139          componentsJoinedByString:@"\n>>> "]];
140		[self writeLog:decorated];
141	}
142}
143
144- (void) logFromEngine:(NSString *)text
145{
146	if (fIsLogging) {
147		[self writeLog:text];
148	}
149}
150
151- (void) launchEngine:(id)arg
152{
153	[fEngineTask launch];
154}
155
156- (void) shutdown
157{
158    fDocument = nil;
159    [self enableEngineMoves:NO];
160    if ([fEngineTask isRunning])
161        [fEngineTask terminate];
162}
163
164- (void) writeToEngine:(NSString *)string
165{
166	NSData * data = [string dataUsingEncoding:NSASCIIStringEncoding];
167
168	[self logToEngine:string];
169	[fToEngine writeData:data];
170}
171
172- (void) interruptEngine
173{
174	[self writeToEngine:@"?"];
175}
176
177- (void) setSearchTime:(int)time
178{
179	if (time < 0)
180		[self writeToEngine:[NSString stringWithFormat:@"sd %d\n",
181									  4+time]];
182	else
183		[self writeToEngine:[NSString stringWithFormat:@"sd 40\nst %d\n",
184                             [MBCEngine secondsForTime:time]]];
185}
186
187+ (int) secondsForTime:(int)time
188{
189    return lround(ldexpf(1.0f, time));
190}
191
192- (MBCMove *) lastPonder
193{
194	return fLastPonder;
195}
196
197- (MBCMove *) lastEngineMove
198{
199	return fLastEngineMove;
200}
201
202- (void) runEngine:(id) sender
203{
204	NSAutoreleasePool * pool  = [[NSAutoreleasePool alloc] init];
205
206    [[[NSThread currentThread] threadDictionary]
207        setObject:fFromEngine forKey:@"InputHandle"];
208    [[[NSThread currentThread] threadDictionary]
209        setObject:self forKey:@"Engine"];
210
211    MBCLexerInstance    scanner;
212    MBCLexerInit(&scanner);
213    while (unsigned cmd = MBCLexerScan(scanner)) {
214		[fMove setMsgid:cmd];
215		[fMove sendBeforeDate:[NSDate distantFuture]];
216		[pool release];
217		pool  = [[NSAutoreleasePool alloc] init];
218    }
219    MBCLexerDestroy(scanner);
220
221    [pool release];
222}
223
224- (void) enableEngineMoves:(BOOL)enable
225{
226	if (enable != fEngineEnabled)
227		if ((fEngineEnabled = enable))
228			[fMainRunLoop addPort:fEngineMoves forMode:NSDefaultRunLoopMode];
229		else
230			[fMainRunLoop removePort:fEngineMoves forMode:NSDefaultRunLoopMode];
231}
232
233- (void) takebackNow
234{
235	[fLastPonder release];
236	fLastPonder = nil;
237	[self writeToEngine:@"remove\n"];
238
239	[[NSNotificationCenter defaultCenter]
240		postNotificationName:MBCTakebackNotification
241		object:fDocument];
242}
243
244- (void) executeMove:(MBCMove *) move;
245{
246	[self flipSide];
247	[fLastPonder release];
248	fLastPonder = nil;
249	[fLastEngineMove release];
250	fLastEngineMove	= [move retain];
251	[[NSNotificationCenter defaultCenter]
252		postNotificationName:[self notificationForSide]
253     object:fDocument userInfo:(id)move];
254}
255
256- (void) handlePortMessage:(NSPortMessage *)message
257{
258	MBCMove	* move = [MBCMove moveFromCompactMove:[message msgid]];
259
260	if (fWaitForStart) { // Suppress all commands until next start
261		if (move->fCommand == kCmdStartGame) {
262			fWaitForStart = false;
263		}
264		return;
265	}
266	//
267	// Otherwise, handle move confirmations or rejections here and
268	// broadcast the rest of the moves
269	//
270	switch (move->fCommand) {
271	case kCmdUndo:
272		//
273		// Last unchecked move was rejected
274		//
275		fThinking = false;
276		[[NSNotificationCenter defaultCenter]
277			postNotificationName:MBCIllegalMoveNotification
278         object:fDocument userInfo:(id)move];
279		break;
280	case kCmdMoveOK:
281		if (fLastMove) { // Ignore confirmations of game setup moves
282			[self flipSide];
283			//
284			// Suspend processing until move performed on board
285			//
286			[self enableEngineMoves:NO];
287			[[NSNotificationCenter defaultCenter]
288				postNotificationName:[self notificationForSide]
289             object:fDocument userInfo:(id)fLastMove];
290			if (fNeedsGo) {
291				fNeedsGo	= false;
292				[self writeToEngine:@"go\n"];
293			}
294		}
295		break;
296	case kCmdPMove:
297	case kCmdPDrop:
298		[fLastPonder release];
299		fLastPonder	=	[move retain];
300		break;
301	case kCmdWhiteWins:
302	case kCmdBlackWins:
303	case kCmdDraw:
304		[[NSNotificationCenter defaultCenter]
305			postNotificationName:MBCGameEndNotification
306         object:fDocument userInfo:(id)move];
307		break;
308	default:
309		if (fSide == kBothSides)
310			[self writeToEngine:@"go\n"]; // Trigger next move
311		else
312			fThinking = false;
313		//
314		// After the engine moved, we defer further moves until the
315		// current move is executed on the board
316		//
317		[self enableEngineMoves:NO];
318
319		NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
320		[self performSelector:@selector(executeMove:) withObject:move
321			  afterDelay: fDontMoveBefore-now];
322
323		if (fSide == kBothSides)
324			fDontMoveBefore = max(now,fDontMoveBefore)+kAutomaticDelay;
325
326		break;
327	}
328}
329
330- (void) flipSide
331{
332	fLastSide = (fLastSide == kBlackSide) ? kWhiteSide : kBlackSide;
333}
334
335- (NSString *) notificationForSide
336{
337	return (fLastSide==kWhiteSide)
338		? MBCWhiteMoveNotification
339		: MBCBlackMoveNotification;
340}
341
342- (void) initGame:(MBCVariant)variant
343{
344	[self writeToEngine:@"?new\n"];
345	switch (variant) {
346	case kVarCrazyhouse:
347		[self writeToEngine:@"variant crazyhouse\n"];
348		break;
349	case kVarSuicide:
350		[self writeToEngine:@"variant suicide\n"];
351		break;
352	case kVarLosers:
353		[self writeToEngine:@"variant losers\n"];
354		break;
355	default:
356		// Regular Chess
357		break;
358	}
359	fTakeback = false;
360}
361
362- (void) setGame:(MBCVariant)variant fen:(NSString *)fen holding:(NSString *)holding moves:(NSString *)moves
363{
364	[self initGame:variant];
365
366	fSetPosition	= true;
367	[fLastMove release];
368	fLastMove		= nil;
369
370	const char * s = [fen UTF8String];
371	while (isspace(*s))
372		++s;
373	while (!isspace(*s))
374		++s;
375	while (isspace(*s))
376		++s;
377	fLastSide	= *s == 'w' ? kBlackSide : kWhiteSide;
378
379	if (moves) {
380		[self writeToEngine:@"force\n"];
381		[self writeToEngine:moves];
382	} else {
383		if (*s == 'b')
384			[self writeToEngine:@"black\n"];
385
386		[self writeToEngine:
387				  [NSString stringWithFormat:@"setboard %@\n", fen]];
388
389		if (variant == kVarCrazyhouse)
390			[self writeToEngine:
391					  [NSString stringWithFormat:@"holding %@\n", holding]];
392	}
393}
394
395- (void) removeChessObservers
396{
397    if (!fHasObservers)
398        return;
399
400    NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter];
401    [notificationCenter removeObserver:self name:MBCUncheckedBlackMoveNotification object:nil];
402    [notificationCenter removeObserver:self name:MBCUncheckedWhiteMoveNotification object:nil];
403    [notificationCenter removeObserver:self name:MBCEndMoveNotification object:nil];
404
405    fHasObservers = NO;
406}
407
408- (void)dealloc
409{
410    [self removeChessObservers];
411    [self shutdown];
412    [fEngineTask release];
413    [fToEnginePipe release];
414    [fFromEnginePipe release];
415    [fMove release];
416    [fLastPonder release];
417    [fLastMove release];
418    [fLastEngineMove release];
419    [super dealloc];
420}
421
422- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay
423{
424	//
425	// Get rid of queued up move notifications
426	//
427	[self enableEngineMoves:NO];
428	if ([fEngineTask isRunning])
429		[NSObject cancelPreviousPerformRequestsWithTarget:self];
430	if (!fSetPosition) {
431		[self initGame:variant];
432		fLastSide = kBlackSide;
433		fNeedsGo  = false;
434	} else {
435		fNeedsGo	 = sideToPlay != kNeitherSide;
436		fSetPosition = false;
437	}
438
439    [self removeChessObservers];
440    NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter];
441	switch (fSide = sideToPlay) {
442	case kWhiteSide:
443		[notificationCenter
444			addObserver:self
445			selector:@selector(opponentMoved:)
446			name:MBCUncheckedBlackMoveNotification
447			object:fDocument];
448		break;
449	case kBothSides:
450		[self writeToEngine:@"go\n"];
451		fThinking = true;
452		break;
453	case kNeitherSide:
454		[notificationCenter
455			addObserver:self
456			selector:@selector(opponentMoved:)
457			name:MBCUncheckedWhiteMoveNotification
458			object:fDocument];
459		[notificationCenter
460			addObserver:self
461			selector:@selector(opponentMoved:)
462			name:MBCUncheckedBlackMoveNotification
463			object:fDocument];
464		[self writeToEngine:@"force\n"];
465		fThinking = false;
466		break;
467	default:
468		// Engine plays black
469		[notificationCenter
470			addObserver:self
471			selector:@selector(opponentMoved:)
472			name:MBCUncheckedWhiteMoveNotification
473			object:fDocument];
474		break;
475	}
476	if (fSide == kWhiteSide || fSide == kBlackSide)
477		if ((fThinking = (fSide != fLastSide))) {
478			fNeedsGo	= false;
479			[self writeToEngine:@"go\n"];
480		}
481
482	[[NSNotificationCenter defaultCenter]
483		addObserver:self
484		selector:@selector(moveDone:)
485		name:MBCEndMoveNotification
486		object:fDocument];
487    fHasObservers  = YES;
488	fWaitForStart	= true;	// Suppress further moves until start
489	[self enableEngineMoves:YES];
490}
491
492- (void) moveDone:(NSNotification *)notification
493{
494	[fLastPonder release];
495	fLastPonder = nil;
496	if (fTakeback) {
497		fTakeback = false;
498		[self takebackNow];
499	}
500	[self enableEngineMoves:YES];
501}
502
503- (void) takeback
504{
505	if (fThinking) {
506		//
507		// Defer
508		//
509		fTakeback = true;
510		[self interruptEngine];
511	} else if (!fEngineEnabled) {
512		//
513		// Move yet to be executed
514		//
515		fTakeback = true;
516	} else
517		[self takebackNow];
518}
519
520- (id)retain
521{
522	return [super retain];
523}
524
525- (void) opponentMoved:(NSNotification *)notification
526{
527	//
528	// Got a human move, ask engine to verify it
529	//
530	const char * piece	= " KQBNRP  kqbnrp ";
531	MBCMove *    move 	= reinterpret_cast<MBCMove *>([notification userInfo]);
532
533	[fLastMove release];
534	fLastMove	= [move retain];
535
536	switch (move->fCommand) {
537	case kCmdMove:
538		if (move->fPromotion)
539			[self writeToEngine:
540					  [NSString stringWithFormat:@"%@%@%c\n",
541								[self squareToCoord:move->fFromSquare],
542								[self squareToCoord:move->fToSquare],
543								piece[move->fPromotion]]];
544		else
545			[self writeToEngine:
546					  [NSString stringWithFormat:@"%@%@\n",
547								[self squareToCoord:move->fFromSquare],
548								[self squareToCoord:move->fToSquare]]];
549		fThinking = fSide != kNeitherSide;
550		break;
551	case kCmdDrop:
552		[self writeToEngine:
553				  [NSString stringWithFormat:@"%c@%@\n",
554							piece[move->fPiece],
555							[self squareToCoord:move->fToSquare]]];
556		fThinking = fSide != kNeitherSide;
557		break;
558	default:
559		break;
560	}
561	fDontMoveBefore	= [NSDate timeIntervalSinceReferenceDate]+kInteractiveDelay;
562}
563
564- (NSString *) squareToCoord:(MBCSquare)square
565{
566	const char * 	row 	= "12345678";
567	const char * 	col 	= "abcdefgh";
568
569	return [NSString stringWithFormat:@"%c%c",
570					 col[square % 8], row[square / 8]];
571}
572
573@end
574
575void MBCIgnoredText(const char * text)
576{
577	// fprintf(stderr, "* %s", text);
578}
579
580int MBCReadInput(char * buf, int max_size)
581{
582	NSFileHandle *	f	=
583		[[[NSThread currentThread] threadDictionary]
584			objectForKey:@"InputHandle"];
585    MBCEngine * e =
586        [[[NSThread currentThread] threadDictionary]
587         objectForKey:@"Engine"];
588
589	ssize_t sz = read([f fileDescriptor], buf, max_size);
590	if (sz > 0)
591		[e logFromEngine: [NSString stringWithFormat:@"%.*s", sz, buf]];
592	return sz;
593}
594
595// Local Variables:
596// mode:ObjC
597// End:
598