/* File: MBCEngine.mm Contains: An agent representing the sjeng chess engine Version: 1.0 Copyright: © 2002-2011 by Apple Computer, Inc., all rights reserved. File Ownership: DRI: Matthias Neeracher x43683 Writers: (MN) Matthias Neeracher Change History (most recent first): $Log: MBCEngine.mm,v $ Revision 1.25 2011/03/13 00:05:45 neerache <rdar://problem/9126277> Second white move gets made by engine when switching from Computer-Computer to Human-Computer Revision 1.24 2010/08/10 21:15:00 neerache <rdar://problem/8248582> Chess hits a zombie when being enlivened Revision 1.23 2010/01/18 18:37:16 neerache <rdar://problem/7297328> Deprecated methods in Chess, part 1 Revision 1.22 2008/10/24 22:45:45 neerache <rdar://problem/5844722> Chess: black may illegally move first in new game Revision 1.21 2007/03/02 20:03:57 neerache Fix undo timing problems <rdar://problem/4139329> Revision 1.20 2007/01/16 08:29:20 neerache Reset search depth when switching to time based level Revision 1.19 2007/01/16 03:55:02 neerache TTS works again in LP64 <rdar://problem/4899456> Revision 1.18 2004/09/11 02:03:23 neerache Implement delays to humanize engine Revision 1.17 2004/08/16 07:48:14 neerache Add weaker levels Revision 1.16 2003/08/15 00:26:46 neerache Reset deferred takeback flag (RADAR 3373117) Revision 1.15 2003/08/13 21:22:01 neerache Execute deferred takebacks (RADAR 3373117) Revision 1.14 2003/07/18 22:14:26 neerache Disable pondering during drag to improve interactive performance (RADAR 2736549) Revision 1.13 2003/07/07 08:49:01 neerache Improve startup time Revision 1.12 2003/06/30 05:15:06 neerache Use proper move generator instead of engine Revision 1.11 2003/05/27 07:25:09 neerache Restart engine when loading Revision 1.10 2003/05/27 03:13:57 neerache Rework game loading/saving code Revision 1.9 2003/05/24 20:28:27 neerache Address race conditions between ploayer and engine Revision 1.8 2003/04/24 23:21:40 neeri Fix takebacks Revision 1.7 2003/04/10 23:03:17 neeri Load positions Revision 1.6 2003/04/05 05:45:08 neeri Add PGN export Revision 1.5 2003/03/28 01:29:53 neeri Support hints, last move Revision 1.4 2002/10/08 22:54:59 neeri Engine logging, better autorelease Revision 1.3 2002/09/13 23:57:06 neeri Support for Crazyhouse display and mouse Revision 1.2 2002/09/12 17:55:18 neeri Introduce level controls Revision 1.1 2002/08/22 23:47:06 neeri Initial Checkin */ #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:@"ChessEngine"]]; [fEngineTask setArguments: [NSArray arrayWithObject:@"sjeng (Chess Engine)"]]; [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:@"sd 40\nst %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]; } - (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 UTF8String]; 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 { // // <rdar://problem/5844722> Get rid of queued up move notifications // [self enableEngineMoves:NO]; if ([fEngineTask isRunning]) [NSObject cancelPreviousPerformRequestsWithTarget:self]; if (!fSetPosition) { [self initGame:variant]; fLastSide = kBlackSide; fNeedsGo = false; } 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]; fWaitForStart = true; // Suppress further moves until start [self enableEngineMoves:YES]; } - (void) moveDone:(NSNotification *)notification { [fLastPonder release]; fLastPonder = nil; if (fTakeback) { fTakeback = false; [self takebackNow]; } [self enableEngineMoves:YES]; } - (void) takeback { if (fThinking) { // // Defer // fTakeback = true; [self interruptEngine]; } else if (!fEngineEnabled) { // // Move yet to be executed // fTakeback = true; } 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 stringWithFormat:@"%.*s", sz, buf]]; return sz; } // Local Variables: // mode:ObjC // End: