shogiops
Version:
Shogi rules and operations
302 lines • 11.6 kB
JavaScript
import { Result } from '@badrap/result';
import { between } from '../attacks.js';
import { COLORS } from '../constants.js';
import { SquareSet } from '../square-set.js';
import { defined, isDrop, lionRoles, makePieceName, opposite, squareFile } from '../util.js';
import { allRoles, fullSquareSet, handRoles, pieceCanPromote, pieceForcePromote, promote, unpromote, } from './util.js';
export const IllegalSetup = {
Empty: 'ERR_EMPTY',
OppositeCheck: 'ERR_OPPOSITE_CHECK',
PiecesOutsideBoard: 'ERR_PIECES_OUTSIDE_BOARD',
InvalidPieces: 'ERR_INVALID_PIECE',
InvalidPiecesHand: 'ERR_INVALID_PIECE_IN_HAND',
InvalidPiecesPromotionZone: 'ERR_PIECES_MUST_PROMOTE',
InvalidPiecesDoublePawns: 'ERR_PIECES_DOUBLE_PAWNS',
Kings: 'ERR_KINGS',
};
export class PositionError extends Error {
}
export class Position {
constructor(rules) {
this.rules = rules;
}
// Doesn't consider safety of the king
illegalMoveDests(square) {
return this.moveDests(square, {
king: undefined,
color: this.turn,
blockers: SquareSet.empty(),
checkers: SquareSet.empty(),
});
}
// Doesn't consider safety of the king
illegalDropDests(piece) {
return this.dropDests(piece, {
king: undefined,
color: this.turn,
blockers: SquareSet.empty(),
checkers: SquareSet.empty(),
});
}
fromSetup(setup) {
this.board = setup.board.clone();
this.hands = setup.hands.clone();
this.turn = setup.turn;
this.moveNumber = setup.moveNumber;
this.lastMoveOrDrop = setup.lastMoveOrDrop;
this.lastLionCapture = setup.lastLionCapture;
}
clone() {
const pos = new this.constructor();
pos.board = this.board.clone();
pos.hands = this.hands.clone();
pos.turn = this.turn;
pos.moveNumber = this.moveNumber;
pos.lastMoveOrDrop = this.lastMoveOrDrop;
pos.lastLionCapture = this.lastLionCapture;
return pos;
}
validate(strict) {
if (!this.board.occupied.intersect(fullSquareSet(this.rules)).equals(this.board.occupied))
return Result.err(new PositionError(IllegalSetup.PiecesOutsideBoard));
for (const [r] of this.hands.color('sente'))
if (!handRoles(this.rules).includes(r))
return Result.err(new PositionError(IllegalSetup.InvalidPiecesHand));
for (const [r] of this.hands.color('gote'))
if (!handRoles(this.rules).includes(r))
return Result.err(new PositionError(IllegalSetup.InvalidPiecesHand));
for (const role of this.board.presentRoles())
if (!allRoles(this.rules).includes(role))
return Result.err(new PositionError(IllegalSetup.InvalidPieces));
const otherKing = this.kingsOf(opposite(this.turn)).singleSquare();
if (defined(otherKing) &&
this.squareAttackers(otherKing, this.turn, this.board.occupied).nonEmpty())
return Result.err(new PositionError(IllegalSetup.OppositeCheck));
if (!strict)
return Result.ok(undefined);
// double pawns
for (const color of COLORS) {
const files = [];
const pawns = this.board.role('pawn').intersect(this.board.color(color));
for (const pawn of pawns) {
const file = squareFile(pawn);
if (files.includes(file))
return Result.err(new PositionError(IllegalSetup.InvalidPiecesDoublePawns));
files.push(file);
}
}
if (this.board.pieces('sente', 'king').size() >= 2 ||
this.board.pieces('gote', 'king').size() >= 2)
return Result.err(new PositionError(IllegalSetup.Kings));
if (this.board.occupied.isEmpty())
return Result.err(new PositionError(IllegalSetup.Empty));
if (this.board.role('king').isEmpty())
return Result.err(new PositionError(IllegalSetup.Kings));
for (const [sq, piece] of this.board)
if (pieceForcePromote(this.rules)(piece, sq))
return Result.err(new PositionError(IllegalSetup.InvalidPiecesPromotionZone));
return Result.ok(undefined);
}
ctx(color) {
color = color || this.turn;
const king = this.kingsOf(color).singleSquare();
if (!defined(king))
return {
color,
king,
blockers: SquareSet.empty(),
checkers: SquareSet.empty(),
};
const snipers = this.squareSnipers(king, opposite(color));
let blockers = SquareSet.empty();
for (const sniper of snipers) {
const b = between(king, sniper).intersect(this.board.occupied);
if (!b.moreThanOne())
blockers = blockers.union(b);
}
const checkers = this.squareAttackers(king, opposite(color), this.board.occupied);
return {
color,
king,
blockers,
checkers,
};
}
kingsOf(color) {
return this.board.role('king').intersect(this.board.color(color));
}
isCheck(color) {
color = color || this.turn;
for (const king of this.kingsOf(color)) {
if (this.squareAttackers(king, opposite(color), this.board.occupied).nonEmpty())
return true;
}
return false;
}
checks() {
let checks = SquareSet.empty();
COLORS.forEach((color) => {
for (const king of this.kingsOf(color)) {
if (this.squareAttackers(king, opposite(color), this.board.occupied).nonEmpty())
checks = checks.with(king);
}
});
return checks;
}
isCheckmate(ctx) {
ctx = ctx || this.ctx();
return ctx.checkers.nonEmpty() && !this.hasDests(ctx);
}
isStalemate(ctx) {
ctx = ctx || this.ctx();
return ctx.checkers.isEmpty() && !this.hasDests(ctx);
}
isDraw(_ctx) {
return COLORS.every((color) => this.board.color(color).size() + this.hands[color].count() < 2);
}
isBareKing(_ctx) {
return false;
}
isWithoutKings(_ctx) {
return false;
}
isSpecialVariantEnd(_ctx) {
return false;
}
isEnd(ctx) {
ctx = ctx || this.ctx();
return (this.isCheckmate(ctx) ||
this.isStalemate(ctx) ||
this.isDraw(ctx) ||
this.isBareKing(ctx) ||
this.isWithoutKings(ctx) ||
this.isSpecialVariantEnd(ctx));
}
outcome(ctx) {
ctx = ctx || this.ctx();
if (this.isCheckmate(ctx))
return {
result: 'checkmate',
winner: opposite(ctx.color),
};
else if (this.isStalemate(ctx)) {
return {
result: 'stalemate',
winner: opposite(ctx.color),
};
}
else if (this.isDraw(ctx)) {
return {
result: 'draw',
winner: undefined,
};
}
else
return;
}
allMoveDests(ctx) {
ctx = ctx || this.ctx();
const d = new Map();
for (const square of this.board.color(ctx.color)) {
d.set(square, this.moveDests(square, ctx));
}
return d;
}
allDropDests(ctx) {
ctx = ctx || this.ctx();
const d = new Map();
for (const role of handRoles(this.rules)) {
const piece = { color: ctx.color, role };
if (this.hands[ctx.color].get(role) > 0) {
d.set(makePieceName(piece), this.dropDests(piece, ctx));
}
else
d.set(makePieceName(piece), SquareSet.empty());
}
return d;
}
hasDests(ctx) {
ctx = ctx || this.ctx();
for (const square of this.board.color(ctx.color)) {
if (this.moveDests(square, ctx).nonEmpty())
return true;
}
for (const [role] of this.hands[ctx.color]) {
if (this.dropDests({ color: ctx.color, role }, ctx).nonEmpty())
return true;
}
return false;
}
isLegal(md, ctx) {
const turn = (ctx === null || ctx === void 0 ? void 0 : ctx.color) || this.turn;
if (isDrop(md)) {
const role = md.role;
if (!handRoles(this.rules).includes(role) || this.hands[turn].get(role) <= 0)
return false;
return this.dropDests({ color: turn, role }, ctx).has(md.to);
}
else {
const piece = this.board.get(md.from);
if (!piece || !allRoles(this.rules).includes(piece.role))
return false;
// Checking whether we can promote
if (md.promotion &&
!pieceCanPromote(this.rules)(piece, md.from, md.to, this.board.get(md.to)))
return false;
if (!md.promotion && pieceForcePromote(this.rules)(piece, md.to))
return false;
return this.moveDests(md.from, ctx).has(md.to);
}
}
unpromoteForHand(role) {
if (handRoles(this.rules).includes(role))
return role;
const unpromotedRole = unpromote(this.rules)(role);
if (unpromotedRole && handRoles(this.rules).includes(unpromotedRole))
return unpromotedRole;
return;
}
storeCapture(capture) {
const unpromotedRole = this.unpromoteForHand(capture.role);
if (unpromotedRole && handRoles(this.rules).includes(unpromotedRole))
this.hands[opposite(capture.color)].capture(unpromotedRole);
}
// doesn't care about validity, just tries to play the move/drop
play(md) {
const turn = this.turn;
this.moveNumber += 1;
this.turn = opposite(turn);
this.lastMoveOrDrop = md;
this.lastLionCapture = undefined;
if (isDrop(md)) {
this.board.set(md.to, { role: md.role, color: turn });
this.hands[turn].drop(this.unpromoteForHand(md.role) || md.role);
}
else {
const piece = this.board.take(md.from), role = piece === null || piece === void 0 ? void 0 : piece.role;
if (!role)
return;
if ((md.promotion &&
pieceCanPromote(this.rules)(piece, md.from, md.to, this.board.get(md.to))) ||
pieceForcePromote(this.rules)(piece, md.to))
piece.role = promote(this.rules)(role) || role;
const capture = this.board.set(md.to, piece), midCapture = defined(md.midStep) ? this.board.take(md.midStep) : undefined;
// process midCapture (if exists) before final destination capture
if (defined(midCapture)) {
if (!lionRoles.includes(role) &&
midCapture.color === this.turn &&
lionRoles.includes(midCapture.role))
this.lastLionCapture = md.midStep;
this.storeCapture(midCapture);
}
if (capture) {
if (!lionRoles.includes(role) &&
capture.color === this.turn &&
lionRoles.includes(capture.role))
this.lastLionCapture = md.to;
this.storeCapture(capture);
}
}
}
}
//# sourceMappingURL=position.js.map