shogiops
Version:
Shogi rules and operations
753 lines (729 loc) • 17.6 kB
text/typescript
import { SquareSet } from '../square-set.js';
import type { Piece, Role, Rules, Square } from '../types.js';
import { squareFile, squareRank } from '../util.js';
import type { Position } from '../variant/position.js';
export function aimingAt(pos: Position, pieces: SquareSet, to: Square): SquareSet {
let ambs = SquareSet.empty();
for (const p of pieces) if (pos.illegalMoveDests(p).has(to)) ambs = ambs.with(p);
return ambs;
}
export function roleToWestern(rules: Rules): (role: Role) => string {
if (rules === 'dobutsu') return doubutsuRoleToWestern;
return (role) => {
switch (role) {
case 'pawn':
return 'P';
case 'lance':
return 'L';
case 'knight':
return 'N';
case 'silver':
return 'S';
case 'gold':
return 'G';
case 'bishop':
return 'B';
case 'rook':
return 'R';
case 'tokin':
return rules === 'kyotoshogi' ? 'T' : '+P';
case 'promotedpawn':
return '+P';
case 'promotedlance':
return '+L';
case 'promotedknight':
return '+N';
case 'promotedsilver':
return '+S';
case 'horse':
return rules === 'chushogi' ? 'H' : '+B';
case 'dragon':
return rules === 'chushogi' ? 'D' : '+R';
case 'king':
return 'K';
case 'leopard':
return 'FL';
case 'copper':
return 'C';
case 'elephant':
return 'DE';
case 'chariot':
return 'RC';
case 'tiger':
return 'BT';
case 'kirin':
return 'Kr';
case 'phoenix':
return 'Ph';
case 'sidemover':
return 'SM';
case 'verticalmover':
return 'VM';
case 'lion':
return 'Ln';
case 'queen':
return 'FK';
case 'gobetween':
return 'GB';
case 'whitehorse':
return '+L';
case 'bishoppromoted':
return '+FL';
case 'sidemoverpromoted':
return '+C';
case 'verticalmoverpromoted':
return '+S';
case 'rookpromoted':
return '+G';
case 'prince':
return '+DE';
case 'whale':
return '+RC';
case 'horsepromoted':
return '+B';
case 'stag':
return '+BT';
case 'lionpromoted':
return '+Kr';
case 'queenpromoted':
return '+Ph';
case 'boar':
return '+SM';
case 'ox':
return '+VM';
case 'falcon':
return '+H';
case 'eagle':
return '+D';
case 'dragonpromoted':
return '+R';
case 'elephantpromoted':
return '+GB';
}
};
}
function doubutsuRoleToWestern(role: Role): string {
switch (role) {
case 'pawn':
return 'C';
case 'bishop':
return 'E';
case 'rook':
return 'G';
case 'tokin':
return 'H';
case 'king':
return 'L';
default:
return '';
}
}
// for kanji disambiguation
export function roleKanjiDuplicates(rules: Rules): (role: Role) => Role[] {
if (rules === 'chushogi')
return (role) => {
const roles: Role[][] = [
['gold', 'promotedpawn'],
['elephant', 'elephantpromoted'],
['sidemover', 'sidemoverpromoted'],
['verticalmover', 'verticalmoverpromoted'],
['horse', 'horsepromoted'],
['dragon', 'dragonpromoted'],
['lion', 'lionpromoted'],
['queen', 'queenpromoted'],
];
for (const rs of roles) {
if (rs.includes(role)) return rs.filter((r) => r !== role);
}
return [];
};
else return () => [];
}
export function roleToKanji(rules: Rules): (role: Role) => string {
if (rules === 'dobutsu') return doubutsuRoleToKanji;
return (role) => {
switch (role) {
case 'pawn':
return '歩';
case 'lance':
return '香';
case 'knight':
return '桂';
case 'silver':
return '銀';
case 'gold':
return '金';
case 'bishop':
return '角';
case 'rook':
return '飛';
case 'tokin':
return 'と';
case 'promotedpawn':
return '金';
case 'promotedlance':
return '成香';
case 'promotedknight':
return '成桂';
case 'promotedsilver':
return '成銀';
case 'horse':
case 'horsepromoted':
return '馬';
case 'dragon':
case 'dragonpromoted':
return '龍';
case 'king':
return '玉';
case 'leopard':
return '豹';
case 'copper':
return '銅';
case 'elephant':
case 'elephantpromoted':
return '象';
case 'chariot':
return '反';
case 'tiger':
return '虎';
case 'kirin':
return '麒';
case 'phoenix':
return '鳳';
case 'sidemover':
case 'sidemoverpromoted':
return '横';
case 'verticalmover':
case 'verticalmoverpromoted':
return '竪';
case 'lion':
case 'lionpromoted':
return '獅';
case 'queen':
case 'queenpromoted':
return '奔';
case 'gobetween':
return '仲';
case 'whitehorse':
return '駒';
case 'bishoppromoted':
return '小角';
case 'rookpromoted':
return '金飛車';
case 'prince':
return '太';
case 'whale':
return '鯨';
case 'stag':
return '鹿';
case 'boar':
return '猪';
case 'ox':
return '牛';
case 'falcon':
return '鷹';
case 'eagle':
return '鷲';
}
};
}
function doubutsuRoleToKanji(role: Role): string {
switch (role) {
case 'pawn':
return 'ひよこ';
case 'bishop':
return 'ぞう';
case 'rook':
return 'きりん';
case 'tokin':
return 'にわとり';
case 'king':
return 'ライオン';
default:
return '';
}
}
export function roleToBoardKanji(rules: Rules): (role: Role) => string {
if (rules === 'dobutsu') return doubutsuRoleToBoardKanji;
return (role) => {
switch (role) {
case 'promotedlance':
return '杏';
case 'promotedknight':
return '圭';
case 'promotedsilver':
return '全';
case 'bishoppromoted':
return '成角';
case 'rookpromoted':
return '成飛';
case 'queenpromoted':
return '成奔';
case 'verticalmoverpromoted':
return '成竪';
case 'sidemoverpromoted':
return '成横';
case 'elephantpromoted':
return '成象';
case 'lionpromoted':
return '成獅';
case 'horsepromoted':
return '成馬';
case 'dragonpromoted':
return '成龍';
case 'promotedpawn':
return '成歩';
default:
return roleToKanji(rules)(role);
}
};
}
function doubutsuRoleToBoardKanji(role: Role): string {
switch (role) {
case 'pawn':
return 'ひ';
case 'bishop':
return 'ぞ';
case 'rook':
return 'き';
case 'tokin':
return 'に';
case 'king':
return 'ラ';
default:
return '';
}
}
export function roleToFullKanji(rules: Rules): (role: Role) => string {
if (rules === 'dobutsu') return doubutsuRoleToKanji;
return (role) => {
switch (role) {
case 'pawn':
return '歩兵';
case 'lance':
return '香車';
case 'knight':
return '桂馬';
case 'silver':
return '銀将';
case 'gold':
return '金将';
case 'bishop':
return '角行';
case 'rook':
return '飛車';
case 'tokin':
return 'と金';
case 'promotedpawn':
return '金将';
case 'promotedlance':
return '成香';
case 'promotedknight':
return '成桂';
case 'promotedsilver':
return '成銀';
case 'horse':
case 'horsepromoted':
return '龍馬';
case 'dragon':
case 'dragonpromoted':
return '龍王';
case 'king':
return '玉将';
case 'leopard':
return '猛豹';
case 'copper':
return '銅将';
case 'elephant':
case 'elephantpromoted':
return '醉象';
case 'chariot':
return '反車';
case 'tiger':
return '盲虎';
case 'kirin':
return '麒麟';
case 'phoenix':
return '鳳凰';
case 'sidemover':
case 'sidemoverpromoted':
return '横行';
case 'verticalmover':
case 'verticalmoverpromoted':
return '竪行';
case 'lion':
case 'lionpromoted':
return '獅子';
case 'queen':
case 'queenpromoted':
return '奔王';
case 'gobetween':
return '仲人';
case 'whitehorse':
return '白駒';
case 'bishoppromoted':
return '小角';
case 'rookpromoted':
return '金飛車';
case 'prince':
return '太子';
case 'whale':
return '鯨鯢';
case 'stag':
return '飛鹿';
case 'boar':
return '奔猪';
case 'ox':
return '飛牛';
case 'falcon':
return '角鷹';
case 'eagle':
return '飛鷲';
}
};
}
export function kanjiToRole(str: string): Role[] {
switch (str) {
case '歩':
case '歩兵':
case 'ひ':
return ['pawn'];
case '香':
case '香車':
return ['lance'];
case '桂':
case '桂馬':
return ['knight'];
case '銀':
case '銀将':
return ['silver'];
case '金':
case '金将':
return ['gold', 'promotedpawn'];
case '成歩':
return ['promotedpawn'];
case '角':
case '角行':
case 'ぞ':
return ['bishop'];
case '飛':
case '飛車':
case 'き':
return ['rook'];
case 'と':
case 'と金':
case 'に':
return ['tokin', 'promotedpawn'];
case '杏':
case '仝':
case '成香':
return ['promotedlance'];
case '圭':
case '今':
case '成桂':
return ['promotedknight'];
case '全':
case '成銀':
return ['promotedsilver'];
case '馬':
case '龍馬':
case '竜馬':
return ['horse', 'horsepromoted'];
case '成馬':
return ['horsepromoted'];
case '龍':
case '龍王':
case '竜':
case '竜王':
return ['dragon', 'dragonpromoted'];
case '成龍':
case '成竜':
return ['dragonpromoted'];
case '玉':
case '王':
case '王将':
case '玉将':
case 'ラ':
return ['king'];
case '豹':
case '猛豹':
return ['leopard'];
case '銅':
case '銅将':
return ['copper'];
case '象':
case '醉象':
return ['elephant', 'elephantpromoted'];
case '成象':
return ['elephantpromoted'];
case '反':
case '反車':
return ['chariot'];
case '虎':
case '盲虎':
return ['tiger'];
case '麒':
case '麒麟':
return ['kirin'];
case '鳳':
case '鳳凰':
return ['phoenix'];
case '横':
case '横行':
return ['sidemover', 'sidemoverpromoted'];
case '成横':
return ['sidemoverpromoted'];
case '竪':
case '竪行':
return ['verticalmover', 'verticalmoverpromoted'];
case '成竪':
return ['verticalmoverpromoted'];
case '獅':
case '師':
case '獅子':
return ['lion', 'lionpromoted'];
case '成獅':
case '成師':
return ['lionpromoted'];
case '奔':
case '奔王':
return ['queen', 'queenpromoted'];
case '成奔':
return ['queenpromoted'];
case '仲':
case '仲人':
return ['gobetween'];
case '駒':
case '白駒':
return ['whitehorse'];
case '小角':
case '成角':
return ['bishoppromoted'];
case '金飛車':
case '金飛':
case '成飛':
return ['rookpromoted'];
case '太':
case '太子':
return ['prince'];
case '鯨':
case '鯨鯢':
return ['whale'];
case '鹿':
case '飛鹿':
return ['stag'];
case '猪':
case '奔猪':
return ['boar'];
case '牛':
case '飛牛':
return ['ox'];
case '鷹':
case '角鷹':
return ['falcon'];
case '鷲':
case '飛鷲':
return ['eagle'];
default:
return [];
}
}
export function roleToCsa(role: Role): string | undefined {
switch (role) {
case 'pawn':
return 'FU';
case 'lance':
return 'KY';
case 'knight':
return 'KE';
case 'silver':
return 'GI';
case 'gold':
return 'KI';
case 'bishop':
return 'KA';
case 'rook':
return 'HI';
case 'tokin':
return 'TO';
case 'promotedlance':
return 'NY';
case 'promotedknight':
return 'NK';
case 'promotedsilver':
return 'NG';
case 'horse':
return 'UM';
case 'dragon':
return 'RY';
case 'king':
return 'OU';
default:
return;
}
}
export function csaToRole(str: string): Role | undefined {
switch (str.toUpperCase()) {
case 'FU':
return 'pawn';
case 'KY':
return 'lance';
case 'KE':
return 'knight';
case 'GI':
return 'silver';
case 'KI':
return 'gold';
case 'KA':
return 'bishop';
case 'HI':
return 'rook';
case 'TO':
return 'tokin';
case 'NY':
return 'promotedlance';
case 'NK':
return 'promotedknight';
case 'NG':
return 'promotedsilver';
case 'UM':
return 'horse';
case 'RY':
return 'dragon';
case 'OU':
return 'king';
default:
return undefined;
}
}
export function filesByRules(rules: Rules): string {
switch (rules) {
case 'chushogi':
return ' 12 11 10 9 8 7 6 5 4 3 2 1';
case 'minishogi':
case 'kyotoshogi':
return ' 5 4 3 2 1';
case 'dobutsu':
return ' 3 2 1';
default:
return ' 9 8 7 6 5 4 3 2 1';
}
}
export function pieceToBoardKanji(rules: Rules): (piece: Piece) => string {
return (piece) => {
if (piece.color === 'gote') return `v${roleToBoardKanji(rules)(piece.role)}`;
else return roleToBoardKanji(rules)(piece.role);
};
}
export function makeNumberSquare(sq: Square): string {
const file = squareFile(sq) + 1;
const rank = squareRank(sq) + 1;
const fileStr = file >= 10 ? String.fromCharCode(file + 87) : file.toString();
const rankStr = rank >= 10 ? String.fromCharCode(rank + 87) : rank.toString();
return fileStr + rankStr;
}
// only for single digit boards - something like 111 would be amiguous
export function parseNumberSquare(str: string): Square | undefined {
if (str.length !== 2) return;
const file = str.charCodeAt(0) - '1'.charCodeAt(0);
const rank = str.charCodeAt(1) - '1'.charCodeAt(0);
if (file < 0 || file >= 16 || rank < 0 || rank >= 16) return;
return file + 16 * rank;
}
export function makeJapaneseSquare(sq: Square): string {
return (
(squareFile(sq) + 1)
.toString()
.split('')
.map((c) => String.fromCharCode(c.charCodeAt(0) + 0xfee0))
.join('') + numberToKanji(squareRank(sq) + 1)
);
}
export function makeJapaneseSquareHalf(sq: Square): string {
return (squareFile(sq) + 1).toString().split('').join('') + numberToKanji(squareRank(sq) + 1);
}
export function parseJapaneseSquare(str: string): Square | undefined {
if (str.length < 2 || str.length > 4) return;
const fileOffset = str.length === 2 || (str.length === 3 && str[1] === '十') ? 1 : 2;
const file =
Number.parseInt(
str
.slice(0, fileOffset)
.split('')
.map((c) =>
c.charCodeAt(0) >= 0xfee0 + 48 ? String.fromCharCode(c.charCodeAt(0) - 0xfee0) : c,
)
.join(''),
10,
) - 1;
const rank = kanjiToNumber(str.slice(fileOffset)) - 1;
if (Number.isNaN(file) || file < 0 || file >= 16 || rank < 0 || rank >= 16) return;
return file + 16 * rank;
}
export function toKanjiDigit(str: string): string {
switch (str) {
case '1':
return '一';
case '2':
return '二';
case '3':
return '三';
case '4':
return '四';
case '5':
return '五';
case '6':
return '六';
case '7':
return '七';
case '8':
return '八';
case '9':
return '九';
case '10':
return '十';
default:
return '';
}
}
export function fromKanjiDigit(str: string): number {
switch (str) {
case '一':
return 1;
case '二':
return 2;
case '三':
return 3;
case '四':
return 4;
case '五':
return 5;
case '六':
return 6;
case '七':
return 7;
case '八':
return 8;
case '九':
return 9;
case '十':
return 10;
default:
return 0;
}
}
// max 99
export function numberToKanji(n: number): string {
n = Math.max(0, Math.min(n, 99));
const res = n >= 20 ? `${toKanjiDigit(Math.floor(n / 10).toString())}十` : n >= 10 ? '十' : '';
return res + toKanjiDigit(Math.floor(n % 10).toString());
}
// max 99
export function kanjiToNumber(str: string): number {
let res = str.startsWith('十') ? 1 : 0;
for (const s of str) {
if (s === '十') res *= 10;
else res += fromKanjiDigit(s);
}
return Math.max(0, Math.min(res, 99));
}