UNPKG

tsgammon-core

Version:

A Backgammon library for Typescript, formerly developed as a part of tsgammon-ui

254 lines (236 loc) 9.53 kB
import { BoardState } from '../BoardState' import { BoardStateNode, NoMove, NO_MOVE } from '../BoardStateNode' import { Dice, DicePip } from '../Dices' import { Move } from '../Move' import { applyDicePipToPoints } from './applyDicePipToPoints' import { buildNodeForEoG, leaveNodeBuilder, NodeBuilder } from './NodeBuilder' /** * ゾロ目でないダイスのペアに対して、バックギャモンのルールに基づいて可能な手を列挙し、BoardStateNodeとして返す * * @param board 盤面 * @param dice1 ダイスの目 * @param dice2 ダイスの目 * @returns */ export function buildNodesForHeteroDice( board: BoardState, dice1: DicePip, dice2: DicePip ): BoardStateNode { if (dice1 === dice2) { throw Error('Unexpected doublet: ' + dice1 + ',' + dice2) } const majorPip = dice1 > dice2 ? dice1 : dice2 const minorPip = dice1 < dice2 ? dice1 : dice2 // 大きい目を先に使った場合の候補手 const [majorTmp, majorMarked] = applyMajorPipFirst( board, majorPip, minorPip ) // 小さい目を先に使う場合、上記の結果を踏まえた冗長判定がある const [minorTmp, minorMarked] = applyMinorPipFirst( board, majorPip, minorPip, majorTmp ) // marked=使えないダイスの個数を見て、適用可能な手を絞り込む let major: (pos: number) => BoardStateNode | NoMove let minor: (pos: number) => BoardStateNode | NoMove let usableDice: Dice[] if (majorMarked === 0) { // 大ぞろめにきい目を先に使えば、全てのダイスが使える major = majorTmp minor = minorMarked === 0 // すべてのダイスを使えるなら、小さい目を先に使っても良い ? minorTmp : () => NO_MOVE usableDice = [ { pip: dice1, used: false }, { pip: dice2, used: false }, ] } else if (minorMarked === 0) { // 小さい目を先に使えば、すべてのダイスが使える minor = minorTmp major = () => NO_MOVE // majorMarked !== 0はすでに確定している usableDice = [ { pip: dice1, used: false }, { pip: dice2, used: false }, ] } else { // 2個使うことはできない場合 // 1個しか使えない場合は、大きい目から使う if (majorMarked === 1) { major = majorTmp minor = () => NO_MOVE usableDice = [ { pip: majorPip, used: false }, { pip: minorPip, used: true }, ] } else if (minorMarked === 1) { major = () => NO_MOVE minor = minorTmp usableDice = [ { pip: minorPip, used: false }, { pip: majorPip, used: true }, ] } else { // どの目も使えない major = () => NO_MOVE minor = () => NO_MOVE usableDice = [ { pip: dice1, used: true }, { pip: dice2, used: true }, ] } } return { hasValue: true, dices: usableDice, board: board, majorFirst: major, minorFirst: minor, lastMoves: () => [], isRedundant: false, // 使えるダイスは左側にある前提 isCommitable: usableDice.length > 0 && usableDice[0].used, } } // 大きい目を先に使う場合の手のリストアップ function applyMajorPipFirst( board: BoardState, majorDice: DicePip, minorDice: DicePip ) { const isRedundantForMajorPipFirstCase = (move1: Move, move2: Move) => { // 見かけ上同じ駒を2回動かすムーブ // すなわち、p/q/r に対してq/r p/qと動かす場合は冗長 if (move2.to === move1.from) { return true } return false } return applyDices( board, majorDice, minorDice, isRedundantForMajorPipFirstCase ) } // 小さい目を先に使う場合の手のリストアップ function applyMinorPipFirst( board: BoardState, majorDice: DicePip, minorDice: DicePip, majorTmp: (pos: number) => BoardStateNode | NoMove ) { // moves = p/p+n q/q+m に対し、すでにmajorTmpに着手を入れ替えた手q/q+m p/p+nがあるはずなので、原則としては冗長 const isRedundantFunc = (move1: Move, move2: Move) => { // 着手を入れ替えた手、q/q+m p/p+nがすでにあれば冗長 const node = majorTmp(move2.from) if (node.hasValue && node.majorFirst(move1.from).hasValue) { return true } // 同じ駒を2回動かすムーブで、かつダイスを入れ替えた場合のムーブと比べる // すなわち、小さい目を先行して使う手p/p+m/rが、p/p+n/rと実質的に同じかどうかを検出する if (move1.to === move2.from) { const swappedMovesNode = majorTmp(move1.from) // 2' ダイスを入れ替えて動かせない(p/p+nが禁手)なら、冗長ではない if (swappedMovesNode.hasValue) { // ダイスを入れ替えても、二つ目のダイス(小さい目)では動かせない // (p+n/rが禁手)なら、冗長ではない(ベアリングオフの場合に発生する) { const majorPip = move2.pip const swappedMoveTo = move1.from + majorPip if (!swappedMovesNode.majorFirst(swappedMoveTo).hasValue) { return false } } // どちらもヒットでない場合は、冗長 const isHit = move1.isHit const swappedMove = swappedMovesNode.lastMoves()[0] return !isHit && !swappedMove.isHit } else { return false } } return false } return applyDices(board, minorDice, majorDice, isRedundantFunc) } function applyDices( board: BoardState, firstDice: DicePip, secondDice: DicePip, isRedundantFunc: (move1: Move, move2: Move) => boolean = () => false ): [(pos: number) => BoardStateNode | NoMove, number] { const dicesAfterUse = { pip: firstDice, used: true, } // ビルダーの事前準備:最初の目を適用した後は、二つ目のダイスに対応するノードを生成する const nodeBuilder: NodeBuilder = leaveNodeAndParentBuilder( secondDice, dicesAfterUse, isRedundantFunc ) // 各ポイントに最初の目を適用して子ノードを生成する // (内部で各ポイントについて上記のnodeBuilderが呼ばれ、孫ノードが生成される) return applyDicePipToPoints(board, firstDice, nodeBuilder, 2) } // ダイスを1個使った状態を表すノードと、その子ノードとなる末端ノードを構築する function leaveNodeAndParentBuilder( pip: DicePip, usedDice: Dice, isRedundantFunc: (move1: Move, move2: Move) => boolean = () => false ): NodeBuilder { return (board: BoardState, firstMove: Move) => { const lastMovesForLeave = [firstMove] const isEog = board.eogStatus().isEndOfGame if (isEog) { // 盤面が終了状態になっている場合は、Leafノードは生成せず、EoGを表すノードを返す // // この時点ではダイスを一個しか使っていないが、ダイスを入れ替えると二個使える場合、 // 一個しか使えない手は無効化されてしまうので、二個目のダイスも使用済とマークし、 // marked=0として返すことにより、ダイスを二個使った手として処理させている // (よって、ロール直後は、表示上ダイスは二つとも使える状態で表示される) return buildNodeForEoG( board, [usedDice, { pip, used: true }], lastMovesForLeave, 0 ) } // 二つ目のpipを使って末端のノードを構築する const nodeBuilder = leaveNodeBuilder( [usedDice], pip, (secondMove) => [firstMove, secondMove], (secondMove) => isRedundantFunc(firstMove, secondMove) ) const [major, unusedDices] = applyDicePipToPoints( board, pip, nodeBuilder, 1 ) // applyDicePipToPointsは未使用のダイスの最小値を返すので、 // unusedDiceはどこかのポイントがムーブ可能であれば0、 // そうでなければ1(ムーブできないのでダイスが1個余る)となる。 // 末端ノードの親ノードを生成して返す return [ { hasValue: true, dices: [usedDice, { pip, used: unusedDices === 1 }], board: board, majorFirst: major, minorFirst: () => NO_MOVE, lastMoves: () => lastMovesForLeave, isRedundant: false, isCommitable: unusedDices === 1, }, unusedDices, ] } }