1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
4 export class MadhouseRules
extends ChessRules
{
6 hoverHighlight([x
, y
], side
) {
7 // Testing move validity results in an infinite update loop.
8 // TODO: find a way to test validity anyway.
10 (this.subTurn
== 2 && this.board
[x
][y
] == V
.EMPTY
) &&
11 (!side
|| side
== this.turn
)
15 setOtherVariables(fen
) {
16 super.setOtherVariables(fen
);
21 canIplay(side
, [x
, y
]) {
22 if (this.subTurn
== 1) return super.canIplay(side
, [x
, y
]);
23 // subturn == 2, drop a piece:
24 return side
== this.turn
&& this.board
[x
][y
] == V
.EMPTY
;
27 getPotentialMovesFrom([x
, y
]) {
28 if (this.subTurn
== 1) return super.getPotentialMovesFrom([x
, y
]);
29 // subTurn == 2: a move is a click, not handled here
34 if (this.subTurn
== 2) return super.filterValid(moves
);
35 const color
= this.turn
;
36 return moves
.filter(m
=> {
39 if (m
.vanish
.length
== 1 || m
.appear
.length
== 2)
41 res
= !this.underCheck(color
);
43 // Capture: find landing square not resulting in check
44 const boundary
= (m
.vanish
[1].p
!= V
.PAWN
? [0, 7] : [1, 6]);
46 m
.vanish
[1].p
== V
.BISHOP
47 ? (m
.vanish
[1].x
+ m
.vanish
[1].y
) % 2
49 outerLoop: for (let i
= boundary
[0]; i
<= boundary
[1]; i
++) {
50 for (let j
=0; j
<8; j
++) {
52 this.board
[i
][j
] == V
.EMPTY
&&
53 (!sqColor
|| (i
+ j
) % 2 == sqColor
)
55 const tMove
= new Move({
65 start: { x: -1, y: -1 }
68 const moveOk
= !this.underCheck(color
);
84 if (this.subTurn
== 1) return super.getAllValidMoves();
85 // Subturn == 2: only replacements
87 const L
= this.firstMove
.length
;
88 const fm
= this.firstMove
[L
- 1];
89 const color
= this.turn
;
90 const oppCol
= V
.GetOppCol(color
);
91 const boundary
= (fm
.vanish
[1].p
!= V
.PAWN
? [0, 7] : [1, 6]);
93 fm
.vanish
[1].p
== V
.BISHOP
94 ? (fm
.vanish
[1].x
+ fm
.vanish
[1].y
) % 2
96 for (let i
= boundary
[0]; i
< boundary
[1]; i
++) {
97 for (let j
=0; j
<8; j
++) {
99 this.board
[i
][j
] == V
.EMPTY
&&
100 (!sqColor
|| (i
+ j
) % 2 == sqColor
)
102 const tMove
= new Move({
112 start: { x: -1, y: -1 }
115 const moveOk
= !this.underCheck(color
);
117 if (moveOk
) moves
.push(tMove
);
125 if (isNaN(square
[0])) return null;
126 // If subTurn == 2 && square is empty && !underCheck, then replacement
127 if (this.subTurn
== 2 && this.board
[square
[0]][square
[1]] == V
.EMPTY
) {
128 const L
= this.firstMove
.length
;
129 const fm
= this.firstMove
[L
- 1];
130 const color
= this.turn
;
131 const oppCol
= V
.GetOppCol(color
);
133 (fm
.vanish
[1].p
== V
.PAWN
&& [0, 7].includes(square
[0])) ||
135 fm
.vanish
[1].p
== V
.BISHOP
&&
136 (square
[0] + square
[1] + fm
.vanish
[1].x
+ fm
.vanish
[1].y
) % 2 != 0
139 // Pawns cannot be replaced on first or last rank,
140 // bishops must be replaced on same square color.
143 const tMove
= new Move({
153 start: { x: -1, y: -1 }
156 const moveOk
= !this.underCheck(color
);
158 if (moveOk
) return tMove
;
164 move.flags
= JSON
.stringify(this.aggregateFlags());
165 if (move.vanish
.length
> 0) {
166 this.epSquares
.push(this.getEpSquare(move));
167 this.firstMove
.push(move);
169 V
.PlayOnBoard(this.board
, move);
172 move.vanish
.length
== 1 ||
173 move.appear
.length
== 2
175 this.turn
= V
.GetOppCol(this.turn
);
179 else this.subTurn
= 2;
180 if (move.vanish
.length
> 0) this.postPlay(move);
184 if (move.appear
[0].p
== V
.KING
)
185 this.kingPos
[move.appear
[0].c
] = [move.appear
[0].x
, move.appear
[0].y
];
186 this.updateCastleFlags(move, move.appear
[0].p
, move.appear
[0].c
);
190 this.disaggregateFlags(JSON
.parse(move.flags
));
191 if (move.vanish
.length
> 0) {
192 this.epSquares
.pop();
193 this.firstMove
.pop();
195 V
.UndoOnBoard(this.board
, move);
196 if (this.subTurn
== 2) this.subTurn
= 1;
198 this.turn
= V
.GetOppCol(this.turn
);
200 this.subTurn
= (move.vanish
.length
> 0 ? 1 : 2);
202 if (move.vanish
.length
> 0) super.postUndo(move);
206 let moves
= this.getAllValidMoves();
207 if (moves
.length
== 0) return null;
208 // Custom "search" at depth 1 (for now. TODO?)
209 const maxeval
= V
.INFINITY
;
210 const color
= this.turn
;
211 const initEval
= this.evalPosition();
214 m
.eval
= (color
== "w" ? -1 : 1) * maxeval
;
215 if (m
.vanish
.length
== 2 && m
.appear
.length
== 1) {
216 const moves2
= this.getAllValidMoves();
218 moves2
.forEach(m2
=> {
220 const score
= this.getCurrentScore();
222 if (["1-0", "0-1"].includes(score
))
223 mvEval
= (score
== "1-0" ? 1 : -1) * maxeval
;
224 else if (score
== "*")
225 // Add small fluctuations to avoid dropping pieces always on the
226 // first square available.
227 mvEval
= initEval
+ 0.05 - Math
.random() / 10;
229 (color
== 'w' && mvEval
> m
.eval
) ||
230 (color
== 'b' && mvEval
< m
.eval
)
239 const score
= this.getCurrentScore();
240 if (score
!= "1/2") {
241 if (score
!= "*") m
.eval
= (score
== "1-0" ? 1 : -1) * maxeval
;
242 else m
.eval
= this.evalPosition();
247 moves
.sort((a
, b
) => {
248 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
250 let candidates
= [0];
251 for (let i
= 1; i
< moves
.length
&& moves
[i
].eval
== moves
[0].eval
; i
++)
253 const mIdx
= candidates
[randInt(candidates
.length
)];
254 if (!moves
[mIdx
].next
) return moves
[mIdx
];
255 const move2
= moves
[mIdx
].next
;
256 delete moves
[mIdx
]["next"];
257 return [moves
[mIdx
], move2
];
261 if (move.vanish
.length
> 0) return super.getNotation(move);
264 move.appear
[0].p
!= V
.PAWN
? move.appear
[0].p
.toUpperCase() : "";
265 return piece
+ "@" + V
.CoordsToSquare(move.end
);