MBCEngine.mm   [plain text]


/*
	File:		MBCEngine.mm
	Contains:	An agent representing the sjeng chess engine
	Copyright:	© 2002-2005 Apple Computer, Inc. All rights reserved.

	IMPORTANT: This Apple software is supplied to you by Apple Computer,
	Inc.  ("Apple") in consideration of your agreement to the following
	terms, and your use, installation, modification or redistribution of
	this Apple software constitutes acceptance of these terms.  If you do
	not agree with these terms, please do not use, install, modify or
	redistribute this Apple software.
	
	In consideration of your agreement to abide by the following terms,
	and subject to these terms, Apple grants you a personal, non-exclusive
	license, under Apple's copyrights in this original Apple software (the
	"Apple Software"), to use, reproduce, modify and redistribute the
	Apple Software, with or without modifications, in source and/or binary
	forms; provided that if you redistribute the Apple Software in its
	entirety and without modifications, you must retain this notice and
	the following text and disclaimers in all such redistributions of the
	Apple Software.  Neither the name, trademarks, service marks or logos
	of Apple Computer, Inc. may be used to endorse or promote products
	derived from the Apple Software without specific prior written
	permission from Apple.  Except as expressly stated in this notice, no
	other rights or licenses, express or implied, are granted by Apple
	herein, including but not limited to any patent rights that may be
	infringed by your derivative works or by other works in which the
	Apple Software may be incorporated.
	
	The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
	MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
	THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND
	FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
	USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
	
	IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
	INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
	PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
	REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE,
	HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING
	NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
	ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "MBCEngine.h"
#import "MBCEngineCommands.h"
#import "MBCController.h"

#include <unistd.h>
#include <algorithm>

//
// Paradoxically enough, moving as quickly as possible is 
// not necessarily desirable. Users tend to get frustrated 
// once they realize how little time their Mac really spends 
// to crush them at low levels. In the interest of promoting 
// harmonious Human - Machine relations, we enforce minimum
// response times.
//
const NSTimeInterval kInteractiveDelay	= 2.0;
const NSTimeInterval kAutomaticDelay	= 4.0;

using std::max;

@implementation MBCEngine

- (id) init
{
	fEngineEnabled	= false;
	fSetPosition	= false;
	fTakeback		= false;
	fNeedsGo		= false;
	fLastMove	 	= nil;
	fLastPonder	 	= nil;
	fLastEngineMove	= nil;
	fDontMoveBefore	= [NSDate timeIntervalSinceReferenceDate];
	fMainRunLoop = [NSRunLoop currentRunLoop];
	fEngineMoves = [[NSPort port] retain];
	[fEngineMoves setDelegate:self];
	fMove = [[NSPortMessage alloc] initWithSendPort: fEngineMoves
								   receivePort: fEngineMoves
								   components: [NSArray array]];
	[self enableEngineMoves:YES];
	fEngineTask 	= [[NSTask alloc] init];
	fToEnginePipe	= [[NSPipe alloc] init];
	fFromEnginePipe = [[NSPipe alloc] init];
	[fEngineTask setStandardInput:fToEnginePipe];
	[fEngineTask setStandardOutput:fFromEnginePipe];
	[fEngineTask setLaunchPath:
					 [[NSBundle mainBundle] pathForResource:@"sjeng" 
											ofType:@""]];
	[self performSelector:@selector(launchEngine:) withObject:nil afterDelay:0.001];
	fToEngine		= [fToEnginePipe fileHandleForWriting];
	fFromEngine		= [fFromEnginePipe fileHandleForReading];
	[NSThread detachNewThreadSelector:@selector(runEngine:) toTarget:self
			  withObject:nil];
	[self writeToEngine:@"xboard\nconfirm_moves\n"];

	return self;
}

- (void) launchEngine:(id)arg
{
	[fEngineTask launch];
}

- (void) shutdown
{
	//
	// Since there is only one Engine per app run, we don't bother
	// deallocating all the resources.
	//
	[self writeToEngine:@"?exit\n"];
}

- (void) writeToEngine:(NSString *)string
{
	NSData * data = [string dataUsingEncoding:NSASCIIStringEncoding];

	[[MBCController controller] logToEngine:string];
	[fToEngine writeData:data];
}

- (void) interruptEngine
{
	[self writeToEngine:@"?"];
}

- (void) setSearchTime:(int)time
{
	fSearchTime = time;
	if (fSearchTime < 0)
		[self writeToEngine:[NSString stringWithFormat:@"sd %d\n", 
									  4+fSearchTime]];
	else
		[self writeToEngine:[NSString stringWithFormat:@"st %d\n", 
									  fSearchTime]];
}

- (MBCMove *) lastPonder
{
	return fLastPonder;
}

- (MBCMove *) lastEngineMove
{
	return fLastEngineMove;
}

- (void) runEngine:(id) sender
{
    unsigned			cmd;
	NSAutoreleasePool * pool  = [[NSAutoreleasePool alloc] init];

    [[[NSThread currentThread] threadDictionary] 
		setObject:fFromEngine forKey:@"InputHandle"];

    while (cmd = yylex()) {
		[fMove setMsgid:cmd];
		[fMove sendBeforeDate:[NSDate distantFuture]];
		[pool release];
		pool  = [[NSAutoreleasePool alloc] init];
    }
}

- (void) enableEngineMoves:(BOOL)enable
{
	if (enable != fEngineEnabled)
		if (fEngineEnabled = enable) 
			[fMainRunLoop addPort:fEngineMoves forMode:NSDefaultRunLoopMode];
		else
			[fMainRunLoop removePort:fEngineMoves forMode:NSDefaultRunLoopMode];
}

- (void) takebackNow
{
	[fLastPonder release];
	fLastPonder = nil;
	[self writeToEngine:@"remove\n"];

	[[NSNotificationCenter defaultCenter] 
		postNotificationName:MBCTakebackNotification
		object:nil];		
}

- (void) executeMove:(MBCMove *) move;
{
	[self flipSide];
	[fLastPonder release];
	fLastPonder = nil;
	[fLastEngineMove release];
	fLastEngineMove	= [move retain];
	[[NSNotificationCenter defaultCenter] 
		postNotificationName:[self notificationForSide]
		object:move];		
	if (fTakeback) {
		fTakeback = false;
		[self takebackNow];
	}
}

- (void) handlePortMessage:(NSPortMessage *)message
{
	MBCMove	* move = [MBCMove moveFromCompactMove:[message msgid]];

	if (fWaitForStart) { // Suppress all commands until next start
		if (move->fCommand == kCmdStartGame) {
			fWaitForStart = false;
		}
		return;
	}
	//
	// Otherwise, handle move confirmations or rejections here and
	// broadcast the rest of the moves
	//
	switch (move->fCommand) {
	case kCmdUndo:
		//
		// Last unchecked move was rejected
		//
		fThinking = false;
		[[NSNotificationCenter defaultCenter] 
			postNotificationName:MBCIllegalMoveNotification
			object:move];
		break;
	case kCmdMoveOK:
		if (fLastMove) { // Ignore confirmations of game setup moves
			[self flipSide];
			//
			// Suspend processing until move performed on board
			//
			[self enableEngineMoves:NO]; 
			[[NSNotificationCenter defaultCenter] 
				postNotificationName:[self notificationForSide]
				object:fLastMove];
			if (fNeedsGo) {
				fNeedsGo	= false;
				[self writeToEngine:@"go\n"];
			}
		}
		break;
	case kCmdPMove:
	case kCmdPDrop:
		[fLastPonder release];
		fLastPonder	=	[move retain];
		break;
	case kCmdWhiteWins:
	case kCmdBlackWins:
	case kCmdDraw:
		[[NSNotificationCenter defaultCenter] 
			postNotificationName:MBCGameEndNotification
			object:move];
		break;
	default:
		if (fSide == kBothSides) 
			[self writeToEngine:@"go\n"]; // Trigger next move
		else
			fThinking = false;
		//	
		// After the engine moved, we defer further moves until the
		// current move is executed on the board
		//
		[self enableEngineMoves:NO];
		
		NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
		[self performSelector:@selector(executeMove:) withObject:move 
			  afterDelay: fDontMoveBefore-now];

		if (fSide == kBothSides)
			fDontMoveBefore = max(now,fDontMoveBefore)+kAutomaticDelay;

		break;
	}
}

- (void) flipSide
{
	fLastSide = (fLastSide == kBlackSide) ? kWhiteSide : kBlackSide;
}

- (NSString *) notificationForSide
{
	return (fLastSide==kWhiteSide) 
		? MBCWhiteMoveNotification
		: MBCBlackMoveNotification;
}

- (void) initGame:(MBCVariant)variant
{
	[self writeToEngine:@"?new\n"];
	switch (variant) {
	case kVarCrazyhouse:
		[self writeToEngine:@"variant crazyhouse\n"];
		break;
	case kVarSuicide:
		[self writeToEngine:@"variant suicide\n"];
		break;
	case kVarLosers:
		[self writeToEngine:@"variant losers\n"];
		break;
	default:
		// Regular Chess
		break;
	}
	[self setSearchTime:fSearchTime];
	fTakeback = false;
}

- (void) setGame:(MBCVariant)variant fen:(NSString *)fen holding:(NSString *)holding moves:(NSString *)moves
{
	[self initGame:variant];

	fSetPosition	= true;
	[fLastMove release];
	fLastMove		= nil;

	const char * s = [fen cString];
	while (isspace(*s))
		++s;
	while (!isspace(*s))
		++s;
	while (isspace(*s))
		++s;
	fLastSide	= *s == 'w' ? kBlackSide : kWhiteSide;

	if (moves) {
		[self writeToEngine:@"force\n"];		
		[self writeToEngine:moves];
	} else {
		if (*s == 'b')
			[self writeToEngine:@"black\n"];
	
		[self writeToEngine:
				  [NSString stringWithFormat:@"setboard %@\n", fen]];
	
		if (variant == kVarCrazyhouse)
			[self writeToEngine:
					  [NSString stringWithFormat:@"holding %@\n", holding]];
	}
}

- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay
{
	if (!fSetPosition) {
		[self initGame:variant];
		fLastSide = kBlackSide;
	} else {
		fNeedsGo	 = sideToPlay != kNeitherSide;
		fSetPosition = false;
	}

	[[NSNotificationCenter defaultCenter] removeObserver:self];
	switch (fSide = sideToPlay) {
	case kWhiteSide:
		[[NSNotificationCenter defaultCenter] 
			addObserver:self
			selector:@selector(opponentMoved:)
			name:MBCUncheckedBlackMoveNotification
			object:nil];
		break;
	case kBothSides:
		[self writeToEngine:@"go\n"];
		fThinking = true;
		break;
	case kNeitherSide:
		[[NSNotificationCenter defaultCenter] 
			addObserver:self
			selector:@selector(opponentMoved:)
			name:MBCUncheckedWhiteMoveNotification
			object:nil];
		[[NSNotificationCenter defaultCenter] 
			addObserver:self
			selector:@selector(opponentMoved:)
			name:MBCUncheckedBlackMoveNotification
			object:nil];
		[self writeToEngine:@"force\n"];
		fThinking = false;
		break;
	default:
		// Engine plays black
		[[NSNotificationCenter defaultCenter] 
			addObserver:self
			selector:@selector(opponentMoved:)
			name:MBCUncheckedWhiteMoveNotification
			object:nil];
		break;
	}
	if (fSide == kWhiteSide || fSide == kBlackSide) 
		if (fThinking = (fSide != fLastSide)) {
			fNeedsGo	= false;
			[self writeToEngine:@"go\n"];
		}

	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(moveDone:)
		name:MBCEndMoveNotification
		object:nil];
	[self enableEngineMoves:YES];
	fWaitForStart	= true;	// Suppress further moves until start
}

- (void) moveDone:(NSNotification *)notification
{
	[fLastPonder release];
	fLastPonder = nil;
	[self enableEngineMoves:YES];
}

- (void) takeback
{
	if (fThinking) {
		//
		// Defer
		//
		fTakeback = true;
		[self interruptEngine];
	} else 
		[self takebackNow];
}

- (id)retain
{
	return [super retain];
}

- (void) opponentMoved:(NSNotification *)notification
{
	//
	// Got a human move, ask engine to verify it
	//
	const char * piece	= " KQBNRP  kqbnrp ";
	MBCMove *    move 	= reinterpret_cast<MBCMove *>([notification object]);

	[fLastMove release];
	fLastMove	= [move retain];

	switch (move->fCommand) {
	case kCmdMove:
		if (move->fPromotion) 
			[self writeToEngine:
					  [NSString stringWithFormat:@"%@%@%c\n", 
								[self squareToCoord:move->fFromSquare],
								[self squareToCoord:move->fToSquare],
								piece[move->fPromotion]]];
		else
			[self writeToEngine:
					  [NSString stringWithFormat:@"%@%@\n", 
								[self squareToCoord:move->fFromSquare],
								[self squareToCoord:move->fToSquare]]];
		fThinking = fSide != kNeitherSide;
		break;
	case kCmdDrop:
		[self writeToEngine:
				  [NSString stringWithFormat:@"%c@%@\n", 
							piece[move->fPiece],
							[self squareToCoord:move->fToSquare]]];
		fThinking = fSide != kNeitherSide;
		break;
	default:
		break;
	}	
	fDontMoveBefore	= [NSDate timeIntervalSinceReferenceDate]+kInteractiveDelay;
}

- (NSString *) squareToCoord:(MBCSquare)square
{
	const char * 	row 	= "12345678";
	const char * 	col 	= "abcdefgh";

	return [NSString stringWithFormat:@"%c%c", 
					 col[square % 8], row[square / 8]];
}

@end

void MBCIgnoredText(const char * text)
{
	// fprintf(stderr, "* %s", text);
}

int MBCReadInput(char * buf, int max_size)
{
	NSFileHandle *	f	=
		[[[NSThread currentThread] threadDictionary] 
			objectForKey:@"InputHandle"];

	ssize_t sz = read([f fileDescriptor], buf, max_size);
	if (sz > 0)
		[[MBCController controller] 
			logFromEngine: [NSString stringWithCString:buf length:sz]];
	return sz;
}

// Local Variables:
// mode:ObjC
// End: