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 // check == -1 if (row, or col) unchecked, 1 if checked and occupied,
260 // 0 if checked and clear
261 let check
= [-1, -1];
262 return moves
.filter(m
=> {
264 m
.end
.y
!= this.kingPos
[oppCol
][1] &&
265 m
.end
.x
!= this.kingPos
[oppCol
][0]
269 // TODO: factor two next "if"...
270 if (m
.end
.x
== this.kingPos
[oppCol
][0]) {
274 let [kingPos1
, kingPos2
] =
275 [this.kingPos
[color
][1], this.kingPos
[oppCol
][1]];
276 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
277 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
278 if (this.board
[m
.end
.x
][i
] != V
.EMPTY
) {
283 return check
[0] == 1;
285 // Check already done:
286 return check
[0] == 1;
288 //if (m.end.y == this.kingPos[oppCol][1]) //true...
292 let [kingPos1
, kingPos2
] =
293 [this.kingPos
[color
][0], this.kingPos
[oppCol
][0]];
294 if (kingPos1
> kingPos2
) [kingPos1
, kingPos2
] = [kingPos2
, kingPos1
];
295 for (let i
= kingPos1
+ 1; i
< kingPos2
; i
++) {
296 if (this.board
[i
][m
.end
.y
] != V
.EMPTY
) {
301 return check
[1] == 1;
303 // Check already done:
304 return check
[1] == 1;
308 getPotentialAdvisorMoves(sq
) {
309 return super.getSlideNJumpMoves(
310 sq
, V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), "oneStep");
313 getPotentialKingMoves([x
, y
]) {
314 if (this.getColor(x
, y
) == 'w') return super.getPotentialKingMoves([x
, y
]);
315 // Dynasty doesn't castle:
316 return super.getSlideNJumpMoves(
318 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
323 getPotentialSoldierMoves([x
, y
]) {
324 const c
= this.getColor(x
, y
);
325 const shiftX
= (c
== 'w' ? -1 : 1);
326 const lastRank
= (c
== 'w' && x
== 0 || c
== 'b' && x
== 9);
328 if (!lastRank
) steps
.push([shiftX
, 0]);
329 if (y
> 0) steps
.push([0, -1]);
330 if (y
< 9) steps
.push([0, 1]);
331 return super.getSlideNJumpMoves([x
, y
], steps
, "oneStep");
334 getPotentialElephantMoves([x
, y
]) {
335 return this.getSlideNJumpMoves([x
, y
], V
.steps
[V
.ELEPHANT
], "oneStep");
338 // NOTE: (mostly) duplicated from Shako (TODO?)
339 getPotentialCannonMoves([x
, y
]) {
340 const oppCol
= V
.GetOppCol(this.turn
);
342 // Look in every direction until an obstacle (to jump) is met
343 for (const step
of V
.steps
[V
.ROOK
]) {
346 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
347 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
351 // Then, search for an enemy (if jumped piece isn't a cannon)
352 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
355 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
359 if (V
.OnBoard(i
, j
) && this.getColor(i
, j
) == oppCol
)
360 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
366 isAttacked(sq
, color
) {
368 super.isAttackedByRook(sq
, color
) ||
369 super.isAttackedByKnight(sq
, color
) ||
370 super.isAttackedByKing(sq
, color
) ||
374 super.isAttackedByPawn(sq
, color
) ||
375 super.isAttackedByBishop(sq
, color
) ||
376 super.isAttackedByQueen(sq
, color
)
382 this.isAttackedByCannon(sq
, color
) ||
383 this.isAttackedBySoldier(sq
, color
) ||
384 this.isAttackedByAdvisor(sq
, color
) ||
385 this.isAttackedByElephant(sq
, color
)
391 // NOTE: (mostly) duplicated from Shako (TODO?)
392 isAttackedByCannon([x
, y
], color
) {
393 // Reversed process: is there an obstacle in line,
394 // and a cannon next in the same line?
395 for (const step
of V
.steps
[V
.ROOK
]) {
396 let [i
, j
] = [x
+step
[0], y
+step
[1]];
397 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
401 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
402 // Keep looking in this direction
405 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
411 this.getPiece(i
, j
) == V
.CANNON
&&
412 this.getColor(i
, j
) == color
421 isAttackedByAdvisor(sq
, color
) {
423 super.isAttackedBySlideNJump(
424 sq
, color
, V
.ADVISOR
,
425 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), "oneStep"
430 isAttackedByElephant(sq
, color
) {
432 this.isAttackedBySlideNJump(
433 sq
, color
, V
.ELEPHANT
, V
.steps
[V
.ELEPHANT
], "oneStep"
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]], "oneStep");
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 super.postPlay(move);
499 // After black move, turn == 'w':
500 if (!!this.reserve
&& this.turn
== 'w' && move.vanish
.length
== 0)
501 if (--this.reserve
['b'][V
.SOLDIER
] == 0) this.reserve
= null;
505 super.postUndo(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
]++;
512 static get VALUES() {
513 return Object
.assign(
524 static get SEARCH_DEPTH() {
529 let evaluation
= super.evalPosition();
530 if (this.turn
== 'b')
532 evaluation
+= this.reserve
['b'][V
.SOLDIER
] * V
.VALUES
[V
.SOLDIER
];