🌳
pt0/gamesapp/testsF/invariantsAI.mjs
1import * as _ from 'lodash-es'
5// AI player names (from getShortModelName) - they auto-respond after drawings
6const aiPlayerPatterns = ['gpt', 'mytho', 'wizard', 'deep', 'claude', 'gemini', 'llama']
7const isAiPlayer = (playerAlias) => {
8 if (!playerAlias) return false
9 const lower = playerAlias.toLowerCase()
10 return aiPlayerPatterns.some(p => lower.startsWith(p))
13export const invariants = {
14 /**
15 * Player can only submit to their assigned stack
16 */
17 assignedStackConsistency: (gameState) => {
18 const { playerStates, hiddenState } = gameState
19 const paperStacks = hiddenState?.paperStacks || {}
21 for (const [sessId, ps] of _.toPairs(playerStates)) {
22 if (ps.hasBeenKicked) continue
23 const { assignedStackName } = ps
24 if (!assignedStackName) continue
26 const stack = paperStacks[assignedStackName]
27 if (!stack) {
28 return { ok: false, message: `Player ${sessId} assigned to non-existent stack ${assignedStackName}` }
29 }
30 }
31 return { ok: true }
32 },
34 /**
35 * Task name matches task type (drawing prompt = DrawDescription, text prompt = DescribeDrawing)
36 */
37 taskTypeConsistency: (gameState) => {
38 const { playerStates } = gameState
40 for (const [sessId, ps] of _.toPairs(playerStates)) {
41 if (ps.hasBeenKicked) continue
42 const { publicState, taskPrompt } = ps
43 const { myTaskName } = publicState || {}
45 if (myTaskName === 'DrawDescription') {
46 if (taskPrompt && typeof taskPrompt !== 'string') {
47 return { ok: false, message: `DrawDescription task should have string prompt, got ${typeof taskPrompt}` }
48 }
49 }
50 if (myTaskName === 'DescribeDrawing') {
51 // taskPrompt can be trails array, imageDataUrl string, or ipfsCid string
52 if (taskPrompt && !Array.isArray(taskPrompt) && typeof taskPrompt !== 'string') {
53 return { ok: false, message: `DescribeDrawing task should have array (trails) or string (imageDataUrl/ipfsCid), got ${typeof taskPrompt}` }
54 }
55 }
56 }
57 return { ok: true }
58 },
60 /**
61 * Responses alternate: description -> drawing -> description -> ...
62 */
63 responseAlternation: (gameState) => {
64 const paperStacks = gameState.hiddenState?.paperStacks || {}
66 for (const [stackName, stack] of _.toPairs(paperStacks)) {
67 const { responses } = stack
68 if (!responses || responses.length === 0) continue
70 // First response should be description (initial prompt)
71 if (!responses[0].lastDescription) {
72 return { ok: false, message: `Stack ${stackName} first response should be description` }
73 }
75 for (let i = 1; i < responses.length; i++) {
76 const prev = responses[i - 1]
77 const curr = responses[i]
79 // After description comes drawing
80 if (prev.lastDescription && !respHasDrawing(curr) && !curr.lastDescription) {
81 return { ok: false, message: `Stack ${stackName} response ${i}: expected drawing after description` }
82 }
83 // After drawing comes description (human or AI auto-describe)
84 if (respHasDrawing(prev) && !curr.lastDescription) {
85 return { ok: false, message: `Stack ${stackName} response ${i}: expected description after drawing` }
86 }
87 // Two descriptions in a row: only OK if second is AI (auto-describe after drawing vision)
88 if (prev.lastDescription && curr.lastDescription && !isAiPlayer(curr.playerAlias)) {
89 return { ok: false, message: `Stack ${stackName} response ${i}: two human descriptions in a row (${prev.playerAlias} then ${curr.playerAlias})` }
90 }
91 // Two drawings in a row is NOT OK
92 if (respHasDrawing(prev) && respHasDrawing(curr)) {
93 return { ok: false, message: `Stack ${stackName} response ${i}: two drawings in a row` }
94 }
95 }
96 }
97 return { ok: true }
98 },
100 /**
101 * No human player responds to same stack twice (AI can respond multiple times)
102 */
103 noDoubleResponses: (gameState) => {
104 const paperStacks = gameState.hiddenState?.paperStacks || {}
106 for (const [stackName, stack] of _.toPairs(paperStacks)) {
107 const { responses } = stack
108 if (!responses) continue
110 const playerResponses = {}
111 for (const resp of responses) {
112 const { playerAlias } = resp
113 if (!playerAlias) continue
114 // Skip AI responses - they auto-describe after drawings
115 if (isAiPlayer(playerAlias)) continue
117 playerResponses[playerAlias] = (playerResponses[playerAlias] || 0) + 1
118 if (playerResponses[playerAlias] > 1) {
119 return { ok: false, message: `Player ${playerAlias} responded to stack ${stackName} multiple times` }
120 }
121 }
122 }
123 return { ok: true }
124 },
128/**
129 * Check all invariants against game state
130 */
131export const checkAllInvariants = (gameState) => {
132 const results = {}
133 let allOk = true
135 for (const [name, checkFn] of _.toPairs(invariants)) {
136 const result = checkFn(gameState)
137 results[name] = result
138 if (!result.ok) allOk = false
139 }
141 return { allOk, results }
144/**
145 * Assert all invariants pass, throw if any fail
146 */
147export const assertInvariants = (gameState, context = '') => {
148 const { allOk, results } = checkAllInvariants(gameState)
149 if (allOk) return true
151 const failures = _.map(_.filter(_.toPairs(results),
152 ([_, r]) => !r.ok), ([name, r]) => `${name}: ${r.message}`)
153 throPtErr('invariantsFailed', {context, failures})