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