Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Services/PerformanceRatingCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export interface AiModuleInterface {
gnubgHints: {
isAvailable: () => Promise<boolean>
getBuildInstructions: () => string
// The engine config is a mutable process-global shared with robot turn
// execution; analysis must be able to re-assert its own config per run.
configure?: (config: {
evalPlies?: number
moveFilter?: number
usePruning?: boolean
noise?: number
}) => void | Promise<void>
getMoveHints: (request: any, maxHints?: number) => Promise<MoveHint[]>
// Prefer positionId-based hints when available to match robot path
getHintsFromPositionId?: (
Expand All @@ -23,6 +31,18 @@ export interface AiModuleInterface {
}
}

// Engine settings PR analysis runs at, independent of any robot's skill
// config. 2-ply, deterministic, with the widest move filter (numeric value of
// MoveFilterSetting.Huge — not imported to keep @nodots/gnubg-hints a
// type-only dependency). Matches what the api's pr routes have historically
// configured.
export const ANALYSIS_HINTS_CONFIG = {
evalPlies: 2,
moveFilter: 4,
usePruning: true,
noise: 0,
} as const

// Lazy-loaded ai module reference
let aiModule: AiModuleInterface | null = null

Expand Down Expand Up @@ -145,6 +165,15 @@ export class PerformanceRatingCalculator {
)
}

// Re-assert the analysis config for every run. The engine config is a
// process-global that robot turns overwrite with per-robot skill
// settings; without this, a game against "GNU Beginner" would be
// analyzed at the robot's weakened settings (or the robot's play
// analyzed by the exact engine call that chose it, yielding PR 0.0).
// The config must be complete — partial configs merge into whatever
// the previous caller left behind.
await ai.gnubgHints.configure?.(ANALYSIS_HINTS_CONFIG)

const playerStats: Record<string, PlayerPR> = {}

// Group consecutive actions into full-turn sequences by (player, dice)
Expand Down
33 changes: 32 additions & 1 deletion src/Services/__tests__/PerformanceRatingCalculator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { describe, expect, test, jest, beforeEach, afterEach } from '@jest/globals'
import { PerformanceRatingCalculator, setAiModuleForTesting, AiModuleInterface } from '../PerformanceRatingCalculator'
import { PerformanceRatingCalculator, setAiModuleForTesting, AiModuleInterface, ANALYSIS_HINTS_CONFIG } from '../PerformanceRatingCalculator'

// Create mock AI module for testing
const createMockAiModule = (overrides: Partial<AiModuleInterface> = {}): AiModuleInterface => ({
Expand Down Expand Up @@ -226,6 +226,37 @@ describe('PerformanceRatingCalculator', () => {
expect(result.playerResults['player1']?.totalMoves).toBe(2)
expect(result.playerResults['player2']?.totalMoves).toBe(1)
})

test('re-asserts the full analysis config on every run', async () => {
// Regression: the engine config is a process-global that robot turns
// overwrite with per-robot skill settings. Analysis must pin its own
// complete config each run or it inherits the robot's.
const configureMock = jest
.fn<(config: Record<string, unknown>) => Promise<void>>()
.mockResolvedValue(undefined)
const moduleWithConfigure = createMockAiModule()
moduleWithConfigure.gnubgHints.configure = configureMock
setAiModuleForTesting(moduleWithConfigure)

await calculator.calculateGamePR('test-game', [], [])
await calculator.calculateGamePR('test-game', [], [])

expect(configureMock).toHaveBeenCalledTimes(2)
expect(configureMock).toHaveBeenCalledWith(ANALYSIS_HINTS_CONFIG)
// The config must be complete: partial configs merge into whatever the
// previous engine caller left behind.
expect(ANALYSIS_HINTS_CONFIG).toEqual({
evalPlies: 2,
moveFilter: 4,
usePruning: true,
noise: 0,
})
})

test('tolerates an ai module without configure', async () => {
const result = await calculator.calculateGamePR('test-game', [], [])
expect(result.analysisComplete).toBe(true)
})
})
})

Expand Down
Loading