tsgammon-core
Version:
A Backgammon library for Typescript, formerly developed as a part of tsgammon-ui
343 lines (290 loc) • 10.7 kB
text/typescript
import { DicePip } from './Dices'
import { eog, EOGStatus } from './EOGStatus'
import { standardConf } from './GameConf'
/**
* 相対表記で表現した盤面。常に動かす側の観点になっていて、0はバーポイント、25は上がり、
* 正の数が自分の駒数となっている。内部的な盤の操作はすべてこのインターフェースを介して行い、
* 外部へ表示する際にはAbsoluteBoardで変換する
*/
export interface BoardState {
/** 各ポイントの駒数を格納した配列。正の数は自駒、負の数は相手駒 */
points: number[]
/** 最後尾の駒の位置 */
lastPiecePos: number
/** 相手の最後尾の駒の位置 */
opponentLastPiecePos: number
/** ベアリングオフの行き先となる位置(標準は25) */
bearOffPos: number
/** 指定の位置を相手側の視点で見た時の位置に変換する(e.g. 1=>24, 6=>18, ...)*/
invertPos(pos: number): number
/** ベアリングオフできるならtrue */
isBearable: boolean
/** 各ポイントの駒数を返す。正の数は自駒、負の数は相手駒 */
piecesAt(n: number): number
/** ベアリングオフ済みの自駒数を返す */
myBornOff: number
/** ベアリングオフ済みの相手駒数を返す */
opponentBornOff: number
/** 指定されたポイントの駒をpip分進めた盤面を返す。
* ポイントに駒がなかったり、行き先がブロックされている・範囲外などの場合、自分自身が返ってくる */
movePiece(from: number, pip: DicePip): BoardState
/** 盤面を相手側視点に変換する */
revert(): BoardState
/** 盤面で自分側の駒が全て上がったかどうかを、ギャモン・バックギャモンの有無とあわせて返す。
* ギャモン・バックギャモンは純粋に自駒の配置で判断され、ジャコビールールなどは適用されない。
* 相手側が終局かどうかは影響しない。
*/
eogStatus(): EOGStatus
/**
* 自分のピップカウント
*/
myPipCount: number
/**
* 相手のピップカウント
*/
opponentPipCount: number
/**
* 終局状態に達しているならtrue
*/
isEndOfGame(): boolean
/**
* 終局状態に達していて、かつギャモンならtrue
*/
isGammonish(): boolean
/**
* 終局状態に達していて、かつバックギャモンならtrue
*/
isBackgammonishAlso(): boolean
/**
* ランニングゲーム(駒同士のコンタクトがない)状態ならtrue
*/
isRunningGame(): boolean
/**
* 自分のまだ上がっていない駒の数
*/
pieceCount: number
/**
* 相手のまだ上がっていない駒の数
*/
opponentPieceCount: number
/**
* 自分のインナーボードの開始点(標準ルールでは19)
*/
innerPos: number
}
/**
* BoardStateの実装に必要な機能で、外部には公開する必要がないものの定義
*/
type Board = {
// all gone
} & BoardState
function countWhitePieces(pieces: number[]) {
return pieces.filter((n) => n > 0).reduce((n, m) => n + m, 0)
}
function countRedPieces(pieces: number[]) {
return countWhitePieces(pieces.map((n) => -n))
}
/**
* 駒の配置を格納した配列から、BoardStateを生成する
*
* @param pieces 駒の配置(省略時は標準ルールでの開始時な配置)
* @param bornOffs すでにベアリングオフした駒の数の対(順に自分、相手:省略時は0)
* @param innerPos この値とそれ以降がインナーボードとなる
* @param isEoGFunc 終局判定関数(終局時にtrueを返す)
* @returns 盤面
*/
export function boardState(
pieces: number[] = standardConf.initialPos,
bornOffs: [number, number] = [0, 0],
innerPos = 19,
isEoGFunc: (board: Board) => boolean = (board: Board) =>
board.pieceCount === 0
): BoardState {
return initBoardState(pieces, bornOffs, innerPos, isEoGFunc)
}
function posInverterFor(points: number[]): (pos: number) => number {
return (pos: number) => points.length - 1 - pos
}
function initBoardState(
points: number[],
bornOffs: [number, number] = [0, 0],
innerPos: number,
isEoGFunc: (board: Board) => boolean
): BoardState {
const myBornOff = bornOffs[0]
const opponentBornOff = bornOffs[1]
const pieceCount = countWhitePieces(points)
const opponentPieceCount = countRedPieces(points)
const bearOffPos = points.length - 1
const reverted = revertPoints(points)
const invertPos = posInverterFor(points)
const lastPiecePos = points.findIndex((n) => 0 < n)
const found = reverted.findIndex((n) => 0 < n)
const opponentLastPiecePos = found === -1 ? 0 : invertPos(found)
const isBearable = innerPos <= lastPiecePos
const myPipCount = countPip(points)
const opponentPipCount = countPip(reverted)
const board = {
points,
pieceCount,
opponentPieceCount,
bearOffPos,
invertPos,
myBornOff,
opponentBornOff,
lastPiecePos,
opponentLastPiecePos,
isBearable,
eogStatus() {
const isEndOfGame = this.isEndOfGame()
const isGammon = isEndOfGame && this.isGammonish()
const isBackgammon = isGammon && this.isBackgammonishAlso()
return eog({
isEndOfGame,
isGammon,
isBackgammon,
})
},
isEndOfGame(): boolean {
return isEoGFunc(this)
},
isGammonish(): boolean {
return this.opponentBornOff === 0
},
isBackgammonishAlso(): boolean {
// 自分のinnerPosとバーの間が、相手のアウターになる
const outerPos = points.length - innerPos // 7 = 26 - 19
const opponentOuterAndBar = [...Array(outerPos)].map(
(_, index) => index + innerPos
)
return (
opponentOuterAndBar
.map((pos) => this.points[pos])
.filter((c) => c < 0)
.reduce((m, n) => m + n, 0) < 0
)
},
isRunningGame(): boolean {
return this.lastPiecePos < this.opponentLastPiecePos
},
piecesAt(n: number): number {
return this.points[n]
},
movePiece(from: number, pip: number): Board {
return doMove(this, from, pip, innerPos)
},
revert(): Board {
const lastPiecePos = invertPos(this.opponentLastPiecePos)
const opponentLastPiecePos = invertPos(this.lastPiecePos)
const isBearable = innerPos <= lastPiecePos
return {
...this,
points: revertPoints(this.points),
myBornOff: this.opponentBornOff,
opponentBornOff: this.myBornOff,
pieceCount: this.opponentPieceCount,
opponentPieceCount: this.pieceCount,
lastPiecePos,
opponentLastPiecePos,
isBearable,
myPipCount: this.opponentPipCount,
opponentPipCount: this.myPipCount,
}
},
myPipCount,
opponentPipCount,
innerPos,
outerPos: points.length - 1 - innerPos, // 6 = 25 - 19
}
return board
}
function revertPoints(points: number[]): number[] {
const invertPos = posInverterFor(points)
return points.map((_, index) => {
const n = -points[invertPos(index)]
// remove negative 0
return n === 0 ? 0 : n
})
}
function doMove(
board: Board,
from: number,
pip: number,
innerPos: number
): Board {
const boardSize = board.points.length - 1 // 25
// 動かそうとする駒の位置が範囲外
if (from < 0 || boardSize < from) {
return board
}
// 動かそうとする場所に駒がない
const piecesToMove = board.points[from]
if (piecesToMove <= 0) {
return board
}
// ベアオフではなく、行先がブロックされている
const to = from + pip > boardSize ? boardSize : from + pip
const isBearOff = boardSize <= to
if (!isBearOff && board.points[to] < -1) {
return board
}
// 駒を取り上げる
const piecesAfter = board.points.slice()
piecesAfter[from] = piecesAfter[from] - 1
// 必要なら自分の最後尾の駒の位置を更新する
// 上がりなら、上がり数を更新して終了
if (isBearOff) {
const lastPiecePos = recalcLastPiecePos(from, board, piecesAfter)
const myPipCount = board.myPipCount - (boardSize - from)
return {
...board,
points: piecesAfter,
myBornOff: board.myBornOff + 1,
pieceCount: board.pieceCount - 1,
lastPiecePos,
myPipCount,
}
}
let opponentLastPiecePos
let opponentPipCount = board.opponentPipCount
// ヒット
if (piecesAfter[to] === -1) {
const bar = boardSize
piecesAfter[to] = 0
piecesAfter[bar] = piecesAfter[bar] - 1
// 相手の最後尾の駒の位置をバーに更新する
opponentLastPiecePos = boardSize
// ヒットした分のpipcountを更新
opponentPipCount += to
} else {
// ヒットでなければそのまま
opponentLastPiecePos = board.opponentLastPiecePos
}
// 移動先に駒を置く
piecesAfter[to] = piecesAfter[to] + 1
const lastPiecePos = recalcLastPiecePos(from, board, piecesAfter)
// 上がれるかどうかの更新が必要なのは、上がりでない時だけ
const isBearable = innerPos <= lastPiecePos
// 自分の分のピップカウントは進めた分だけ減る
const myPipCount = board.myPipCount - pip
return {
...board,
points: piecesAfter,
lastPiecePos,
opponentLastPiecePos,
isBearable,
myPipCount,
opponentPipCount,
}
}
function recalcLastPiecePos(from: number, board: Board, piecesAfter: number[]) {
return from == board.lastPiecePos && board.points[from] == 1
? piecesAfter.findIndex((n) => 0 < n)
: board.lastPiecePos
}
function countPip(points: number[]) {
return points.reduce((prev, cur, idx) => {
prev += cur > 0 ? cur * (points.length - 1 - idx) : 0
return prev
}, 0)
}