shogiops
Version:
Shogi rules and operations
206 lines • 7.08 kB
JavaScript
import { Result } from '@badrap/result';
import { Board } from '../board.js';
import { Hands } from '../hands.js';
import { initialSfen, parseSfen } from '../sfen.js';
import { boolToColor, defined, isDrop, parseCoordinates } from '../util.js';
import { Shogi } from '../variant/shogi.js';
import { allRoles, handRoles, promote } from '../variant/util.js';
import { csaToRole, makeNumberSquare, parseNumberSquare, roleToCsa } from './util.js';
// Only supports standard shogi no variants
//
// CSA HEADER
//
export const InvalidCsa = {
CSA: 'ERR_CSA',
Board: 'ERR_BOARD',
Handicap: 'ERR_HANDICAP',
Hands: 'ERR_HANDS',
AdditionalInfo: 'ERR_ADDITIONAL',
};
export class CsaError extends Error {
}
// exporting handicaps differently is prob not worth it, so let's always go with the whole board
export function makeCsaHeader(pos) {
return [
makeCsaBoard(pos.board),
makeCsaHand(pos.hands.color('sente'), 'P+'),
makeCsaHand(pos.hands.color('gote'), 'P-'),
pos.turn === 'gote' ? '-' : '+',
]
.filter((p) => p.length > 0)
.join('\n');
}
export function makeCsaBoard(board) {
let csaBoard = '';
for (let rank = 0; rank < 9; rank++) {
csaBoard += `P${rank + 1}`;
for (let file = 8; file >= 0; file--) {
const square = parseCoordinates(file, rank);
const piece = board.get(square);
if (!piece)
csaBoard += ' * ';
else {
const colorSign = piece.color === 'gote' ? '-' : '+';
csaBoard += colorSign + roleToCsa(piece.role);
}
if (file === 0 && rank < 8)
csaBoard += '\n';
}
}
return csaBoard;
}
export function makeCsaHand(hand, prefix) {
if (hand.isEmpty())
return '';
return (prefix +
handRoles('standard')
.map((role) => {
const r = roleToCsa(role);
const n = hand.get(role);
return `00${r}`.repeat(Math.min(n, 18));
})
.filter((p) => p.length > 0)
.join(''));
}
// Import
export function parseCsaHeader(csa) {
const lines = normalizedCsaLines(csa);
const handicap = lines.find((l) => l.startsWith('PI'));
const isWholeBoard = lines.some((l) => l.startsWith('P1'));
const baseBoard = defined(handicap) && !isWholeBoard
? parseCsaHandicap(handicap)
: parseCsaBoard(lines.filter((l) => /^P\d/.test(l)));
const turn = lines.some((l) => l === '-') ? 'gote' : 'sente';
return baseBoard.chain((board) => {
return Shogi.from({ board, hands: Hands.empty(), turn, moveNumber: 1 }, true).chain((pos) => parseAdditions(pos, lines.filter((l) => /P[+|-]/.test(l))));
});
}
export function parseCsaHandicap(handicap) {
const splitted = handicap.substring(2).match(/.{4}/g) || [];
const intitalBoard = parseSfen('standard', initialSfen('standard'), false).unwrap().board;
for (const s of splitted) {
const sq = parseNumberSquare(s.substring(0, 2));
if (defined(sq)) {
intitalBoard.take(sq);
}
else {
return Result.err(new CsaError(InvalidCsa.Handicap));
}
}
return Result.ok(intitalBoard);
}
function parseCsaBoard(csaBoard) {
if (csaBoard.length !== 9)
return Result.err(new CsaError(InvalidCsa.Board));
const board = Board.empty();
let rank = 0;
for (const r of csaBoard.map((r) => r.substring(2))) {
let file = 8;
for (const s of r.match(/.{1,3}/g) || []) {
if (s.includes('*'))
file--;
else {
const square = parseCoordinates(file, rank);
if (!defined(square))
return Result.err(new CsaError(InvalidCsa.Board));
const role = csaToRole(s.substring(1));
if (defined(role) && allRoles('standard').includes(role)) {
const piece = { role: role, color: boolToColor(!s.startsWith('-')) };
board.set(square, piece);
file--;
}
}
}
rank++;
}
return Result.ok(board);
}
function parseAdditions(initialPos, additions) {
for (const line of additions) {
const color = line[1] === '+' ? 'sente' : 'gote';
for (const sp of line.substring(2).match(/.{4}/g) || []) {
const sqString = sp.substring(0, 2);
const sq = parseNumberSquare(sqString);
const role = csaToRole(sp.substring(2, 4));
if ((defined(sq) || sqString === '00') && defined(role)) {
if (!defined(sq)) {
if (!handRoles('standard').includes(role))
return Result.err(new CsaError(InvalidCsa.Hands));
initialPos.hands[color].capture(role);
}
else {
initialPos.board.set(sq, { role: role, color: color });
}
}
else
return Result.err(new CsaError(InvalidCsa.AdditionalInfo));
}
}
return Result.ok(initialPos);
}
export function parseTags(csa) {
return normalizedCsaLines(csa)
.filter((l) => l.startsWith('$'))
.map((l) => l.substring(1).split(/:(.*)/, 2));
}
export function normalizedCsaLines(csa) {
return csa
.replace(/,/g, '\n')
.split(/[\r\n]+/)
.map((l) => l.trim())
.filter((l) => l);
}
//
// CSA MOVES/DROPS
//
// Parsing CSA moves/drops
export function parseCsaMoveOrDrop(pos, csaMd) {
var _a;
// Move
const match = csaMd.match(/(?:[+-])?([1-9][1-9])([1-9][1-9])(OU|HI|RY|KA|UM|KI|GI|NG|KE|NK|KY|NY|FU|TO)/);
if (!match) {
// Drop
const match = csaMd.match(/(?:[+-])?00([1-9][1-9])(HI|KA|KI|GI|KE|KY|FU)/);
if (!match)
return;
const drop = {
role: csaToRole(match[2]),
to: parseNumberSquare(match[1]),
};
return drop;
}
const role = csaToRole(match[3]);
const orig = parseNumberSquare(match[1]);
return {
from: orig,
to: parseNumberSquare(match[2]),
promotion: ((_a = pos.board.get(orig)) === null || _a === void 0 ? void 0 : _a.role) !== role,
};
}
export function parseCsaMovesOrDrops(pos, csaMds) {
pos = pos.clone();
const mds = [];
for (const m of csaMds) {
const md = parseCsaMoveOrDrop(pos, m);
if (!md)
return mds;
pos.play(md);
mds.push(md);
}
return mds;
}
// Making CSA formatted moves/drops
export function makeCsaMoveOrDrop(pos, md) {
if (isDrop(md)) {
return `00${makeNumberSquare(md.to)}${roleToCsa(md.role)}`;
}
else {
const role = pos.board.getRole(md.from);
if (!role)
return undefined;
return (makeNumberSquare(md.from) +
makeNumberSquare(md.to) +
roleToCsa((md.promotion && promote('standard')(role)) || role));
}
}
//# sourceMappingURL=csa.js.map