1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
3 export class SynochessRules
extends ChessRules
{
17 static get LoseOnRepetition() {
21 static IsGoodFlags(flags
) {
22 // Only white can castle
23 return !!flags
.match(/^[a-z]{2,2}$/);
26 static IsGoodFen(fen
) {
27 if (!ChessRules
.IsGoodFen(fen
)) return false;
28 const fenParsed
= V
.ParseFen(fen
);
30 if (!fenParsed
.reserve
|| !fenParsed
.reserve
.match(/^[0-2]$/))
35 static ParseFen(fen
) {
36 const fenParts
= fen
.split(" ");
38 ChessRules
.ParseFen(fen
),
39 { reserve: fenParts
[5] }
43 static GenRandInitFen(options
) {
45 return "rneakenr/8/1c4c1/1ss2ss1/8/8/PPPPPPPP/RNBQKBNR w 0 ah - 2";
47 // Mapping kingdom --> dynasty:
56 // Always symmetric (randomness = 1), because open files.
57 const baseFen
= ChessRules
.GenRandInitFen({ randomness: 1 });
59 baseFen
.substr(0, 8).split("").map(p
=> piecesMap
[p
]).join("") +
60 "/8/1c4c1/1ss2ss1/" + baseFen
.substr(22, 28) + " - 2"
65 return (!!this.reserve
? this.reserve
["b"][V
.SOLDIER
] : 0);
69 return super.getFen() + " " + this.getReserveFen();
73 return super.getFenForRepeat() + "_" + this.getReserveFen();
76 setOtherVariables(fen
) {
77 super.setOtherVariables(fen
);
78 // Also init reserve (used by the interface to show landable soldiers)
79 const reserve
= parseInt(V
.ParseFen(fen
).reserve
, 10);
80 if (reserve
> 0) this.reserve
= { 'b': { [V
.SOLDIER
]: reserve
} };
84 if (i
>= V
.size
.x
) return 'b';
85 return this.board
[i
][j
].charAt(0);
89 if (i
>= V
.size
.x
) return V
.SOLDIER
;
90 return this.board
[i
][j
].charAt(1);
93 getReservePpath(index
, color
) {
94 // Only one piece type: soldier
95 return "Synochess/" + color
+ V
.SOLDIER
;
98 static get RESERVE_PIECES() {
103 const color
= this.turn
;
104 if (!this.reserve
|| this.reserve
[color
][V
.SOLDIER
] == 0) return [];
106 for (let i
= 0; i
< V
.size
.y
; i
++) {
107 if (this.board
[3][i
] == V
.EMPTY
) {
118 start: { x: x
, y: 0 }, //a bit artificial...
128 return (ChessRules
.PIECES
.includes(b
[1]) ? "" : "Synochess/") + b
;
132 return this.castleFlags
['w'].map(V
.CoordToColumn
).join("");
136 this.castleFlags
= { 'w': [-1, -1] };
137 for (let i
= 0; i
< 2; i
++)
138 this.castleFlags
['w'][i
] = V
.ColumnToCoord(fenflags
.charAt(i
));
141 static get ELEPHANT() {
145 static get CANNON() {
149 static get SOLDIER() {
153 static get ADVISOR() {
157 static get PIECES() {
159 ChessRules
.PIECES
.concat([V
.ELEPHANT
, V
.ADVISOR
, V
.SOLDIER
, V
.CANNON
])
164 return Object
.assign(
182 getPotentialMovesFrom(sq
) {
183 if (sq
[0] >= V
.size
.x
)
184 // Reserves, outside of board: x == sizeX(+1)
185 return this.getReserveMoves(sq
[0]);
187 const piece
= this.getPiece(sq
[0], sq
[1]);
190 moves
= this.getPotentialCannonMoves(sq
);
193 moves
= this.getPotentialElephantMoves(sq
);
196 moves
= this.getPotentialAdvisorMoves(sq
);
199 moves
= this.getPotentialSoldierMoves(sq
);
202 moves
= super.getPotentialMovesFrom(sq
);
206 this.kingPos
['w'][0] != this.kingPos
['b'][0] &&
207 this.kingPos
['w'][1] != this.kingPos
['b'][1]
211 // TODO: from here, copy/paste from EmpireChess
212 // TODO: factor two next "if" into one (rank/column...)
213 if (this.kingPos
['w'][1] == this.kingPos
['b'][1]) {
214 const colKing
= this.kingPos
['w'][1];
215 let intercept
= 0; //count intercepting pieces
216 let [kingPos1
, kingPos2
] = [this.kingPos
['w'][0], this.kingPos
['b'][0]];
217 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
218 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
219 if (this.board
[i
][colKing
] != V
.EMPTY
) intercept
++;
221 if (intercept
>= 2) return moves
;
222 // intercept == 1 (0 is impossible):
223 // Any move not removing intercept is OK
224 return moves
.filter(m
=> {
226 // From another column?
227 m
.start
.y
!= colKing
||
228 // From behind a king? (including kings themselves!)
229 m
.start
.x
<= kingPos1
||
230 m
.start
.x
>= kingPos2
||
231 // Intercept piece moving: must remain in-between
233 m
.end
.y
== colKing
&&
234 m
.end
.x
> kingPos1
&&
240 if (this.kingPos
['w'][0] == this.kingPos
['b'][0]) {
241 const rowKing
= this.kingPos
['w'][0];
242 let intercept
= 0; //count intercepting pieces
243 let [kingPos1
, kingPos2
] = [this.kingPos
['w'][1], this.kingPos
['b'][1]];
244 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
245 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
246 if (this.board
[rowKing
][i
] != V
.EMPTY
) intercept
++;
248 if (intercept
>= 2) return moves
;
249 // intercept == 1 (0 is impossible):
250 // Any move not removing intercept is OK
251 return moves
.filter(m
=> {
254 m
.start
.x
!= rowKing
||
255 // From "behind" a king? (including kings themselves!)
256 m
.start
.y
<= kingPos1
||
257 m
.start
.y
>= kingPos2
||
258 // Intercept piece moving: must remain in-between
260 m
.end
.x
== rowKing
&&
261 m
.end
.y
> kingPos1
&&
267 // piece == king: check only if move.end.y == enemy king column,
268 // or if move.end.x == enemy king rank.
269 const color
= this.getColor(sq
[0], sq
[1]);
270 const oppCol
= V
.GetOppCol(color
);
271 return moves
.filter(m
=> {
273 m
.end
.y
!= this.kingPos
[oppCol
][1] &&
274 m
.end
.x
!= this.kingPos
[oppCol
][0]
278 // check == -1 if (row, or col) unchecked, 1 if checked and occupied,
279 // 0 if checked and clear
280 let check
= [-1, -1];
281 // TODO: factor two next "if"...
282 if (m
.end
.x
== this.kingPos
[oppCol
][0]) {
286 let [kingPos1
, kingPos2
] = [m
.end
.y
, this.kingPos
[oppCol
][1]];
287 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
288 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
289 if (this.board
[m
.end
.x
][i
] != V
.EMPTY
) {
294 return check
[0] == 1;
296 // Check already done:
297 return check
[0] == 1;
299 //if (m.end.y == this.kingPos[oppCol][1]) //true...
303 let [kingPos1
, kingPos2
] = [m
.end
.x
, this.kingPos
[oppCol
][0]];
304 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
305 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
306 if (this.board
[i
][m
.end
.y
] != V
.EMPTY
) {
311 return check
[1] == 1;
313 // Check already done:
314 return check
[1] == 1;
318 getPotentialAdvisorMoves(sq
) {
319 return super.getSlideNJumpMoves(
320 sq
, V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), 1);
323 getPotentialKingMoves([x
, y
]) {
324 if (this.getColor(x
, y
) == 'w') return super.getPotentialKingMoves([x
, y
]);
325 // Dynasty doesn't castle:
326 return super.getSlideNJumpMoves(
327 [x
, y
], V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), 1);
330 getPotentialSoldierMoves([x
, y
]) {
331 const c
= this.getColor(x
, y
);
332 const shiftX
= (c
== 'w' ? -1 : 1);
333 const lastRank
= (c
== 'w' && x
== 0 || c
== 'b' && x
== 9);
335 if (!lastRank
) steps
.push([shiftX
, 0]);
336 if (y
> 0) steps
.push([0, -1]);
337 if (y
< 9) steps
.push([0, 1]);
338 return super.getSlideNJumpMoves([x
, y
], steps
, 1);
341 getPotentialElephantMoves([x
, y
]) {
342 return this.getSlideNJumpMoves([x
, y
], V
.steps
[V
.ELEPHANT
], 1);
345 // NOTE: (mostly) duplicated from Shako (TODO?)
346 getPotentialCannonMoves([x
, y
]) {
347 const oppCol
= V
.GetOppCol(this.turn
);
349 // Look in every direction until an obstacle (to jump) is met
350 for (const step
of V
.steps
[V
.ROOK
]) {
353 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
357 // Then, search for an enemy (if jumped piece isn't a cannon)
358 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
361 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
362 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
366 if (V
.OnBoard(i
, j
) && this.getColor(i
, j
) == oppCol
)
367 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
373 isAttacked(sq
, color
) {
375 super.isAttackedByRook(sq
, color
) ||
376 super.isAttackedByKnight(sq
, color
) ||
377 super.isAttackedByKing(sq
, color
) ||
381 super.isAttackedByPawn(sq
, color
) ||
382 super.isAttackedByBishop(sq
, color
) ||
383 super.isAttackedByQueen(sq
, color
)
389 this.isAttackedByCannon(sq
, color
) ||
390 this.isAttackedBySoldier(sq
, color
) ||
391 this.isAttackedByAdvisor(sq
, color
) ||
392 this.isAttackedByElephant(sq
, color
)
398 // NOTE: (mostly) duplicated from Shako (TODO?)
399 isAttackedByCannon([x
, y
], color
) {
400 // Reversed process: is there an obstacle in line,
401 // and a cannon next in the same line?
402 for (const step
of V
.steps
[V
.ROOK
]) {
403 let [i
, j
] = [x
+step
[0], y
+step
[1]];
404 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
408 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
409 // Keep looking in this direction
412 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
418 this.getPiece(i
, j
) == V
.CANNON
&&
419 this.getColor(i
, j
) == color
428 isAttackedByAdvisor(sq
, color
) {
429 return super.isAttackedBySlideNJump(
430 sq
, color
, V
.ADVISOR
, V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), 1);
433 isAttackedByElephant(sq
, color
) {
434 return this.isAttackedBySlideNJump(
435 sq
, color
, V
.ELEPHANT
, V
.steps
[V
.ELEPHANT
], 1);
438 isAttackedBySoldier([x
, y
], color
) {
439 const shiftX
= (color
== 'w' ? 1 : -1); //shift from king
440 return super.isAttackedBySlideNJump(
441 [x
, y
], color
, V
.SOLDIER
, [[shiftX
, 0], [0, 1], [0, -1]], 1);
445 let moves
= super.getAllPotentialMoves();
446 const color
= this.turn
;
447 if (!!this.reserve
&& color
== 'b')
448 moves
= moves
.concat(this.getReserveMoves(V
.size
.x
+ 1));
449 return this.filterValid(moves
);
453 if (!super.atLeastOneMove()) {
454 if (!!this.reserve
&& this.turn
== 'b') {
455 let moves
= this.filterValid(this.getReserveMoves(V
.size
.x
+ 1));
456 if (moves
.length
> 0) return true;
465 const color
= V
.GetOppCol(this.turn
);
466 const lastRank
= (color
== 'w' ? 0 : 7);
467 if (this.kingPos
[color
][0] == lastRank
)
468 // The opposing edge is reached!
469 return color
== "w" ? "1-0" : "0-1";
470 if (this.atLeastOneMove()) return "*";
472 const oppCol
= this.turn
;
473 return (oppCol
== "w" ? "0-1" : "1-0");
476 updateCastleFlags(move, piece
) {
477 // Only white can castle:
479 if (piece
== V
.KING
&& move.appear
[0].c
== 'w')
480 this.castleFlags
['w'] = [8, 8];
482 move.start
.x
== firstRank
&&
483 this.castleFlags
['w'].includes(move.start
.y
)
485 const flagIdx
= (move.start
.y
== this.castleFlags
['w'][0] ? 0 : 1);
486 this.castleFlags
['w'][flagIdx
] = 8;
489 move.end
.x
== firstRank
&&
490 this.castleFlags
['w'].includes(move.end
.y
)
492 const flagIdx
= (move.end
.y
== this.castleFlags
['w'][0] ? 0 : 1);
493 this.castleFlags
['w'][flagIdx
] = 8;
498 // After black move, turn == 'w':
499 if (!!this.reserve
&& this.turn
== 'w' && move.vanish
.length
== 0) {
500 if (--this.reserve
['b'][V
.SOLDIER
] == 0) this.reserve
= null;
502 else super.postPlay(move);
506 if (this.turn
== 'b' && move.vanish
.length
== 0) {
507 if (!this.reserve
) this.reserve
= { 'b': { [V
.SOLDIER
]: 1 } };
508 else this.reserve
['b'][V
.SOLDIER
]++;
510 else super.postUndo(move);
513 static get VALUES() {
514 return Object
.assign(
525 static get SEARCH_DEPTH() {
530 let evaluation
= super.evalPosition();
531 if (this.turn
== 'b' && !!this.reserve
)
533 evaluation
+= this.reserve
['b'][V
.SOLDIER
] * V
.VALUES
[V
.SOLDIER
];
538 if (move.vanish
.length
== 0) return "@" + V
.CoordsToSquare(move.end
);
539 return super.getNotation(move);