shogiops
Version:
Shogi rules and operations
187 lines (170 loc) • 6.47 kB
text/typescript
import type { Result } from '@badrap/result';
import { attacks, between, ray } from '../attacks.js';
import { 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 { standardSquareAttacks, standardSquareSnipers } from './shogi.js';
import { fullSquareSet } from './util.js';
export class Annanshogi extends Position {
private constructor() {
super('annanshogi');
}
static from(setup: Setup, strict: boolean): Result<Annanshogi, PositionError> {
const pos = new Annanshogi();
pos.fromSetup(setup);
return pos.validate(strict).map((_) => pos);
}
validation = {
doublePawn: false,
oppositeCheck: true,
unpromotedForcedPromotion: true,
maxNumberOfRoyalPieces: 1,
};
squareAttackers(square: Square, attacker: Color, occupied: SquareSet): SquareSet {
return standardSquareAttacks(square, attacker, annanAttackBoard(this.board), occupied);
}
squareSnipers(square: number, attacker: Color): SquareSet {
return standardSquareSnipers(square, attacker, annanAttackBoard(this.board));
}
moveDests(square: Square, ctx?: Context): SquareSet {
ctx = ctx || this.ctx();
const realPiece = this.board.get(square);
if (!realPiece || realPiece.color !== ctx.color) return SquareSet.empty();
const pieceBehind = this.board.get(directlyBehind(realPiece.color, square));
let pseudo = attacks(
pieceBehind?.color === realPiece.color ? pieceBehind : realPiece,
square,
this.board.occupied,
);
pseudo = pseudo.diff(this.board.color(ctx.color));
if (defined(ctx.king)) {
if (realPiece.role === 'king') {
const occ = this.board.occupied.without(square);
for (const to of pseudo) {
const boardClone = this.board.clone();
boardClone.take(to);
if (
standardSquareAttacks(
to,
opposite(ctx.color),
annanAttackBoard(boardClone),
occ,
).nonEmpty()
)
pseudo = pseudo.without(to);
}
} else {
const stdAttackers = standardSquareAttacks(
ctx.king,
opposite(ctx.color),
this.board,
this.board.occupied,
);
pseudo = pseudo.diff(
(ctx.color === 'sente' ? stdAttackers.shr256(16) : stdAttackers.shl256(16)).intersect(
this.board.occupied,
),
);
if (ctx.checkers.nonEmpty()) {
if (ctx.checkers.size() > 2) return SquareSet.empty();
const singularChecker = ctx.checkers.singleSquare();
const moveGivers = (
ctx.color === 'sente' ? ctx.checkers.shr256(16) : ctx.checkers.shl256(16)
).intersect(pseudo);
if (defined(singularChecker))
pseudo = pseudo.intersect(between(singularChecker, ctx.king).with(singularChecker));
else pseudo = SquareSet.empty();
for (const moveGiver of moveGivers) {
const boardClone = this.board.clone();
boardClone.take(square);
boardClone.set(moveGiver, realPiece);
if (
standardSquareAttacks(
ctx.king,
opposite(ctx.color),
annanAttackBoard(boardClone),
boardClone.occupied,
).isEmpty()
) {
pseudo = pseudo.with(moveGiver);
}
}
}
if (ctx.blockers.has(square)) {
let rayed = pseudo.intersect(ray(square, ctx.king));
const occ = this.board.occupied.without(square);
for (const to of pseudo.diff(rayed)) {
if (this.board.getColor(to) !== ctx.color) {
const boardClone = this.board.clone();
boardClone.take(square);
boardClone.set(to, realPiece);
if (
standardSquareAttacks(
ctx.king,
opposite(ctx.color),
annanAttackBoard(boardClone),
occ,
).isEmpty()
) {
rayed = rayed.with(to);
break;
}
}
}
pseudo = rayed;
}
}
}
return pseudo.intersect(fullSquareSet(this.rules));
}
dropDests(piece: Piece, ctx?: Context): SquareSet {
ctx = ctx || this.ctx();
if (piece.color !== ctx.color) return SquareSet.empty();
const role = piece.role;
let mask = this.board.occupied.complement();
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 = this.board.role('pawn').intersect(this.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 = this.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 = this.clone();
child.play({ role: 'pawn', to: kingFront });
const childResult = child.outcome()?.result;
if (childResult && ['checkmate', 'stalemate'].includes(childResult))
mask = mask.without(kingFront);
}
}
return mask.intersect(fullSquareSet(this.rules));
}
}
export const directlyBehind = (color: Color, square: Square): Square => {
return color === 'sente' ? square + 16 : square - 16;
};
// Changes the pieces in front of other friendly piece to said pieces
export const annanAttackBoard = (board: Board): Board => {
const newBoard = Board.empty();
for (const [sq, piece] of board) {
const pieceBehind = board.get(directlyBehind(piece.color, sq));
const role = pieceBehind?.color === piece.color ? pieceBehind.role : piece.role;
newBoard.set(sq, { role, color: piece.color });
}
return newBoard;
};