UNPKG

tsgammon-core

Version:

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

292 lines (258 loc) 10.5 kB
import { encode as encodeAsBase64 } from '@borderless/base64' import { CubeOwner } from '../CubeState' import { GameState } from '../GameState' import { MatchState } from '../MatchState' import { ResignOffer } from '../ResignOffer' /** * 指定の局面から、GNU Backgammon仕様のMatchIDを含むオブジェクトを生成する * * @param matchState マッチスコアの状態 * @param gameState 手番など、現在のゲームの状態 * @returns */ export function toMatchID(matchState: MatchState, gameState: GameState) { const cubeState = gameState.cubeState // 1. Bit 1-4 contains the 2-logarithm of the cube value. For example, a 8-cube is encoded as 0011 binary (or 3), since 2 to the power of 3 is 8. The maximum value of the cube in with this encoding is 2 to the power of 15, i.e., a 32768-cube. const bit1_4 = Math.log2(cubeState.value) // 2. Bit 5-6 contains the cube owner. 00 if player 0 owns the cube, 01 if player 1 owns the cube, or 11 for a centered cube. const bit5_6 = cubeState.owner === CubeOwner.RED ? 0 : cubeState.owner == CubeOwner.WHITE ? 1 : 3 // 3. Bit 7 is the player on roll or the player who did roll (0 and 1 for player 0 and 1, respectively). const bit7 = gameState.tag === 'GSInPlay' ? // キューブレスポンス中はロールしたプレイヤーとレスポンス中のプレイヤーが逆になる gameState.isDoubleOffered ? gameState.isRed ? 1 : 0 : gameState.isRed ? 0 : 1 : 0 // 4. Bit 8 is the Crawford flag: 1 if this game is the Crawford game, 0 otherwise. const bit8 = matchState.isCrawford ? 1 : 0 // 5. Bit 9-11 is the game state: 000 for no game started, 001 for playing a game, 010 if the game is over, 011 if the game was resigned, or 100 if the game was ended by dropping a cube. const bit9_11 = gameState.tag === 'GSOpening' ? 0 : gameState.tag === 'GSInPlay' ? 1 : gameState.isWonByPass ? 4 : gameState.isWonByResign ? 3 : 2 // 6. Bit 12 indicates whose turn it is. For example, suppose player 0 is on roll then bit 7 above will be 0. Player 0 now decides to double, this will make bit 12 equal to 1, since it is now player 1's turn to decide whether she takes or passes the cube. const bit12 = gameState.tag === 'GSOpening' || gameState.tag === 'GSEoG' ? 0 // value for these cases are undefined : gameState.isRed ? 0 : 1 // 7. Bit 13 indicates whether an doubled is being offered. 0 if no double is being offered and 1 if a double is being offered. const bit13 = gameState.tag === 'GSInPlay' && gameState.isDoubleOffered ? 1 : 0 // Bit 14-15 indicates whether an resignation was offered. 00 for no resignation, 01 for resign of a single game, 10 for resign of a gammon, or 11 for resign of a backgammon. The player offering the resignation is the inverse of bit 12, e.g., if player 0 resigns a gammon then bit 12 will be 1 (as it is now player 1 now has to decide whether to accept or reject the resignation) and bit 13-14 will be 10 for resign of a gammon. const bit14_15 = isResignOffered(gameState) // Bit 16-18 and bit 19-21 is the first and second die, respectively. 0 if the dice has not yet be rolled, otherwise the binary encoding of the dice, e.g., if 5-2 was rolled bit 16-21 will be 101-010. const { dice1, dice2 } = dices(gameState) const bit16_18 = dice1 > dice2 ? dice1 : dice2 const bit19_21 = dice1 > dice2 ? dice2 : dice1 // Bit 22 to 36 is the match length. The maximum value for the match length is 32767. A match score of zero indicates that the game is a money game. const bit22_36 = matchState.matchLength // Bit 37-51 and bit 52-66 is the score for player 0 and player 1 respectively. The maximum value of the match score is 32767. const matchScore = matchState.isEoG ? matchState.scoreAfter : matchState.scoreBefore const bit37_51 = matchScore.redScore const bit52_66 = matchScore.whiteScore // Bit67 : no Jacoby: これはドキュメントに記載がない:Jacobyが無効(ポイントマッチ)なら1, 有効(マネーゲームなど)なら0 const bit67 = matchState.stakeConf.jacobyRule ? 0 : 1 const toEnc = [ { bit: bit1_4, len: 4 }, { bit: bit5_6, len: 2 }, { bit: bit7 }, { bit: bit8 }, { bit: bit9_11, len: 3 }, { bit: bit12 }, { bit: bit13 }, { bit: bit14_15, len: 2 }, { bit: bit16_18, len: 3 }, { bit: bit19_21, len: 3 }, { bit: bit22_36, len: 15 }, { bit: bit37_51, len: 15 }, { bit: bit52_66, len: 15 }, { bit: bit67 }, ] const buffer = new ArrayBuffer(9) const reducer = littleEndianReducer(buffer) toEnc.map(revertBits).reduce(reducer) return { matchID: encodeAsBase64(buffer).substring(0, 12), cube: bit1_4, cubeOwner: bit5_6, diceOwner: bit7, crawford: bit8, gameState: bit9_11, turnOwner: bit12, double: bit13, resign: bit14_15, dice1: bit16_18, dice2: bit19_21, matchLen: bit22_36, score1: bit37_51, score2: bit52_66, noJacoby: bit67, bit: Array.from(new Uint8Array(buffer)).map((b) => b.toString(2)), } } function dices(gameState: GameState): { dice1: number; dice2: number } { return gameState.tag === 'GSInPlay' && gameState.dices !== undefined ? { dice1: gameState.dices[0].pip, dice2: gameState.dices[1].pip, } : { dice1: 0, dice2: 0 } } function isResignOffered(gameState: GameState) { if ( gameState.tag == 'GSOpening' || gameState.tag == 'GSEoG' || !gameState.isResignOffered || gameState.offer == undefined ) { return 0 } const offer = gameState.offer switch (offer) { case ResignOffer.Single: return 1 case ResignOffer.Gammon: return 2 case ResignOffer.Backgammon: return 3 } } function revertBits(v: Bit): Bit { let n = v.bit let ret = 0 for (let i = 0; i < (v.len ?? 1); i++) { ret = (ret << 1) | (n & 1) n = n >> 1 } return { bit: ret, len: v.len } } /** * 与えられたBitオブジェクトを8bitずつリトルエンディアンでまとめてBufferに書き込むreducerを生成する * @param buffer * @param pos * @returns */ export function littleEndianReducer( buffer: ArrayBuffer, pos = 0 ): (prev: Bit, value: Bit, idx: number, arr: Bit[]) => Bit { const dataView = new DataView(buffer) const byteWriter = { pos, write: (value: number) => { if (byteWriter.pos < buffer.byteLength) { dataView.setUint8(byteWriter.pos, revertByte(value)) byteWriter.pos += 1 } }, } return (prev: Bit, value: Bit, idx: number, arr: Bit[]) => { // データの最後になったら、とにかく1バイト分書くように補完する const isLast = idx == arr.length - 1 return accumulateBit(prev, value, byteWriter.write, isLast) } // little endian に変換 function revertByte(v: number) { return ( (v & 1) * 128 + ((v >> 1) & 1) * 64 + ((v >> 2) & 1) * 32 + ((v >> 3) & 1) * 16 + ((v >> 4) & 1) * 8 + ((v >> 5) & 1) * 4 + ((v >> 6) & 1) * 2 + ((v >> 7) & 1) ) } } /** * 任意の長さのビット列を表す */ export type Bit = { bit: number len?: number } /** * 与えられたビット列を既存のビット列の後に接続し、1バイト以上になれば * 各バイトごとにbyteConsumerに渡して切り捨てた結果を返す * @param prev 既存のビット列 * @param value prevに付加するビット列 * @param byteConsumer 1バイトごとに呼ばれる関数 * @param isLast valueが最後であるならtrue(1バイトに足りない分を0で詰めてbyteConsumerを呼ぶ) * @returns */ function accumulateBit( prev: Bit, value: Bit, byteConsumer: (b: number) => void, isLast = false ): Bit { let { bit: curBit = 0 } = prev const { len: lastLen = 0 } = prev let { bit, len = 1 } = value // bitが1バイト分に満たないうちは、追加されたbitを保持する // ただし、データの最後になったら、とにかく1バイト分書く let curLen = lastLen + len if (curLen < 8) { if (!isLast) { return { bit: (curBit << len) | bit, len: curLen, } } // データの最後で、書き込む分が1バイトに満たなければ0を詰める bit = bit << (8 - curLen) len = len + (8 - curLen) } // オーバーフローしないようにマスク bit = bit & ((1 << len) - 1) // まず以前から引き継いでいるbitがあれば、1バイト分書き出す if (lastLen > 0) { const shiftForLast = 8 - lastLen const shiftForCur = len - shiftForLast // == lenToWrite - 8 なので、常に > 0 const valueToWrite = (curBit << shiftForLast) | (bit >> shiftForCur) byteConsumer(valueToWrite) curLen -= 8 curBit = ((1 << shiftForCur) - 1) & bit } else { curBit = bit } // 新規追加のbitから1バイト分書ける分を全部書き出す while (curLen >= 8) { const shiftForCur = curLen - 8 const valueToWrite = (curBit >> shiftForCur) & 255 byteConsumer(valueToWrite) curLen -= 8 curBit = ((1 << shiftForCur) - 1) & curBit } // 最後の場合は、まだ残っているビットに0を詰めて出力する if (isLast && curLen > 0) { const valueToWrite = (curBit << (8 - curLen)) & 255 byteConsumer(valueToWrite) // 一応一貫性のある値を返させる curLen = 0 curBit = 0 } // 1バイトに満たない分を次に引き継ぐ(len<8が保証される) return { bit: curBit, len: curLen } }