tsgammon-core
Version:
A Backgammon library for Typescript, formerly developed as a part of tsgammon-ui
240 lines (239 loc) • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.littleEndianReducer = exports.toMatchID = void 0;
const base64_1 = require("@borderless/base64");
const CubeState_1 = require("../CubeState");
const ResignOffer_1 = require("../ResignOffer");
/**
* 指定の局面から、GNU Backgammon仕様のMatchIDを含むオブジェクトを生成する
*
* @param matchState マッチスコアの状態
* @param gameState 手番など、現在のゲームの状態
* @returns
*/
function toMatchID(matchState, 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 === CubeState_1.CubeOwner.RED
? 0
: cubeState.owner == CubeState_1.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: (0, base64_1.encode)(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)),
};
}
exports.toMatchID = toMatchID;
function dices(gameState) {
return gameState.tag === 'GSInPlay' && gameState.dices !== undefined
? {
dice1: gameState.dices[0].pip,
dice2: gameState.dices[1].pip,
}
: { dice1: 0, dice2: 0 };
}
function isResignOffered(gameState) {
if (gameState.tag == 'GSOpening' ||
gameState.tag == 'GSEoG' ||
!gameState.isResignOffered ||
gameState.offer == undefined) {
return 0;
}
const offer = gameState.offer;
switch (offer) {
case ResignOffer_1.ResignOffer.Single:
return 1;
case ResignOffer_1.ResignOffer.Gammon:
return 2;
case ResignOffer_1.ResignOffer.Backgammon:
return 3;
}
}
function revertBits(v) {
var _a;
let n = v.bit;
let ret = 0;
for (let i = 0; i < ((_a = v.len) !== null && _a !== void 0 ? _a : 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
*/
function littleEndianReducer(buffer, pos = 0) {
const dataView = new DataView(buffer);
const byteWriter = {
pos,
write: (value) => {
if (byteWriter.pos < buffer.byteLength) {
dataView.setUint8(byteWriter.pos, revertByte(value));
byteWriter.pos += 1;
}
},
};
return (prev, value, idx, arr) => {
// データの最後になったら、とにかく1バイト分書くように補完する
const isLast = idx == arr.length - 1;
return accumulateBit(prev, value, byteWriter.write, isLast);
};
// little endian に変換
function revertByte(v) {
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));
}
}
exports.littleEndianReducer = littleEndianReducer;
/**
* 与えられたビット列を既存のビット列の後に接続し、1バイト以上になれば
* 各バイトごとにbyteConsumerに渡して切り捨てた結果を返す
* @param prev 既存のビット列
* @param value prevに付加するビット列
* @param byteConsumer 1バイトごとに呼ばれる関数
* @param isLast valueが最後であるならtrue(1バイトに足りない分を0で詰めてbyteConsumerを呼ぶ)
* @returns
*/
function accumulateBit(prev, value, byteConsumer, isLast = false) {
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 };
}