🌳
pt0/gamesapp/testsF/agentsAI.mts
1import * as _ from 'lodash-es'
6const fakeDrawing = [{ points: [{ x: 100, y: 100 }, { x: 200, y: 200 }], brushRadius: 3 }]
7const fakeDescriptions = ['cat on mat', 'dancing robot', 'happy cloud', 'confused penguin', 'wizard cooking']
9type CreateAgentOpts = { sessId: string, playerAlias: string, seed?: string, type?: 'random' | 'chaotic' }
11/** Creates an agent that plays the game */
12export const createAgent = ({ sessId, playerAlias, seed = 'agent', type = 'random' }: CreateAgentOpts): TestAgent => {
13 const seededLo = seedLodash(`${seed}-${sessId}`)
15 const chooseAction = (gameState: GameState): GameAction | null => {
16 const ps = _.get(gameState, ['playerStates', sessId])
17 if (!ps) return { sessId, playerAction: 'setPlayerAlias', playerAlias }
18 if (ps.hasBeenKicked) return null
20 const { assignedStackName } = ps
21 const task = ps.publicState?.myTaskName
23 if (task === 'InitialPrompt' || task === 'DescribeDrawing') {
24 return { sessId, playerAction: 'submitDescription', descriptionText: seededLo.sample(fakeDescriptions), assignedStackName }
25 }
26 if (task === 'DrawDescription') {
27 return { sessId, playerAction: 'submitDrawing', finishedTrails: fakeDrawing, assignedStackName }
28 }
29 return null
30 }
32 const chooseChaoticAction = (gameState: GameState): GameAction | null => {
33 const action = chooseAction(gameState)
34 if (!action) return null
35 const roll = seededLo.random(0, 100)
36 if (roll < 10 && action.assignedStackName) {
37 const stacks = _.keys(_.get(gameState, ['hiddenState', 'paperStacks'], {}))
38 const wrongStack = seededLo.sample(stacks.filter(s => s !== action.assignedStackName))
39 if (wrongStack) return { ...action, assignedStackName: wrongStack }
40 }
41 if (roll >= 10 && roll < 15) return { sessId, playerAction: 'leaveGame' }
42 return action
43 }
45 return { sessId, playerAlias, chooseAction: type === 'chaotic' ? chooseChaoticAction : chooseAction }
48export { testPlayerPrefix }
50type CreateTeamOpts = { count?: number, seed?: string, type?: 'random' | 'chaotic' }
52export const createAgentTeam = ({ count = 4, seed = 'team', type = 'random' }: CreateTeamOpts = {}): TestAgent[] => {
53 return _.times(count, i => createAgent({
54 sessId: `${testPlayerPrefix}${i + 1}`,
55 playerAlias: `${testPlayerPrefix}${i + 1}`,
56 seed: `${seed}-${i}`,
57 type
58 }))
61export const runAgentsUntilDone = async ({ harness, agents, maxRounds = 200, onRound }: RunAgentsOpts): Promise<RunAgentsResult> => {
62 let round = 0
63 let noActionRounds = 0
64 let initialGameId: string | null = null
66 while (round++ < maxRounds) {
67 const gameState = harness.getState()
68 if (gameState?.isGameFin) break
70 // Track game ID - if it changes, the game finished and reset
71 // This is the primary way to detect game completion in live tests since
72 // the server resets state immediately after saving finished games
73 if (!initialGameId && gameState?.id) {
74 initialGameId = gameState.id
75 } else if (initialGameId && gameState?.id && gameState.id !== initialGameId) {
76 // Game ID changed = game finished and a new one started
77 return { rounds: round, isGameFin: true, gameIdChanged: true, reachedMaxRounds: false }
78 }
80 let actionsThisRound = 0
81 for (const agent of agents) {
82 const action = agent.chooseAction(gameState)
83 if (action) {
84 await harness.applyAction(action)
85 actionsThisRound++
86 }
87 }
89 onRound?.({ round, actionsThisRound, isGameFin: gameState?.isGameFin })
91 // Detect stuck state - if no agent can act for several rounds, break
92 if (actionsThisRound === 0) {
93 noActionRounds++
94 if (noActionRounds >= 5) {
95 break // All agents are waiting or blocked
96 }
97 } else {
98 noActionRounds = 0
99 }
100 }
101 const finalState = harness.getState()
102 return { rounds: round, isGameFin: finalState?.isGameFin, reachedMaxRounds: round >= maxRounds }