UNPKG

tsshogi

Version:

TypeScript library for Shogi (Japanese chess)

1,285 lines 94.7 kB
import { millisecondsToHHMMSS, millisecondsToMSS } from "./helpers/time.mjs"; import { Color, reverseColor } from "./color.mjs"; import { InvalidMoveError, InvalidUSIError } from "./errors.mjs"; import { Move, SpecialMoveType, areSameMoves, parseUSIMove, specialMove, } from "./move.mjs"; import { InitialPositionSFEN, Position } from "./position.mjs"; import { formatMove, formatSpecialMove } from "./text.mjs"; import { PieceType } from "./piece.mjs"; import { Square } from "./square.mjs"; const usenHandTable = { [PieceType.PAWN]: 81 + 10, [PieceType.LANCE]: 81 + 11, [PieceType.KNIGHT]: 81 + 12, [PieceType.SILVER]: 81 + 13, [PieceType.GOLD]: 81 + 9, [PieceType.BISHOP]: 81 + 14, [PieceType.ROOK]: 81 + 15, [PieceType.KING]: 81 + 8, [PieceType.PROM_PAWN]: 81 + 2, [PieceType.PROM_LANCE]: 81 + 3, [PieceType.PROM_KNIGHT]: 81 + 4, [PieceType.PROM_SILVER]: 81 + 5, [PieceType.HORSE]: 81 + 6, [PieceType.DRAGON]: 81 + 7, }; const usenHandReverseTable = { [81 + 10]: PieceType.PAWN, [81 + 11]: PieceType.LANCE, [81 + 12]: PieceType.KNIGHT, [81 + 13]: PieceType.SILVER, [81 + 9]: PieceType.GOLD, [81 + 14]: PieceType.BISHOP, [81 + 15]: PieceType.ROOK, [81 + 8]: PieceType.KING, [81 + 2]: PieceType.PROM_PAWN, [81 + 3]: PieceType.PROM_LANCE, [81 + 4]: PieceType.PROM_KNIGHT, [81 + 5]: PieceType.PROM_SILVER, [81 + 6]: PieceType.HORSE, [81 + 7]: PieceType.DRAGON, }; export var RecordMetadataKey; (function (RecordMetadataKey) { RecordMetadataKey["TITLE"] = "title"; RecordMetadataKey["BLACK_NAME"] = "blackName"; RecordMetadataKey["WHITE_NAME"] = "whiteName"; RecordMetadataKey["SHITATE_NAME"] = "shitateName"; RecordMetadataKey["UWATE_NAME"] = "uwateName"; RecordMetadataKey["BLACK_SHORT_NAME"] = "blackShortName"; RecordMetadataKey["WHITE_SHORT_NAME"] = "whiteShortName"; RecordMetadataKey["START_DATETIME"] = "startDatetime"; RecordMetadataKey["END_DATETIME"] = "endDatetime"; RecordMetadataKey["DATE"] = "date"; RecordMetadataKey["TOURNAMENT"] = "tournament"; RecordMetadataKey["STRATEGY"] = "strategy"; RecordMetadataKey["TIME_LIMIT"] = "timeLimit"; RecordMetadataKey["BLACK_TIME_LIMIT"] = "blackTimeLimit"; RecordMetadataKey["WHITE_TIME_LIMIT"] = "whiteTimeLimit"; RecordMetadataKey["BYOYOMI"] = "byoyomi"; RecordMetadataKey["TIME_SPENT"] = "timeSpent"; RecordMetadataKey["MAX_MOVES"] = "maxMoves"; RecordMetadataKey["JISHOGI"] = "jishogi"; RecordMetadataKey["PLACE"] = "place"; RecordMetadataKey["POSTED_ON"] = "postedOn"; RecordMetadataKey["NOTE"] = "note"; RecordMetadataKey["SCOREKEEPER"] = "scorekeeper"; // 詰将棋に関する項目 RecordMetadataKey["OPUS_NO"] = "opusNo"; RecordMetadataKey["OPUS_NAME"] = "opusName"; RecordMetadataKey["AUTHOR"] = "author"; RecordMetadataKey["PUBLISHED_BY"] = "publishedBy"; RecordMetadataKey["PUBLISHED_AT"] = "publishedAt"; RecordMetadataKey["SOURCE"] = "source"; RecordMetadataKey["LENGTH"] = "length"; RecordMetadataKey["INTEGRITY"] = "integrity"; RecordMetadataKey["CATEGORY"] = "category"; RecordMetadataKey["AWARD"] = "award"; })(RecordMetadataKey || (RecordMetadataKey = {})); /** * 先手の対局者名をフルネーム優先で取得します。 * @param metadata */ export function getBlackPlayerName(metadata) { return (metadata.getStandardMetadata(RecordMetadataKey.BLACK_NAME) || metadata.getStandardMetadata(RecordMetadataKey.BLACK_SHORT_NAME) || metadata.getStandardMetadata(RecordMetadataKey.SHITATE_NAME)); } /** * 後手の対局者名をフルネーム優先で取得します。 * @param metadata */ export function getWhitePlayerName(metadata) { return (metadata.getStandardMetadata(RecordMetadataKey.WHITE_NAME) || metadata.getStandardMetadata(RecordMetadataKey.WHITE_SHORT_NAME) || metadata.getStandardMetadata(RecordMetadataKey.UWATE_NAME)); } /** * 先手の対局者名を省略名優先で取得します。 * @param metadata */ export function getBlackPlayerNamePreferShort(metadata) { return (metadata.getStandardMetadata(RecordMetadataKey.BLACK_SHORT_NAME) || metadata.getStandardMetadata(RecordMetadataKey.BLACK_NAME) || metadata.getStandardMetadata(RecordMetadataKey.SHITATE_NAME)); } /** * 後手の対局者名を省略名優先で取得します。 * @param metadata */ export function getWhitePlayerNamePreferShort(metadata) { return (metadata.getStandardMetadata(RecordMetadataKey.WHITE_SHORT_NAME) || metadata.getStandardMetadata(RecordMetadataKey.WHITE_NAME) || metadata.getStandardMetadata(RecordMetadataKey.UWATE_NAME)); } /** * 棋譜メタデータ */ export class RecordMetadata { standard = new Map(); custom = new Map(); /** * 定義済みのメタデータのキーの一覧を取得します。 */ get standardMetadataKeys() { return this.standard.keys(); } /** * 定義済みのメタデータを取得します。 * @param key */ getStandardMetadata(key) { return this.standard.get(key); } /** * 定義済みのメタデータを設定します。 * @param key * @param value */ setStandardMetadata(key, value) { if (value) { this.standard.set(key, value); } else { this.standard.delete(key); } } /** * カスタムメタデータのキーの一覧を取得します。 */ get customMetadataKeys() { return this.custom.keys(); } /** * カスタムメタデータを取得します。 * @param key */ getCustomMetadata(key) { return this.custom.get(key); } /** * カスタムメタデータを設定します。 * @param key * @param value */ setCustomMetadata(key, value) { if (value) { this.custom.set(key, value); } else { this.custom.delete(key); } } } function copyNodeMetadata(source, target) { target.comment = source.comment; target.bookmark = source.bookmark; target.customData = source.customData; target.setElapsedMs(source.elapsedMs); } class NodeImpl { ply; prev; branchIndex; activeBranch; nextColor; move; isCheck; displayText; sfen; next = null; branch = null; comment = ""; customData; elapsedMs = 0; totalElapsedMs = 0; bookmark = ""; constructor(ply, prev, branchIndex, activeBranch, nextColor, move, isCheck, displayText, sfen) { this.ply = ply; this.prev = prev; this.branchIndex = branchIndex; this.activeBranch = activeBranch; this.nextColor = nextColor; this.move = move; this.isCheck = isCheck; this.displayText = displayText; this.sfen = sfen; } get timeText() { const elapsed = millisecondsToMSS(this.elapsedMs); const totalElapsed = millisecondsToHHMMSS(this.totalElapsedMs); return `${elapsed} / ${totalElapsed}`; } get hasBranch() { return !!this.prev && !!this.prev.next && !!this.prev.next.branch; } get isFirstBranch() { return !this.prev || this.prev.next === this; } get isLastMove() { if (!this.next) { return true; } for (let p = this.next; p; p = p.branch) { if (p.move instanceof Move) { return false; } } return true; } updateTotalElapsedMs() { this.totalElapsedMs = this.elapsedMs; if (this.prev && this.prev.prev) { this.totalElapsedMs += this.prev.prev.totalElapsedMs; } } setElapsedMs(elapsedMs) { this.elapsedMs = elapsedMs; this.updateTotalElapsedMs(); let p = this.next; const stack = []; while (p) { p.updateTotalElapsedMs(); if (p.branch) { stack.push(p.branch); } if (p.next) { p = p.next; } else { p = stack.pop() || null; } } } static newRootEntry(position) { return new NodeImpl(0, // ply null, // prev 0, // branchIndex true, // activeBranch position.color, // color specialMove(SpecialMoveType.START), // move false, // isCheck "開始局面", // displayText position.sfen); } } /** * 棋譜 */ export class Record { metadata; _initialPosition; _position; _first; _current; repetitionCounts = {}; repetitionStart = {}; onChangePosition = () => { /* noop */ }; onClear = () => { /* noop */ }; onAddNode = () => { /* noop */ }; onRemoveNode = () => { /* noop */ }; constructor(position) { this.metadata = new RecordMetadata(); this._initialPosition = position ? position.clone() : new Position(); this._position = this.initialPosition.clone(); this._first = NodeImpl.newRootEntry(this._initialPosition); this._current = this._first; this.incrementRepetition(); } /** * 初期局面を返します。 */ get initialPosition() { return this._initialPosition; } /** * 現在の局面を返します。 */ get position() { return this._position; } /** * 初期局面のノードを返します。 * このノードには必ず SpecialMoveType.START が設定されます。 * first.next が1手目に該当します。 */ get first() { return this._first; } /** * 現在の局面のノードを返します。 */ get current() { return this._current; } /** * アクティブな経路の指し手の一覧を返します。 */ get moves() { const moves = this.movesBefore; for (let p = this._current.next; p; p = p.next) { while (!p.activeBranch) { p = p.branch; } moves.push(p); } return moves; } /** * 現在の局面までの指し手の一覧を返します。 */ get movesBefore() { return this._movesBefore; } get _movesBefore() { const moves = new Array(); moves.unshift(this._current); for (let p = this._current.prev; p; p = p.prev) { moves.unshift(p); } return moves; } /** * アクティブな経路の総手数を返します。 */ get length() { let len = this._current.ply; for (let p = this._current.next; p; p = p.next) { while (!p.activeBranch) { p = p.branch; } len = p.ply; } return len; } /** * 最初の兄弟ノードを返します。 */ get branchBegin() { return this._current.prev ? this._current.prev.next : this._current; } /** * 指定した局面で棋譜を初期化します。 * @param position */ clear(position) { this.metadata = new RecordMetadata(); if (position) { this._initialPosition = position.clone(); } this._position = this.initialPosition.clone(); this._first = NodeImpl.newRootEntry(this._initialPosition); this._current = this._first; this.repetitionCounts = {}; this.repetitionStart = {}; this.incrementRepetition(); this.onClear(this._initialPosition); this.onChangePosition(); } /** * 1手前に戻ります。 */ goBack() { if (this._goBack()) { this.onChangePosition(); return true; } return false; } _goBack() { if (this._current.prev) { if (this._current.move instanceof Move) { this.decrementRepetition(); this._position.undoMove(this._current.move); } this._current = this._current.prev; return true; } return false; } /** * 1手先に進みます。 */ goForward() { if (this._goForward()) { this.onChangePosition(); return true; } return false; } _goForward() { if (this._current.next) { this._current = this._current.next; while (!this._current.activeBranch) { this._current = this._current.branch; } if (this._current.move instanceof Move) { this._position.doMove(this._current.move, { ignoreValidation: true, }); this.incrementRepetition(); } return true; } return false; } /** * アクティブな経路上で指定した手数まで移動します。 * @param ply */ goto(ply) { const orgPly = this._current.ply; this._goto(ply); if (orgPly !== this._current.ply) { this.onChangePosition(); } } gotoNode(node) { const variation = []; let first = node; for (let p = node; p.prev; p = p.prev) { variation.unshift(p); first = p.prev; } if (this.first !== first) { return false; } const orgNode = this._current; this._goto(0); for (const p of variation) { this._goForward(); this._switchBranchByIndex(p.branchIndex); } if (orgNode !== this._current) { this.onChangePosition(); } return true; } _goto(ply) { while (ply < this._current.ply) { if (!this._goBack()) { break; } } while (ply > this._current.ply) { if (!this._goForward()) { break; } } } /** * 全ての分岐選択を初期化して最初のノードをアクティブにします。 */ resetAllBranchSelection() { let confluence = this._current; for (let node = this._current; node.prev; node = node.prev) { if (!node.isFirstBranch) { confluence = node.prev; } } this._forEach((node) => { node.activeBranch = node.isFirstBranch; }); if (this._current !== confluence) { while (this._current !== confluence) { this._goBack(); } this.onChangePosition(); } } /** * インデクスを指定して兄弟ノードを選択します。 * @param index */ switchBranchByIndex(index) { if (this.current.branchIndex === index) { return true; } if (!this._switchBranchByIndex(index)) { return false; } this.onChangePosition(); return true; } _switchBranchByIndex(index) { if (this.current.branchIndex === index) { return true; } if (!this._current.prev) { return false; } let ok = false; for (let p = this._current.prev.next; p; p = p.branch) { if (p.branchIndex === index) { p.activeBranch = true; if (this._current.move instanceof Move) { this.decrementRepetition(); this._position.undoMove(this._current.move); } this._current = p; if (this._current.move instanceof Move) { this._position.doMove(this._current.move, { ignoreValidation: true, }); this.incrementRepetition(); } ok = true; } else { p.activeBranch = false; } } if (!ok) { this._current.activeBranch = true; } return ok; } /** * 指し手を追加して1手先に進みます。 * 現在のノードが特殊な指し手(ex. 投了)の場合は前のノードに戻ってから追加します。 * 既に同じ指し手が存在する場合はそのノードへ移動します。 */ append(move, opt) { if (this._append(move, opt)) { this.onChangePosition(); return true; } return false; } _append(move, opt) { // convert SpecialMoveType to SpecialMove if (typeof move === "string") { move = specialMove(move); } // 指し手を表す文字列を取得する。 const lastMove = this.current.move instanceof Move ? this.current.move : undefined; const displayText = move instanceof Move ? formatMove(this.position, move, { lastMove }) : formatSpecialMove(move, this.current.nextColor); // 局面を動かす。 let isCheck = false; if (move instanceof Move) { if (!this._position.doMove(move, opt)) { return false; } isCheck = this.position.checked; } // 特殊な指し手のノードの場合は前のノードに戻る。 if (this._current !== this.first && !(this._current.move instanceof Move)) { this._goBack(); } if (move instanceof Move) { this.incrementRepetition(this._current.ply + 1); } // 最終ノードの場合は単に新しいノードを追加する。 if (!this._current.next) { this._current.next = new NodeImpl(this._current.ply + 1, // number this._current, // prev 0, // branchIndex true, // activeBranch this.position.color, // nextColor move, isCheck, displayText, this.position.sfen); this._current = this._current.next; this._current.setElapsedMs(0); this.onAddNode(this._current); return true; } // 既存の兄弟ノードから選択を解除する。 let p; for (p = this._current.next; p; p = p.branch) { p.activeBranch = false; } // 同じ指し手が既に存在する場合はそのノードへ移動して終わる。 let lastBranch = this._current.next; for (p = this._current.next; p; p = p.branch) { if (areSameMoves(move, p.move)) { this._current = p; this._current.activeBranch = true; return true; } lastBranch = p; } // 兄弟ノードを追加する。 this._current = new NodeImpl(this._current.ply + 1, // number this._current, // prev lastBranch.branchIndex + 1, // branchIndex true, // activeBranch this.position.color, // nextColor move, isCheck, displayText, this.position.sfen); this._current.setElapsedMs(0); lastBranch.branch = this._current; this.onAddNode(this._current); return true; } /** * 次の兄弟ノードと順序を入れ替えます。 */ swapWithNextBranch() { if (!this._current.branch) { return false; } return Record.swapWithPreviousBranch(this._current.branch); } /** * 前の兄弟ノードと順序を入れ替えます。 */ swapWithPreviousBranch() { return Record.swapWithPreviousBranch(this._current); } static swapWithPreviousBranch(target) { const prev = target.prev; if (!prev || !prev.next || prev.next == target) { return false; } if (prev.next.branch === target) { const pair = prev.next; pair.branch = target.branch; target.branch = pair; prev.next = target; [target.branchIndex, pair.branchIndex] = [pair.branchIndex, target.branchIndex]; return true; } for (let p = prev.next; p.branch; p = p.branch) { if (p.branch.branch === target) { const pair = p.branch; pair.branch = target.branch; target.branch = pair; p.branch = target; [target.branchIndex, pair.branchIndex] = [pair.branchIndex, target.branchIndex]; return true; } } return false; } /** * 現在の指し手を削除します。 */ removeCurrentMove() { const target = this._current; if (!this.goBack()) { return this.removeNextMove(); } this.onRemoveSubTree(target); if (this._current.next === target) { this._current.next = target.branch; } else { for (let p = this._current.next; p; p = p.branch) { if (p.branch === target) { p.branch = target.branch; break; } } } let branchIndex = 0; for (let p = this._current.next; p; p = p.branch) { p.branchIndex = branchIndex; branchIndex += 1; } if (this._current.next) { this._current.next.activeBranch = true; } this.onChangePosition(); return true; } /** * 後続の手を全て削除します。 */ removeNextMove() { if (this._current.next) { for (let p = this.current.next; p; p = p.branch) { this.onRemoveSubTree(p); } this._current.next = null; return true; } return false; } onRemoveSubTree(root) { let p = root; while (p) { if (p.next) { p = p.next; continue; } this.onRemoveNode(p); if (p === root) { return; } while (!p.branch) { if (!p.prev) { return; } p = p.prev; this.onRemoveNode(p); if (p === root) { return; } } p = p.branch; } } /** * 棋譜をマージします。 * 経過時間やコメント、しおりが両方にある場合は自分の側を優先します。 * 初期局面が異なる場合はマージできません。 * @param record */ merge(record) { // 初期局面が異なる場合はマージできない。 if (this.initialPosition.sfen !== record.initialPosition.sfen) { return false; } // 元居た局面までのパスを記憶する。 const path = this.movesBefore; // 開始局面に戻してマージを実行する。 this._goto(0); this.mergeIntoCurrentPosition(record); // 元居た局面まで戻す。 for (let i = 1; i < path.length; i++) { this._append(path[i].move, { ignoreValidation: true }); } return true; } /** * 棋譜を現在の局面からのサブツリーとしてマージします。 * 経過時間やコメント、しおりが両方にある場合は自分の側を優先します。 * 開始局面が一致していなくてもマージできますが、指し手が挿入不能な場合その子ノードは無視されます。 * @param record */ mergeIntoCurrentPosition(record, option) { const begin = this._current.ply; let errorPly = null; let successCount = 0; let skipCount = 0; // 指し手をマージする。 record.forEach((node) => { if (node.ply === 0) { return; } const ply = begin + node.ply - 1; if (errorPly !== null && ply > errorPly) { skipCount++; return; } this._goto(ply); if (!this._append(node.move, option)) { errorPly = ply; skipCount++; return; } errorPly = null; successCount++; if (node.elapsedMs && !this.current.elapsedMs) { this.current.setElapsedMs(node.elapsedMs); } if (node.comment && !this.current.comment) { this.current.comment = node.comment; } if (node.bookmark && !this.current.bookmark) { this.current.bookmark = node.bookmark; } if (node.customData && !this.current.customData) { this.current.customData = node.customData; } }); // 元居た局面まで戻す。 this._goto(begin); return { successCount, skipCount }; } /** * 指定したしおりがある局面まで移動します。 * @param bookmark */ jumpToBookmark(bookmark) { // 既に該当する局面にいる場合は何もしない。 if (this._current.bookmark === bookmark) { return true; } // 一致するブックマークを探す。 const node = this.find((node) => node.bookmark === bookmark); if (!node) { return false; } // ブックマークのある局面までの経路を配列に書き出す。 const route = []; for (let p = node; p; p = p.prev) { route[p.ply] = p; } // 合流するところまで局面を戻す。 while (this._current !== route[this._current.ply]) { this.goBack(); } // ブックマークのある局面まで指し手を進める。 while (route.length > this._current.ply + 1) { this.append(route[this._current.ply + 1].move); } this.onChangePosition(); return true; } incrementRepetition(ply) { const sfen = this.position.sfen; if (this.repetitionCounts[sfen]) { this.repetitionCounts[sfen] += 1; } else { this.repetitionCounts[sfen] = 1; this.repetitionStart[sfen] = ply ?? this.current.ply; } } decrementRepetition() { const sfen = this.position.sfen; this.repetitionCounts[sfen] -= 1; if (this.repetitionCounts[sfen] === 0) { delete this.repetitionCounts[sfen]; delete this.repetitionStart[sfen]; } } /** * 千日手かどうかを判定します。 * 現在の局面が4回目以上の同一局面である場合に true を返します。 */ get repetition() { return this.repetitionCounts[this.position.sfen] >= 4; } /** * 現在の局面まで(Record.current着手後を含む)に指定された局面が何回現れたかを返します。 * @param position */ getRepetitionCount(position) { return this.repetitionCounts[position.sfen] || 0; } /** * 連続王手の千日手かどうかを判定します。 * 現在の局面が4回目以上の同一局面であり、かつ同一局面が最初に出現したときから一方の王手が連続している場合に true を返します。 */ get perpetualCheck() { if (!this.repetition) { return null; } const sfen = this.position.sfen; const since = this.repetitionStart[sfen]; let black = true; let white = true; let color = this.position.color; for (let p = this.current; p && p.ply >= since; p = p.prev) { color = reverseColor(color); if (p.isCheck) { continue; } if (color === Color.BLACK) { black = false; } else { white = false; } } return black ? Color.BLACK : white ? Color.WHITE : null; } /** * getUSI をオプション無しで呼び出した場合と同じ値を返します。 */ get usi() { return this.getUSI(); } /** * USI形式の文字列を返します。 * @param opts */ getUSI(opts) { const sfen = this.initialPosition.sfen; const useStartpos = opts?.startpos !== false && sfen === InitialPositionSFEN.STANDARD; const position = "position " + (useStartpos ? "startpos" : "sfen " + this.initialPosition.sfen); const moves = []; for (let p = this.first;; p = p.next) { while (!p.activeBranch) { p = p.branch; } if (p.move instanceof Move) { moves.push(p.move.usi); } else if (opts?.resign && p.move.type === SpecialMoveType.RESIGN) { moves.push("resign"); } else if (opts?.repDraw && p.move.type === SpecialMoveType.REPETITION_DRAW) { moves.push("rep_draw"); } else if (opts?.draw && p.move.type === SpecialMoveType.DRAW) { moves.push("draw"); } else if (opts?.timeout && p.move.type === SpecialMoveType.TIMEOUT) { moves.push("timeout"); } else if (opts?.break && p.move.type === SpecialMoveType.INTERRUPT) { moves.push("break"); } else if (opts?.win && p.move.type === SpecialMoveType.ENTERING_OF_KING) { moves.push("win"); } if (!p.next || (!opts?.allMoves && p === this.current)) { break; } } if (moves.length === 0) { return position; } return [position, "moves"].concat(moves).join(" "); } /** * 現在の局面のSFEN形式の文字列を返します。 */ get sfen() { return this.position.getSFEN(this._current.ply + 1); } /** * USEN (Url Safe sfen-Extended Notation) 形式の文字列を返します。 * https://www.slideshare.net/slideshow/scalajs-web/92707205#15 * @returns [usen, branchIndex] */ get usen() { const sfen = this.initialPosition.sfen; let usen = sfen === InitialPositionSFEN.STANDARD ? "" : sfen.replace(/ 1$/, "").replace(/\//g, "_").replace(/ /g, ".").replace(/\+/g, "z"); let moves = "0."; let special = ""; let lastPly = 0; let bi = 0; let branchIndex = 0; this.forEach((node) => { if (node.ply === 0) { // root node return; } const move = node.move; if (lastPly + 1 !== node.ply) { usen += `~${moves}.${special}`; moves = `${node.ply - 1}.`; bi++; } if (this.current === node) { branchIndex = bi; } if (!(move instanceof Move)) { switch (move.type) { case SpecialMoveType.RESIGN: special = "r"; break; case SpecialMoveType.TIMEOUT: special = "t"; break; case SpecialMoveType.MAX_MOVES: case SpecialMoveType.IMPASS: case SpecialMoveType.DRAW: special = "j"; break; default: // 未定義のものは全て中断として扱う。 special = "p"; break; } return; } const from = move.from instanceof Square ? (move.from.rank - 1) * 9 + (move.from.file - 1) : usenHandTable[move.from]; const to = (move.to.rank - 1) * 9 + (move.to.file - 1); const m = (from * 81 + to) * 2 + (move.promote ? 1 : 0); moves += m.toString(36).padStart(3, "0"); lastPly = node.ply; }); usen += `~${moves}.${special}`; return [usen, branchIndex]; } /** * しおりの一覧を返します。 */ get bookmarks() { const bookmarks = []; const existed = {}; this.forEach((node) => { if (node.bookmark && !existed[node.bookmark]) { bookmarks.push(node.bookmark); existed[node.bookmark] = true; } }); return bookmarks; } // 深さ優先で全てのノードを訪問します。 forEach(handler) { this._forEach(handler); } _forEach(handler) { this.find((node) => { handler(node); return false; }); } find(handler) { let p = this._first; while (true) { if (handler(p)) { return p; } if (p.next) { p = p.next; continue; } while (!p.branch) { if (!p.prev) { return null; } p = p.prev; } p = p.branch; } } getSubtree() { // Create a new Record instance with the initial position. const subtree = new Record(this.position); // Copy the metadata from the current record to the subtree. for (const key of Object.values(RecordMetadataKey)) { const value = this.metadata.getStandardMetadata(key); if (value) { subtree.metadata.setStandardMetadata(key, value); } } for (const key of this.metadata.customMetadataKeys) { const value = this.metadata.getCustomMetadata(key); if (value) { subtree.metadata.setCustomMetadata(key, value); } } // Copy the nodes from the current record to the subtree. let p = this.current; copyNodeMetadata(p, subtree.current); if (!p.next) { return subtree; } p = p.next; while (true) { subtree.append(p.move, { ignoreValidation: true }); copyNodeMetadata(p, subtree.current); if (p.next) { p = p.next; continue; } while (!p.branch) { if (!p.prev || p.prev === this.current) { subtree.goto(0); return subtree; } subtree.goBack(); p = p.prev; } subtree.goBack(); p = p.branch; } } on(event, handler) { switch (event) { case "changePosition": this.onChangePosition = handler; break; case "clear": this.onClear = handler; break; case "addNode": this.onAddNode = handler; break; case "removeNode": this.onRemoveNode = handler; break; } } /** * USI形式の文字列から棋譜を読み込みます。 * @param data */ static newByUSI(data) { const positionStartpos = "position startpos"; const startpos = "startpos"; const prefixPositionStartpos = "position startpos "; const prefixPositionSFEN = "position sfen "; const prefixStartpos = "startpos "; const prefixSFEN = "sfen "; const prefixMoves = "moves "; if (data === positionStartpos || data === startpos) { return new Record(); } else if (data.startsWith(prefixPositionStartpos)) { return Record.newByUSIFromMoves(data.slice(prefixPositionStartpos.length)); } else if (data.startsWith(prefixPositionSFEN)) { return Record.newByUSIFromSFEN(data.slice(prefixPositionSFEN.length)); } else if (data.startsWith(prefixStartpos)) { return Record.newByUSIFromMoves(data.slice(prefixStartpos.length)); } else if (data.startsWith(prefixSFEN)) { return Record.newByUSIFromSFEN(data.slice(prefixSFEN.length)); } else if (data.startsWith(prefixMoves)) { return Record.newByUSIFromMoves(data); } else { return new InvalidUSIError(data); } } static newByUSIFromSFEN(data) { const sections = data.split(" "); if (sections.length < 3) { return new InvalidUSIError(data); } const movesIndex = sections.length === 3 || sections[3] === "moves" ? 3 : 4; const position = Position.newBySFEN(sections.slice(0, movesIndex).join(" ")); if (!position) { return new InvalidUSIError(data); } return Record.newByUSIFromMoves(sections.slice(movesIndex).join(" "), position); } static newByUSIFromMoves(data, position) { const record = new Record(position); if (data.length === 0) { return record; } const sections = data.split(" "); if (sections[0] !== "moves") { return new InvalidUSIError(data); } for (let i = 1; i < sections.length; i++) { if (sections[i] === "resign") { record.append(SpecialMoveType.RESIGN); break; } else if (sections[i] === "rep_draw") { record.append(SpecialMoveType.REPETITION_DRAW); break; } else if (sections[i] === "draw") { record.append(SpecialMoveType.DRAW); break; } else if (sections[i] === "timeout") { record.append(SpecialMoveType.TIMEOUT); break; } else if (sections[i] === "break") { record.append(SpecialMoveType.INTERRUPT); break; } else if (sections[i] === "win") { record.append(SpecialMoveType.ENTERING_OF_KING); break; } const parsed = parseUSIMove(sections[i]); if (!parsed) { break; } let move = record.position.createMove(parsed.from, parsed.to); if (!move) { return new InvalidMoveError(sections[i]); } if (parsed.promote) { move = move.withPromote(); } record.append(move, { ignoreValidation: true }); } return record; } /** * USEN (Url Safe sfen-Extended Notation) 形式の文字列から棋譜を読み込みます。 * https://www.slideshare.net/slideshow/scalajs-web/92707205#15 */ static newByUSEN(usen, branchIndex, ply) { const sections = usen.split("~"); if (sections.length < 2) { return new Error("USEN must have at least 2 sections."); } const sfen = sections[0].replace(/_/g, "/").replace(/\./g, " ").replace(/z/g, "+"); const position = sfen === "" ? new Position() : Position.newBySFEN(sfen + " 1"); if (!position) { return new Error("Invalid SFEN in USEN."); } const record = new Record(position); let activeNode = record.first; for (let si = 1; si < sections.length; si++) { const [n, moves, special] = sections[si].split("."); if (!/[0-9]+/.test(n)) { return new Error("Invalid USEN ply format."); } record.goto(parseInt(n)); for (let i = 0; i < moves.length; i += 3) { const m = parseInt(moves.slice(i, i + 3), 36); const f = Math.floor(m / 162); const from = f < 81 ? new Square((f % 9) + 1, Math.floor(f / 9) + 1) : usenHandReverseTable[f]; const t = Math.floor((m % 162) / 2); const to = new Square((t % 9) + 1, Math.floor(t / 9) + 1); const promote = m % 2 === 1; const move = record.position.createMove(from, to); if (!move) { return new Error("Invalid move in USEN."); } record.append(promote ? move.withPromote() : move, { ignoreValidation: true }); if (si - 1 === branchIndex && record.current.ply === ply) { activeNode = record.current; } } if (special === "r") { record.append(specialMove(SpecialMoveType.RESIGN)); } else if (special === "t") { record.append(specialMove(SpecialMoveType.TIMEOUT)); } else if (special === "j") { record.append(specialMove(SpecialMoveType.IMPASS)); } else if (special === "p") { record.append(specialMove(SpecialMoveType.INTERRUPT)); } if (si - 1 === branchIndex && record.current.ply === ply) { activeNode = record.current; } } if (activeNode === record.first) { record.goto(0); } else { const route = []; for (let p = activeNode; p; p = p.prev) { route[p.ply] = p; } while (record._current !== route[record._current.ply]) { record.goBack(); } while (route.length > record._current.ply + 1) { record.append(route[record._current.ply + 1].move); } } return record; } } /** * USI形式の文字列から次の手番を取得します。 * @param usi */ export function getNextColorFromUSI(usi) { const sections = usi.trim().split(" "); const baseColor = sections[1] === "startpos" || sections[3] === "b" ? Color.BLACK : Color.WHITE; const firstMoveIndex = sections[1] === "startpos" ? sections[2] === "moves" ? 3 : 2 : sections[6] === "moves" ? 7 : 6; return (sections.length - firstMoveIndex) % 2 === 0 ? baseColor : reverseColor(baseColor); } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVjb3JkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlY29yZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUN6RSxPQUFPLEVBQUUsS0FBSyxFQUFFLFlBQVksRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsZUFBZSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQzdELE9BQU8sRUFDTCxJQUFJLEVBRUosZUFBZSxFQUNmLFlBQVksRUFDWixZQUFZLEVBQ1osV0FBVyxHQUNaLE1BQU0sUUFBUSxDQUFDO0FBQ2hCLE9BQU8sRUFBbUMsbUJBQW1CLEVBQUUsUUFBUSxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBQzVGLE9BQU8sRUFBRSxVQUFVLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDdkQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNwQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBRWxDLE1BQU0sYUFBYSxHQUFHO0lBQ3BCLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFO0lBQ3pCLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFO0lBQzFCLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFO0lBQzNCLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFO0lBQzNCLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQ3hCLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFO0lBQzNCLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFO0lBQ3pCLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQ3hCLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQzdCLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQzlCLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQy9CLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQy9CLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0lBQ3pCLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxFQUFFLEVBQUUsR0FBRyxDQUFDO0NBQzNCLENBQUM7QUFFRixNQUFNLG9CQUFvQixHQUFHO0lBQzNCLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxJQUFJO0lBQ3pCLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxLQUFLO0lBQzFCLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxNQUFNO0lBQzNCLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxNQUFNO0lBQzNCLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxJQUFJO0lBQ3hCLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxNQUFNO0lBQzNCLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxJQUFJO0lBQ3pCLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxJQUFJO0lBQ3hCLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxTQUFTO0lBQzdCLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxFQUFFLFNB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