shogiops
Version:
Shogi rules and operations
228 lines • 10.9 kB
JavaScript
import { attacks, bishopAttacks, boarAttacks, chariotAttacks, copperAttacks, eagleAttacks, eagleLionAttacks, elephantAttacks, falconAttacks, goBetweenAttacks, goldAttacks, kingAttacks, kirinAttacks, lanceAttacks, leopardAttacks, lionAttacks, oxAttacks, pawnAttacks, phoenixAttacks, rookAttacks, sideMoverAttacks, silverAttacks, stagAttacks, tigerAttacks, verticalMoverAttacks, whaleAttacks, whiteHorseAttacks, } from '../attacks.js';
import { SquareSet } from '../square-set.js';
import { defined, isMove, lionRoles, opposite, squareDist } from '../util.js';
import { Position } from './position.js';
import { dimensions, fullSquareSet } from './util.js';
export class Chushogi extends Position {
constructor() {
super('chushogi');
this.validation = {
doublePawn: false,
oppositeCheck: false,
unpromotedForcedPromotion: false,
maxNumberOfRoyalPieces: 2,
};
}
static from(setup, strict) {
const pos = new Chushogi();
pos.fromSetup(setup);
return pos.validate(strict).map((_) => pos);
}
squareAttackers(square, attacker, occupied) {
const defender = opposite(attacker);
const board = this.board;
return board.color(attacker).intersect(lanceAttacks(square, defender, occupied)
.intersect(board.role('lance'))
.union(leopardAttacks(square).intersect(board.role('leopard')))
.union(copperAttacks(square, defender).intersect(board.role('copper')))
.union(silverAttacks(square, defender).intersect(board.role('silver')))
.union(goldAttacks(square, defender).intersect(board.roles('gold', 'promotedpawn')))
.union(kingAttacks(square).intersect(board.roles('king', 'prince', 'dragon', 'dragonpromoted', 'horse', 'horsepromoted')))
.union(elephantAttacks(square, defender).intersect(board.roles('elephant', 'elephantpromoted')))
.union(chariotAttacks(square, occupied).intersect(board.role('chariot')))
.union(bishopAttacks(square, occupied).intersect(board.roles('bishop', 'bishoppromoted', 'horse', 'horsepromoted', 'queen', 'queenpromoted')))
.union(tigerAttacks(square, defender).intersect(board.role('tiger')))
.union(kirinAttacks(square).intersect(board.role('kirin')))
.union(phoenixAttacks(square).intersect(board.role('phoenix')))
.union(sideMoverAttacks(square, occupied).intersect(board.roles('sidemover', 'sidemoverpromoted')))
.union(verticalMoverAttacks(square, occupied).intersect(board.roles('verticalmover', 'verticalmoverpromoted')))
.union(rookAttacks(square, occupied).intersect(board.roles('rook', 'rookpromoted', 'dragon', 'dragonpromoted', 'queen', 'queenpromoted')))
.union(lionAttacks(square).intersect(board.roles('lion', 'lionpromoted')))
.union(pawnAttacks(square, defender).intersect(board.role('pawn')))
.union(goBetweenAttacks(square).intersect(board.role('gobetween')))
.union(whiteHorseAttacks(square, defender, occupied).intersect(board.role('whitehorse')))
.union(whaleAttacks(square, defender, occupied).intersect(board.role('whale')))
.union(stagAttacks(square, occupied).intersect(board.role('stag')))
.union(boarAttacks(square, occupied).intersect(board.role('boar')))
.union(oxAttacks(square, occupied).intersect(board.role('ox')))
.union(falconAttacks(square, defender, occupied).intersect(board.role('falcon')))
.union(eagleAttacks(square, defender, occupied).intersect(board.role('eagle'))));
}
// we can move into check - not needed
squareSnipers(_square, _attacker) {
return SquareSet.empty();
}
kingsOf(color) {
return this.board.roles('king', 'prince').intersect(this.board.color(color));
}
moveDests(square, ctx) {
ctx = ctx || this.ctx();
const piece = this.board.get(square);
if (!piece || piece.color !== ctx.color)
return SquareSet.empty();
let pseudo = attacks(piece, square, this.board.occupied).diff(this.board.color(ctx.color));
const oppColor = opposite(ctx.color);
const oppLions = this.board.color(oppColor).intersect(this.board.roles('lion', 'lionpromoted'));
// considers only the first step destinations, for second step - secondLionStepDests
if (lionRoles.includes(piece.role)) {
const neighbors = kingAttacks(square);
// don't allow capture of a non-adjacent lion protected by an enemy piece
for (const lion of pseudo.diff(neighbors).intersect(oppLions)) {
if (this.squareAttackers(lion, oppColor, this.board.occupied.without(square)).nonEmpty())
pseudo = pseudo.without(lion);
}
}
else if (defined(this.lastLionCapture)) {
// can't recapture lion on another square (allow capturing lion on the same square from kirin promotion)
for (const lion of oppLions.intersect(pseudo)) {
if (lion !== this.lastLionCapture)
pseudo = pseudo.without(lion);
}
}
return pseudo.intersect(fullSquareSet(this.rules));
}
dropDests(_piece, _ctx) {
return SquareSet.empty();
}
outcome(ctx) {
ctx = ctx || this.ctx();
if (this.kingsOf(ctx.color).isEmpty())
return {
result: 'kingsLost',
winner: opposite(ctx.color),
};
else if (!this.hasDests(ctx)) {
return {
result: 'stalemate',
winner: opposite(ctx.color),
};
}
else if (isBareKing(this, 'sente')) {
return {
result: 'bareKing',
winner: 'gote',
};
}
else if (isBareKing(this, 'gote')) {
return {
result: 'bareKing',
winner: 'sente',
};
}
else if (isDraw(this)) {
return {
result: 'draw',
winner: undefined,
};
}
else
return;
}
isLegal(md, ctx) {
return (isMove(md) &&
((!defined(md.midStep) && super.isLegal(md, ctx)) ||
(defined(md.midStep) &&
super.isLegal({ from: md.from, to: md.midStep }, ctx) &&
secondLionStepDests(this, md.from, md.midStep).has(md.to))));
}
}
// chushogi position before piece is moved from initial square
export function secondLionStepDests(before, initialSq, midSq) {
const piece = before.board.get(initialSq);
if (!piece || piece.color !== before.turn)
return SquareSet.empty();
if (lionRoles.includes(piece.role)) {
if (!kingAttacks(initialSq).has(midSq))
return SquareSet.empty();
let pseudoDests = kingAttacks(midSq)
.diff(before.board.color(before.turn).without(initialSq))
.intersect(fullSquareSet(before.rules));
const oppColor = opposite(before.turn);
const oppLions = before.board
.color(oppColor)
.intersect(before.board.roles('lion', 'lionpromoted'))
.intersect(pseudoDests);
const capture = before.board.get(midSq);
const clearOccupied = before.board.occupied.withoutMany(initialSq, midSq);
// can't capture a non-adjacent lion protected by an enemy piece,
// unless we captured something valuable first (not a pawn or go-between)
for (const lion of oppLions) {
if (squareDist(initialSq, lion) > 1 &&
before.squareAttackers(lion, oppColor, clearOccupied).nonEmpty() &&
(!capture || capture.role === 'pawn' || capture.role === 'gobetween'))
pseudoDests = pseudoDests.without(lion);
}
return pseudoDests;
}
else if (piece.role === 'falcon') {
if (!pawnAttacks(initialSq, piece.color).has(midSq))
return SquareSet.empty();
let pseudoDests = goBetweenAttacks(midSq)
.diff(before.board.color(before.turn).without(initialSq))
.intersect(fullSquareSet(before.rules));
if (defined(before.lastLionCapture))
pseudoDests = removeLions(before, pseudoDests);
return pseudoDests;
}
else if (piece.role === 'eagle') {
let pseudoDests = eagleLionAttacks(initialSq, piece.color)
.diff(before.board.color(before.turn))
.with(initialSq);
if (!pseudoDests.has(midSq) || squareDist(initialSq, midSq) > 1)
return SquareSet.empty();
pseudoDests = pseudoDests.intersect(kingAttacks(midSq)).intersect(fullSquareSet(before.rules));
if (defined(before.lastLionCapture))
pseudoDests = removeLions(before, pseudoDests);
return pseudoDests;
}
else
return SquareSet.empty();
}
function removeLions(pos, dests) {
const oppColor = opposite(pos.turn);
const oppLions = pos.board
.color(oppColor)
.intersect(pos.board.roles('lion', 'lionpromoted'))
.intersect(dests);
for (const lion of oppLions) {
if (lion !== pos.lastLionCapture)
dests = dests.without(lion);
}
return dests;
}
function isBareKing(pos, color) {
// was our king bared
const theirColor = opposite(color);
const ourKing = pos.kingsOf(color).singleSquare();
const ourPieces = pos.board
.color(color)
.diff(pos.board
.roles('pawn', 'lance')
.intersect(SquareSet.fromRank(color === 'sente' ? 0 : dimensions(pos.rules).ranks - 1)));
const theirKing = pos.kingsOf(theirColor).singleSquare();
const theirPieces = pos.board
.color(theirColor)
.diff(pos.board
.roles('pawn', 'gobetween')
.union(pos.board
.role('lance')
.intersect(SquareSet.fromRank(theirColor === 'sente' ? 0 : dimensions(pos.rules).ranks - 1))));
return (ourPieces.size() === 1 &&
defined(ourKing) &&
theirPieces.size() > 1 &&
defined(theirKing) &&
!pos.isCheck(theirColor) &&
(theirPieces.size() > 2 || kingAttacks(ourKing).intersect(theirPieces).isEmpty()));
}
function isDraw(pos) {
const oneWayRoles = pos.board.roles('pawn', 'lance');
const occ = pos.board.occupied.diff(oneWayRoles
.intersect(pos.board.color('sente').intersect(SquareSet.fromRank(0)))
.union(oneWayRoles.intersect(pos.board.color('gote').intersect(SquareSet.fromRank(dimensions(pos.rules).ranks - 1)))));
return (occ.size() === 2 &&
pos.kingsOf('sente').isSingleSquare() &&
!pos.isCheck('sente') &&
pos.kingsOf('gote').isSingleSquare() &&
!pos.isCheck('gote'));
}
//# sourceMappingURL=chushogi.js.map