Created
December 14, 2011 19:41
-
-
Save fogus/1478126 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
\ FCP.F, version 1.3 by Ian Osgood iano@quirkster.com | |
\ A simple chess program written in ANS Forth. | |
\ Uses Core Extension words: | |
\ .( .R U.R <> U> ?DO ERASE FALSE NIP TO TRUE TUCK VALUE WITHIN \ | |
\ Uses Exception words: CATCH THROW ABORT" | |
\ Uses Tools Extension words: [IF] [ELSE] [THEN] | |
\ Uses String words: TOLOWER /STRING | |
\ Uses Win32Forth utilities: ms@ CELL CELL- DEFER IS [DEFINED] [UNDEFINED] | |
\ Will use DEFER/IS/[IS] if available for board iterator and vectors. | |
\ Uses defacto-standard HEX constant specifier: $FF | |
\ (I would use decimal #99, but it isn't supported by Win32Forth. | |
\ Thus, BASE 10 is assumed. Use the next line if this is not the case.) | |
\ BASE @ DECIMAL | |
UNUSED | |
\ Assumes 32-bit cells or larger | |
\ Recursive: assumes a data stack larger than MAX_PLY | |
\ and a return stack larger than 3*MAX_PLY. | |
\ Uses about 64K bytes of dictionary space. Some structures (eval boards etc.) | |
\ could use signed chars instead of cells for some space savings. | |
\ For further commentary on the basic chess data structures and algorithms, | |
\ please refer to the C source of the original TSCP program (v1.73), | |
\ written by Tom Kerrigan. | |
\ http://home.comcast.net/~tckerrigan/ | |
\ Change log: | |
\ (Initial port from TSCP C program) | |
\ 1* 0x88 vs. mailbox edge detection | |
\ * different piece & color values | |
\ * fine-grain factoring | |
\ * different command set, UI | |
\ 2* track king positions for inCheck? | |
\ * [reps implemented] | |
\ 3* material and pawn files updated incrementally for eval | |
\ * narrow starting a-b window with fail-high/low for more cutoffs | |
\ * setup EPD command | |
\ 4* style improvements based on c.l.f commentary | |
\ * fixed bugs in time display, reps | |
\ * use $ for hex constants instead of using DECIMAL & HEX | |
\ * .board options: showCoords, rotateBoard | |
\ * .epd command for recording a position, .moveList to list moves | |
\ (hiatus) | |
\ 5* more factoring and commentary | |
\ * inLine? optimized (thanks to Bruce Moreland's excellent web site) | |
\ http://www.seanet.com/~brucemo/topics/0x88.htm | |
\ * bd@ inlined and made into CELLS for a large speedup! | |
\ * fixed bug in genPush to bring node counts in line with TSCP.EXE | |
\ * st command to set thinking time (seconds per move) | |
\ * replaced most VARIABLEs with VALUEs (faster and more concise) | |
\ 6* inline and tweak hottest words for 25% speed boost | |
\ * fix bugs in epd, reps | |
\ * more factoring: move generation greatly simplified | |
\ * use new DEFER-based board iterators for move gen and eval | |
\ 7* inner loop unrolling | |
\ * optimize PV handling, eval, genPush | |
\ * remove forEachPiece/forEachOfMyPieces abstraction layer | |
\ * clarify word and variable names | |
\ 8* elegant stack machine alpha-beta search uses half the data stack | |
\ * fix some search and eval bugs (fixes node count diffs) | |
\ * more search debugging words (uses FILE wordset) | |
\ * fcp-help command | |
\ 9* further optimized attacks?, eval, moveGen | |
\ * use CATCH/THROW to abort a search more simply | |
\ A* use :inline from Wil Baden for inline words | |
\ * smaller, more effective history table | |
\ * killer moves, new sorting mechanism | |
\ * fix some MAX_PLY limit bugs, thanks to running test suites (setup2) | |
\ * move debug code to separate file (available upon request) | |
\ * check for king errors in setup, use ABORT" for input | |
\ (Sufficiently enhanced to rename to FCP.) | |
\ 1.0 * null-move heuristic (major tactical speedup!) | |
\ * better check search extension (ditto!) | |
\ 1.1 * added an opening book (extensions to fcp-book.f are welcome!) | |
\ * refactored input routines, ?research | |
\ * support for being an X/WinBoard engine (quiet flag, onSearchStatus) | |
\ * small eval tweaks | |
\ * new renamed to newGame; .bookHint, bookOff commands | |
\ 1.2 * minor bugfixes, king opposition fixed, some more refactoring | |
\ 1.3 * piece lists (10%-250% speedup) | |
\ 1.4 * trapped rook penalty, ep bugfix, more commands | |
\ * some 64-bit cell bugs, found porting to iForth64 & gforth64 | |
\ * use [IS] vs IS for iForth | |
\ Possible future enhancements: | |
\ + experiment with other search extensions: | |
\ single response, null-move mated, recapture, passed pawn advance | |
\ + add a transposition table | |
\ + further optimize eval routines | |
\ + clean up input routines | |
\ + update squares attacked incrementally | |
\ + enhance evaluation routines | |
\ bonus 50 for two bishops | |
S" [UNDEFINED]" PAD C! PAD CHAR+ PAD C@ CMOVE | |
PAD FIND NIP 0= [IF] | |
: [UNDEFINED] ( "word" -- tf ) BL WORD FIND NIP 0= ; IMMEDIATE | |
[THEN] | |
[UNDEFINED] [DEFINED] [IF] | |
: [DEFINED] ( "word" -- tf ) BL WORD FIND NIP ; IMMEDIATE | |
[THEN] | |
[UNDEFINED] CELL [IF] | |
1 CELLS CONSTANT CELL | |
[THEN] | |
[UNDEFINED] CELL- [IF] | |
: CELL- POSTPONE CELL POSTPONE - ; IMMEDIATE | |
[THEN] | |
FALSE [IF] | |
: :inline : ; \ don't inline when profiling | |
[ELSE] [UNDEFINED] :inline [IF] | |
\ Wil Baden implements :inline for portably inlining code | |
: :inline ( "name <char> ccc<char>" -- ) | |
: [CHAR] ; PARSE POSTPONE SLITERAL POSTPONE EVALUATE | |
POSTPONE ; IMMEDIATE | |
; | |
[THEN] [THEN] | |
1120 CONSTANT GEN_STACK | |
400 CONSTANT HIST_STACK | |
32 CONSTANT MAX_PLY | |
: MAX_PLY* 5 LSHIFT ; | |
\ *** Square (piece + color) *** | |
\ 0 CONSTANT EMPTY \ and BLANK are taken; just use 0 | |
1 CONSTANT PAWN | |
2 CONSTANT KNIGHT | |
3 CONSTANT BISHOP | |
4 CONSTANT ROOK | |
5 CONSTANT QUEEN | |
6 CONSTANT KING | |
\ 7 CONSTANT PIECEMASK | |
:inline piece 7 AND ; ( [sq] -- piece ) | |
\ 0 CONSTANT EMPTY | |
$10 CONSTANT LIGHT | |
$20 CONSTANT DARK | |
$30 CONSTANT COLORMASK | |
\ : color ( [sq] -- color ) COLORMASK AND ; | |
:inline otherSide COLORMASK XOR ; ( color -- ~color ) | |
\ : mine? ( [sq] color+piece -- tf ) XOR COLORMASK AND 0= ; | |
\ if input is just a color, simply use AND | |
\ : enemy? ( [sq] color -- tf ) XOR COLORMASK AND COLORMASK = ; | |
\ mine? and enemy? are symmetric ( color [sq] -- tf ) | |
\ color can also be a piece+color | |
DARK PAWN + CONSTANT DARKPAWN | |
LIGHT PAWN + CONSTANT LIGHTPAWN | |
\ *** Board *** | |
\ The board structure is changed from the original TSCP to this Forth | |
\ version. This representation has the advantages of: | |
\ 1. One board index is used both for the main board and the eval boards | |
\ The original TSCP had a 120 element board for the pieces and edge | |
\ detection, and 64 element boards for eval piece-square tables, requiring | |
\ extra tables for translating between the two indices. This | |
\ design uses one half of a 128 element board for both functions. | |
\ 2. Edge detection testing on board index vs. board contents | |
\ 3. One board element contains both piece and color data. The original | |
\ TSCP had two separate boards, one for color and one for piece type. | |
\ 4. Easy to translate between rank/file and board index. | |
\ 5. Efficient test for whether a piece on one square can attack a piece | |
\ on another square (heavily used by inCheck?) | |
CREATE board $80 CELLS ALLOT | |
: eraseBoard board $80 CELLS ERASE ; | |
$00 CONSTANT sqA8 | |
$02 CONSTANT sqC8 | |
$04 CONSTANT sqE8 | |
$06 CONSTANT sqG8 | |
$07 CONSTANT sqH8 | |
$70 CONSTANT sqA1 | |
$72 CONSTANT sqC1 | |
$74 CONSTANT sqE1 | |
$76 CONSTANT sqG1 | |
$77 CONSTANT sqH1 | |
:inline edge? $88 AND ; ( sq+dir -- nz ) | |
:inline bd! CELLS board + ! ; ( piece+color sq -- ) | |
:inline bd@ CELLS board + @ ; ( sq -- piece+color ) | |
:inline piece@ bd@ piece ; ( sq -- piece ) | |
: bdMove ( from to -- ) OVER bd@ SWAP bd! 0 SWAP bd! ; | |
:inline rank 4 RSHIFT ; ( sq -- rank ) | |
:inline file $F AND ; ( sq -- file ) | |
: fileRank>sq ( file rank -- sq ) 4 LSHIFT OR ; | |
:inline rank8? rank 0= ; ( sq -- tf ) | |
:inline rank7? rank 1 = ; ( sq -- tf ) | |
:inline rank2? rank 6 = ; ( sq -- tf ) | |
:inline rank1? rank 7 = ; ( sq -- tf ) | |
:inline rotate NEGATE $77 + ; ( sq -- sq' ) | |
: cRank ( sq -- c ) rank NEGATE [CHAR] 8 + ; | |
: cFile ( sq -- c ) file [CHAR] a + ; | |
: .sq ( sq -- ) DUP cFile EMIT cRank EMIT ; | |
\ *** Globals *** | |
0 VALUE nodes \ must be 32-bit (or higher for long searches) | |
0 VALUE ep \ destination square of possible en-passant capture | |
0 VALUE fifty \ fifty move draw count | |
1 CONSTANT wkCastleBit | |
2 CONSTANT wqCastleBit | |
4 CONSTANT bkCastleBit | |
8 CONSTANT bqCastleBit | |
1 2 OR CONSTANT wCastleBits | |
4 8 OR CONSTANT bCastleBits | |
$F CONSTANT allCastleBits | |
allCastleBits VALUE castle \ flags for castling capability | |
LIGHT VALUE side \ color to move during search | |
TRUE VALUE wtm? \ : wtm? ( -- tf ) side LIGHT AND ; | |
: setWtm TRUE TO wtm? LIGHT TO side ; | |
: setBtm FALSE TO wtm? DARK TO side ; | |
: switchColors wtm? 0= TO wtm? side otherSide TO side ; | |
\ *** Board Iterator *** | |
[DEFINED] DEFER [IF] \ this is fastest (less stack fiddling) | |
[UNDEFINED] [IS] [IF] | |
: [IS] POSTPONE IS ; IMMEDIATE | |
[THEN] | |
DEFER doSq \ use of global DEFER means this iterator is not reentrant!!! | |
: forEveryRow ( sq -- sq+7 ) | |
doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq ; | |
: forEverySq ( [st] 'word -- ) \ doSq ( [st] sq -- [st] sq ) | |
[IS] doSq | |
0 forEveryRow 9 + forEveryRow 9 + forEveryRow 9 + forEveryRow | |
9 + forEveryRow 9 + forEveryRow 9 + forEveryRow 9 + forEveryRow DROP ; | |
[ELSE] TRUE [IF] \ Marcel Hendrix: inline and use a local | |
:inline doSq 'doSq EXECUTE ; | |
:inline doRow doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq 1+ doSq ; | |
: forEverySq ( [st] 'word -- ) \ doSq ( [st] sq -- [st] sq ) | |
LOCALS| 'doSq | | |
0 doRow 9 + doRow 9 + doRow 9 + doRow | |
9 + doRow 9 + doRow 9 + doRow 9 + doRow DROP ; | |
[ELSE] TRUE [IF] \ unroll loops for speed and simplicity | |
: forEveryRow ( 'word sq -- 'word sq+7 ) | |
OVER EXECUTE 1+ OVER EXECUTE 1+ OVER EXECUTE 1+ OVER EXECUTE | |
1+ OVER EXECUTE 1+ OVER EXECUTE 1+ OVER EXECUTE 1+ OVER EXECUTE ; | |
: forEverySq ( [st] 'word -- ) \ word ( [st] sq -- [st] sq ) | |
0 forEveryRow 9 + forEveryRow 9 + forEveryRow 9 + forEveryRow | |
9 + forEveryRow 9 + forEveryRow 9 + forEveryRow 9 + forEveryRow 2DROP ; | |
[ELSE] | |
: forEverySq ( [st] 'word -- ) \ word ( [st] sq -- [st] sq ) | |
$80 0 DO | |
I 8 + I DO | |
I SWAP DUP >R EXECUTE DROP R> | |
LOOP | |
$10 +LOOP DROP ; | |
[THEN] [THEN] [THEN] | |
\ *** Board Display *** | |
FALSE VALUE quiet | |
CREATE symbols | |
CHAR . , CHAR P , CHAR N , CHAR B , | |
CHAR R , CHAR Q , CHAR K , CHAR # , | |
[UNDEFINED] tolower [IF] | |
: tolower ( C -- c ) $20 OR ; | |
[THEN] | |
: .piece ( piece[+color] -- ) | |
DUP piece CELLS symbols + @ \ symbol for piece | |
SWAP DARK AND IF tolower THEN EMIT ; \ dark is lowercase | |
FALSE VALUE showCoords? | |
FALSE VALUE blackAtBottom? | |
: ?rotate ( sq -- sq' ) blackAtBottom? IF rotate THEN ; | |
: .aSq ( sq -- sq ) | |
showCoords? OVER 1- edge? AND IF | |
DUP ?rotate cRank EMIT 2 SPACES | |
THEN | |
DUP ?rotate bd@ .piece | |
DUP 1+ edge? IF CR ELSE SPACE THEN ; | |
: .board | |
CR ['] .aSq forEverySq | |
showCoords? IF CR 3 SPACES | |
blackAtBottom? IF ." h g f e d c b a" | |
ELSE ." a b c d e f g h" THEN CR | |
THEN ; | |
\ Display board as an Extended Position Definition (EPD) string | |
VARIABLE epdBlCount | |
: ?.epdBl | |
epdBlCount @ ?DUP IF | |
[CHAR] 0 + EMIT | |
0 epdBlCount ! | |
THEN ; | |
: .epdSq ( sq -- sq ) | |
DUP bd@ ?DUP IF | |
?.epdBl .piece | |
ELSE | |
1 epdBlCount +! | |
THEN | |
DUP 1+ edge? IF | |
?.epdBl DUP rank1? 0= IF [CHAR] / EMIT THEN | |
THEN ; | |
: .epd CR 0 epdBlCount ! | |
['] .epdSq forEverySq SPACE | |
wtm? IF [CHAR] w ELSE [CHAR] b THEN EMIT CR ; | |
\ !!! show castling rights and ep square? | |
\ *** Piece Lists *** | |
\ Iterating through piece lists greatly speeds up attack detection, | |
\ move generation, and evaluation; especially in the endgame. | |
CREATE lightPieces HERE CELL+ , 16 CELLS ALLOT | |
CREATE darkPieces HERE CELL+ , 16 CELLS ALLOT | |
lightPieces VALUE myPieces | |
darkPieces VALUE otherPieces | |
: addPiece ( sq list -- ) | |
DUP >R @ ! CELL R> +! ; | |
: findPiece ( sq list -- entry ) | |
@ BEGIN CELL- 2DUP @ = UNTIL NIP ; | |
: removePiece ( sq list -- ) | |
DUP >R findPiece | |
R@ @ CELL- DUP R> ! | |
@ SWAP ! ; | |
: movePiece ( from to -- ) | |
2DUP SWAP myPieces findPiece ! bdMove ; | |
: listForSq ( sq -- list ) | |
bd@ LIGHT AND IF lightPieces ELSE darkPieces THEN ; | |
: putPiece ( sq -- sq ) | |
DUP bd@ 0= IF EXIT THEN | |
DUP piece@ KING = if \ kings go in front | |
DUP DUP listForSq | |
DUP CELL+ @ OVER addPiece | |
CELL+ ! | |
ELSE | |
DUP DUP listForSq addPiece | |
THEN ; | |
: initPieces | |
lightPieces TO myPieces | |
darkPieces TO otherPieces | |
lightPieces DUP CELL+ SWAP ! | |
darkPieces DUP CELL+ SWAP ! | |
['] putPiece forEverySq ; | |
: switchPieces myPieces otherPieces TO myPieces TO otherPieces ; | |
: forEachPiece ( list xt -- ) \ xt ( [st] sq -- [st] sq ) | |
[IS] doSq | |
DUP @ SWAP CELL+ DO I @ doSq DROP CELL +LOOP ; | |
: .listSq DUP .sq SPACE ; | |
: .list ( list -- ) ['] .listSq forEachPiece ; | |
\ *** Attack and check detection *** | |
$10 CONSTANT So \ rank | |
-16 CONSTANT No | |
1 CONSTANT Ea \ file | |
-1 CONSTANT We | |
So Ea + CONSTANT SE \ diagonals | |
So We + CONSTANT SW | |
No Ea + CONSTANT NE | |
No We + CONSTANT NW | |
No NE + CONSTANT Kn1 Ea NE + CONSTANT Kn2 \ knight moves | |
Ea SE + CONSTANT Kn3 So SE + CONSTANT Kn4 | |
So SW + CONSTANT Kn5 We SW + CONSTANT Kn6 | |
We NW + CONSTANT Kn7 No NW + CONSTANT Kn8 | |
: Northerly? ( dir -- tf ) POSTPONE 0< ; IMMEDIATE \ a1 to a8 direction? | |
\ This table relies on pieces being an enumeration of small integers (1..6) | |
\ This table exploits a property of the 0x88 board representation: the | |
\ spacial relationship between two squares is uniquely specified by | |
\ (the absolute value of) the difference of their indicies. See Bruce | |
\ Moreland's website for a clear explanation. | |
1 ROOK LSHIFT 1 QUEEN LSHIFT + CONSTANT RQ | |
1 BISHOP LSHIFT 1 QUEEN LSHIFT + CONSTANT BQ | |
1 KNIGHT LSHIFT CONSTANT Kn | |
1 PAWN LSHIFT CONSTANT pawnMask | |
: +K 1 KING LSHIFT + ; | |
: +KP pawnMask + +K ; | |
CREATE inLineTable 0 , RQ +K , RQ , RQ , RQ , RQ , RQ , RQ , 0 , | |
0 , 0 , 0 , 0 , 0 , Kn , BQ +KP , RQ +K , BQ +KP , Kn , 0 , 0 , 0 , 0 , 0 , 0 , | |
0 , 0 , 0 , 0 , 0 , BQ , Kn , RQ , Kn , BQ , 0 , 0 , 0 , 0 , 0 , 0 , | |
0 , 0 , 0 , 0 , BQ , 0 , 0 , RQ , 0 , 0 , BQ , 0 , 0 , 0 , 0 , 0 , | |
0 , 0 , 0 , BQ , 0 , 0 , 0 , RQ , 0 , 0 , 0 , BQ , 0 , 0 , 0 , 0 , | |
0 , 0 , BQ , 0 , 0 , 0 , 0 , RQ , 0 , 0 , 0 , 0 , BQ , 0 , 0 , 0 , | |
0 , BQ , 0 , 0 , 0 , 0 , 0 , RQ , 0 , 0 , 0 , 0 , 0 , BQ , 0 , 0 , | |
BQ , 0 , 0 , 0 , 0 , 0 , 0 , RQ , 0 , 0 , 0 , 0 , 0 , 0 , BQ , | |
\ example: ROOK sqE8 sqE1 - inLine ( -- [1 ROOK LSHIFT] ) | |
: inLine? ( sq diff -- mask ) | |
ABS CELLS inLineTable + @ DUP IF | |
1 ROT piece@ LSHIFT AND EXIT ELSE | |
NIP ( FALSE ) THEN ; | |
: diff>dir ( diff -- dir ) \ could also be a table | |
DUP ABS 8 U< IF | |
0< IF We ELSE Ea THEN EXIT THEN | |
\ dir = diff / number of ranks spanned | |
DUP ABS 8 + 4 RSHIFT / ; \ expensive division | |
\ assumes dir sq sqSrc - inLine? (so no need to check for edge) | |
: sqSliderAttacks? ( sq sqSrc dir -- sq tf ) | |
>R | |
BEGIN \ R: dir | |
R@ + 2DUP = IF \ clear line | |
R> 2DROP TRUE EXIT | |
THEN | |
DUP bd@ | |
UNTIL | |
R> 2DROP FALSE ; | |
Kn +KP CONSTANT adjacentMask | |
: sqAttacks? ( sq sqSrc -- sq sqSrc tf ) | |
2DUP - ( sq sqSrc diff ) | |
2DUP inLine? DUP IF | |
adjacentMask AND ?DUP IF | |
pawnMask AND IF Northerly? OVER bd@ LIGHTPAWN = = EXIT THEN | |
DROP TRUE EXIT | |
THEN | |
OVER >R diff>dir sqSliderAttacks? R> SWAP EXIT | |
THEN | |
NIP ( FALSE ) ; | |
: attacks? ( sq pieces -- tf ) | |
DUP @ SWAP CELL+ DO | |
I @ sqAttacks? IF | |
2DROP TRUE UNLOOP EXIT | |
THEN DROP | |
CELL +LOOP | |
DROP FALSE ; | |
\ access the king pos in the first cell of array (never removed) | |
: lkSq lightPieces CELL+ @ ; | |
: dkSq darkPieces CELL+ @ ; | |
: inCheck? ( wtm? -- tf ) | |
IF lkSq darkPieces ELSE dkSq lightPieces THEN attacks? ; | |
\ *** Move Type *** | |
\ A stored move is a cell containing four byte fields: | |
\ F from square, T to square, B bit flags, P promotion piece | |
\ cell layout: MSB [xxxxxPPP xxBBBBBB xTTTTTTT xFFFFFFF] LSB | |
CELL CONSTANT moveSize | |
$10000 CONSTANT captureBit | |
$20000 CONSTANT castleBit | |
$40000 CONSTANT pawnBit | |
$80000 CONSTANT 2sqBit | |
$100000 CONSTANT epBit | |
$200000 CONSTANT promoteBit | |
$3F0000 castleBit XOR CONSTANT reset50Bits | |
: mvPromote! ( mv piece -- mv ) | |
24 LSHIFT OR [ pawnBit promoteBit OR ] LITERAL OR ; | |
:inline mvFrom $FF AND ; ( mv -- sqFrom ) | |
:inline mvTo 8 RSHIFT $FF AND ; ( mv -- sqTo ) | |
: mvPromote ( mv -- piece ) 24 RSHIFT piece ; | |
:inline fromTo>mv 8 LSHIFT OR ; ( from to -- mv ) | |
:inline fromTo>fromMv 8 LSHIFT OVER OR ; ( from to -- from mv ) | |
: epSq ( mv -- mv ep ) DUP mvFrom OVER mvTo + 2/ ; | |
: epCapSq ( mv -- mv epCap ) DUP mvFrom $F0 AND OVER mvTo file OR ; | |
: .move ( mv -- ) | |
DUP 0= IF ." (null)" EXIT THEN | |
DUP mvFrom .sq | |
DUP captureBit AND IF [CHAR] x ELSE [CHAR] - THEN EMIT | |
DUP mvTo .sq | |
DUP promoteBit AND IF | |
[CHAR] = EMIT DUP mvPromote .piece | |
THEN | |
epBit AND IF ." ep" THEN ; | |
\ *** History (for improved move ordering) *** | |
\ Smaller table indexed by destination, piece, and color should work as well | |
\ as the original table, indexed by source and destination | |
\ CREATE historyTable $80 6 * CELLS ALLOT \ smallest, but harder to index | |
CREATE historyTable $80 $16 * CELLS ALLOT | |
: historyErase historyTable $80 $16 * CELLS ERASE ; | |
\ experiment: age off history instead of clearing between moves | |
8 VALUE historyAgeFactor \ proportional to last depth? nodes? max? mean? | |
: historyAge | |
historyTable $80 $16 * CELLS + historyTable DO | |
I @ historyAgeFactor RSHIFT I ! | |
CELL +LOOP ; | |
: xMaxHistory 0 historyTable $80 $16 * CELLS + historyTable DO | |
I @ MAX | |
CELL +LOOP . ; | |
: xMeanHistory 0 0 historyTable $80 $16 * CELLS + historyTable DO | |
I @ ?DUP IF + >R 1+ R> THEN | |
CELL +LOOP 2DUP . ." / " . ." = " SWAP / . ; | |
: mvHistory ( mv -- mv ^hist ) | |
\ DUP mvFrom bd@ DUP piece 1- 7 LSHIFT SWAP color 2/ 8 - + | |
DUP mvFrom bd@ $11 - 7 LSHIFT | |
OVER mvTo + CELLS historyTable + ; | |
\ *** Killer Moves (improves move ordering) *** | |
\ Killer moves serve the same function as the history table above. Whereas | |
\ the history table is used to value a move made anywhere in the tree at | |
\ any time, the killer move table values moves which made a cutoff at the | |
\ same depth in the tree recently in the search. | |
0 VALUE ply \ current depth of search | |
CREATE killers MAX_PLY 2* CELLS ALLOT | |
: killersErase killers MAX_PLY 2* CELLS ERASE ; | |
\ NOTE: more efficient to not erase between moves | |
: getKillers ( -- ^k ) ply 2* CELLS killers + ; | |
: killer1 ( -- mv ) getKillers @ ; | |
: killer2 ( -- mv ) ply 2 U< IF 0 EXIT THEN getKillers 4 CELLS - @ ; | |
: killer3 ( -- mv ) getKillers CELL+ @ ; | |
: killer4 ( -- mv ) ply 2 U< IF 0 EXIT THEN getKillers 3 CELLS - @ ; | |
: setKiller ( mv -- ) | |
DUP captureBit AND IF DROP EXIT THEN \ captures already sorted high | |
getKillers 2DUP @ = IF 2DROP EXIT THEN | |
DUP @ OVER CELL+ ! ! ; \ swap and replace | |
\ *** Move Generation *** | |
moveSize CELL+ CONSTANT genSize | |
: genSize* 2* CELLS ; | |
CREATE genStack GEN_STACK genSize* ALLOT | |
CREATE firstMove MAX_PLY 1+ CELLS ALLOT \ stores addresses within gen_dat | |
genStack firstMove ! | |
:inline ^firstMovePly firstMove ply CELLS + ; ( -- ^first ) | |
: firstMovePly ( -- ^gen ) ^firstMovePly @ ; | |
: lastMovePly ( -- ^gen ) ^firstMovePly CELL+ @ ; | |
: forMovesAtPly ( -- lastMovePly firstMovePly ) \ init ?DO LOOP | |
^firstMovePly DUP CELL+ @ SWAP @ ; | |
: genPutMove ( score mv -- ) \ genStack[firstMove[ply+1]++] = mv+score | |
^firstMovePly CELL+ DUP @ DUP genSize + ROT ! 2! ; | |
: genInitPly ^firstMovePly DUP @ SWAP CELL+ ! ; | |
$100000 CONSTANT mvSortFirst | |
: genPushPromotions ( mv -- ) | |
KING KNIGHT DO | |
mvSortFirst I 4 LSHIFT + \ score: sort promotions first | |
OVER I mvPromote! \ move | |
genPutMove \ push it | |
LOOP DROP ; | |
: genPushCapture ( mv -- ) \ most-valuable-victim least-valuable-attacker | |
DUP mvFrom piece@ \ piece moved | |
OVER mvTo piece@ 4 LSHIFT \ piece captured * 16 | |
+ mvSortFirst + \ sort captures first | |
SWAP genPutMove ; | |
: genPush ( mv -- ) mvHistory @ SWAP genPutMove ; | |
pawnBit captureBit OR CONSTANT pawnCaptureBits | |
: ?pushEP ( from piece -- ) | |
OVER edge? IF 2DROP EXIT THEN | |
OVER bd@ <> IF DROP EXIT THEN | |
ep fromTo>mv [ pawnCaptureBits epBit OR ] LITERAL OR | |
[ PAWN DUP 4 LSHIFT + mvSortFirst + ] LITERAL SWAP genPutMove ; | |
: genEP | |
ep IF | |
wtm? IF | |
SW ep + LIGHTPAWN ?pushEP | |
SE ep + LIGHTPAWN ?pushEP | |
ELSE | |
NW ep + DARKPAWN ?pushEP | |
NE ep + DARKPAWN ?pushEP | |
THEN | |
THEN ; | |
: genCastle | |
castle DUP | |
wtm? IF | |
wkCastleBit AND IF | |
sqE1 sqG1 fromTo>mv castleBit OR genPush THEN | |
wqCastleBit AND IF | |
sqE1 sqC1 fromTo>mv castleBit OR genPush THEN | |
ELSE | |
bkCastleBit AND IF | |
sqE8 sqG8 fromTo>mv castleBit OR genPush THEN | |
bqCastleBit AND IF | |
sqE8 sqC8 fromTo>mv castleBit OR genPush THEN | |
THEN ; | |
: ?genCapture ( sq dest piece -- sq ) | |
side AND IF DROP EXIT THEN \ mine? | |
fromTo>fromMv captureBit OR genPushCapture ; | |
[DEFINED] DEFER [IF] | |
DEFER ?genEmpty ' DROP IS ?genEmpty ( sq dest -- sq ) | |
[ELSE] | |
: ?genEmpty POSTPONE DROP ; IMMEDIATE | |
[THEN] | |
: genCapAdjacent ( sq dir -- sq ) | |
OVER + DUP edge? IF DROP EXIT THEN | |
DUP bd@ ?DUP IF ?genCapture EXIT ELSE ?genEmpty THEN ; | |
: genCapSlider ( sq dir -- sq ) \ slide to edge or other piece | |
OVER | |
BEGIN | |
OVER + DUP edge? IF 2DROP EXIT THEN | |
DUP bd@ ?DUP | |
UNTIL | |
ROT DROP ?genCapture ; | |
: genSlider ( sq dir -- sq dest ) \ gen moves until edge or other piece | |
>R DUP | |
BEGIN | |
R@ + DUP edge? IF R> 2DROP EXIT THEN | |
DUP bd@ ?DUP 0= | |
WHILE | |
2DUP fromTo>mv genPush | |
REPEAT ( sq dest ) | |
R> DROP ?genCapture ; | |
: genLPCaptures ( sq dir -- sq ) | |
OVER + | |
DUP edge? IF DROP EXIT THEN | |
DUP bd@ DARK AND IF | |
OVER SWAP fromTo>mv pawnCaptureBits OR | |
OVER rank7? IF | |
genPushPromotions ELSE | |
genPushCapture THEN EXIT | |
ELSE DROP THEN ; | |
: genDPCaptures ( sq dir -- sq ) | |
OVER + | |
DUP edge? IF DROP EXIT THEN | |
DUP bd@ LIGHT AND IF | |
OVER SWAP fromTo>mv pawnCaptureBits OR | |
OVER rank2? IF | |
genPushPromotions ELSE | |
genPushCapture THEN EXIT | |
ELSE DROP THEN ; | |
: genCapsLP ( sq -- sq ) | |
NW genLPCaptures NE genLPCaptures | |
DUP rank7? IF | |
DUP No + bd@ IF EXIT THEN | |
DUP DUP No + fromTo>mv genPushPromotions | |
THEN ; | |
: genCapsDP | |
SW genDPCaptures SE genDPCaptures | |
DUP rank2? IF | |
DUP So + bd@ IF EXIT THEN | |
DUP DUP So + fromTo>mv genPushPromotions | |
THEN ; | |
: genCapsN | |
Kn1 genCapAdjacent Kn2 genCapAdjacent Kn3 genCapAdjacent Kn4 genCapAdjacent | |
Kn5 genCapAdjacent Kn6 genCapAdjacent Kn7 genCapAdjacent Kn8 genCapAdjacent ; | |
: genCapsK | |
No genCapAdjacent Ea genCapAdjacent So genCapAdjacent We genCapAdjacent | |
NE genCapAdjacent SE genCapAdjacent SW genCapAdjacent NW genCapAdjacent ; | |
: genCapsB | |
NE genCapSlider SE genCapSlider SW genCapSlider NW genCapSlider ; | |
: genCapsR | |
No genCapSlider Ea genCapSlider So genCapSlider We genCapSlider ; | |
: genCapsQ | |
genCapsR genCapsB ; | |
pawnBit 2sqBit OR CONSTANT pawn2sqBits | |
: genLP ( sq -- sq ) | |
NW genLPCaptures NE genLPCaptures | |
DUP No + bd@ IF EXIT THEN | |
DUP DUP No + fromTo>mv | |
OVER rank7? IF | |
genPushPromotions ELSE | |
pawnBit OR genPush THEN | |
DUP rank2? IF | |
DUP No 2* + bd@ IF EXIT THEN | |
DUP DUP No 2* + fromTo>mv pawn2sqBits OR genPush | |
THEN ; | |
: genDP | |
SW genDPCaptures SE genDPCaptures | |
DUP So + bd@ IF EXIT THEN | |
DUP DUP So + fromTo>mv | |
OVER rank2? IF | |
genPushPromotions ELSE | |
pawnBit OR genPush THEN | |
DUP rank7? IF | |
DUP So 2* + bd@ IF EXIT THEN | |
DUP DUP So 2* + fromTo>mv pawn2sqBits OR genPush | |
THEN ; | |
[DEFINED] DEFER [IF] | |
: genPushEmpty ( from to -- from ) fromTo>fromMv genPush ; | |
: genN | |
['] genPushEmpty [IS] ?genEmpty genCapsN ['] DROP [IS] ?genEmpty ; | |
: genK | |
['] genPushEmpty [IS] ?genEmpty genCapsK ['] DROP [IS] ?genEmpty ; | |
[ELSE] | |
: genAdjacent ( sq dir -- sq ) \ almost identical to genCapAdjacent | |
OVER + DUP edge? IF DROP EXIT THEN | |
DUP bd@ ?DUP IF ?genCapture EXIT ELSE fromTo>fromMv genPush THEN ; | |
: genN | |
Kn1 genAdjacent Kn2 genAdjacent Kn3 genAdjacent Kn4 genAdjacent | |
Kn5 genAdjacent Kn6 genAdjacent Kn7 genAdjacent Kn8 genAdjacent ; | |
: genK | |
No genAdjacent Ea genAdjacent So genAdjacent We genAdjacent | |
NE genAdjacent SE genAdjacent SW genAdjacent NW genAdjacent ; | |
[THEN] | |
: genB | |
NE genSlider SE genSlider SW genSlider NW genSlider ; | |
: genR | |
No genSlider Ea genSlider So genSlider We genSlider ; | |
: genQ | |
genR genB ; | |
: genNil ." Illegal piece on square " DUP .sq CR ; | |
\ genVector is an interleaved table, | |
\ genSq indexes by [sq]-8 (piece + color: $11..$16, $21..$26) | |
\ genCapsSq indexes by [sq] | |
CREATE genVector | |
' genNil , ' genNil , ' genNil , ' genNil , | |
' genNil , ' genNil , ' genNil , ' genNil , | |
' genNil , ' genLP , ' genN , ' genB , | |
' genR , ' genQ , ' genK , ' genNil , | |
' genNil , ' genCapsLP , ' genCapsN , ' genCapsB , | |
' genCapsR , ' genCapsQ , ' genCapsK , ' genNil , | |
' genNil , ' genDP , ' genN , ' genB , | |
' genR , ' genQ , ' genK , ' genNil , | |
' genNil , ' genCapsDP , ' genCapsN , ' genCapsB , | |
' genCapsR , ' genCapsQ , ' genCapsK , ' genNil , | |
: genCaps | |
genInitPly | |
myPieces DUP @ SWAP CELL+ DO | |
I @ DUP bd@ CELLS genVector + @ EXECUTE DROP | |
CELL +LOOP | |
genEP ; | |
: gen | |
genInitPly | |
myPieces DUP @ SWAP CELL+ DO | |
I @ DUP bd@ CELLS [ genVector 8 CELLS - ] LITERAL + @ EXECUTE DROP | |
CELL +LOOP | |
genCastle | |
genEP ; | |
\ *** Move History Stack *** | |
2 CELLS CONSTANT histSize | |
: histSize* 2* CELLS ; | |
CREATE histStack HIST_STACK histSize* ALLOT | |
histStack VALUE histTop | |
: initHist histStack TO histTop ; | |
: .moveList | |
CR 5 SPACES ." White Black" 0 ( halfmoveNumber ) | |
histTop histStack ?DO | |
DUP 1 AND 0= IF | |
CR DUP 2/ 1+ 3 .R SPACE | |
THEN | |
SPACE I @ .move SPACE | |
1+ | |
histSize +LOOP DROP CR ; | |
: histPush ( mv -- ) | |
histTop \ move (32 bits) | |
2DUP ! CELL+ SWAP | |
mvTo bd@ \ captured piece (w/color 6 bits) | |
castle 8 LSHIFT OR \ castle (4 bits) | |
ep 16 LSHIFT OR \ ep square (7 bits) | |
fifty 24 LSHIFT OR \ fifty move count (7 bits) | |
OVER ! CELL+ TO histTop ; | |
: histPop ( -- capt mv ) | |
histTop | |
CELL- DUP @ | |
DUP 24 RSHIFT TO fifty | |
DUP 16 RSHIFT $FF AND TO ep | |
DUP 8 RSHIFT $FF AND TO castle | |
$FF AND SWAP | |
CELL- DUP TO histTop @ ; | |
\ *** Move and Undo *** | |
\ these items are updated incrementally to save time in eval | |
\ pawnRank[c][f] is the rank of the least advanced pawn of color c | |
\ on file f - 1. If no pawn, set to promotion rank. | |
CREATE darkPawnRank 10 CELLS ALLOT | |
CREATE lightPawnRank 10 CELLS ALLOT | |
: openFile? ( f+1 -- tf ) | |
CELLS DUP lightPawnRank + @ 0= | |
SWAP darkPawnRank + @ 7 = AND ; | |
\ pawn moved, captured, or promoted: update the file (both sides) | |
: updatePawnFile ( file -- ) | |
DUP 1+ CELLS | |
0 OVER lightPawnRank + ! | |
7 OVER darkPawnRank + ! | |
SWAP $70 + DUP $60 - DO | |
I bd@ DUP LIGHTPAWN = IF DROP | |
DUP lightPawnRank + DUP @ I rank MAX SWAP ! | |
ELSE DARKPAWN = IF | |
DUP darkPawnRank + DUP @ I rank MIN SWAP ! | |
THEN THEN | |
So +LOOP DROP ; | |
: updatePawnFiles ( mv -- ) | |
DUP mvFrom file SWAP mvTo file | |
DUP updatePawnFile OVER = IF DROP EXIT THEN | |
updatePawnFile ; | |
VARIABLE lightPieceMat VARIABLE darkPieceMat | |
VARIABLE lightPawnMat VARIABLE darkPawnMat | |
100 CONSTANT pawnValue | |
CREATE pieceValues 0 , pawnValue , 300 , 310 , 500 , 900 , 0 , | |
1200 CONSTANT endgameThreshold | |
3140 CONSTANT maxPieceMat \ $C1C * 1.32129 = $1000 ( 12 RSHIFT ) | |
\ Update material and pawn files incrementally during move and takeback: | |
\ 1. captures 1a. pawn captured 2. promotions 3. en passant 4. pawn moves | |
: updatePieceMaterial ( value -- ) | |
wtm? IF darkPieceMat ELSE lightPieceMat THEN +! ; | |
: updatePawnMaterial ( value -- ) | |
wtm? IF darkPawnMat ELSE lightPawnMat THEN +! ; | |
: updatePromotionMaterial ( pawnValue pieceValue -- ) | |
wtm? IF lightPieceMat +! lightPawnMat ELSE | |
darkPieceMat +! darkPawnMat THEN +! ; | |
\ Update king positions during move and takeback for inCheck? | |
: takeBack ( -- ) | |
switchColors switchPieces | |
ply 1- TO ply | |
histPop ( capt mv ) | |
DUP 0= IF 2DROP EXIT THEN \ null-move abort | |
DUP promoteBit AND IF | |
PAWN side OR OVER mvFrom bd! | |
\ update material | |
pawnValue OVER mvPromote CELLS pieceValues + @ NEGATE | |
updatePromotionMaterial | |
ELSE | |
DUP mvTo bd@ OVER mvFrom bd! | |
THEN | |
DUP mvFrom OVER mvTo myPieces findPiece ! | |
SWAP DUP IF ( mv capt ) | |
\ capture: update material | |
DUP piece DUP PAWN = IF DROP | |
pawnValue updatePawnMaterial | |
OVER pawnBit AND 0= IF | |
OVER mvTo 2DUP bd! file updatePawnFile | |
THEN | |
ELSE | |
CELLS pieceValues + @ updatePieceMaterial | |
THEN | |
OVER mvTo otherPieces addPiece | |
THEN | |
OVER mvTo bd! ( mv ) | |
DUP castleBit AND IF | |
DUP mvTo DUP sqG1 = OVER sqG8 = OR IF \ O-O | |
DUP 1- SWAP 1+ movePiece \ undo the rook | |
ELSE DUP sqC1 = OVER sqC8 = OR IF \ O-O-O | |
DUP 1+ SWAP 1- 1- movePiece \ undo the rook | |
THEN THEN | |
THEN | |
DUP pawnBit AND IF | |
DUP epBit AND IF | |
\ update material | |
pawnValue updatePawnMaterial | |
epCapSq DUP otherPieces addPiece | |
PAWN side otherSide OR SWAP bd! | |
THEN | |
updatePawnFiles EXIT | |
THEN DROP ; | |
: sqCastleMask ( sq -- mask ) | |
allCastleBits SWAP DUP rank8? IF | |
DUP sqA8 = IF DROP bqCastleBit XOR EXIT THEN | |
DUP sqE8 = IF DROP bCastleBits XOR EXIT THEN | |
sqH8 = IF bkCastleBit XOR THEN EXIT | |
ELSE DUP rank1? IF | |
DUP sqA1 = IF DROP wqCastleBit XOR EXIT THEN | |
DUP sqE1 = IF DROP wCastleBits XOR EXIT THEN | |
sqH1 = IF wkCastleBit XOR THEN EXIT | |
ELSE | |
DROP THEN THEN ; | |
FALSE VALUE lastMoveCheck? | |
: makeMove ( mv -- legal? ) | |
DUP castleBit AND IF | |
DUP mvTo DUP sqG1 = OVER sqG8 = OR IF \ O-O | |
DUP bd@ OVER 1- bd@ OR IF 2DROP FALSE EXIT THEN | |
wtm? inCheck? IF 2DROP FALSE EXIT THEN | |
DUP 1- otherPieces attacks? IF 2DROP FALSE EXIT THEN | |
DUP 1+ SWAP 1- movePiece \ OK: move the rook | |
ELSE DUP sqC1 = OVER sqC8 = OR IF \ O-O-O | |
DUP bd@ OVER 1- bd@ OR OVER 1+ bd@ OR IF | |
2DROP FALSE EXIT THEN | |
wtm? inCheck? IF 2DROP FALSE EXIT THEN | |
DUP 1+ otherPieces attacks? IF 2DROP FALSE EXIT THEN | |
DUP 1- 1- SWAP 1+ movePiece \ OK: move the rook | |
ELSE | |
DROP .move ." : bad castle" FALSE EXIT | |
THEN THEN | |
THEN | |
DUP histPush | |
ply 1+ TO ply | |
castle IF | |
castle OVER mvTo sqCastleMask AND OVER mvFrom sqCastleMask AND TO castle | |
THEN | |
DUP 2sqBit AND IF | |
epSq ELSE | |
0 THEN TO ep | |
DUP reset50Bits AND IF | |
0 ELSE | |
fifty 1+ THEN TO fifty | |
DUP mvTo | |
DUP bd@ ?DUP IF | |
OVER otherPieces removePiece | |
\ capture: update material | |
piece DUP PAWN = IF DROP | |
pawnValue NEGATE updatePawnMaterial | |
OVER pawnBit AND 0= IF | |
0 OVER bd! DUP file updatePawnFile THEN | |
ELSE | |
CELLS pieceValues + @ NEGATE updatePieceMaterial | |
THEN | |
THEN | |
OVER mvFrom SWAP movePiece | |
DUP pawnBit AND IF | |
DUP promoteBit AND IF | |
DUP mvPromote side OR OVER mvTo bd! | |
\ update material | |
pawnValue NEGATE OVER mvPromote CELLS pieceValues + @ | |
updatePromotionMaterial | |
ELSE DUP epBit AND IF | |
\ update material | |
pawnValue NEGATE updatePawnMaterial | |
epCapSq 0 OVER bd! | |
otherPieces removePiece | |
THEN THEN | |
updatePawnFiles | |
ELSE | |
DROP THEN | |
wtm? switchColors switchPieces | |
inCheck? IF | |
takeBack FALSE ELSE | |
wtm? inCheck? TO lastMoveCheck? TRUE THEN ; | |
\ *** Evaluation *** | |
\ Eval should return the same value as the original TSCP. | |
\ It is only modified for the different board type, use of a vector | |
\ instead of a switch to evaluate pieces, and some terms updated | |
\ incrementally by makemove/takeback instead of recalculated every time. | |
\ Evaluation is much better commented in the original TSCP source. | |
: evalSetupSq ( sq -- sq ) | |
DUP bd@ DUP LIGHTPAWN = IF | |
DROP pawnValue lightPawnMat +! | |
DUP file 1+ CELLS lightPawnRank + ( sq ^lpr ) | |
OVER rank OVER @ MAX SWAP ! | |
ELSE DUP DARKPAWN = IF | |
DROP pawnValue darkPawnMat +! | |
DUP file 1+ CELLS darkPawnRank + | |
OVER rank OVER @ MIN SWAP ! | |
ELSE ?DUP IF | |
DUP piece CELLS pieceValues + @ | |
SWAP LIGHT AND IF lightPieceMat ELSE darkPieceMat THEN +! | |
THEN THEN THEN ; | |
\ !!! piece lists | |
: evalSetup ( -- ) \ call after setting up a position or new game | |
10 0 DO | |
0 lightPawnRank I CELLS + ! | |
7 darkPawnRank I CELLS + ! | |
LOOP | |
0 lightPieceMat ! 0 darkPieceMat ! | |
0 lightPawnMat ! 0 darkPawnMat ! | |
['] evalSetupSq forEverySq ; | |
-10 CONSTANT DOUBLED_PAWN_PENALTY | |
-20 CONSTANT ISOLATED_PAWN_PENALTY | |
-8 CONSTANT BACKWARD_PAWN_PENALTY | |
20 CONSTANT PASSED_PAWN_BONUS | |
10 CONSTANT ROOK_SEMI_OPEN_FILE_BONUS | |
15 CONSTANT ROOK_OPEN_FILE_BONUS | |
20 CONSTANT ROOK_ON_SEVENTH_BONUS | |
\ The following tables are 128 * 64 piece square tables. | |
\ Each table has two "entry points" so that sq values | |
\ can act directly as indices and we save space. | |
\ The tables are flipped vertically if used for black. | |
\ Since they are all symetrical horizontally, we can rotate them instead. | |
\ exception: king table has light and dark versions | |
\ exception: king endgame table is 8-fold symmetric, can be used unrotated | |
CREATE pawnPcSq | |
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , | |
-10 , -10 , -10 , -10 , -10 , -10 , -10 , -10 , | |
5 , 10 , 15 , 20 , 20 , 15 , 10 , 5 , | |
-10 , 0 , 0 , 0 , 0 , 0 , 0 , -10 , | |
4 , 8 , 12 , 16 , 16 , 12 , 8 , 4 , | |
-10 , 0 , 5 , 5 , 5 , 5 , 0 , -10 , | |
3 , 6 , 9 , 12 , 12 , 9 , 6 , 3 , | |
-10 , 0 , 5 , 10 , 10 , 5 , 0 , -10 , | |
2 , 4 , 6 , 8 , 8 , 6 , 4 , 2 , | |
-10 , 0 , 5 , 10 , 10 , 5 , 0 , -10 , | |
1 , 2 , 3 , -10 , -10 , 3 , 2 , 1 , | |
-10 , 0 , 5 , 5 , 5 , 5 , 0 , -10 , | |
0 , 0 , 0 , -40 , -40 , 0 , 0 , 0 , | |
-10 , 0 , 0 , 0 , 0 , 0 , 0 , -10 , | |
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , | |
-10 , -30 , -10 , -10 , -10 , -10 , -30 , -10 , | |
pawnPcSq 8 CELLS + CONSTANT knightPcSq | |
CREATE bishopPcSq | |
-10 , -10 , -10 , -10 , -10 , -10 , -10 , -10 , | |
0 , 10 , 20 , 30 , 30 , 20 , 10 , 0 , | |
-10 , 0 , 0 , 0 , 0 , 0 , 0 , -10 , | |
10 , 20 , 30 , 40 , 40 , 30 , 20 , 10 , | |
-10 , 0 , 5 , 5 , 5 , 5 , 0 , -10 , | |
20 , 30 , 40 , 50 , 50 , 40 , 30 , 20 , | |
-10 , 0 , 5 , 10 , 10 , 5 , 0 , -10 , | |
30 , 40 , 50 , 60 , 60 , 50 , 40 , 30 , | |
-10 , 0 , 5 , 10 , 10 , 5 , 0 , -10 , | |
30 , 40 , 50 , 60 , 60 , 50 , 40 , 30 , | |
-10 , 0 , 5 , 5 , 5 , 5 , 0 , -10 , | |
20 , 30 , 40 , 50 , 50 , 40 , 30 , 20 , | |
-10 , 0 , 0 , 0 , 0 , 0 , 0 , -10 , | |
10 , 20 , 30 , 40 , 40 , 30 , 20 , 10 , | |
-10 , -10 , -20 , -10 , -10 , -20 , -10 , -10 , | |
0 , 10 , 20 , 30 , 30 , 20 , 10 , 0 , | |
bishopPcSq 8 CELLS + CONSTANT kingEndgamePcSq | |
CREATE kingLtPcSq | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
0 , 20 , 40 , -20 , 0 , -20 , 40 , 20 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-20 , -20 , -20 , -20 , -20 , -20 , -20 , -20 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
-20 , -20 , -20 , -20 , -20 , -20 , -20 , -20 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
0 , 20 , 40 , -20 , 0 , -20 , 40 , 20 , | |
-40 , -40 , -40 , -40 , -40 , -40 , -40 , -40 , | |
kingLtPcSq 8 CELLS + CONSTANT kingDkPcSq | |
VARIABLE lightScore VARIABLE darkScore | |
CREATE myCastledPawnRankPenalties | |
-20 , 0 , -10 , -20 , -20 , -20 , -20 , -25 , | |
CREATE enemyCastledPawnRankPenalties | |
0 , 0 , 0 , 0 , -5 , -10 , 0 , -15 , | |
: evalLightKP ( file+1 -- value ) | |
DUP CELLS lightPawnRank + @ NEGATE 7 + | |
CELLS myCastledPawnRankPenalties + @ | |
SWAP CELLS darkPawnRank + @ | |
CELLS enemyCastledPawnRankPenalties + @ | |
+ ; | |
: evalDarkKP ( file+1 -- value ) | |
DUP CELLS darkPawnRank + @ | |
CELLS myCastledPawnRankPenalties + @ | |
SWAP CELLS lightPawnRank + @ NEGATE 7 + | |
CELLS enemyCastledPawnRankPenalties + @ | |
+ ; | |
: evalCenterKPs ( val file -- val ) | |
TUCK openFile? IF -10 + THEN | |
OVER 1+ openFile? IF -10 + THEN | |
SWAP 1+ 1+ openFile? IF -10 + THEN ; | |
: taxicab ( sq1 sq2 -- dist ) \ taxicab distance | |
OVER rank OVER rank - ABS >R | |
file SWAP file - ABS R> + ; | |
: opposition ( -- bonus ) | |
lkSq dkSq taxicab | |
64 OVER 2/ RSHIFT \ worth more if kings closer | |
SWAP 1 AND IF NEGATE THEN ; | |
: rookTrapped? ( val dir sq -- val ) | |
BEGIN OVER + | |
DUP edge? IF 2DROP EXIT THEN | |
DUP piece@ ROOK = | |
UNTIL 2DROP -60 + ; | |
: evalLK ( sq -- sq ) | |
darkPieceMat @ endgameThreshold U< IF | |
DUP CELLS kingEndgamePcSq + @ lightScore +! | |
wtm? IF opposition lightScore +! THEN EXIT | |
THEN | |
DUP CELLS kingLtPcSq + @ ( sq value ) | |
OVER file DUP 3 U< IF DROP | |
OVER We SWAP rookTrapped? | |
1 evalLightKP + | |
2 evalLightKP + | |
3 evalLightKP 2/ + | |
ELSE 4 OVER U< IF DROP | |
OVER Ea SWAP rookTrapped? | |
8 evalLightKP + | |
7 evalLightKP + | |
6 evalLightKP 2/ + | |
ELSE | |
evalCenterKPs | |
THEN THEN | |
darkPieceMat @ M* maxPieceMat SM/REM NIP \ expensive? | |
lightScore +! ; | |
: evalDK ( sq -- sq ) | |
lightPieceMat @ endgameThreshold U< IF | |
DUP CELLS kingEndgamePcSq + @ darkScore +! | |
wtm? 0= IF opposition darkScore +! THEN EXIT | |
THEN | |
DUP CELLS kingDkPcSq + @ ( sq value ) | |
OVER file | |
DUP 3 U< IF DROP | |
OVER We SWAP rookTrapped? | |
1 evalDarkKP + | |
2 evalDarkKP + | |
3 evalDarkKP 2/ + | |
ELSE 4 OVER U< IF DROP | |
OVER Ea SWAP rookTrapped? | |
8 evalDarkKP + | |
7 evalDarkKP + | |
6 evalDarkKP 2/ + | |
ELSE | |
evalCenterKPs | |
THEN THEN | |
lightPieceMat @ M* maxPieceMat SM/REM NIP \ expensive? | |
darkScore +! ; | |
: evalLP ( sq -- sq ) | |
DUP DUP CELLS pawnPcSq + @ >R ( R: value ) | |
DUP file 1+ CELLS ( sq f+1 ) | |
DUP lightPawnRank + ( sq f+1 ^lpr ) | |
ROT rank ( f+1 ^lpr r ) | |
OVER @ OVER U> IF | |
R> DOUBLED_PAWN_PENALTY + >R THEN | |
SWAP DUP CELL+ @ SWAP CELL- @ MAX ( f+1 r max[lpr[f+2],lpr[f]] ) | |
DUP 0= IF | |
R> ISOLATED_PAWN_PENALTY + >R DROP ELSE ( f+1 r ) | |
OVER U< IF | |
R> BACKWARD_PAWN_PENALTY + >R THEN THEN ( f+1 r ) | |
SWAP darkPawnRank + ( r ^dpr ) | |
DUP CELL+ @ OVER CELL- @ MIN SWAP @ MIN OVER U< IF ( r ) | |
DROP R> lightScore +! EXIT ELSE | |
NEGATE 7 + PASSED_PAWN_BONUS * R> + lightScore +! THEN ; | |
: evalDP ( sq -- sq ) | |
DUP DUP rotate CELLS pawnPcSq + @ >R ( R: value ) | |
DUP file 1+ CELLS | |
DUP darkPawnRank + | |
ROT rank ( f+1 ^lpr r ) | |
OVER @ OVER U< IF | |
R> DOUBLED_PAWN_PENALTY + >R THEN | |
SWAP DUP CELL+ @ SWAP CELL- @ MIN | |
DUP 7 = IF | |
R> ISOLATED_PAWN_PENALTY + >R DROP ELSE ( f+1 r ) | |
OVER U> IF | |
R> BACKWARD_PAWN_PENALTY + >R THEN THEN ( f+1 r ) | |
SWAP lightPawnRank + | |
DUP CELL+ @ OVER CELL- @ MAX SWAP @ MAX OVER U> IF ( r ) | |
DROP R> darkScore +! EXIT ELSE | |
PASSED_PAWN_BONUS * R> + darkScore +! THEN ; | |
: evalLR ( sq -- sq ) | |
DUP file 1+ CELLS DUP lightPawnRank + @ 0= IF | |
darkPawnRank + @ 7 = IF | |
ROOK_OPEN_FILE_BONUS ELSE | |
ROOK_SEMI_OPEN_FILE_BONUS THEN | |
lightScore +! | |
ELSE | |
DROP THEN | |
DUP rank7? IF | |
ROOK_ON_SEVENTH_BONUS lightScore +! THEN ; | |
: evalDR ( sq -- sq ) | |
DUP file 1+ CELLS DUP darkPawnRank + @ 7 = IF | |
lightPawnRank + @ 0= IF | |
ROOK_OPEN_FILE_BONUS ELSE | |
ROOK_SEMI_OPEN_FILE_BONUS THEN | |
darkScore +! | |
ELSE | |
DROP THEN | |
DUP rank2? IF | |
ROOK_ON_SEVENTH_BONUS darkScore +! THEN ; | |
\ all the evalVector words are ( sq -- sq ) | |
: evalLN DUP CELLS knightPcSq + @ lightScore +! ; | |
: evalLB DUP CELLS bishopPcSq + @ lightScore +! ; | |
: evalDN DUP rotate CELLS knightPcSq + @ darkScore +! ; | |
: evalDB DUP rotate CELLS bishopPcSq + @ darkScore +! ; | |
: evalQ ; | |
: evalNil ." Illegal piece on square " DUP .sq CR ; | |
[UNDEFINED] NOOP [IF] | |
: NOOP ; | |
[THEN] | |
CREATE evalVector \ indexed by [sq] (piece + color: $11..$26) | |
' NOOP , ' evalNil , ' evalNil , ' evalNil , | |
' evalNil , ' evalNil , ' evalNil , ' evalNil , | |
' evalNil , ' evalNil , ' evalNil , ' evalNil , | |
' evalNil , ' evalNil , ' evalNil , ' evalNil , | |
' evalNil , ' evalLP , ' evalLN , ' evalLB , | |
' evalLR , ' evalQ , ' evalLK , ' evalNil , | |
' evalNil , ' evalNil , ' evalNil , ' evalNil , | |
' evalNil , ' evalNil , ' evalNil , ' evalNil , | |
' evalNil , ' evalDP , ' evalDN , ' evalDB , | |
' evalDR , ' evalQ , ' evalDK , ' evalNil , | |
: evalSq ( sq -- sq ) | |
DUP bd@ CELLS evalVector + @ EXECUTE ; | |
: eval ( -- value ) | |
lightPieceMat @ lightPawnMat @ + lightScore ! | |
darkPieceMat @ darkPawnMat @ + darkScore ! | |
lightPieces ['] evalSq forEachPiece | |
darkPieces ['] evalSq forEachPiece | |
wtm? IF | |
lightScore @ darkScore @ - EXIT ELSE | |
darkScore @ lightScore @ - THEN ; | |
\ *** Search *** | |
\ Principal Variation tracking | |
\ ( can be made 2* smaller with more expensive indexing ) | |
CREATE pv MAX_PLY ( 1+ ) MAX_PLY* ( 2/ ) CELLS ALLOT | |
CREATE pvLen MAX_PLY 1+ CELLS ALLOT | |
: pvErase pv MAX_PLY ( 1+ ) MAX_PLY* ( 2/ ) CELLS ERASE ; | |
: getPV ( ply -- ^mv ) CELLS DUP MAX_PLY* + pv + ; | |
\ : getPV ( ply -- ^mv ) DUP NEGATE 1+ MAX_PLY 2* + * 2/ CELLS pv + ; | |
: getPVlen ( ply -- len ) CELLS pvLen + @ ; | |
: setPVlen ( len ply -- ) CELLS pvLen + ! ; | |
: resetPVlen 0 ply setPVlen ; | |
: updatePV ( mv -- ) \ copy mv + pv[ply+1] to pv[ply] | |
ply getPV TUCK ! CELL+ | |
ply 1+ getPVlen ?DUP IF | |
>R ply 1+ getPV SWAP R@ CELLS MOVE R> 1+ | |
ELSE DROP 1 THEN ply setPVlen ; | |
: .pv pvLen @ 0 ?DO I CELLS pv + @ .move SPACE LOOP ; | |
\ *** Move sorting *** | |
\ Instead of using mvSortFirst shifted to order the high priority moves (PV, killers) | |
\ use a "vector iterator". Avoids iterating once to find the move and | |
\ set its value highest, then iterating again later to find that highest value. | |
\ The sort word invoked by curSort changes as higher priority moves are exhausted. | |
CREATE pSort MAX_PLY CELLS ALLOT \ points into sorts or sortsQ below | |
: pSortForPly ( -- ^pSort ) ply CELLS pSort + ; | |
: setFirstSort ( ^pSort -- ) pSortForPly ! ; | |
: nextSort CELL pSortForPly +! ; | |
: curSort ( ^from -- ) pSortForPly @ @ EXECUTE ; | |
: findMove ( ^from mv -- ^from ^mv|0 ) | |
DUP 0= IF | |
( FALSE ) EXIT THEN | |
OVER lastMovePly SWAP ?DO | |
DUP I @ = IF | |
DROP I UNLOOP EXIT | |
THEN | |
genSize +LOOP | |
DROP FALSE ; | |
: swapMoves ( ^m1 ^m2 -- ) \ swap two moves on genStack | |
DUP 2@ 2>R OVER 2@ ROT 2! 2R> ROT 2! ; | |
\ all sort words are ( ^from -- ) \ from points into genStack | |
: sortNoop DROP ; | |
: sort ( ^from -- ) \ sort by move score | |
DUP DUP CELL+ @ ( ^from ^best bestScore ) | |
OVER CELL+ lastMovePly CELL+ SWAP ?DO | |
DUP I @ U< IF | |
2DROP I CELL- I @ | |
THEN | |
genSize +LOOP | |
0= IF | |
2DROP nextSort EXIT THEN \ sortNoop | |
2DUP = IF | |
2DROP EXIT THEN \ already have best move in position | |
swapMoves ; | |
: sortKiller ( ^from mv -- ) nextSort | |
findMove ?DUP IF | |
swapMoves EXIT THEN | |
curSort ; \ killer isn't valid: try next move | |
: sortKiller4 killer4 sortKiller ; | |
: sortKiller3 killer3 sortKiller ; | |
: sortKiller2 killer2 sortKiller ; | |
: sortKiller1 killer1 sortKiller ; | |
: sortCaptures ( ^from -- ) | |
DUP sort | |
CELL+ @ mvSortFirst U< IF \ no more captures | |
nextSort THEN ; \ sortKiller1 | |
VARIABLE 'firstSort \ points to sortPV or sortCaptures | |
VARIABLE 'firstSortQ \ points to sortPV or sort | |
\ iterative deepening search is optimal if we first follow the previous PV | |
: sortPV ( ^from -- ) nextSort | |
ply CELLS pv + @ findMove ?DUP IF | |
swapMoves EXIT THEN | |
CELL 'firstSortQ +! \ sort | |
CELL 'firstSort +! curSort ; \ sortCaptures | |
CREATE sorts \ sort order for main search | |
' sortPV , ( ' sortHash , ) ' sortCaptures , ' sortKiller1 , | |
' sortKiller2 , ' sortKiller3 , ' sortKiller4 , ' sort , ' sortNoop , | |
CREATE sortsQ \ sort order for quiescence search | |
' sortPV , ( ' sortHash , ) ' sort , ' sortNoop , | |
: followPV sorts 'firstSort ! sortsQ 'firstSortQ ! ; | |
: initSortQ 'firstSortQ @ setFirstSort ; | |
: initSort 'firstSort @ setFirstSort ; | |
\ Win32Forth word ms@ returns number of milliseconds since system start. | |
\ Define as appropriate for your Forth dialect | |
3600000 VALUE msMaxThinkTime | |
[UNDEFINED] ms@ [IF] | |
[DEFINED] ?MS [IF] ( -- ms ) | |
: ms@ ?MS ; \ iForth | |
[ELSE] [DEFINED] cputime [IF] ( -- Dusec ) | |
: ms@ cputime d+ 1000 um/mod nip ; \ gforth: Anton Ertl | |
[ELSE] [DEFINED] timer@ [IF] ( -- Dusec ) | |
: ms@ timer@ >us 1000 um/mod nip ; \ bigForth | |
[ELSE] [DEFINED] gettimeofday [IF] ( -- usec sec ) | |
: ms@ gettimeofday 1000 MOD 1000 * SWAP 1000 / + ; \ PFE | |
[ELSE] [DEFINED] counter [IF] | |
: ms@ counter ; \ swiftForth | |
[ELSE] [DEFINED] GetTickCount [IF] | |
: ms@ GetTickCount ; \ VFX Forth (Windows) | |
[ELSE] [DEFINED] ticks [IF] | |
: ms@ ticks ; \ VFX Forth (Linux) | |
[ELSE] [DEFINED] MICROSECS [IF] | |
: ms@ microsecs 1000 UM/MOD nip ; \ MacForth | |
[ELSE] | |
CR .( Warning! need definition for a millisecond timer ms@ ) | |
10 CONSTANT npms | |
: ms@ ( -- n ) nodes npms / ; \ bogus | |
[THEN] [THEN] [THEN] [THEN] [THEN] [THEN] [THEN] [THEN] | |
[ELSE] | |
\ Win32Forth microsecond counter rolls over every day | |
1000 60 * 60 * 24 * constant MaxMsDay | |
[THEN] | |
: .ms ( ms -- ) S>D 1000 UM/MOD 5 U.R ." ." 0 <# # # # #> TYPE ; | |
0 VALUE msStart | |
: startTimer ms@ TO msStart ; | |
: readTimer ( -- ms ) ms@ msStart - ; | |
[DEFINED] MaxMsDay [IF] | |
: readTimer ( -- ms ) \ >>> Also right when 00:00 passes | |
readTimer DUP 0< IF MaxMsDay + THEN ; | |
[THEN] | |
0 VALUE keyHit | |
: keyHitQ? keyHit tolower [char] q = 0 TO keyHit ; | |
: (checkTime) ( -- ) | |
readTimer msMaxThinkTime > | |
KEY? ( DUP IF KEY TO keyHit THEN ) \ or interpret; break on ? or force | |
OR 1 AND THROW ; \ caught by think | |
[DEFINED] DEFER [IF] | |
DEFER checkTime ' (checkTime) IS checkTime | |
[ELSE] | |
: checkTime (checkTime) ; | |
[THEN] | |
0 CONSTANT drawScore | |
100 CONSTANT 50moveCount \ ply 2* | |
-10000 CONSTANT mateScore | |
: incNodes ( -- nodes ) nodes 1+ DUP TO nodes ; | |
: ?checkTime ( nodes -- ) $3FF AND IF EXIT ELSE checkTime THEN ; | |
: maxSearch? ( -- exit? ) | |
[ MAX_PLY 1- ] LITERAL ply U< | |
[ histStack HIST_STACK histSize* + 1- ] LITERAL histTop U< OR ; | |
: quiesce ( -b a -- -b value ) ( adds -b -- when recursing ) | |
incNodes ?checkTime | |
resetPVlen | |
maxSearch? IF DROP eval EXIT THEN | |
eval MAX \ a < e: a = e | |
OVER NEGATE OVER > 0= IF | |
DROP DUP NEGATE EXIT THEN \ e >= b: return beta | |
genCaps | |
initSortQ | |
forMovesAtPly ?DO \ foreach move | |
I curSort | |
I @ makeMove IF | |
OVER RECURSE NEGATE ( -b a value ) \ negamax | |
takeBack | |
2DUP < IF \ value > a: new best move | |
NIP \ a = value | |
OVER NEGATE OVER > 0= IF \ value >= b: cutoff | |
DROP DUP NEGATE UNLOOP EXIT THEN \ return beta | |
I @ updatePV | |
ELSE | |
DROP THEN | |
THEN | |
genSize +LOOP ; \ return alpha | |
: asIndex ( sq -- 0-63 ) DUP file SWAP $F0 AND 2/ OR ; | |
CREATE repsBd 64 CELLS ALLOT | |
: reps ( -- n ) | |
fifty 4 U< IF 0 EXIT THEN | |
repsBd 64 CELLS ERASE 0 0 ( reps count ) | |
histTop histSize - DUP fifty 1- histSize* - SWAP DO | |
I @ 0= IF LEAVE THEN \ meaningless if a null-move variation | |
I @ mvFrom asIndex CELLS repsBd + DUP @ 1+ DUP ROT ! | |
IF 1+ ELSE 1- THEN | |
I @ mvTo asIndex CELLS repsBd + DUP @ 1- DUP ROT ! | |
IF 1+ ELSE 1- THEN | |
?DUP 0= IF 1+ 0 THEN | |
histSize NEGATE +LOOP DROP ; | |
\ these things would normally be on the call frame | |
1 VALUE _depth | |
CREATE searchFlags MAX_PLY CELLS ALLOT | |
: sfPly ( -- ^flags ) ply CELLS searchFlags + ; | |
: sfClear _depth sfPly ! ; \ saves depth and clears flags | |
: sfDepth sfPly @ $3F AND ; | |
: sfCheck! $40 sfPly +! ; | |
: sfCheck? sfPly @ $40 AND ; | |
: sfMoves! $100 sfPly +! ; | |
: sfMoves? sfPly @ $FF00 AND ; | |
: sfRestoreDepth sfDepth TO _depth ; \ undo depth extensions | |
: .searchHeader quiet IF EXIT THEN | |
CR ." ply nodes time score pv" CR ; | |
\ code char: BL depth complete, '&' new best move, '-' '+' fail low/high | |
: .score ( value -- ) | |
mateScore ABS OVER ABS 100 + U< IF | |
." Mat" DUP 0< IF [CHAR] - ELSE [CHAR] + THEN EMIT | |
ABS mateScore + ABS 1+ 2/ . | |
ELSE 6 .R SPACE THEN ; | |
[DEFINED] DEFER [IF] | |
\ support for test suites | |
DEFER onSearchStatus ' NOOP IS onSearchStatus ( value code ) | |
[ELSE] | |
: onSearchStatus ; IMMEDIATE \ noop | |
[THEN] | |
: .searchStatus ( value codeChar -- ) onSearchStatus \ BASE @ >R DECIMAL | |
quiet IF 2DROP EXIT THEN | |
EMIT readTimer sfDepth 2 .R nodes 10 U.R | |
.ms ( value ) .score SPACE .pv CR ; \ R> BASE ! ; | |
: ?.searchStatus ( v code -- ) 3 sfDepth U< IF .searchStatus ELSE 2DROP THEN ; | |
0 VALUE moveCount | |
: .curMove ( ^move -- ) quiet IF DROP EXIT THEN | |
moveCount 1+ DUP U. to moveCount | |
DUP @ .move SPACE CELL+ @ U. 8 SPACES 13 EMIT ; | |
\ null move heuristic, R=2 | |
\ Don't search at a position if we are doing fine even if we make no move. | |
: makeNullMove _depth -2 + TO _depth | |
0 histPush 0 TO ep switchColors switchPieces ply 1+ TO ply ; \ !!! fifty 1+ | |
: undoNullMove _depth 2 + TO _depth | |
histPop ABORT" Not a null move!" DROP switchColors switchPieces ply 1- TO ply ; | |
: tryNullMove? ( -- tf ) | |
\ _depth 0> 0= IF FALSE EXIT THEN \ not at leaf | |
lastMoveCheck? IF FALSE EXIT THEN \ not in check | |
ply 0= IF FALSE EXIT THEN \ and not at root | |
histTop histSize - @ 0= IF FALSE EXIT THEN \ and last move not null | |
darkPieceMat @ lightPieceMat @ + ( MIN) endgameThreshold < IF FALSE EXIT THEN | |
genInitPly makeNullMove TRUE ; \ and not endgame [zugzwang] | |
: _search ( -b a -- -b value ) \ recursive | |
incNodes ?checkTime | |
ply IF resetPVlen reps IF DROP drawScore EXIT THEN ELSE 0 TO moveCount THEN \ draw: repeated pos | |
maxSearch? IF DROP eval EXIT THEN | |
sfClear | |
lastMoveCheck? IF | |
sfCheck! ELSE \ extend search a ply | |
_depth 1- TO _depth THEN \ undo before exit with sfRestoreDepth | |
tryNullMove? IF \ _depth 2- | |
OVER DUP NEGATE 1- SWAP ( b-1 -b ) | |
_depth 0> IF RECURSE ELSE quiesce THEN NEGATE ( b-1 value ) | |
undoNullMove \ _depth 2+ | |
< IF | |
DROP DUP NEGATE sfRestoreDepth EXIT THEN | |
THEN | |
gen | |
initSort | |
forMovesAtPly ?DO \ foreach move | |
I curSort | |
\ ply 0= IF 5 sfDepth U< IF I .curMove THEN THEN ( optional) | |
I @ makeMove IF \ ply++ | |
OVER _depth lastMoveCheck? OR IF RECURSE ELSE quiesce THEN NEGATE | |
takeBack \ ply-- | |
sfMoves! | |
( -b a value ) 2DUP < IF \ value > a: new best move | |
NIP ( or MAX ) \ a = value | |
_depth 1+ I @ mvHistory NIP +! | |
OVER NEGATE OVER > 0= IF \ value >= b: cutoff | |
ply 0= IF I @ updatePV THEN \ save fail-high move for re-search | |
I @ setKiller | |
DROP DUP NEGATE sfRestoreDepth UNLOOP EXIT \ return beta | |
THEN | |
I @ updatePV | |
ply 0= IF DUP [CHAR] & ?.searchStatus THEN | |
ELSE | |
DROP THEN | |
THEN | |
genSize +LOOP | |
ply getPVlen IF ply getPV @ setKiller THEN | |
sfRestoreDepth | |
sfMoves? IF | |
fifty 50moveCount U< 0= IF | |
DROP drawScore THEN EXIT \ draw, 50-move rule (else return alpha) | |
ELSE | |
DROP sfCheck? IF | |
mateScore ply + ELSE \ checkmate | |
drawScore THEN \ stalemate | |
THEN ; | |
MAX_PLY 2/ VALUE maxDepth | |
: startDepth ( -- n ) pvErase 1 ; | |
: prepSearch wtm? inCheck? TO lastMoveCheck? followPV ; | |
: failHighSearch ( v newAlpha -- newV ) | |
SWAP [CHAR] + ?.searchStatus prepSearch | |
mateScore SWAP _search NIP ; | |
: failLowSearch ( v newBeta -- newV ) | |
1 pvLen ! SWAP [CHAR] - ?.searchStatus prepSearch | |
NEGATE mateScore _search NIP ; | |
: ?research ( alpha -beta value -- value ) >R NEGATE R> | |
2DUP > 0= IF | |
OVER 1- failHighSearch | |
2DUP > IF | |
mateScore NEGATE failLowSearch | |
THEN NIP | |
ELSE NIP ( a v ) | |
2DUP < 0= IF | |
OVER 1+ failLowSearch | |
2DUP < IF | |
mateScore failHighSearch | |
THEN | |
THEN THEN NIP ; | |
: thinker | |
0 TO ply | |
historyAge | |
.searchHeader | |
startTimer | |
mateScore DUP ( alpha -beta ) | |
0 TO nodes | |
maxDepth startDepth DO \ iterative deepening | |
I TO _depth | |
prepSearch | |
OVER _search ( alpha -beta value ) | |
?research ( value ) | |
DUP BL .searchStatus | |
mateScore 100 + ABS OVER ABS U< IF DUP LEAVE THEN \ mate | |
pawnValue 2/ - DUP pawnValue + NEGATE \ restrict a-b window for more cutoffs | |
LOOP 2DROP ; | |
: ?thinkAbort ( err -- ) | |
DUP 1 <> IF THROW EXIT THEN DROP | |
quiet 0= IF | |
." Time's up! " nodes . ." nodes" readTimer .ms ." seconds" CR | |
THEN | |
ply 0 DO takeBack LOOP | |
0 TO ply sfRestoreDepth ; | |
: .thinkResult quiet IF EXIT THEN | |
readTimer ?DUP IF | |
nodes 1000 UM* ROT UM/MOD NIP ELSE | |
." at least " nodes THEN | |
U. ." nps " ; | |
: think ( -- err ) ['] thinker CATCH DUP ?thinkAbort .thinkResult ; | |
\ *** high level (validated) *** | |
: initVars 0 TO ply 0 TO ep 0 TO fifty | |
evalSetup initHist historyErase killersErase ; | |
: initBoard | |
eraseBoard | |
ROOK KNIGHT BISHOP KING QUEEN BISHOP KNIGHT ROOK 8 0 DO | |
DUP DARK + I bd! \ top row of pieces | |
DARKPAWN I $10 + bd! \ pawns | |
LIGHTPAWN I $60 + bd! \ pawns | |
LIGHT + I $70 + bd! \ bottom row of pieces | |
LOOP | |
initPieces | |
allCastleBits TO castle | |
setWtm | |
initVars ; | |
: str>sq ( c-addr -- sq T | x F ) | |
DUP C@ tolower [CHAR] a - ( ^c file ) | |
SWAP CHAR+ C@ [CHAR] 8 SWAP - ( file rank ) | |
2DUP OR 0 8 WITHIN IF | |
fileRank>sq TRUE ELSE | |
DROP FALSE THEN ; | |
: char>piece ( c -- piece | 0 ) | |
0 SWAP | |
KING 1+ PAWN DO | |
I CELLS symbols + @ | |
2DUP = IF 2DROP | |
LIGHT I OR SWAP LEAVE | |
ELSE tolower OVER = IF DROP | |
DARK I OR SWAP LEAVE | |
THEN THEN | |
LOOP DROP ; | |
: findFromTo ( to from -- ^mv|0 ) | |
SWAP fromTo>mv 0 TO ply gen | |
forMovesAtPly ?DO | |
DUP I @ $FFFF AND = IF | |
DROP I UNLOOP EXIT \ found! its legal | |
THEN | |
genSize +LOOP DROP 0 ; | |
: ?promoteMv ( str count ^mv -- str count ^mv ) | |
DUP @ promoteBit AND IF \ get promotion piece | |
>R OVER C@ char>piece piece | |
DUP KNIGHT KING WITHIN 0= IF DROP QUEEN THEN | |
KNIGHT - genSize* >R 1 /STRING 2R> + | |
THEN ; | |
: str>mv ( str count -- str' count' mv ) | |
OVER str>sq 0= ABORT" Malformed move." | |
>R 2 /STRING OVER str>sq 0= ABORT" Malformed move." | |
R> findFromTo DUP 0= ABORT" Illegal move." | |
>R 2 /STRING R> ?promoteMv @ ; | |
: inmv ( "e2e4" -- mv ) \ usage: inmv e2e4 | |
BASE @ >R DECIMAL \ important! e2e4 is a hex number! | |
BL WORD COUNT DUP 4 < ABORT" Malformed move." | |
R> BASE ! str>mv NIP NIP ; | |
: epdstr>board ( str count -- ) | |
OVER + SWAP ( ^end ^cur ) | |
2DUP 0 ROT ROT DO I C@ [CHAR] k = IF 1+ THEN LOOP | |
1 <> ABORT" Must have exactly one black king!" | |
2DUP 0 ROT ROT DO I C@ [CHAR] K = IF 1+ THEN LOOP | |
1 <> ABORT" Must have exactly one white king!" | |
0 >R \ R: current sq | |
eraseBoard | |
DUP C@ [CHAR] / = IF CHAR+ THEN | |
BEGIN | |
DUP C@ | |
DUP [CHAR] 0 - | |
DUP 1 9 WITHIN IF | |
R> + >R \ 1-8 empty squares | |
ELSE DROP DUP [CHAR] / = IF | |
R> $F0 AND So + >R \ next rank, 1st file | |
ELSE DUP char>piece ?DUP IF | |
R> TUCK bd! 1+ >R \ piece | |
ELSE | |
CR ." Bad EPD character: " EMIT ." at " R> .sq ABORT | |
THEN THEN THEN | |
DROP CHAR+ 2DUP = | |
UNTIL | |
initPieces | |
R> DROP 2DROP ; | |
: epd ( "epd" "w|b" -- ) | |
BL WORD COUNT epdstr>board | |
BL WORD COUNT 1 <> ABORT" Bad color!" | |
C@ tolower DUP [CHAR] w = IF DROP setWtm | |
ELSE [CHAR] b = IF setBtm switchPieces | |
ELSE TRUE ABORT" Bad color!" THEN THEN | |
0 TO castle | |
initVars | |
wtm? 0= inCheck? ABORT" Side to move can capture king!" ; | |
: ?epdCB ( "KQkq" testChar bit -- "Qkq" ) | |
>R OVER C@ = IF R> castle OR TO castle CHAR+ ELSE R> DROP THEN ; | |
: epdFlags ( "KQkq|-" "ep|-" -- ) | |
BL WORD COUNT 2DUP + 0 SWAP C! IF 0 TO castle | |
[CHAR] K wkCastleBit ?epdCB | |
[CHAR] Q wqCastleBit ?epdCB | |
[CHAR] k bkCastleBit ?epdCB | |
[CHAR] q bqCastleBit ?epdCB DROP | |
THEN | |
BL WORD COUNT IF | |
str>sq 0= IF DROP 0 THEN TO ep | |
THEN ; | |
: noLegalMoves? ( -- tf ) | |
0 TO ply gen | |
forMovesAtPly ?DO | |
I @ makeMove IF takeBack FALSE UNLOOP EXIT THEN | |
genSize +LOOP TRUE ; | |
: .color wtm? IF ." White" ELSE ." Black" THEN ; | |
: .result? ( -- tf ) | |
FALSE noLegalMoves? IF CR | |
wtm? inCheck? IF | |
.color ." is checkmated" | |
ELSE | |
." Stalemate" | |
THEN | |
DROP TRUE | |
ELSE reps 3 = IF | |
CR ." Draw by repetition" DROP TRUE | |
ELSE fifty 50moveCount = IF | |
CR ." Draw by fifty-move rule" DROP TRUE | |
THEN THEN THEN CR ; | |
: retract histTop histStack - IF takeBack THEN ; | |
\ *** Opening book support *** | |
\ tree of book moves | |
\ node is 3 cells: next variation, next move, move itself | |
: >var @ ; | |
: >next CELL+ ; | |
: >mv CELL+ CELL+ @ ; | |
\ book moves have a "bad move" flag (recognized, but never chosen) | |
$400000 CONSTANT badMoveBit | |
: >goodVar | |
BEGIN >var DUP WHILE | |
DUP >mv badMoveBit AND 0= | |
UNTIL THEN ; | |
: bkSame? ( move bkMv -- ? ) XOR badMoveBit INVERT AND 0= ; | |
VARIABLE bookRoot | |
VARIABLE bookCurrent | |
: resetBook bookRoot bookCurrent ! ; | |
: initBook 0 bookRoot ! ; | |
: bkAdd ( mv parent -- ) \ parent could be >var or >next or bookRoot | |
HERE SWAP ! 0 , 0 , , ; | |
: bkFind ( mv ^node -- mv ^node' ? ) | |
BEGIN DUP >var WHILE >var | |
2DUP >mv bkSame? IF TRUE EXIT THEN | |
REPEAT FALSE ; | |
: bkFindOrAdd ( mv ^node -- ^node' ) | |
bkFind IF NIP ELSE TUCK bkAdd >var THEN >next ; | |
: inBkMv ( "mv?" -- mv|0 ) | |
BL WORD COUNT DUP 4 < IF 2DROP 0 EXIT THEN \ end of line | |
str>mv SWAP IF SWAP C@ [CHAR] ? = IF badMoveBit OR THEN ELSE NIP THEN ; | |
: line: ( "move ... move"<EOL> -- ) | |
resetBook initBoard | |
BEGIN inBkMv ?DUP WHILE | |
DUP makeMove 0= ABORT" Illegal move!" | |
bookCurrent @ bkFindOrAdd bookCurrent ! | |
REPEAT ; | |
: inBook? bookCurrent @ DUP IF >var THEN ; | |
: bookOff 0 bookCurrent ! ; | |
: makeBookMove ( mv -- mv ) DUP 0= ABORT" make null book move" | |
inBook? 0= IF EXIT THEN | |
bookCurrent @ bkFind IF >next ELSE DROP 0 THEN | |
bookCurrent ! ; | |
: bkCountGoodMoves ( var -- count ) | |
0 BEGIN SWAP >goodVar DUP WHILE SWAP 1+ REPEAT DROP ; | |
HERE VALUE seed | |
: RANDOM ( -- u ) seed $107465 * $234567 + DUP TO seed ; | |
: CHOOSE ( n -- 0..n-1 ) RANDOM UM* NIP ; | |
: chooseBookMove ( -- ) \ puts move chosen in pv | |
bookCurrent @ | |
DUP bkCountGoodMoves CHOOSE 1+ 0 DO >goodVar LOOP | |
DUP >mv pv ! | |
>next bookCurrent ! ; | |
: .bkNode >mv DUP .move badMoveBit AND IF [CHAR] ? EMIT THEN ; | |
: .bookHint inBook? 0= IF ." Out of book." EXIT THEN | |
bookCurrent @ BEGIN >var ?DUP WHILE SPACE DUP .bkNode REPEAT ; | |
\ *** User Commands *** | |
\ other words usable as commands: .board .epd .moveList | |
: newGame resetBook initBoard .board ; \ setup a new game | |
: sd ( depth -- ) 1 MAX 1+ MAX_PLY 1- MIN TO maxDepth ; \ Set Depth | |
: st ( seconds -- ) 1000 * TO msMaxThinkTime ; \ Set Time | |
: go \ ask the computer to choose move | |
inBook? IF chooseBookMove ." Book " ELSE think DROP THEN | |
\ press any key to stop thinking and make a move | |
pv @ ?DUP IF | |
." Move found: " DUP .move CR | |
makeMove DROP .board | |
THEN .result? DROP ; | |
: (mv) ( "e2e4" -- result ) | |
inmv makeBookMove makeMove 0= ABORT" Can't move there." | |
.board .result? ; | |
: mv (mv) 0= IF go THEN ; \ for alternating turns with the computer | |
: domove (mv) DROP ; \ for forcing a sequence of moves | |
: ex \ play expected move | |
pvLen @ 2 < IF ." No expected move." EXIT THEN | |
pv CELL+ @ ." Playing expected move " DUP .move | |
makeMove 0= ABORT" Can't move there." | |
.board .result? 0= IF go THEN ; | |
: undos ( n -- ) | |
bookOff 0 DO retract LOOP .board ; | |
: undo 1 undos ; \ take back one ply (switches sides) | |
: undo2 2 undos ; \ take back one full move | |
: redo histTop @ makeMove 0= ABORT" Can't redo move." .board ; | |
: .whoseTurn .color ." to move" CR ; | |
: rotateBoard blackAtBottom? 0= TO blackAtBottom? .board ; | |
: showCoords showCoords? 0= TO showCoords? .board ; | |
: autoDepth ( depth1 depth2 -- ) | |
BEGIN | |
SWAP DUP sd go | |
.result? keyHitQ? OR | |
UNTIL 2DROP ; | |
: autoTime ( time1 time2 -- ) | |
BEGIN | |
SWAP DUP st go | |
.result? keyHitQ? OR | |
UNTIL 2DROP ; | |
\ EPD position setup (great for testing) | |
\ examples: setup2 rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR W KQkq - | |
\ setup 2r2rk1/1bqnbpp1/1p1ppn1p/pP6/N1P1P3/P2B1N1P/1B2QPP1/R2R2K1 b | |
: setup ( "epd" "w|b" -- ) bookOff epd .board .whoseTurn ; | |
: setup2 ( epd w|b KQkq|- ep|- -- ) bookOff epd epdFlags .board .whoseTurn ; | |
: FEN bookOff epd epdFlags BL PARSE EVALUATE TO fifty BL PARSE TYPE | |
.board .whoseTurn ; | |
: benchThink ( -- time ) | |
['] thinker CATCH DUP ?thinkAbort IF QUIT THEN readTimer ; | |
: bench \ Fischer-Sherwin NJ Championships 1957, move 17 | |
S" setup 1rb2rk/p4ppp/1p1qp1n/3n2N/2pP4/2P3P/PPQ2PBP/R1B1R1K w" evaluate | |
5 sd benchThink benchThink benchThink MIN MIN \ best of three | |
CR ." Results: " nodes 1000 UM* ROT UM/MOD NIP U. ." nps" CR ; | |
\ some bench results: | |
\ 14715 nps, gforth-fast 0.5.0, 1.5 GHz G4, Mac OS X 10.4 | |
\ 68200 nps, gforth-fast 0.6.2, 1.5 GHz G4, Mac OS X 10.4 | |
\ 66714 nps, gforth-fast 0.6.2, 2.33 GHz Core2Duo, Mac OS X 10.4 | |
\ 104415 nps, gforth-fast 0.7.0, 2.33 GHz Core2Duo, Mac OS X 10.4 | |
: fcp-help | |
CR ." go Make the computer move (hit any key to stop thinking)" | |
CR ." domove e2e4 Move a piece from square e2 to square e4" | |
CR ." undo Take back the last move" | |
CR ." undo2 Take back a full move" | |
CR ." mv h7h8Q domove h7h8Q go (promote to a queen)" | |
CR ." mv e1g1 Castle kingside" | |
CR ." ex Play the expected move" | |
CR ." 5 sd Set maximum depth for computer search" | |
CR ." 10 st Set maximum thinking time in seconds" | |
CR ." newGame Start a new game" | |
CR ." rotateBoard Display black at the bottom of the board" | |
CR ." showCoords Display algebraic coordinates" | |
CR ." .board Display the board" | |
CR ." .moveList Display the list of moves played so far" | |
CR ." .epd Display the EPD description of the board" CR ." .whoseTurn Display which color moves next" | |
CR ." .bookHint Show opening book moves from current position" | |
CR ." setup EPD Setup an EPD position" | |
CR ." setup2 EPD Full EPD, with castling and en-passant square" | |
CR ." bench Run a benchmark" | |
CR ; | |
[UNDEFINED] WINBOARD-ENGINE [IF] | |
\ *** EXECUTE WHEN LOADING *** | |
CR .( FCP 1.4 loaded. ) | |
INCLUDE fcp-book.f \ opening book (lots of line:s) | |
CR UNUSED - . .( total bytes used. ) | |
.( Type "fcp-help" for instructions. ) | |
newGame | |
[ELSE] | |
INCLUDE fcp-book.f | |
UNUSED - DROP | |
[THEN] | |
\ BASE ! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment