1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
4 export class FanoronaRules
extends ChessRules
{
6 static get HasFlags() {
10 static get HasEnpassant() {
14 static get Monochrome() {
20 // Draw all inter-squares lines, shifted:
21 for (let i
= 0; i
< V
.size
.x
; i
++)
22 lines
.push([[i
+0.5, 0.5], [i
+0.5, V
.size
.y
-0.5]]);
23 for (let j
= 0; j
< V
.size
.y
; j
++)
24 lines
.push([[0.5, j
+0.5], [V
.size
.x
-0.5, j
+0.5]]);
26 [[0.5, 0.5], [2.5, 2.5]],
27 [[0.5, 2.5], [2.5, 0.5]],
28 [[2.5, 0.5], [4.5, 2.5]],
29 [[4.5, 0.5], [2.5, 2.5]]
31 for (let j
of [0, 2, 4, 6]) {
33 columnDiags
.map(L
=> [[L
[0][0], L
[0][1] + j
], [L
[1][0], L
[1][1] + j
]])
39 static get Notoodark() {
43 static GenRandInitFen() {
44 return "ppppppppp/ppppppppp/pPpP1pPpP/PPPPPPPPP/PPPPPPPPP w 0";
47 setOtherVariables(fen
) {
48 // Local stack of captures during a turn (squares + directions)
49 this.captures
= [ [] ];
53 return { x: 5, y: 9 };
60 static IsGoodPosition(position
) {
61 if (position
.length
== 0) return false;
62 const rows
= position
.split("/");
63 if (rows
.length
!= V
.size
.x
) return false;
64 for (let row
of rows
) {
66 for (let i
= 0; i
< row
.length
; i
++) {
67 if (row
[i
].toLowerCase() == V
.PAWN
) sumElts
++;
69 const num
= parseInt(row
[i
], 10);
70 if (isNaN(num
) || num
<= 0) return false;
74 if (sumElts
!= V
.size
.y
) return false;
80 return "Fanorona/" + b
;
83 getPPpath(m
, orientation
) {
84 // m.vanish.length >= 2, first capture gives direction
85 const ref
= (Math
.abs(m
.vanish
[1].x
- m
.start
.x
) == 1 ? m
.start : m
.end
);
86 const step
= [m
.vanish
[1].x
- ref
.x
, m
.vanish
[1].y
- ref
.y
];
87 const multStep
= (orientation
== 'w' ? 1 : -1);
88 const normalizedStep
= [
89 multStep
* step
[0] / Math
.abs(step
[0]),
90 multStep
* step
[1] / Math
.abs(step
[1])
94 (normalizedStep
[0] || 0) + "_" + (normalizedStep
[1] || 0)
98 // After moving, add stones captured in "step" direction from new location
99 // [x, y] to mv.vanish (if any captured stone!)
100 addCapture([x
, y
], step
, move) {
101 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
102 const oppCol
= V
.GetOppCol(move.vanish
[0].c
);
105 this.board
[i
][j
] != V
.EMPTY
&&
106 this.getColor(i
, j
) == oppCol
108 move.vanish
.push(new PiPo({ x: i
, y: j
, c: oppCol
, p: V
.PAWN
}));
112 return (move.vanish
.length
>= 2);
115 getPotentialMovesFrom([x
, y
]) {
116 const L0
= this.captures
.length
;
117 const captures
= this.captures
[L0
- 1];
118 const L
= captures
.length
;
120 var c
= captures
[L
-1];
121 if (x
!= c
.square
.x
+ c
.step
[0] || y
!= c
.square
.y
+ c
.step
[1])
124 const oppCol
= V
.GetOppCol(this.turn
);
125 let steps
= V
.steps
[V
.ROOK
];
126 if ((x
+ y
) % 2 == 0) steps
= steps
.concat(V
.steps
[V
.BISHOP
]);
128 for (let s
of steps
) {
129 if (L
> 0 && c
.step
[0] == s
[0] && c
.step
[1] == s
[1]) {
130 // Add a move to say "I'm done capturing"
135 start: { x: x
, y: y
},
136 end: { x: x
- s
[0], y: y
- s
[1] }
141 let [i
, j
] = [x
+ s
[0], y
+ s
[1]];
142 if (captures
.some(c
=> c
.square
.x
== i
&& c
.square
.y
== j
)) continue;
143 if (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
144 // The move is potentially allowed. Might lead to 2 different captures
145 let mv
= super.getBasicMove([x
, y
], [i
, j
]);
146 const capt
= this.addCapture([i
, j
], s
, mv
);
149 mv
= super.getBasicMove([x
, y
], [i
, j
]);
151 const capt_bw
= this.addCapture([x
, y
], [-s
[0], -s
[1]], mv
);
152 if (capt_bw
) moves
.push(mv
);
153 // Captures take priority (if available)
154 if (!capt
&& !capt_bw
&& L
== 0) moves
.push(mv
);
160 atLeastOneCapture() {
161 const color
= this.turn
;
162 const oppCol
= V
.GetOppCol(color
);
163 const L0
= this.captures
.length
;
164 const captures
= this.captures
[L0
- 1];
165 const L
= captures
.length
;
167 // If some adjacent enemy stone, with free space to capture it,
168 // toward a square not already visited, through a different step
169 // from last one: then yes.
170 const c
= captures
[L
-1];
171 const [x
, y
] = [c
.square
.x
+ c
.step
[0], c
.square
.y
+ c
.step
[1]];
172 let steps
= V
.steps
[V
.ROOK
];
173 if ((x
+ y
) % 2 == 0) steps
= steps
.concat(V
.steps
[V
.BISHOP
]);
174 // TODO: half of the steps explored are redundant
175 for (let s
of steps
) {
176 if (s
[0] == c
.step
[0] && s
[1] == c
.step
[1]) continue;
177 const [i
, j
] = [x
+ s
[0], y
+ s
[1]];
180 this.board
[i
][j
] != V
.EMPTY
||
181 captures
.some(c
=> c
.square
.x
== i
&& c
.square
.y
== j
)
186 V
.OnBoard(i
+ s
[0], j
+ s
[1]) &&
187 this.board
[i
+ s
[0]][j
+ s
[1]] != V
.EMPTY
&&
188 this.getColor(i
+ s
[0], j
+ s
[1]) == oppCol
193 V
.OnBoard(x
- s
[0], y
- s
[1]) &&
194 this.board
[x
- s
[0]][y
- s
[1]] != V
.EMPTY
&&
195 this.getColor(x
- s
[0], y
- s
[1]) == oppCol
202 for (let i
= 0; i
< V
.size
.x
; i
++) {
203 for (let j
= 0; j
< V
.size
.y
; j
++) {
205 this.board
[i
][j
] != V
.EMPTY
&&
206 this.getColor(i
, j
) == color
&&
207 // TODO: this could be more efficient
208 this.getPotentialMovesFrom([i
, j
]).some(m
=> m
.vanish
.length
>= 2)
217 static KeepCaptures(moves
) {
218 return moves
.filter(m
=> m
.vanish
.length
>= 2);
221 getPossibleMovesFrom(sq
) {
222 let moves
= this.getPotentialMovesFrom(sq
);
223 const L0
= this.captures
.length
;
224 const captures
= this.captures
[L0
- 1];
225 if (captures
.length
> 0) return this.getPotentialMovesFrom(sq
);
226 const captureMoves
= V
.KeepCaptures(moves
);
227 if (captureMoves
.length
> 0) return captureMoves
;
228 if (this.atLeastOneCapture()) return [];
233 const moves
= super.getAllValidMoves();
234 if (moves
.some(m
=> m
.vanish
.length
>= 2)) return V
.KeepCaptures(moves
);
247 const color
= this.turn
;
248 move.turn
= color
; //for undo
249 V
.PlayOnBoard(this.board
, move);
250 if (move.vanish
.length
>= 2) {
251 const L0
= this.captures
.length
;
252 let captures
= this.captures
[L0
- 1];
255 step: [move.end
.x
- move.start
.x
, move.end
.y
- move.start
.y
]
257 if (this.atLeastOneCapture())
258 // There could be other captures (optional)
259 move.notTheEnd
= true;
261 if (!move.notTheEnd
) {
262 this.turn
= V
.GetOppCol(color
);
264 this.captures
.push([]);
269 V
.UndoOnBoard(this.board
, move);
270 if (!move.notTheEnd
) {
271 this.turn
= move.turn
;
275 if (move.vanish
.length
>= 2) {
276 const L0
= this.captures
.length
;
277 let captures
= this.captures
[L0
- 1];
283 const color
= this.turn
;
284 // If no stones on board, I lose
286 this.board
.every(b
=> {
287 return b
.every(cell
=> {
288 return (cell
== "" || cell
[0] != color
);
292 return (color
== 'w' ? "0-1" : "1-0");
298 const moves
= this.getAllValidMoves();
299 if (moves
.length
== 0) return null;
300 const color
= this.turn
;
301 // Capture available? If yes, play it
302 let captures
= moves
.filter(m
=> m
.vanish
.length
>= 2);
304 while (captures
.length
>= 1) {
305 // Then just pick random captures (trying to maximize)
306 let candidates
= captures
.filter(c
=> !!c
.notTheEnd
);
308 if (candidates
.length
>= 1) mv
= candidates
[randInt(candidates
.length
)];
309 else mv
= captures
[randInt(captures
.length
)];
312 captures
= (this.turn
== color
? this.getAllValidMoves() : []);
314 if (mvArray
.length
>= 1) {
315 for (let i
= mvArray
.length
- 1; i
>= 0; i
--) this.undo(mvArray
[i
]);
318 // Just play a random move, which if possible does not let a capture
320 for (let m
of moves
) {
322 if (!this.atLeastOneCapture()) candidates
.push(m
);
325 if (candidates
.length
>= 1) return candidates
[randInt(candidates
.length
)];
326 return moves
[randInt(moves
.length
)];
330 if (move.appear
.length
== 0) return "stop";
332 V
.CoordsToSquare(move.start
) +
333 V
.CoordsToSquare(move.end
) +
334 (move.vanish
.length
>= 2 ? "X" : "")