From 20b480fa3bf676f290e9c1179ee3f52989b14347 Mon Sep 17 00:00:00 2001 From: Ken Riley Date: Sun, 31 May 2026 10:37:09 -0600 Subject: [PATCH 1/3] fix: enforce must-use-both-dice and must-use-larger-die rules A player could complete a turn using fewer dice than the rules require (e.g. roll [5,2], play only the 5 and strand the 2 even though playing the 2 first then the 5 uses both). pureMove validated against the post-move state, where the stranded die was already consumed, so the alternative- sequence search always found nothing. Compute the maximum dice any legal ordering can use from the turn-start board in Play.initialize, persist it on the play (maxDiceUsable, dieValuesPlayableAtStart), and enforce it in pureMove once no ready moves remain: throw MustUseBothDiceError when fewer dice were used than possible, and MustUseLargerDieError for the either-but-not-both case. Also: - Make the Sim engine rule-compliant (try candidates best-first, fall back on rule rejection) so simulations only play legal moves. - Fix the Sim LCG normalization: (s & 0xffffffff) is signed in JS and produced negative values, yielding invalid die values once the seeded RNG was wired into Dice for deterministic runs. - Add an optional injectable random source to Dice (default unchanged) so simulations are deterministic per seed. --- src/Dice/index.ts | 14 ++ .../__tests__/must-use-both-dice-rule.test.ts | 203 ++++++++++++++++++ src/Play/index.ts | 124 ++++++++--- src/Sim/engine.ts | 105 ++++++--- 4 files changed, 383 insertions(+), 63 deletions(-) create mode 100644 src/Play/__tests__/must-use-both-dice-rule.test.ts diff --git a/src/Dice/index.ts b/src/Dice/index.ts index 1d9ffff..2b1ca4b 100644 --- a/src/Dice/index.ts +++ b/src/Dice/index.ts @@ -14,7 +14,16 @@ import { } from '@nodots/backgammon-types' import { generateId } from '..' +// Optional deterministic random source for simulations/tests. When unset, +// rollDie uses Node's CSPRNG (randomInt). When set, it must return a float in +// [0, 1); rollDie maps it to 1..6. Production code never sets this. +let diceRandomSource: (() => number) | null = null + export class Dice { + static setRandomSource(source: (() => number) | null): void { + diceRandomSource = source + } + // Overloads for precise return types based on state public static initialize( color: BackgammonColor @@ -159,6 +168,11 @@ export class Dice { } static rollDie = function rollDie(): BackgammonDieValue { + if (diceRandomSource) { + // Map the injected [0,1) source to 1..6; cast narrows number → + // BackgammonDieValue (1|2|3|4|5|6). + return (Math.floor(diceRandomSource() * 6) + 1) as BackgammonDieValue + } // randomInt(1, 7) is exclusive on upper bound → yields 1..6 uniformly from // Node's CSPRNG; cast narrows number → BackgammonDieValue (1|2|3|4|5|6). return randomInt(1, 7) as BackgammonDieValue diff --git a/src/Play/__tests__/must-use-both-dice-rule.test.ts b/src/Play/__tests__/must-use-both-dice-rule.test.ts new file mode 100644 index 0000000..cd095c1 --- /dev/null +++ b/src/Play/__tests__/must-use-both-dice-rule.test.ts @@ -0,0 +1,203 @@ +import { + BackgammonBoard, + BackgammonDieValue, + BackgammonMoveOrigin, + BackgammonPlayerMoving, + BackgammonPlayMoving, + BackgammonPointValue, +} from '@nodots/backgammon-types' +import { Board, Dice, Play, Player } from '../..' + +// Reproduces the production bug from game 01727ded-7a6a-4125-8729-02f44a6b60db: +// a player rolled [5,2] and was allowed to play only the 5 (stranding the 2), +// even though playing the 2 first then the 5 uses both dice. The rule that a +// player must use as many dice as legally possible was not enforced. + +type CheckerSpec = { point: number; color: 'white' | 'black'; qty: number } + +// `point` is the clockwise point number; counterclockwise is its mirror (25 - point). +const buildBoard = (specs: CheckerSpec[]): BackgammonBoard => + Board.buildBoard( + specs.map((s) => ({ + position: { + clockwise: s.point as BackgammonPointValue, + counterclockwise: (25 - s.point) as BackgammonPointValue, + }, + checkers: { color: s.color, qty: s.qty }, + })) + ) + +const movingPlayer = ( + color: 'white' | 'black', + direction: 'clockwise' | 'counterclockwise', + roll: [BackgammonDieValue, BackgammonDieValue] +): BackgammonPlayerMoving => { + const base = Player.initialize(color, direction, 'inactive', false, 'test-user') + return { + ...base, + stateKind: 'moving', + dice: Dice.initialize(color, 'rolled', undefined, roll), + } as BackgammonPlayerMoving +} + +const originAt = ( + board: BackgammonBoard, + direction: 'clockwise' | 'counterclockwise', + position: number +): BackgammonMoveOrigin => { + const point = board.points.find((p) => p.position[direction] === position) + if (!point) throw new Error(`No point at ${direction} ${position}`) + return point as BackgammonMoveOrigin +} + +const diceUsed = (play: { moves: { stateKind: string; moveKind: string }[] }) => + play.moves.filter( + (m) => m.stateKind === 'completed' && m.moveKind !== 'no-move' + ).length + +const captureThrow = (fn: () => unknown): { name?: string } | undefined => { + try { + fn() + return undefined + } catch (e) { + return e as { name?: string } + } +} + +describe('Play - must use both dice (stranded-die bug)', () => { + describe('counterclockwise player (reproduces production board)', () => { + // White is counterclockwise. cc7 (=cw18) holds the only checker that can + // play the 2; cc23/cc24 are blocked for the 2 by black on cc21/cc22. + const makeBoard = () => + buildBoard([ + { point: 18, color: 'white', qty: 1 }, // cc7 - only legal source for the 2 + { point: 2, color: 'white', qty: 2 }, // cc23 + { point: 1, color: 'white', qty: 2 }, // cc24 + { point: 4, color: 'black', qty: 2 }, // cc21 - blocks cc23->cc21 + { point: 3, color: 'black', qty: 2 }, // cc22 - blocks cc24->cc22 + ]) + + test('initialize computes maxDiceUsable = 2', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'counterclockwise', [5, 2])) + expect(play.maxDiceUsable).toBe(2) + }) + + test('playing the 5 from cc7 (stranding the 2) is rejected', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'counterclockwise', [5, 2])) + const thrown = captureThrow(() => + Play.move(board, play, originAt(board, 'counterclockwise', 7), 5) + ) + expect(thrown?.name).toBe('MustUseBothDiceError') + }) + + test('playing the 2 first, then the 5, uses both dice', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'counterclockwise', [5, 2])) + + const afterTwo = Play.move(board, play, originAt(board, 'counterclockwise', 7), 2) + expect(afterTwo.play.moves.some((m) => m.stateKind === 'ready')).toBe(true) + + const afterFive = Play.move( + afterTwo.board, + afterTwo.play as BackgammonPlayMoving, + originAt(afterTwo.board, 'counterclockwise', 23), + 5 + ) + expect(diceUsed(afterFive.play)).toBe(2) + }) + }) + + describe('clockwise player (direction independence)', () => { + const makeBoard = () => + buildBoard([ + { point: 7, color: 'white', qty: 1 }, // only legal source for the 2 + { point: 23, color: 'white', qty: 2 }, + { point: 24, color: 'white', qty: 2 }, + { point: 21, color: 'black', qty: 2 }, // blocks 23->21 + { point: 22, color: 'black', qty: 2 }, // blocks 24->22 + ]) + + test('playing the 5 from point 7 (stranding the 2) is rejected', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'clockwise', [5, 2])) + expect(play.maxDiceUsable).toBe(2) + const thrown = captureThrow(() => + Play.move(board, play, originAt(board, 'clockwise', 7), 5) + ) + expect(thrown?.name).toBe('MustUseBothDiceError') + }) + + test('playing the 2 first, then the 5, uses both dice', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'clockwise', [5, 2])) + const afterTwo = Play.move(board, play, originAt(board, 'clockwise', 7), 2) + const afterFive = Play.move( + afterTwo.board, + afterTwo.play as BackgammonPlayMoving, + originAt(afterTwo.board, 'clockwise', 23), + 5 + ) + expect(diceUsed(afterFive.play)).toBe(2) + }) + }) + + describe('must use the larger die when only one can be played', () => { + // One mobile checker on point 9 can play the 6 (9->3) or the 3 (9->6), but + // not both. A blocked back checker on 24 prevents bearing off either die. + const makeBoard = () => + buildBoard([ + { point: 9, color: 'white', qty: 1 }, + { point: 24, color: 'white', qty: 1 }, + { point: 18, color: 'black', qty: 2 }, // blocks 24->18 (the 6) + { point: 21, color: 'black', qty: 2 }, // blocks 24->21 (the 3) + ]) + + test('initialize computes maxDiceUsable = 1 with both dice playable at start', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'clockwise', [6, 3])) + expect(play.maxDiceUsable).toBe(1) + expect([...(play.dieValuesPlayableAtStart ?? [])].sort()).toEqual([3, 6]) + }) + + test('playing the smaller die (3) is rejected', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'clockwise', [6, 3])) + const thrown = captureThrow(() => + Play.move(board, play, originAt(board, 'clockwise', 9), 3) + ) + expect(thrown?.name).toBe('MustUseLargerDieError') + }) + + test('playing the larger die (6) is accepted', () => { + const board = makeBoard() + const play = Play.initialize(board, movingPlayer('white', 'clockwise', [6, 3])) + const result = Play.move(board, play, originAt(board, 'clockwise', 9), 6) + expect(diceUsed(result.play)).toBe(1) + }) + }) + + describe('doubles', () => { + // One mobile checker on 13 can play two 6s (13->7->1); a blocked back + // checker on 24 caps usage at 2 of the four 6s. + test('initialize computes maxDiceUsable = 2 and the two sixes complete', () => { + const board = buildBoard([ + { point: 13, color: 'white', qty: 1 }, + { point: 24, color: 'white', qty: 1 }, + { point: 18, color: 'black', qty: 2 }, // blocks 24->18 + ]) + const play = Play.initialize(board, movingPlayer('white', 'clockwise', [6, 6])) + expect(play.maxDiceUsable).toBe(2) + + const first = Play.move(board, play, originAt(board, 'clockwise', 13), 6) + const second = Play.move( + first.board, + first.play as BackgammonPlayMoving, + originAt(first.board, 'clockwise', 7), + 6 + ) + expect(diceUsed(second.play)).toBe(2) + }) + }) +}) diff --git a/src/Play/index.ts b/src/Play/index.ts index 4d640a3..1bc4cd0 100644 --- a/src/Play/index.ts +++ b/src/Play/index.ts @@ -17,11 +17,7 @@ import { } from '@nodots/backgammon-types' import { Board, generateId } from '..' import { debug, logger } from '../utils/logger' -import { - InvalidMoveSequenceError, - MustUseBothDiceError, - MustUseLargerDieError, -} from './errors' +import { MustUseBothDiceError, MustUseLargerDieError } from './errors' export * from '../index' export interface MoveExecutionOptions { @@ -526,37 +522,49 @@ export class Play { // Step 2: Execute the plan (pure) const result = Play.executePlannedMove(board, play, plan) - // Step 3: Validate the result if sequence is complete (pure) - // FIX: Never validate during individual move execution when play has pre-completed no-moves - // Only validate when all dice have been rolled and used, not when executing partial sequences - const originalHadNoMoves = play.moves.some((m) => m.moveKind === 'no-move') + // Step 3: Validate the completed sequence against the turn-start dice budget. + // The maximum number of dice any legal ordering can use was computed in + // Play.initialize (play.maxDiceUsable) from the turn-start board. Enforcing + // it here — once no ready moves remain — guarantees the player used as many + // dice as the rules require, regardless of which ordering they chose. const resultHasReadyMoves = result.newPlay.moves.some( (m) => m.stateKind === 'ready' ) - // Only validate if: - // 1. No ready moves remain in result AND - // 2. No pre-completed no-moves existed (meaning this is a fresh complete sequence) + if (!resultHasReadyMoves) { + const maxDiceUsable = play.maxDiceUsable + const actualDiceUsed = result.newPlay.moves.filter( + (m) => m.stateKind === 'completed' && m.moveKind !== 'no-move' + ).length - if (!resultHasReadyMoves && !originalHadNoMoves) { - const validation = Play.validateMoveSequence( - result.newBoard, - result.newPlay - ) - if (!validation.isValid) { - // Sequence violates backgammon rules - throw appropriate error - if (validation.error?.includes('both dice')) { + // Only enforce when the budget was computed (plays produced by + // Play.initialize). Guards against undefined for legacy/synthetic plays. + if (typeof maxDiceUsable === 'number') { + if (actualDiceUsed < maxDiceUsable) { throw MustUseBothDiceError( - `${validation.error}. Alternative sequences exist that would use both dice.` - ) - } else if (validation.error?.includes('larger die')) { - throw MustUseLargerDieError( - `${validation.error}. The larger die value must be used when only one die can be played.` + `Must use ${maxDiceUsable} dice when legally possible; only ${actualDiceUsed} used. A legal ordering exists that uses more dice.` ) - } else { - throw InvalidMoveSequenceError( - `Invalid move sequence: ${validation.error}` + } + + // Must-use-larger-die rule: when only one die can be used and both were + // individually playable from the turn-start board, the larger must be used. + const playableAtStart = play.dieValuesPlayableAtStart ?? [] + if (maxDiceUsable === 1 && actualDiceUsed === 1) { + const usedMove = result.newPlay.moves.find( + (m) => m.stateKind === 'completed' && m.moveKind !== 'no-move' ) + const distinctPlayable = [...new Set(playableAtStart)] + if ( + usedMove && + distinctPlayable.length >= 2 && + usedMove.dieValue < Math.max(...distinctPlayable) + ) { + throw MustUseLargerDieError( + `Must use the larger die (${Math.max( + ...distinctPlayable + )}) when only one die can be played; ${usedMove.dieValue} was used.` + ) + } } } @@ -860,6 +868,60 @@ export class Play { // Step 4: Combine all moves const allMoves = [...reentryMoves, ...regularMoves] + // Step 5: Compute the maximum number of dice any legal ordering can use, + // measured from the turn-start board. This is the authoritative target for + // the must-use-both-dice / must-use-larger-die rules, enforced later in + // pureMove. Reentries are mandatory and consume one die each; regular dice + // are explored against the post-reentry board (matching how their + // possibleMoves were derived above) across both orderings. + const playableReentries = reentryMoves.filter( + (m) => m.moveKind !== 'no-move' + ).length + + const regularPlay = { + id: generateId(), + board: boardAfterReentries, + player, + moves: regularMoves, + stateKind: 'moving', + } as BackgammonPlayMoving + + const distinctRegular = [...new Set(regularDiceValues)] + let maxFromRegular = 0 + if (regularDiceValues.length > 0) { + maxFromRegular = + distinctRegular.length >= 2 + ? Math.max( + Play.testSequenceDiceUsage(boardAfterReentries, regularPlay, [ + distinctRegular[0], + distinctRegular[1], + ]), + Play.testSequenceDiceUsage(boardAfterReentries, regularPlay, [ + distinctRegular[1], + distinctRegular[0], + ]) + ) + : // Single value: doubles (2-4 same dice) or one leftover regular die + Play.testSequenceDiceUsage( + boardAfterReentries, + regularPlay, + regularDiceValues + ) + } + + const maxDiceUsable = playableReentries + maxFromRegular + const dieValuesPlayableAtStart = [ + ...new Set( + allMoves + .filter( + (m) => + Array.isArray((m as BackgammonMoveReady).possibleMoves) && + (m as BackgammonMoveReady).possibleMoves.length > 0 + ) + .map((m) => m.dieValue) + ), + ] as BackgammonDieValue[] + // Check if all moves are no-moves and auto-complete the play const allMovesAreNoMoves = allMoves.every( (move) => move.moveKind === 'no-move' @@ -887,6 +949,8 @@ export class Play { player, moves: completedMoves, stateKind: 'moving', + maxDiceUsable, + dieValuesPlayableAtStart, } as BackgammonPlayMoving } @@ -934,6 +998,8 @@ export class Play { player, moves: normalized, stateKind: 'moving', + maxDiceUsable, + dieValuesPlayableAtStart, } as BackgammonPlayMoving } @@ -943,6 +1009,8 @@ export class Play { player, moves: allMoves, stateKind: 'moving', + maxDiceUsable, + dieValuesPlayableAtStart, } as BackgammonPlayMoving } diff --git a/src/Sim/engine.ts b/src/Sim/engine.ts index af2c348..4eebe7c 100644 --- a/src/Sim/engine.ts +++ b/src/Sim/engine.ts @@ -1,4 +1,5 @@ import { + BackgammonDieValue, BackgammonGame, BackgammonGameMoving, BackgammonGameRolledForStart, @@ -6,6 +7,7 @@ import { BackgammonMoveSkeleton, } from '@nodots/backgammon-types' import { Board } from '../Board' +import { Dice } from '../Dice' import { Game } from '../Game' import { Play } from '../Play' @@ -20,10 +22,12 @@ export class EngineRunner { constructor(private opts: EngineOptions = {}) { const seed = opts.seed ?? Date.now() >>> 0 let s = seed >>> 0 - // Simple LCG for deterministic runs + // Simple LCG for deterministic runs. s is kept unsigned via >>> 0, so divide + // by 2^32 directly — `s & 0xffffffff` would reinterpret s as a signed 32-bit + // int and yield negative values, breaking the [0, 1) contract. this.rng = () => { s = (1664525 * s + 1013904223) >>> 0 - return (s & 0xffffffff) / 0x100000000 + return s / 0x100000000 } } @@ -57,9 +61,43 @@ export class EngineRunner { return Game.checkAndCompleteTurn(game) } - // Try each ready die in order; pick first with any legal moves from valid origins + // Heuristic: prefer bear-offs, then hits, then moves that advance closest to home + const dir = (game as any).activePlay.player.direction as + | 'clockwise' + | 'counterclockwise' + const destInfo = (mv: any) => { + const dest = mv.destination + if (dest.kind === 'off') return { kind: 'off' as const, pos: 0, hit: false } + if (dest.kind === 'point') { + const pos = dest.position[dir] as number + const boardDest = game.board.points.find((p: any) => p.id === dest.id) + const hit = + boardDest && + Array.isArray(boardDest.checkers) && + boardDest.checkers.length === 1 && + boardDest.checkers[0].color !== (game as any).activePlay.player.color + return { kind: 'point' as const, pos, hit: !!hit } + } + return { kind: 'point' as const, pos: 24, hit: false } + } + const scoreOf = (mv: any) => { + const info = destInfo(mv) + let score = 0 + if (info.kind === 'off') score += 1000 + if (info.hit) score += 500 + score += 25 - (info.pos || 25) + return score + } + + // Build ordered attempts preserving the original priority: dice in roll + // order, each die's candidates best-first. The engine enforces the + // must-use-both-dice and must-use-larger-die rules by throwing; on a + // rejected move fall back to the next attempt so the simulation only plays + // rule-compliant moves without otherwise changing move selection. + type Attempt = { originId: string; dieValue: BackgammonDieValue } + const attempts: Attempt[] = [] for (const rm of ready) { - const die = rm.dieValue + const die = rm.dieValue as BackgammonDieValue const pm = Board.getPossibleMoves( game.board, (game as any).activePlay.player, @@ -68,38 +106,24 @@ export class EngineRunner { const arr = Array.isArray(pm) ? pm : (pm?.moves || []) const byOrigin = arr.filter((m) => validOrigins.includes((m as any).origin?.id)) const candidates = byOrigin.length > 0 ? byOrigin : arr - if (candidates.length > 0) { - // Heuristic: prefer bear-offs, then hits, then moves that advance closest to home (lowest destination position) - const dir = (game as any).activePlay.player.direction as 'clockwise' | 'counterclockwise' - const destInfo = (mv: any) => { - const dest = mv.destination - if (dest.kind === 'off') return { kind: 'off' as const, pos: 0, hit: false } - if (dest.kind === 'point') { - const pos = dest.position[dir] as number - // Determine hit by looking at current board destination occupancy - const boardDest = game.board.points.find((p: any) => p.id === dest.id) - const hit = boardDest && Array.isArray(boardDest.checkers) && boardDest.checkers.length === 1 && boardDest.checkers[0].color !== (game as any).activePlay.player.color - return { kind: 'point' as const, pos, hit: !!hit } - } - return { kind: 'point' as const, pos: 24, hit: false } - } - let best: any = null - let bestScore = -Infinity - for (const mv of candidates) { - const info = destInfo(mv as any) - let score = 0 - if (info.kind === 'off') score += 1000 - if (info.hit) score += 500 - score += 25 - (info.pos || 25) - if (score > bestScore) { - bestScore = score - best = mv - } - } - if (best && (best as any).origin && (best as any).origin.id) { - const originId = (best as any).origin.id as string - return Game.executeAndRecalculate(game, originId) + const sorted = [...candidates].sort((a, b) => scoreOf(b) - scoreOf(a)) + for (const mv of sorted) { + const originId = (mv as any).origin?.id as string | undefined + if (originId) attempts.push({ originId, dieValue: die }) + } + } + + for (const a of attempts) { + try { + return Game.executeAndRecalculate(game, a.originId, { + expectedDieValue: a.dieValue, + }) + } catch (e: any) { + const name = e?.name + if (name === 'MustUseBothDiceError' || name === 'MustUseLargerDieError') { + continue } + throw e } } @@ -108,6 +132,17 @@ export class EngineRunner { } runUntilWin(maxTurns = 400): { game: BackgammonGame; turns: number } { + // Drive dice from this engine's seeded RNG so runs are deterministic per + // seed. Reset in the finally so production dice (CSPRNG) are unaffected. + Dice.setRandomSource(this.rng) + try { + return this.runLoop(maxTurns) + } finally { + Dice.setRandomSource(null) + } + } + + private runLoop(maxTurns: number): { game: BackgammonGame; turns: number } { let turns = 0 let s0 = this.init() // Resolve roll for start From d3326eee85e780eaab3e7f22d67448b97be46789 Mon Sep 17 00:00:00 2001 From: Ken Riley Date: Sun, 31 May 2026 10:46:17 -0600 Subject: [PATCH 2/3] chore: require @nodots/backgammon-types ^1.0.3 for maxDiceUsable The must-use-both-dice enforcement reads play.maxDiceUsable / dieValuesPlayableAtStart, added in backgammon-types 1.0.3. Lockfile regeneration is deferred until 1.0.3 is published to npm. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1ac9ca..0407532 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@nodots/backgammon-types": "^1.0.0", + "@nodots/backgammon-types": "^1.0.3", "@nodots/gnubg-hints": "^1.0.0", "uuid": "^11.1.0" }, From 9e57320e110f63e01c291455f6ed040f6f2944b2 Mon Sep 17 00:00:00 2001 From: Ken Riley Date: Sun, 31 May 2026 11:06:54 -0600 Subject: [PATCH 3/3] chore: bump to 1.0.1 and sync lockfile to types 1.0.3 / gnubg-hints 1.0.4 Resolves the pre-existing npm ci drift (lock pinned rc.1/rc.3 against ^1.0.0 ranges) and points at the published backgammon-types 1.0.3 that carries the maxDiceUsable fields. --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c98deb..38d22f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@nodots/backgammon-core", - "version": "1.0.0-rc.6", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nodots/backgammon-core", - "version": "1.0.0-rc.6", + "version": "1.0.1", "license": "GPL-3.0", "dependencies": { - "@nodots/backgammon-types": "^1.0.0-rc.1", - "@nodots/gnubg-hints": "^1.0.0-rc.1", + "@nodots/backgammon-types": "^1.0.3", + "@nodots/gnubg-hints": "^1.0.0", "uuid": "^11.1.0" }, "devDependencies": { @@ -1330,22 +1330,22 @@ } }, "node_modules/@nodots/backgammon-types": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@nodots/backgammon-types/-/backgammon-types-1.0.0-rc.1.tgz", - "integrity": "sha512-3H7YyE0PGfyZwF6l56cC8nr47Z1CxXzbU6idB7wWcYwphy6lje6FxCth5+p+HfU1G12H6HmZ0vP1tvdJJz3sqA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@nodots/backgammon-types/-/backgammon-types-1.0.3.tgz", + "integrity": "sha512-2o3jTgG2pioUTKUZshE9lDnwl/D4u5y+nA+Y7fBJsR6pzgF8O+BLDfIZigTsa91zodU6uIOLnpUoq+XaSyrw9A==", "license": "GPL-3.0", "engines": { "node": ">=20" } }, "node_modules/@nodots/gnubg-hints": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@nodots/gnubg-hints/-/gnubg-hints-1.0.0-rc.3.tgz", - "integrity": "sha512-uJEG/skZsW9AHVkn89ybiZ9DooGJHcHHNzMTLUIq9jnkAmoD1Nxf0ryaF9ggbROO2xmDnMA2FIEjdc0qQqZdjw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@nodots/gnubg-hints/-/gnubg-hints-1.0.4.tgz", + "integrity": "sha512-c6V2KVNN2Z7Qpf/QWq0UURm2QQLD1KmrbvleJLhyeKUx6zMZdVzwfOQZp1zbu5j1hLpE4vA1ufu5l8dih+Of1Q==", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { - "@nodots/backgammon-types": "^1.0.0-rc.1", + "@nodots/backgammon-types": "^1.0.0", "node-addon-api": "^7.0.0", "node-gyp": "^10.0.1" }, diff --git a/package.json b/package.json index 0407532..40e5595 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nodots/backgammon-core", - "version": "1.0.0", + "version": "1.0.1", "description": "Core game logic for Nodots Backgammon", "main": "dist/index.js", "types": "dist/index.d.ts",