shogiops
Version:
Shogi rules and operations
155 lines (138 loc) • 5.46 kB
text/typescript
import type { Result } from '@badrap/result';
import {
attacks,
between,
bishopAttacks,
goldAttacks,
kingAttacks,
knightAttacks,
lanceAttacks,
pawnAttacks,
ray,
rookAttacks,
silverAttacks,
} from '../attacks.js';
import type { Board } from '../board.js';
import { SquareSet } from '../square-set.js';
import type { Color, Piece, Setup, Square } from '../types.js';
import { defined, opposite, squareFile } from '../util.js';
import type { Context, PositionError } from './position.js';
import { Position } from './position.js';
import { dimensions, fullSquareSet } from './util.js';
export class Shogi extends Position {
private constructor() {
super('standard');
}
static from(setup: Setup, strict: boolean): Result<Shogi, PositionError> {
const pos = new Shogi();
pos.fromSetup(setup);
return pos.validate(strict).map((_) => pos);
}
squareAttackers(square: Square, attacker: Color, occupied: SquareSet): SquareSet {
return standardSquareAttacks(square, attacker, this.board, occupied);
}
squareSnipers(square: number, attacker: Color): SquareSet {
return standardSquareSnipers(square, attacker, this.board);
}
dropDests(piece: Piece, ctx?: Context): SquareSet {
return standardDropDests(this, piece, ctx);
}
moveDests(square: Square, ctx?: Context): SquareSet {
return standardMoveDests(this, square, ctx);
}
}
export const standardSquareAttacks = (
square: Square,
attacker: Color,
board: Board,
occupied: SquareSet,
): SquareSet => {
const defender = opposite(attacker);
return board.color(attacker).intersect(
rookAttacks(square, occupied)
.intersect(board.roles('rook', 'dragon'))
.union(bishopAttacks(square, occupied).intersect(board.roles('bishop', 'horse')))
.union(lanceAttacks(square, defender, occupied).intersect(board.role('lance')))
.union(knightAttacks(square, defender).intersect(board.role('knight')))
.union(silverAttacks(square, defender).intersect(board.role('silver')))
.union(
goldAttacks(square, defender).intersect(
board.roles('gold', 'tokin', 'promotedlance', 'promotedknight', 'promotedsilver'),
),
)
.union(pawnAttacks(square, defender).intersect(board.role('pawn')))
.union(kingAttacks(square).intersect(board.roles('king', 'dragon', 'horse'))),
);
};
export const standardSquareSnipers = (square: number, attacker: Color, board: Board): SquareSet => {
const empty = SquareSet.empty();
return rookAttacks(square, empty)
.intersect(board.roles('rook', 'dragon'))
.union(bishopAttacks(square, empty).intersect(board.roles('bishop', 'horse')))
.union(lanceAttacks(square, opposite(attacker), empty).intersect(board.role('lance')))
.intersect(board.color(attacker));
};
export const standardMoveDests = (pos: Position, square: Square, ctx?: Context): SquareSet => {
ctx = ctx || pos.ctx();
const piece = pos.board.get(square);
if (!piece || piece.color !== ctx.color) return SquareSet.empty();
let pseudo = attacks(piece, square, pos.board.occupied).intersect(fullSquareSet(pos.rules));
pseudo = pseudo.diff(pos.board.color(ctx.color));
if (defined(ctx.king)) {
if (piece.role === 'king') {
const occ = pos.board.occupied.without(square);
for (const to of pseudo) {
if (pos.squareAttackers(to, opposite(ctx.color), occ).nonEmpty())
pseudo = pseudo.without(to);
}
} else {
if (ctx.checkers.nonEmpty()) {
const checker = ctx.checkers.singleSquare();
if (!defined(checker)) return SquareSet.empty();
pseudo = pseudo.intersect(between(checker, ctx.king).with(checker));
}
if (ctx.blockers.has(square)) pseudo = pseudo.intersect(ray(square, ctx.king));
}
}
return pseudo;
};
export const standardDropDests = (pos: Position, piece: Piece, ctx?: Context): SquareSet => {
ctx = ctx || pos.ctx();
if (piece.color !== ctx.color) return SquareSet.empty();
const role = piece.role;
let mask = pos.board.occupied.complement();
// Removing backranks, where no legal drop would be possible
const dims = dimensions(pos.rules);
if (role === 'pawn' || role === 'lance')
mask = mask.diff(SquareSet.fromRank(ctx.color === 'sente' ? 0 : dims.ranks - 1));
else if (role === 'knight')
mask = mask.diff(
ctx.color === 'sente' ? SquareSet.ranksAbove(2) : SquareSet.ranksBelow(dims.ranks - 3),
);
if (defined(ctx.king) && ctx.checkers.nonEmpty()) {
const checker = ctx.checkers.singleSquare();
if (!defined(checker)) return SquareSet.empty();
mask = mask.intersect(between(checker, ctx.king));
}
if (role === 'pawn') {
// Checking for double pawns
const pawns = pos.board.role('pawn').intersect(pos.board.color(ctx.color));
for (const pawn of pawns) {
const file = SquareSet.fromFile(squareFile(pawn));
mask = mask.diff(file);
}
// Checking for a pawn checkmate
const kingSquare = pos.kingsOf(opposite(ctx.color)).singleSquare();
const kingFront = defined(kingSquare)
? ctx.color === 'sente'
? kingSquare + 16
: kingSquare - 16
: undefined;
if (defined(kingFront) && mask.has(kingFront)) {
const child = pos.clone();
child.play({ role: 'pawn', to: kingFront });
if (child.outcome()?.result === 'checkmate') mask = mask.without(kingFront);
}
}
return mask.intersect(fullSquareSet(pos.rules));
};