1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
3 export class SynochessRules
extends ChessRules
{
5 static get LoseOnRepetition() {
9 static IsGoodFlags(flags
) {
10 // Only white can castle
11 return !!flags
.match(/^[a-z]{2,2}$/);
14 static IsGoodFen(fen
) {
15 if (!ChessRules
.IsGoodFen(fen
)) return false;
16 const fenParsed
= V
.ParseFen(fen
);
18 if (!fenParsed
.reserve
|| !fenParsed
.reserve
.match(/^[0-2]$/))
23 static ParseFen(fen
) {
24 const fenParts
= fen
.split(" ");
26 ChessRules
.ParseFen(fen
),
27 { reserve: fenParts
[5] }
31 static GenRandInitFen(randomness
) {
33 return "rneakenr/8/1c4c1/1ss2ss1/8/8/PPPPPPPP/RNBQKBNR w 0 ah - 2";
35 // Mapping kingdom --> dynasty:
44 // Always symmetric (randomness = 1), because open files.
45 const baseFen
= ChessRules
.GenRandInitFen(1);
47 baseFen
.substr(0, 8).split("").map(p
=> piecesMap
[p
]).join("") +
48 "/8/1c4c1/1ss2ss1/" + baseFen
.substr(22, 28) + " - 2"
53 return (!!this.reserve
? this.reserve
["b"][V
.SOLDIER
] : 0);
57 return super.getFen() + " " + this.getReserveFen();
61 return super.getFenForRepeat() + "_" + this.getReserveFen();
64 setOtherVariables(fen
) {
65 super.setOtherVariables(fen
);
66 // Also init reserve (used by the interface to show landable soldiers)
67 const reserve
= parseInt(V
.ParseFen(fen
).reserve
, 10);
68 if (reserve
> 0) this.reserve
= { 'b': { [V
.SOLDIER
]: reserve
} };
72 if (i
>= V
.size
.x
) return 'b';
73 return this.board
[i
][j
].charAt(0);
77 if (i
>= V
.size
.x
) return V
.SOLDIER
;
78 return this.board
[i
][j
].charAt(1);
81 getReservePpath(index
, color
) {
82 // Only one piece type: soldier
83 return "Synochess/" + color
+ V
.SOLDIER
;
86 static get RESERVE_PIECES() {
91 const color
= this.turn
;
92 if (!this.reserve
|| this.reserve
[color
][V
.SOLDIER
] == 0) return [];
94 for (let i
= 0; i
< V
.size
.y
; i
++) {
95 if (this.board
[3][i
] == V
.EMPTY
) {
106 start: { x: x
, y: 0 }, //a bit artificial...
116 return (ChessRules
.PIECES
.includes(b
[1]) ? "" : "Synochess/") + b
;
120 return this.castleFlags
['w'].map(V
.CoordToColumn
).join("");
124 this.castleFlags
= { 'w': [-1, -1] };
125 for (let i
= 0; i
< 2; i
++)
126 this.castleFlags
['w'][i
] = V
.ColumnToCoord(fenflags
.charAt(i
));
129 static get ELEPHANT() {
133 static get CANNON() {
137 static get SOLDIER() {
141 static get ADVISOR() {
145 static get PIECES() {
147 ChessRules
.PIECES
.concat([V
.ELEPHANT
, V
.ADVISOR
, V
.SOLDIER
, V
.CANNON
])
152 return Object
.assign(
170 getPotentialMovesFrom(sq
) {
171 if (sq
[0] >= V
.size
.x
)
172 // Reserves, outside of board: x == sizeX(+1)
173 return this.getReserveMoves(sq
[0]);
175 const piece
= this.getPiece(sq
[0], sq
[1]);
178 moves
= this.getPotentialCannonMoves(sq
);
181 moves
= this.getPotentialElephantMoves(sq
);
184 moves
= this.getPotentialAdvisorMoves(sq
);
187 moves
= this.getPotentialSoldierMoves(sq
);
190 moves
= super.getPotentialMovesFrom(sq
);
194 this.kingPos
['w'][0] != this.kingPos
['b'][0] &&
195 this.kingPos
['w'][1] != this.kingPos
['b'][1]
199 // TODO: from here, copy/paste from EmpireChess
200 // TODO: factor two next "if" into one (rank/column...)
201 if (this.kingPos
['w'][1] == this.kingPos
['b'][1]) {
202 const colKing
= this.kingPos
['w'][1];
203 let intercept
= 0; //count intercepting pieces
204 let [kingPos1
, kingPos2
] = [this.kingPos
['w'][0], this.kingPos
['b'][0]];
205 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
206 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
207 if (this.board
[i
][colKing
] != V
.EMPTY
) intercept
++;
209 if (intercept
>= 2) return moves
;
210 // intercept == 1 (0 is impossible):
211 // Any move not removing intercept is OK
212 return moves
.filter(m
=> {
214 // From another column?
215 m
.start
.y
!= colKing
||
216 // From behind a king? (including kings themselves!)
217 m
.start
.x
<= kingPos1
||
218 m
.start
.x
>= kingPos2
||
219 // Intercept piece moving: must remain in-between
221 m
.end
.y
== colKing
&&
222 m
.end
.x
> kingPos1
&&
228 if (this.kingPos
['w'][0] == this.kingPos
['b'][0]) {
229 const rowKing
= this.kingPos
['w'][0];
230 let intercept
= 0; //count intercepting pieces
231 let [kingPos1
, kingPos2
] = [this.kingPos
['w'][1], this.kingPos
['b'][1]];
232 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
233 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
234 if (this.board
[rowKing
][i
] != V
.EMPTY
) intercept
++;
236 if (intercept
>= 2) return moves
;
237 // intercept == 1 (0 is impossible):
238 // Any move not removing intercept is OK
239 return moves
.filter(m
=> {
242 m
.start
.x
!= rowKing
||
243 // From "behind" a king? (including kings themselves!)
244 m
.start
.y
<= kingPos1
||
245 m
.start
.y
>= kingPos2
||
246 // Intercept piece moving: must remain in-between
248 m
.end
.x
== rowKing
&&
249 m
.end
.y
> kingPos1
&&
255 // piece == king: check only if move.end.y == enemy king column,
256 // or if move.end.x == enemy king rank.
257 const color
= this.getColor(sq
[0], sq
[1]);
258 const oppCol
= V
.GetOppCol(color
);
259 return moves
.filter(m
=> {
261 m
.end
.y
!= this.kingPos
[oppCol
][1] &&
262 m
.end
.x
!= this.kingPos
[oppCol
][0]
266 // check == -1 if (row, or col) unchecked, 1 if checked and occupied,
267 // 0 if checked and clear
268 let check
= [-1, -1];
269 // TODO: factor two next "if"...
270 if (m
.end
.x
== this.kingPos
[oppCol
][0]) {
274 let [kingPos1
, kingPos2
] = [m
.end
.y
, this.kingPos
[oppCol
][1]];
275 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
276 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
277 if (this.board
[m
.end
.x
][i
] != V
.EMPTY
) {
282 return check
[0] == 1;
284 // Check already done:
285 return check
[0] == 1;
287 //if (m.end.y == this.kingPos[oppCol][1]) //true...
291 let [kingPos1
, kingPos2
] = [m
.end
.x
, this.kingPos
[oppCol
][0]];
292 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
293 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
294 if (this.board
[i
][m
.end
.y
] != V
.EMPTY
) {
299 return check
[1] == 1;
301 // Check already done:
302 return check
[1] == 1;
306 getPotentialAdvisorMoves(sq
) {
307 return super.getSlideNJumpMoves(
308 sq
, V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), "oneStep");
311 getPotentialKingMoves([x
, y
]) {
312 if (this.getColor(x
, y
) == 'w') return super.getPotentialKingMoves([x
, y
]);
313 // Dynasty doesn't castle:
314 return super.getSlideNJumpMoves(
316 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
321 getPotentialSoldierMoves([x
, y
]) {
322 const c
= this.getColor(x
, y
);
323 const shiftX
= (c
== 'w' ? -1 : 1);
324 const lastRank
= (c
== 'w' && x
== 0 || c
== 'b' && x
== 9);
326 if (!lastRank
) steps
.push([shiftX
, 0]);
327 if (y
> 0) steps
.push([0, -1]);
328 if (y
< 9) steps
.push([0, 1]);
329 return super.getSlideNJumpMoves([x
, y
], steps
, "oneStep");
332 getPotentialElephantMoves([x
, y
]) {
333 return this.getSlideNJumpMoves([x
, y
], V
.steps
[V
.ELEPHANT
], "oneStep");
336 // NOTE: (mostly) duplicated from Shako (TODO?)
337 getPotentialCannonMoves([x
, y
]) {
338 const oppCol
= V
.GetOppCol(this.turn
);
340 // Look in every direction until an obstacle (to jump) is met
341 for (const step
of V
.steps
[V
.ROOK
]) {
344 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
348 // Then, search for an enemy (if jumped piece isn't a cannon)
349 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
352 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
353 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
357 if (V
.OnBoard(i
, j
) && this.getColor(i
, j
) == oppCol
)
358 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
364 isAttacked(sq
, color
) {
366 super.isAttackedByRook(sq
, color
) ||
367 super.isAttackedByKnight(sq
, color
) ||
368 super.isAttackedByKing(sq
, color
) ||
372 super.isAttackedByPawn(sq
, color
) ||
373 super.isAttackedByBishop(sq
, color
) ||
374 super.isAttackedByQueen(sq
, color
)
380 this.isAttackedByCannon(sq
, color
) ||
381 this.isAttackedBySoldier(sq
, color
) ||
382 this.isAttackedByAdvisor(sq
, color
) ||
383 this.isAttackedByElephant(sq
, color
)
389 // NOTE: (mostly) duplicated from Shako (TODO?)
390 isAttackedByCannon([x
, y
], color
) {
391 // Reversed process: is there an obstacle in line,
392 // and a cannon next in the same line?
393 for (const step
of V
.steps
[V
.ROOK
]) {
394 let [i
, j
] = [x
+step
[0], y
+step
[1]];
395 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
399 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
400 // Keep looking in this direction
403 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
409 this.getPiece(i
, j
) == V
.CANNON
&&
410 this.getColor(i
, j
) == color
419 isAttackedByAdvisor(sq
, color
) {
421 super.isAttackedBySlideNJump(
422 sq
, color
, V
.ADVISOR
,
423 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), "oneStep"
428 isAttackedByElephant(sq
, color
) {
430 this.isAttackedBySlideNJump(
431 sq
, color
, V
.ELEPHANT
, V
.steps
[V
.ELEPHANT
], "oneStep"
436 isAttackedBySoldier([x
, y
], color
) {
437 const shiftX
= (color
== 'w' ? 1 : -1); //shift from king
438 return super.isAttackedBySlideNJump(
439 [x
, y
], color
, V
.SOLDIER
, [[shiftX
, 0], [0, 1], [0, -1]], "oneStep");
443 let moves
= super.getAllPotentialMoves();
444 const color
= this.turn
;
445 if (!!this.reserve
&& color
== 'b')
446 moves
= moves
.concat(this.getReserveMoves(V
.size
.x
+ 1));
447 return this.filterValid(moves
);
451 if (!super.atLeastOneMove()) {
452 if (!!this.reserve
&& this.turn
== 'b') {
453 let moves
= this.filterValid(this.getReserveMoves(V
.size
.x
+ 1));
454 if (moves
.length
> 0) return true;
463 const color
= V
.GetOppCol(this.turn
);
464 const lastRank
= (color
== 'w' ? 0 : 7);
465 if (this.kingPos
[color
][0] == lastRank
)
466 // The opposing edge is reached!
467 return color
== "w" ? "1-0" : "0-1";
468 if (this.atLeastOneMove()) return "*";
470 const oppCol
= this.turn
;
471 return (oppCol
== "w" ? "0-1" : "1-0");
474 updateCastleFlags(move, piece
) {
475 // Only white can castle:
477 if (piece
== V
.KING
&& move.appear
[0].c
== 'w')
478 this.castleFlags
['w'] = [8, 8];
480 move.start
.x
== firstRank
&&
481 this.castleFlags
['w'].includes(move.start
.y
)
483 const flagIdx
= (move.start
.y
== this.castleFlags
['w'][0] ? 0 : 1);
484 this.castleFlags
['w'][flagIdx
] = 8;
487 move.end
.x
== firstRank
&&
488 this.castleFlags
['w'].includes(move.end
.y
)
490 const flagIdx
= (move.end
.y
== this.castleFlags
['w'][0] ? 0 : 1);
491 this.castleFlags
['w'][flagIdx
] = 8;
496 super.postPlay(move);
497 // After black move, turn == 'w':
498 if (!!this.reserve
&& this.turn
== 'w' && move.vanish
.length
== 0)
499 if (--this.reserve
['b'][V
.SOLDIER
] == 0) this.reserve
= null;
503 super.postUndo(move);
504 if (this.turn
== 'b' && move.vanish
.length
== 0) {
505 if (!this.reserve
) this.reserve
= { 'b': { [V
.SOLDIER
]: 1 } };
506 else this.reserve
['b'][V
.SOLDIER
]++;
510 static get VALUES() {
511 return Object
.assign(
522 static get SEARCH_DEPTH() {
527 let evaluation
= super.evalPosition();
528 if (this.turn
== 'b' && !!this.reserve
)
530 evaluation
+= this.reserve
['b'][V
.SOLDIER
] * V
.VALUES
[V
.SOLDIER
];