From 270072006cabd0a9b3b66ee87d1ed958edc2f2b1 Mon Sep 17 00:00:00 2001 From: Woody Folsom Date: Sun, 18 Nov 2012 18:44:09 -0500 Subject: [PATCH] Root_par beats GoFree! --- .../white_lvl_1/gogame-121118184053-0500.sgf | 1 + .../tourney1/gogame-121118181254-0500.sgf | 1 + .../tourney1/gogame-121118181337-0500.sgf | 1 + .../tourney1/gogame-121118181422-0500.sgf | 1 + .../tourney1/gogame-121118181528-0500.sgf | 1 + .../tourney1/gogame-121118181615-0500.sgf | 1 + .../tourney1/gogame-121118181711-0500.sgf | 1 + .../tourney1/gotournament-121118181711.txt | 9 ++ data/networks/Pass.nn | Bin 0 -> 3954 bytes data/test/ScoreTest.sgf | 50 +++++++++ data/test/scoretest1.sgf | 1 - src/net/woodyfolsom/msproj/GameState.java | 28 ++--- .../woodyfolsom/msproj/StandAloneGame.java | 7 +- .../woodyfolsom/msproj/policy/MonteCarlo.java | 18 ++-- .../msproj/policy/MonteCarloAMAF.java | 98 ++++++++++++++++- .../msproj/policy/MonteCarloUCT.java | 102 ++++++++++-------- .../msproj/policy/RandomMovePolicy.java | 17 ++- .../woodyfolsom/msproj/policy/Rollout.java | 65 +++++++++++ .../msproj/policy/RootParallelization.java | 14 --- .../msproj/policy/ValidMoveGenerator.java | 16 ++- .../msproj/tree/AMAFProperties.java | 18 ++++ .../net/woodyfolsom/msproj/GameScoreTest.java | 57 ++++------ 22 files changed, 381 insertions(+), 126 deletions(-) create mode 100644 data/games/rootpar_vs_gofree/white_lvl_1/gogame-121118184053-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gogame-121118181254-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gogame-121118181337-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gogame-121118181422-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gogame-121118181528-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gogame-121118181615-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gogame-121118181711-0500.sgf create mode 100644 data/games/rootpar_vs_random/tourney1/gotournament-121118181711.txt create mode 100644 data/networks/Pass.nn create mode 100644 data/test/ScoreTest.sgf delete mode 100644 data/test/scoretest1.sgf create mode 100644 src/net/woodyfolsom/msproj/policy/Rollout.java create mode 100644 src/net/woodyfolsom/msproj/tree/AMAFProperties.java diff --git a/data/games/rootpar_vs_gofree/white_lvl_1/gogame-121118184053-0500.sgf b/data/games/rootpar_vs_gofree/white_lvl_1/gogame-121118184053-0500.sgf new file mode 100644 index 0000000..89cfb2e --- /dev/null +++ b/data/games/rootpar_vs_gofree/white_lvl_1/gogame-121118184053-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[W+13.5];B[de];W[eb];B[gf];W[db];B[dc];W[hf];B[cc];W[dh];B[bf];W[he];B[ge];W[ea];B[gd];W[ca];B[bb];W[fg];B[eg];W[gc];B[gb];W[hc];B[hd];W[fh];B[gh];W[eh];B[ef];W[ed];B[fe];W[dg];B[cg];W[gg];B[df];W[id];B[fd];W[fc];B[fa];W[hg];B[ih];W[ic];B[hb];W[ec];B[fb];W[ie];B[gi];W[dd];B[cd];W[cb];B[ba];W[di];B[ch];W[ib];B[ci];W[fi];B[hh];W[ab];B[bc];W[ha];B[ig];W[bd];B[be];W[ga];B[ee];W[ii];B[ff];W[if];B[hi];W[ii];B[hi];W[ih];B[];W[hh];B[];W[aa];B[ac];W[ab];B[];W[gb];B[gh];W[fb];B[];W[ad];B[aa];W[bh];B[ah];W[af];B[ag];W[];B[bg];W[];B[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gogame-121118181254-0500.sgf b/data/games/rootpar_vs_random/tourney1/gogame-121118181254-0500.sgf new file mode 100644 index 0000000..505c426 --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gogame-121118181254-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[W+7.5];B[bg];W[gc];B[ba];W[ic];B[gd];W[ce];B[cf];W[eb];B[gi];W[ci];B[ab];W[bf];B[eh];W[cc];B[be];W[ca];B[ag];W[fd];B[de];W[ig];B[gb];W[fc];B[eg];W[dc];B[id];W[ee];B[bb];W[gf];B[ed];W[hc];B[ei];W[he];B[ia];W[gg];B[dh];W[hd];B[bi];W[ge];B[da];W[cb];B[db];W[ec];B[ga];W[bh];B[cd];W[hh];B[af];W[hb];B[ai];W[];B[ha];W[ad];B[ih];W[cg];B[hi];W[gh];B[ah];W[fi];B[fh];W[if];B[hg];W[hf];B[dg];W[ie];B[ch];W[];B[bc];W[fe];B[bd];W[ff];B[ef];W[fa];B[di];W[dd];B[fg];W[ea];B[db];W[da];B[ae];W[];B[ib];W[fb];B[ac];W[ha];B[ia];W[ga];B[];W[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gogame-121118181337-0500.sgf b/data/games/rootpar_vs_random/tourney1/gogame-121118181337-0500.sgf new file mode 100644 index 0000000..e8c5c55 --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gogame-121118181337-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[W+11.5];B[cg];W[cf];B[eh];W[gh];B[bc];W[af];B[ge];W[fh];B[ci];W[ed];B[de];W[bb];B[ib];W[ab];B[ad];W[ag];B[db];W[bd];B[hg];W[gd];B[bg];W[ea];B[ha];W[ch];B[gg];W[cc];B[ac];W[dd];B[gc];W[ah];B[fe];W[bh];B[ei];W[hd];B[ba];W[gb];B[be];W[dg];B[gi];W[fg];B[id];W[];B[eb];W[fb];B[ca];W[ae];B[eg];W[hc];B[cd];W[ce];B[hf];W[fd];B[gf];W[ie];B[bc];W[ac];B[bi];W[ga];B[ff];W[he];B[fi];W[fc];B[ig];W[df];B[aa];W[ai];B[ii];W[ic];B[ef];W[dc];B[hh];W[da];B[fg];W[dh];B[fh];W[if];B[ee];W[ec];B[hb];W[];B[di];W[cb];B[ba];W[eb];B[ca];W[];B[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gogame-121118181422-0500.sgf b/data/games/rootpar_vs_random/tourney1/gogame-121118181422-0500.sgf new file mode 100644 index 0000000..04ce9a4 --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gogame-121118181422-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[W+6.5];B[hg];W[ef];B[bg];W[ih];B[eh];W[hi];B[gi];W[dg];B[fd];W[gg];B[ic];W[cf];B[fe];W[da];B[ie];W[cd];B[be];W[ei];B[bb];W[df];B[ae];W[gc];B[hd];W[ci];B[cc];W[fg];B[ig];W[gd];B[gh];W[ce];B[gb];W[dd];B[bi];W[fc];B[ia];W[ec];B[ai];W[dc];B[if];W[ea];B[ab];W[hc];B[bh];W[fh];B[ff];W[fa];B[he];W[dh];B[ha];W[eg];B[bf];W[];B[cb];W[ad];B[bd];W[db];B[af];W[ee];B[ga];W[fi];B[hh];W[hb];B[ed];W[hf];B[cg];W[gf];B[ba];W[ah];B[ib];W[eb];B[bc];W[];B[fb];W[id];B[gb];W[ha];B[fb];W[ic];B[ca];W[ch];B[ia];W[];B[ac];W[ga];B[ag];W[fb];B[ii];W[];B[ge];W[];B[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gogame-121118181528-0500.sgf b/data/games/rootpar_vs_random/tourney1/gogame-121118181528-0500.sgf new file mode 100644 index 0000000..2c5fecf --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gogame-121118181528-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[B+27.5];B[ci];W[cf];B[de];W[hh];B[ec];W[hg];B[ga];W[ie];B[dh];W[da];B[df];W[ig];B[fd];W[ha];B[dd];W[bi];B[ff];W[ah];B[dc];W[ib];B[ef];W[ad];B[ce];W[eg];B[ab];W[di];B[fh];W[bd];B[eh];W[bg];B[gd];W[gi];B[he];W[hb];B[fg];W[fc];B[bf];W[ca];B[bb];W[cb];B[be];W[cc];B[id];W[ee];B[ea];W[ch];B[ae];W[gc];B[eb];W[gg];B[hd];W[ih];B[fi];W[fa];B[ba];W[af];B[ic];W[cg];B[fb];W[ei];B[dg];W[fe];B[if];W[ii];B[ed];W[hc];B[gf];W[db];B[ci];W[bc];B[gh];W[ac];B[hi];W[gb];B[hf];W[ih];B[ag];W[ei];B[bh];W[fa];B[ai];W[bg];B[cf];W[hg];B[];W[ig];B[ga];W[ii];B[];W[aa];B[ba];W[cd];B[bb];W[ab];B[ba];W[cg];B[bb];W[gg];B[hh];W[ih];B[];W[ig];B[];W[ii];B[];W[cd];B[];W[bc];B[gg];W[ca];B[];W[ab];B[];W[db];B[da];W[aa];B[cc];W[ac];B[bd];W[fa];B[ad];W[ac];B[ch];W[bg];B[cg];W[ab];B[cb];W[aa];B[ga];W[];B[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gogame-121118181615-0500.sgf b/data/games/rootpar_vs_random/tourney1/gogame-121118181615-0500.sgf new file mode 100644 index 0000000..5672815 --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gogame-121118181615-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[B+2.5];B[di];W[bd];B[fa];W[gd];B[fe];W[aa];B[ch];W[ae];B[eb];W[db];B[dh];W[fc];B[ec];W[ee];B[cd];W[ah];B[df];W[gb];B[ic];W[ff];B[af];W[ed];B[ii];W[ib];B[ac];W[ga];B[ia];W[eh];B[dg];W[ha];B[dc];W[gh];B[ig];W[ba];B[ef];W[be];B[bh];W[gc];B[de];W[cg];B[fh];W[bf];B[dd];W[gg];B[if];W[ce];B[fd];W[he];B[bg];W[bb];B[ed];W[fb];B[cb];W[hf];B[ea];W[cf];B[bc];W[ie];B[ei];W[];B[hd];W[gi];B[ai];W[hg];B[hh];W[hb];B[fi];W[ge];B[ca];W[fg];B[hc];W[ih];B[da];W[ig];B[ad];W[ci];B[bd];W[hi];B[be];W[id];B[hc];W[eg];B[ab];W[cf];B[ba];W[ic];B[bi];W[ce];B[ag];W[hd];B[cg];W[];B[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gogame-121118181711-0500.sgf b/data/games/rootpar_vs_random/tourney1/gogame-121118181711-0500.sgf new file mode 100644 index 0000000..f58f9ee --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gogame-121118181711-0500.sgf @@ -0,0 +1 @@ +(;FF[4]GM[1]SZ[9]KM[5.5]RE[B+11.5];B[ch];W[id];B[gh];W[hf];B[hc];W[ei];B[fg];W[ec];B[bi];W[gi];B[fb];W[bb];B[ih];W[af];B[di];W[ah];B[ha];W[gd];B[ge];W[cg];B[db];W[df];B[dc];W[gb];B[bf];W[hh];B[hg];W[ce];B[be];W[ag];B[fc];W[fa];B[dg];W[ae];B[gc];W[bc];B[if];W[ga];B[gf];W[ba];B[ff];W[ai];B[ee];W[hb];B[cd];W[cc];B[bg];W[ca];B[ed];W[fi];B[cf];W[bd];B[da];W[hd];B[de];W[eg];B[eb];W[ac];B[bh];W[hi];B[ic];W[ea];B[ad];W[af];B[ai];W[fe];B[ia];W[ef];B[ie];W[ab];B[dh];W[eh];B[ah];W[ag];B[fd];W[cb];B[];W[ii];B[fh];W[ii];B[fi];W[gi];B[];W[eg];B[hh];W[ib];B[ha];W[df];B[];W[ef];B[];W[ae];B[ad];W[ia];B[ha];W[ea];B[hb];W[ag];B[];W[ei];B[fa];W[ae];B[];W[ga];B[];W[ib];B[];W[af];B[];W[]) \ No newline at end of file diff --git a/data/games/rootpar_vs_random/tourney1/gotournament-121118181711.txt b/data/games/rootpar_vs_random/tourney1/gotournament-121118181711.txt new file mode 100644 index 0000000..a1137e4 --- /dev/null +++ b/data/games/rootpar_vs_random/tourney1/gotournament-121118181711.txt @@ -0,0 +1,9 @@ +Cumulative results for 3 games (BLACK=RANDOM, WHITE=ROOT_PAR) +1. W+7.5 +2. W+11.5 +3. W+6.5 +Cumulative results for 3 games (BLACK=ROOT_PAR, WHITE=RANDOM) +1. B+27.5 +2. B+2.5 +3. B+11.5 +Elapsed Time: 300.899 seconds. \ No newline at end of file diff --git a/data/networks/Pass.nn b/data/networks/Pass.nn new file mode 100644 index 0000000000000000000000000000000000000000..ebeb1928a3b6a1cea45746e485874f21a5ef2fb1 GIT binary patch literal 3954 zcma)9U2GIp6u#ZvvZYX3iYb5cSCLR?b}=DDg1QCT!pbbHZ9(NhdfA=sj?B)CGk4l8 zjTjT7FPdnKno4|7LVQ6IUQBo@NDL1qD#VB(zWAbv5#vu{f+4|k&Yi#A?P53C$;>_9 z`TOoU_vgYL*6P^oQPVD^4Ar$wtDG_nRitvRE;NNtt9D+s3#uh-(_r!!i!oNU+4`tX z!L-%XVKBh;!>X7v?Nc5&5o1Y(E!7Om6(~0iN3bsCMLx;XuF&+fqB&xZ!di8HQq=|P zmLi=SrSzzZ>=` z&0gmp#aT<1B^>RvTD5?k*i-_Z?Vz+N$|SL0Fe_Fn8*=86*&>-=8N~<82lEq;u|*2& zBFguhV6PxFpxsNigT{wlgGL40Qj;6eM~F1J_i@1tp_RD8mI0HEwwGAkRt-UABiO1& z%On9?UBc#uGojkPeZl%0(LN|WR)Wm)Z(aHsU>oA z6NQ@8Jr$^WsJ%|dtyG}&!no%tKgl0>iy|TG&M6ASS%9((IY7$dTGJ%lFfVFNtixuR z`BYGbQ8KKM(RfgpYa1Mfa~>)~IkZpMW<|EQV5=KvQi_0rHet$loW6&lvn3_-D#=(O zb;Nekgf9uE-3t;$>uGQ;J=+67P!|YUPQyL)T-A^%A~un1Exz{BYlEv0(21x%V7ajo zWz1p8v<$M=Nv^xl@^gRjyGMWcbuTJ!mBzrt#wf*M@9)#NBZ&%mW>r)T_HjD+<9^S+|H-UVHACc^Q4(Sjc?NZ z?L#=UUqa}arf1C}f{-RudJ!Vx>Cn$1di}pJM+nuQxp(n;etKmoRHra~=~ox7zBfMm zc_`hD&OGVd-6xcL$x|^Ldgg&t26bVxo@jN^PC+ZMS5vT2K<~<(7tO*)(vW*Je!_Cs zAs2LSOBZb8{H+~q`p~RlcSQ&rq(rEGlTh@HM+bkYPmOV-T-!d5ZdxjwH?}q85-Qd0 z57C~n2D~ph1p{p52cLYo?Y3m_g)myLzA#X#ESedbfI|z3dsL{nspAiJ=@_*A^g-Bl z23cZSF{j8vA@hd>_)qlZm+0T0UH|uu{yPhrSkxP*bWRRUM|Q;5{WK{R-*d6b!d zk~6!$?iokelLT|>*GsDCpb8Ytfa9J=2uuR5y|Q@FM8H0Ncnjc_wt=%<*FHXnL%S9l z_b{Tz9uW8M`T_UG*_#O4j*mTF@c90+PauBuRs~~39(+ic`RD_v`6>{ z+*Ho2z=6xH?Bj(~7M$LBiQ{3fhJ?0?2ZAQ*Yim1(%irAwwAIMSaWd+4pvkwf)nxxlw@qtk1TU}7JnbUa{vB)mU->;PX{hO zG?V#O(t&6W6|+3Nh8*Nh87?!e$=g@2^^D($v)C|e*G*m=<^^HeS+-b|ZPh88da-I{ z2FT9RDJ;k?__s{fT@LALQu(yrf~a1fXxr|$~3G#VVrk|cSjAw`B=SUppBeV|zI1QyH$ yUh=#oSvTE)ml4Z&RZR^U!QFy{mS8=(qsmzJgtGVfp^+2$p^^PV`LU5BhyMf4!)DI_ literal 0 HcmV?d00001 diff --git a/data/test/ScoreTest.sgf b/data/test/ScoreTest.sgf new file mode 100644 index 0000000..35d1323 --- /dev/null +++ b/data/test/ScoreTest.sgf @@ -0,0 +1,50 @@ +(;SZ[9]CA[UTF-8]FF[4]GM[1]KM[6]PB[Player1]RE[W+5]PW[Player2] +;B[ga] +;W[fa] +;B[fb] +;W[ea] +;B[fc] +;W[eb] +;B[ec] +;W[db] +;B[dc] +;W[cc] +;B[ed] +;W[dd] +;B[ee] +;W[ce] +;B[de] +;W[cd] +;B[ef] +;W[bd] +;B[be] +;W[ae] +;B[bf] +;W[ad] +;B[af] +;W[bb] +;B[cf] +;W[ic] +;B[ib] +;W[id] +;B[hb] +;W[ie] +;B[hc] +;W[he] +;B[hd] +;W[hf] +;B[ge] +;W[ff] +;B[fe] +;W[gg] +;B[eg] +;W[fh] +;B[eh] +;W[hh] +;B[di] +;W[ei] +;B[ch] +;W[fi] +;B[] +;W[] +) \ No newline at end of file diff --git a/data/test/scoretest1.sgf b/data/test/scoretest1.sgf deleted file mode 100644 index fd10cfe..0000000 --- a/data/test/scoretest1.sgf +++ /dev/null @@ -1 +0,0 @@ -(;FF[4]GM[1]SZ[5]KM[3.5]RE[W+11.5];B[ad];W[bd];B[be];W[ed];B[dd];W[ba];B[cd];W[ca];B[ee];W[cb];B[];W[dc];B[db];W[ac];B[];W[ec];B[ae];W[bc];B[ce];W[cc];B[aa];W[de];B[be];W[];B[ce];W[ad];B[dd];W[];B[ea];W[eb];B[ae];W[];B[ee];W[da];B[bb];W[de];B[];W[]) \ No newline at end of file diff --git a/src/net/woodyfolsom/msproj/GameState.java b/src/net/woodyfolsom/msproj/GameState.java index 342711c..cc1107e 100644 --- a/src/net/woodyfolsom/msproj/GameState.java +++ b/src/net/woodyfolsom/msproj/GameState.java @@ -122,13 +122,15 @@ public class GameState { public boolean isSelfFill(Action action, Player player) { return gameBoard.isSelfFill(action, player); } - + /** - * Used for setting up the board. Places the player's stone at the specified coordinates. + * Used for setting up the board. Places the player's stone at the specified + * coordinates. * - * Returns false if the requested intersection is occupied or the resulting position is illegal. - * Returns false if the requested action is PASS, RESIGN or NONE. - * Returns false if the moveHistory's size is already >0 (method should only be used to set up board). + * Returns false if the requested intersection is occupied or the resulting + * position is illegal. Returns false if the requested action is PASS, + * RESIGN or NONE. Returns false if the moveHistory's size is already >0 + * (method should only be used to set up board). * * Does NOT advance the playerToMove or add the action to the move history. * @@ -140,23 +142,23 @@ public class GameState { if (action.isPass() || action.isResign() || action.isNone()) { return false; } - + if (moveHistory.size() > 0) { return false; } - + Player actualPTM = playerToMove; - + playerToMove = player; - - boolean validMove = playStone(player,action); - + + boolean validMove = playStone(player, action); + moveHistory.clear(); playerToMove = actualPTM; - + return validMove; } - + public boolean playStone(Player player, String move) { return playStone(player, Action.getInstance(move)); } diff --git a/src/net/woodyfolsom/msproj/StandAloneGame.java b/src/net/woodyfolsom/msproj/StandAloneGame.java index af4979c..740347b 100644 --- a/src/net/woodyfolsom/msproj/StandAloneGame.java +++ b/src/net/woodyfolsom/msproj/StandAloneGame.java @@ -12,6 +12,7 @@ import java.util.List; import net.woodyfolsom.msproj.gui.Goban; import net.woodyfolsom.msproj.policy.HumanGuiInput; import net.woodyfolsom.msproj.policy.HumanKeyboardInput; +import net.woodyfolsom.msproj.policy.MonteCarloAMAF; import net.woodyfolsom.msproj.policy.MonteCarloUCT; import net.woodyfolsom.msproj.policy.Policy; import net.woodyfolsom.msproj.policy.RandomMovePolicy; @@ -25,7 +26,7 @@ public class StandAloneGame { private static final int DEFAULT_SIZE = 9; enum PLAYER_TYPE { - HUMAN, HUMAN_GUI, ROOT_PAR, UCT, RANDOM + HUMAN, HUMAN_GUI, ROOT_PAR, UCT, RANDOM, RAVE }; public static void main(String[] args) { @@ -75,6 +76,8 @@ public class StandAloneGame { return PLAYER_TYPE.HUMAN_GUI; } else if ("RANDOM".equalsIgnoreCase(playerTypeStr)) { return PLAYER_TYPE.RANDOM; + } else if ("RAVE".equalsIgnoreCase(playerTypeStr)) { + return PLAYER_TYPE.RAVE; } else { throw new RuntimeException("Unknown player type: " + playerTypeStr); } @@ -175,6 +178,8 @@ public class StandAloneGame { turnLength * 1000L); case RANDOM: return new RandomMovePolicy(); + case RAVE: + return new MonteCarloAMAF(new RandomMovePolicy(), turnLength * 1000L); default: throw new IllegalArgumentException("Invalid PLAYER_TYPE: " + playerType); diff --git a/src/net/woodyfolsom/msproj/policy/MonteCarlo.java b/src/net/woodyfolsom/msproj/policy/MonteCarlo.java index 3d241c6..d38602f 100644 --- a/src/net/woodyfolsom/msproj/policy/MonteCarlo.java +++ b/src/net/woodyfolsom/msproj/policy/MonteCarlo.java @@ -13,7 +13,7 @@ import net.woodyfolsom.msproj.tree.GameTreeNode; import net.woodyfolsom.msproj.tree.MonteCarloProperties; public abstract class MonteCarlo implements Policy { - protected static final int ROLLOUT_DEPTH_LIMIT = 100; + protected static final int ROLLOUT_DEPTH_LIMIT = 150; protected int numStateEvaluations = 0; protected Policy movePolicy; @@ -36,6 +36,11 @@ public abstract class MonteCarlo implements Policy { public abstract List> descend( GameTreeNode node); + protected GameTreeNode createRootNode(GameState rootGameState) { + return new GameTreeNode( + rootGameState, new MonteCarloProperties()); + } + private GameTreeNode buildTree(GameConfig gameConfig, GameState gameState, Player player) { //System.out.println(player + " is thinking for up to " // + (searchTimeLimit / 1000.0) + " seconds..."); @@ -47,8 +52,7 @@ public abstract class MonteCarlo implements Policy { + gameState.getPlayerToMove()); } - GameTreeNode rootNode = new GameTreeNode( - gameState, new MonteCarloProperties()); + GameTreeNode rootNode = createRootNode(gameState); do { @@ -67,8 +71,8 @@ public abstract class MonteCarlo implements Policy { } for (GameTreeNode newLeaf : newLeaves) { - int reward = rollout(gameConfig, newLeaf, player); - update(newLeaf, reward); + Rollout rollout = rollout(gameConfig, newLeaf, player); + update(newLeaf, rollout); } elapsedTime = System.currentTimeMillis() - startTime; @@ -103,11 +107,11 @@ public abstract class MonteCarlo implements Policy { GameConfig gameConfig, GameTreeNode node, Player player); - public abstract int rollout(GameConfig gameConfig, + public abstract Rollout rollout(GameConfig gameConfig, GameTreeNode node, Player player); public abstract void update(GameTreeNode node, - int reward); + Rollout rollout); public long getSearchTimeLimit() { return searchTimeLimit; diff --git a/src/net/woodyfolsom/msproj/policy/MonteCarloAMAF.java b/src/net/woodyfolsom/msproj/policy/MonteCarloAMAF.java index 7c46576..3ade6e1 100644 --- a/src/net/woodyfolsom/msproj/policy/MonteCarloAMAF.java +++ b/src/net/woodyfolsom/msproj/policy/MonteCarloAMAF.java @@ -1,10 +1,106 @@ package net.woodyfolsom.msproj.policy; +import java.util.ArrayList; +import java.util.List; + +import net.woodyfolsom.msproj.Action; +import net.woodyfolsom.msproj.GameState; +import net.woodyfolsom.msproj.Player; +import net.woodyfolsom.msproj.tree.AMAFProperties; +import net.woodyfolsom.msproj.tree.GameTreeNode; +import net.woodyfolsom.msproj.tree.MonteCarloProperties; public class MonteCarloAMAF extends MonteCarloUCT { - + public MonteCarloAMAF(Policy movePolicy, long searchTimeLimit) { super(movePolicy, searchTimeLimit); } + + @Override + public void update(GameTreeNode node, Rollout rollout) { + GameTreeNode currentNode = node; + //List subTreeActions = new ArrayList(rollout.getPlayout()); + + List playout = rollout.getPlayout(); + int reward = rollout.getReward(); + while (currentNode != null) { + AMAFProperties nodeProperties = (AMAFProperties)currentNode.getProperties(); + + //Always update props for the current node + nodeProperties.setWins(nodeProperties.getWins() + reward); + nodeProperties.setVisits(nodeProperties.getVisits() + 1); + nodeProperties.setAmafWins(nodeProperties.getAmafWins() + reward); + nodeProperties.setAmafVisits(nodeProperties.getAmafVisits() + 1); + + GameTreeNode parentNode = currentNode.getParent(); + if (parentNode != null) { + Player playerToMove = parentNode.getGameState().getPlayerToMove(); + for (Action actionFromParent : parentNode.getActions()) { + if (playout.contains(actionFromParent)) { + GameTreeNode subTreeChild = parentNode.getChild(actionFromParent); + //Don't count AMAF properties for the current node twice + if (subTreeChild == currentNode) { + continue; + } + + AMAFProperties siblingProperties = (AMAFProperties)subTreeChild.getProperties(); + //Only update AMAF properties if the sibling is reached by the same action with the same player to move + if (rollout.hasPlay(playerToMove,actionFromParent)) { + siblingProperties.setAmafWins(siblingProperties.getAmafWins() + reward); + siblingProperties.setAmafVisits(siblingProperties.getAmafVisits() + 1); + } + } + } + } + + currentNode = currentNode.getParent(); + } + } + + @Override + protected GameTreeNode createRootNode(GameState rootGameState) { + return new GameTreeNode( + rootGameState, new AMAFProperties()); + } + @Override + protected double getNodeScore(GameTreeNode gameTreeNode) { + //double nodeVisits = gameTreeNode.getParent().getProperties().getVisits(); + double parentAmafVisits = ((AMAFProperties)gameTreeNode.getParent().getProperties()).getAmafVisits(); + + double nodeScore; + if (gameTreeNode.getGameState().isTerminal()) { + nodeScore = 0.0; + } else { + /* + MonteCarloProperties properties = gameTreeNode.getProperties(); + nodeScore = (double) (properties.getWins() / properties + .getVisits()) + + (TUNING_CONSTANT * Math.sqrt(Math.log(nodeVisits) + / gameTreeNode.getProperties().getVisits())); + * + */ + AMAFProperties properties = (AMAFProperties) gameTreeNode.getProperties(); + nodeScore = (double) (properties.getAmafWins() / properties + .getAmafVisits()) + + (TUNING_CONSTANT * Math.sqrt(Math.log(parentAmafVisits) + / properties.getAmafVisits())); + + } + return nodeScore; + } + + @Override + protected List> addNewChildren( + GameTreeNode node, Action action, + GameState successorState) { + List> newChildren = new ArrayList>(); + + GameTreeNode newChild = new GameTreeNode( + successorState, new AMAFProperties()); + + newChildren.add(newChild); + node.addChild(action, newChild); + return newChildren; + } } \ No newline at end of file diff --git a/src/net/woodyfolsom/msproj/policy/MonteCarloUCT.java b/src/net/woodyfolsom/msproj/policy/MonteCarloUCT.java index 5a0019c..d0e6b80 100644 --- a/src/net/woodyfolsom/msproj/policy/MonteCarloUCT.java +++ b/src/net/woodyfolsom/msproj/policy/MonteCarloUCT.java @@ -31,7 +31,6 @@ public class MonteCarloUCT extends MonteCarlo { // From Kocsis and Szepesvari, the value of an actual terminal node is // 0, so it will never be grown. - double nodeVisits = node.getProperties().getVisits(); Set actionsExplored = node.getActions(); GameState gameState = node.getGameState(); @@ -46,16 +45,7 @@ public class MonteCarloUCT extends MonteCarlo { GameTreeNode childNode = node .getChild(action); - double childScore; - if (childNode.getGameState().isTerminal()) { - childScore = 0.0; - } else { - MonteCarloProperties properties = childNode.getProperties(); - childScore = (double) (properties.getWins() / properties - .getVisits()) - + (TUNING_CONSTANT * Math.sqrt(Math.log(nodeVisits) - / childNode.getProperties().getVisits())); - } + double childScore = getNodeScore(childNode); // TODO add random tie breaker? // otherwise the child that is selected first will be biased if (childScore >= bestScore) { @@ -74,35 +64,38 @@ public class MonteCarloUCT extends MonteCarlo { } } + protected double getNodeScore(GameTreeNode gameTreeNode) { + double nodeScore; + double parentVisits = gameTreeNode.getParent().getProperties().getVisits(); + + if (gameTreeNode.getGameState().isTerminal()) { + nodeScore = 0.0; + } else { + MonteCarloProperties properties = gameTreeNode.getProperties(); + nodeScore = (double) (properties.getWins() / properties + .getVisits()) + + (TUNING_CONSTANT * Math.sqrt(Math.log(parentVisits) + / gameTreeNode.getProperties().getVisits())); + } + return nodeScore; + } + @Override public Action getBestAction(GameTreeNode node) { Action bestAction = Action.NONE; double bestScore = Double.NEGATIVE_INFINITY; GameTreeNode bestChild = null; - - //int nActions = node.getNumChildren(); - //GameState rootGameState = node.getGameState(); - //boolean playerToMoveIsWinning = rootGameState.getResult().isWinner(rootGameState.getPlayerToMove()); - //playerToMove is winning or only one move (PASS) is available - //boolean allowPass = playerToMoveIsWinning || nActions == 1; - for (Action action : node.getActions()) { - ///HEURISTIC - work on ways of removing this go-specific logic ///// - //If action is PASS and the play who moved is not the winner while other moves are available, don't pass - //i.e. don't pass when losing - //if (action.isPass() && !allowPass) { - // continue; //If the best rated action is PASS and I'm not winning and there are other valid actions, - // //keep searching. - //} - //////////////////////////////////////////////////////////////////// - + for (Action action : node.getActions()) { GameTreeNode childNode = node .getChild(action); - MonteCarloProperties properties = childNode.getProperties(); - double childScore = (double) properties.getWins() - / properties.getVisits(); + //MonteCarloProperties properties = childNode.getProperties(); + //double childScore = (double) properties.getWins() + // / properties.getVisits(); + double childScore = getNodeScore(childNode); + if (childScore >= bestScore) { bestScore = childScore; bestAction = action; @@ -130,68 +123,91 @@ public class MonteCarloUCT extends MonteCarlo { @Override public List> grow(GameConfig gameConfig, GameTreeNode node, Player player) { + Policy randomMovePolicy = new RandomMovePolicy(); Set exploredActions = node.getActions(); + Action action = randomMovePolicy.getAction(gameConfig, node.getGameState(), exploredActions, player); + + if (exploredActions.contains(action)) { + throw new RuntimeException("Bad action selection at this state: not a NEW leaf node for this Monte Carlo tree."); + } + if (Action.NONE == action) { throw new RuntimeException( "Unable to grow node - are all actions already explored? Board state: " + node.getGameState() + "\nExplored actions: " + exploredActions); } + GameState nextGameState = new GameState(node.getGameState()); nextGameState.playStone(player, action); + + //In principle, more than 1 new child could be generated from a call to grow. However, + //this algorithm only generates one. This interface is mainly for compatibility with Naive Monte Carlo. + + + return addNewChildren(node, action, nextGameState); + } + + protected List> addNewChildren(GameTreeNode node, Action action, GameState successorState) { List> newChildren = new ArrayList>(); + GameTreeNode newChild = new GameTreeNode( - nextGameState, new MonteCarloProperties()); + successorState, new MonteCarloProperties()); newChildren.add(newChild); node.addChild(action, newChild); - return newChildren; } - + @Override /** * Rollout currently depends on the hardcoded ROLLOUT_DEPTH_LIMIT superclass parameter, * Even with super-ko detection, a rollout might take an unrealistically long time due to unlikely playouts. */ - public int rollout(GameConfig gameConfig, + public Rollout rollout(GameConfig gameConfig, GameTreeNode node, Player player) { Policy randomMovePolicy = new RandomMovePolicy(); - Action action; + Action randomAction; int rolloutDepth = 0; - GameState rolloutGameState = new GameState(node.getGameState()); + GameState initialGameState = node.getGameState(); + GameState rolloutGameState = new GameState(initialGameState); Player currentPlayer = rolloutGameState.getPlayerToMove(); + List rolloutActions = new ArrayList(); do { rolloutDepth++; - action = randomMovePolicy.getAction(gameConfig, rolloutGameState, + randomAction = randomMovePolicy.getAction(gameConfig, rolloutGameState, currentPlayer); - if (action != Action.NONE) { - if (!rolloutGameState.playStone(currentPlayer, action)) { + if (randomAction != Action.NONE) { + if (!rolloutGameState.playStone(currentPlayer, randomAction)) { throw new RuntimeException( "Failed to play move selected by RandomMovePolicy"); } + rolloutActions.add(randomAction); currentPlayer = GoGame.getNextPlayer(currentPlayer); } - } while (action != Action.NONE && rolloutDepth < ROLLOUT_DEPTH_LIMIT); + } while (randomAction != Action.NONE && rolloutDepth < ROLLOUT_DEPTH_LIMIT); numStateEvaluations++; GameResult gameScore = rolloutGameState.getResult(); if (gameScore.isWinner(player)) { - return 1; + return new Rollout(initialGameState,rolloutActions,1); } else { - return 0; + return new Rollout(initialGameState,rolloutActions,0); } } @Override - public void update(GameTreeNode node, int reward) { + public void update(GameTreeNode node, Rollout rollout) { GameTreeNode currentNode = node; + + int reward = rollout.getReward(); + while (currentNode != null) { MonteCarloProperties nodeProperties = currentNode.getProperties(); nodeProperties.setWins(nodeProperties.getWins() + reward); diff --git a/src/net/woodyfolsom/msproj/policy/RandomMovePolicy.java b/src/net/woodyfolsom/msproj/policy/RandomMovePolicy.java index d686469..eae9485 100644 --- a/src/net/woodyfolsom/msproj/policy/RandomMovePolicy.java +++ b/src/net/woodyfolsom/msproj/policy/RandomMovePolicy.java @@ -34,6 +34,13 @@ public class RandomMovePolicy implements Policy, ActionGenerator { */ public List getActions(GameConfig gameConfig, GameState gameState, Collection prohibitedMoves, Player player, int nMoves) { + List randomActions = new ArrayList(); + + /*if (gameState.isTerminal()) { + randomActions.add(Action.NONE); + return randomActions; + }*/ + if (player != gameState.getPlayerToMove()) { throw new IllegalArgumentException("It is not " + player + "'s turn to move!"); @@ -45,7 +52,6 @@ public class RandomMovePolicy implements Policy, ActionGenerator { List possibleActions = actionGenerator.getActions(gameConfig, gameStateCopy, prohibitedMoves, player, ActionGenerator.ALL_ACTIONS); - List randomActions = new ArrayList(); // boolean playerIsWinning = gameState.getResult().isWinner(player); @@ -60,13 +66,20 @@ public class RandomMovePolicy implements Policy, ActionGenerator { } } + //if (randomActions.size() == 0) { // randomActions.add(Action.NONE); //} //PASS is always the move of last resort if no valid moves exist + //Action.NONE exists for a reason - if the fail-safe was to ALWAYS return PASS, then MCTS would + //fail to descend properly because the root node would always appear to have additional unexplored actions. if (randomActions.size() == 0) { - randomActions.add(Action.PASS); + if (prohibitedMoves.contains(Action.PASS)) { + randomActions.add(Action.NONE); + } else { + randomActions.add(Action.PASS); + } } //when to resign? diff --git a/src/net/woodyfolsom/msproj/policy/Rollout.java b/src/net/woodyfolsom/msproj/policy/Rollout.java new file mode 100644 index 0000000..1780050 --- /dev/null +++ b/src/net/woodyfolsom/msproj/policy/Rollout.java @@ -0,0 +1,65 @@ +package net.woodyfolsom.msproj.policy; + +import java.util.ArrayList; +import java.util.List; + +import net.woodyfolsom.msproj.Action; +import net.woodyfolsom.msproj.GameState; +import net.woodyfolsom.msproj.Player; + +public class Rollout { + private GameState initialGameState; + private List blackPlays; + private List playout; + private List whitePlays; + private int reward; + + public Rollout(GameState initialGameState, List playout, int reward) { + this.initialGameState = initialGameState; + this.playout = playout; + this.reward = reward; + + blackPlays = new ArrayList(); + whitePlays = new ArrayList(); + + Player playerToMove = initialGameState.getPlayerToMove(); + + List> plays = new ArrayList>(); + + if (playerToMove == Player.BLACK) { + plays.add(blackPlays); + plays.add(whitePlays); + } else if (playerToMove == Player.WHITE) { + plays.add(whitePlays); + plays.add(blackPlays); + } else { + throw new RuntimeException("Invalid player: " + playerToMove); + } + + for (int i = 0; i < playout.size(); i++) { + plays.get(i%2).add(playout.get(i)); + } + } + + public GameState getInitialGameState() { + return initialGameState; + } + + public List getPlayout() { + return playout; + } + + public int getReward() { + return reward; + } + + public boolean hasPlay(Player player, Action action) { + if (player == Player.BLACK) { + return blackPlays.contains(action); + } else if (player == Player.WHITE) { + return whitePlays.contains(action); + } else { + throw new RuntimeException("Invalid player: " + player); + } + } +} \ No newline at end of file diff --git a/src/net/woodyfolsom/msproj/policy/RootParallelization.java b/src/net/woodyfolsom/msproj/policy/RootParallelization.java index f7713d7..b648df1 100644 --- a/src/net/woodyfolsom/msproj/policy/RootParallelization.java +++ b/src/net/woodyfolsom/msproj/policy/RootParallelization.java @@ -76,22 +76,8 @@ public class RootParallelization implements Policy { int bestWins = 0; int bestSims = 0; - //int nActions = totalReward.size(); - //boolean playerToMoveIsWinning = gameState.getResult().isWinner(player); - //playerToMove is winning or only one move (PASS) is available - //boolean allowPass = playerToMoveIsWinning || nActions == 1; - for (Action action : totalReward.keySet()) { - //HEURISTIC - work on ways of removing this go-specific logic - // - //This heuristic must be duplicated here because RootPar. does not benefit - //from MonteCarloUCT culling PASS just before returning from getAction(). - //if (action.isPass() && !allowPass) { - // continue; //If the best rated action is PASS and I'm not winning and there are other valid actions, - // //keep searching. - //} - int totalWins = totalReward.get(action); int totalSims = numSims.get(action); diff --git a/src/net/woodyfolsom/msproj/policy/ValidMoveGenerator.java b/src/net/woodyfolsom/msproj/policy/ValidMoveGenerator.java index 526b31a..0a76138 100644 --- a/src/net/woodyfolsom/msproj/policy/ValidMoveGenerator.java +++ b/src/net/woodyfolsom/msproj/policy/ValidMoveGenerator.java @@ -22,9 +22,19 @@ public class ValidMoveGenerator implements ActionGenerator { public List getActions(GameConfig gameConfig, GameState gameState, Collection prohibitedMoves, Player color, int nMoves) { + List validMoves = new ArrayList(); + + if (gameState.isTerminal()) { + return validMoves; + } + GameState gameStateCopy = new GameState(gameState); List emptyCoordinates = gameStateCopy.getEmptyCoords(); - List validMoves = new ArrayList(); + + //Pass is always valid unless prohibited (or if the state is terminal, as above) + if (!prohibitedMoves.contains(Action.PASS)) { + validMoves.add(Action.PASS); + } while (emptyCoordinates.size() > 0) { Action nextMove = Action.getInstance(emptyCoordinates @@ -38,10 +48,6 @@ public class ValidMoveGenerator implements ActionGenerator { } } - if (!prohibitedMoves.contains(Action.PASS)) { - validMoves.add(Action.PASS); - } - return validMoves; } } diff --git a/src/net/woodyfolsom/msproj/tree/AMAFProperties.java b/src/net/woodyfolsom/msproj/tree/AMAFProperties.java new file mode 100644 index 0000000..c28143f --- /dev/null +++ b/src/net/woodyfolsom/msproj/tree/AMAFProperties.java @@ -0,0 +1,18 @@ +package net.woodyfolsom.msproj.tree; + +public class AMAFProperties extends MonteCarloProperties { + int amafWins = 0; + int amafVisits = 0; + public int getAmafWins() { + return amafWins; + } + public void setAmafWins(int amafWins) { + this.amafWins = amafWins; + } + public int getAmafVisits() { + return amafVisits; + } + public void setAmafVisits(int amafVisits) { + this.amafVisits = amafVisits; + } +} \ No newline at end of file diff --git a/test/net/woodyfolsom/msproj/GameScoreTest.java b/test/net/woodyfolsom/msproj/GameScoreTest.java index 417b839..582af1d 100644 --- a/test/net/woodyfolsom/msproj/GameScoreTest.java +++ b/test/net/woodyfolsom/msproj/GameScoreTest.java @@ -3,38 +3,15 @@ package net.woodyfolsom.msproj; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import net.woodyfolsom.msproj.GameResult; -import net.woodyfolsom.msproj.policy.MonteCarloUCT; -import net.woodyfolsom.msproj.policy.RandomMovePolicy; -import net.woodyfolsom.msproj.policy.RootParallelization; -import net.woodyfolsom.msproj.sgf.SGFLexer; -import net.woodyfolsom.msproj.sgf.SGFNodeCollection; -import net.woodyfolsom.msproj.sgf.SGFParser; - -import org.antlr.runtime.ANTLRInputStream; -import org.antlr.runtime.ANTLRStringStream; -import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.junit.Test; public class GameScoreTest { - // public static final String endGameSGF = - // "(;FF[4]GM[1]SZ[9]KM[5.5];B[ef];W[ff];B[dg];W[aa];B[fc];W[da];B[cg];W[ei];B[gf]" - // + - // ";W[fi];B[ag];W[ii];B[bi];W[if];B[db];W[ci];B[cf];W[ih];B[bc];W[hb];B[eb];W[fh];B[ig];W[hc];B[be];W[he];B[gc];" - // + - // "W[id];B[cd];W[df];B[hf];W[ah];B[bh];W[fa];B[bg];W[fe];B[ec];W[eh];B[ee];W[bd];B[hg];W[ie];B[fg];W[ca];B[eg];" - // + - // "W[cb];B[ad];W[ba];B[ch];W[dh];B[gd];W[ic];B[ha];W[ab];B[gh];W[gb];B[ed];W[];B[])"; - - //public static final String endGameSGF = "(;FF[4]GM[1]SZ[9]KM[5.5]RE[W+0.5];B[ef];W[cb];B[fe];W[da];B[cd];W[hh];B[ed];W[cc];B[ci];W[bc];B[cg];W[fi];B[be];W[ea];B[hi];W[df];B[fd];W[bg];B[cf];W[aa];B[gd];W[ch];B[ad];W[dg];B[de];W[ge];B[bh];W[fa];B[ag];W[hd];B[if];W[bi];B[gf];W[bd];B[ah];W[gc];B[ff];W[ca];B[hf];W[dd];B[ce];W[ae];B[ga];W[hc];B[ac];W[gg];B[fg];W[fb];B[ie];W[dh];B[af];W[ec];B[dc];W[id];B[dd];W[eh];B[eb];W[gb];B[ae];W[ic];B[di];W[fh];B[ig];W[ab];B[ha];W[hg];B[hb];W[gi];B[ii];W[ia];B[fc];W[ba];B[eg];W[];B[db];W[];B[])"; - - public static final String endGameSGF = "(;FF[4]GM[1]SZ[6]KM[1.5]RE[B+0.5];B[bb];W[];B[ec];W[ef];B[ac];W[ed];B[ba];W[dc];B[cf];W[];B[])"; @Test public void testGetAggregateScoreZero() { GameResult gameScore = new GameResult(0, 0, 19, 0, true); @@ -56,21 +33,23 @@ public class GameScoreTest { @Test public void testScoreEndGame() throws IOException, RecognitionException { - InputStream is = new ByteArrayInputStream(endGameSGF.getBytes()); - GameRecord gameRecord = Referee.replay(is); - assertEquals(11, gameRecord.getNumTurns()); - - GameState gameState9 = gameRecord.getGameState(9); + //test case from: + //http://www.online-go.com/faq.php?name=rules + GameRecord gameRecord = Referee.replay(new FileInputStream(new File("data/test/ScoreTest.sgf"))); + GameState gameState = gameRecord.getGameState(gameRecord.getNumTurns()); + GameConfig gameConfig = gameState.getGameConfig(); - for (int i = 0; i < 5; i++) { - //Action action = new RootParallelization(4, 1000L).getAction(gameRecord.getGameConfig(), gameState9, Player.WHITE); - Action action = new MonteCarloUCT(new RandomMovePolicy(),1000L).getAction(gameRecord.getGameConfig(), gameState9, Player.WHITE); - System.out.println("Suggested action for "+Player.WHITE+": " + action); - } + System.out.println(gameState); + System.out.println(gameState.getResult()); - gameState9.playStone(Player.WHITE, Action.PASS); - gameState9.playStone(Player.BLACK, Action.PASS); - assertTrue(gameState9.isTerminal()); - System.out.println(gameState9.getResult()); + GameState gameStateCopy = new GameState(gameState); + TerritoryMarker.markTerritory(gameStateCopy.getGameBoard()); + System.out.println(gameStateCopy); + + assertEquals(9,gameConfig.getSize()); + assertEquals(6.0,gameConfig.getKomi(),0.1); + + assertTrue(gameState.isTerminal()); + assertTrue(gameState.getResult().isWinner(Player.WHITE)); } } \ No newline at end of file