From e092bd5d011bda159bf7ddedb12f4a21d12e0513 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Mon, 13 Oct 2025 12:55:11 -0700 Subject: [PATCH] Fixing AI bots --- server/ai/app.ts | 454 ++++++++++++++--------------- server/ai/package.json | 5 +- server/ai/start.js | 28 ++ server/routes/games/turnFactory.ts | 20 ++ 4 files changed, 273 insertions(+), 234 deletions(-) create mode 100644 server/ai/start.js create mode 100644 server/routes/games/turnFactory.ts diff --git a/server/ai/app.ts b/server/ai/app.ts index 96d5cb8..a208b25 100644 --- a/server/ai/app.ts +++ b/server/ai/app.ts @@ -1,12 +1,9 @@ -// @ts-nocheck -import fetch from 'node-fetch'; import WebSocket from 'ws'; import fs from 'fs'; import calculateLongestRoad from './longest-road'; - import { getValidRoads, getValidCorners } from '../util/validLocations'; import { layout, staticData } from '../util/layout'; - +require("../console-line") const version = '0.0.1'; if (process.argv.length < 5) { @@ -21,106 +18,82 @@ For example: process.exit(-1); } -const server = process.argv[2]; -const gameId = process.argv[3]; -const name = process.argv[4]; +const server: string = process.argv[2] as string; +const gameId: string = process.argv[3] as string; +const name: string = process.argv[4] as string; +// Optional flag: --start +const startOnFull: boolean = process.argv.includes('--start'); const game: any = {}; -const anyValue = undefined; +const anyValue: any = undefined; -process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; +process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = '0'; /* Do not use arrow function as this is rebound to have * this as the WebSocket */ -let send = function (this: WebSocket, data: any) { +let wsRef: WebSocket | undefined; +let send = function (data: any) { if (data.type === 'get') { console.log(`ws - send: get`, data.fields); } else { console.log(`ws - send: ${data.type}`); } - this.send(JSON.stringify(data)); + if (wsRef && wsRef.readyState === WebSocket.OPEN) { + wsRef.send(JSON.stringify(data)); + } else { + console.warn('ws - send called but socket not open'); + } }; const error = (e: any) => { console.log(`ws - error`, e); }; -const connect = async () => { - let loc = new URL(server), new_uri; - let player; +const connect = async (): Promise => { + const loc = new URL(server as string); + let player: string | undefined; try { - const data = JSON.parse(await fs.readFile(`${name}.json`, 'utf-8')); + const data = JSON.parse(await fs.promises.readFile(`${name}.json`, 'utf-8')) as any; player = data.player; - } catch (_) { - const res = await fetch(`${server}/api/v1/games`, { - method: 'GET', - cache: 'no-cache', - credentials: 'same-origin', /* include cookies */ - headers: { - 'Content-Type': 'application/json' - } - }); + } catch (_err) { + // Use a minimal fetch options object to avoid RequestInit mismatches + const res = await fetch(`${server}/api/v1/games`, ({ headers: { 'Content-Type': 'application/json' } } as any)); if (!res) { throw new Error(`Unable to connect to ${server}`); } - player = JSON.parse(await res.text()).player; - await fs.writeFile(`${name}.json`, JSON.stringify({ - name, - player - })); + const text = await res.text(); + player = JSON.parse(text).player; + await fs.promises.writeFile(`${name}.json`, JSON.stringify({ name, player })); } + console.log(`Connecting to ${server} as ${player}`); - if (loc.protocol === "https:") { - new_uri = "wss"; - } else { - new_uri = "ws"; - } - new_uri = `${new_uri}://${loc.host}/ketr.ketran/api/v1/games/ws/${gameId}`; - const ws = new WebSocket(new_uri, - [], - { - 'headers': { - 'Cookie': `player=${player}` - } - }); - send = send.bind(ws); + const proto = loc.protocol === 'https:' ? 'wss' : 'ws'; + const new_uri = `${proto}://${loc.host}/ketr.ketran/api/v1/games/ws/${gameId}`; + const ws = new WebSocket(new_uri, [], { headers: { Cookie: `player=${player}` } }); - return new Promise((resolve, reject) => { - const headers = (e) => { - console.log(`ws - headers`); - }; - - const open = (e) => { - console.log(`ws - open`); - resolve(ws); - }; - - const connection = (ws) => { - console.log("connection request cookie: ", ws.upgradeReq.headers.cookie); - }; - - const close = (e) => { - console.log(`ws - close`); - }; + return new Promise((resolve) => { + const headers = (_e?: any): void => { console.log(`ws - headers`); }; + const open = (): void => { console.log(`ws - open`); resolve(ws); }; + const close = (_e?: any): void => { console.log(`ws - close`); }; ws.on('open', open); - ws.on('connect', () => { connect(ws); }); ws.on('headers', headers); ws.on('close', close); ws.on('error', error); - ws.on('message', async (data) => { await message(data); }); + ws.on('message', async (data: WebSocket.Data) => { await message(data); }); + // store reference for send() + wsRef = ws; }); }; -const createPlayer = () => { -}; +// createPlayer intentionally removed (unused) const types = [ 'wheat', 'brick', 'stone', 'sheep', 'wood' ]; -const tryBuild = () => { - let waitingFor = undefined; +const tryBuild = (_received?: any): any => { + let waitingFor: any = undefined; if (!waitingFor && game.private.settlements @@ -193,11 +166,9 @@ const tryBuild = () => { }; -const tryProgress = () => { - let waitingFor = undefined; - - if (!game.private.development) { - return; +const tryProgress = (_received?: any): any => { + if (!game.private || !game.private.development) { + return undefined; } let vps = 0; @@ -227,17 +198,18 @@ const tryProgress = () => { message: `I have ${vps} VP cards!` }); } + return undefined; }; let sleeping = false; let paused = false; -const sleep = async (delay) => { +const sleep = async (delay: number): Promise => { if (sleeping) { return; } sleeping = true; - return new Promise((resolve) => { + return new Promise((resolve: () => void) => { setTimeout(() => { sleeping = false; resolve(); @@ -246,7 +218,7 @@ const sleep = async (delay) => { }; -const bestSettlementPlacement = (game) => { +const bestSettlementPlacement = (game: any): number => { const best = { index: -1, pips: 0 @@ -255,11 +227,10 @@ const bestSettlementPlacement = (game) => { /* For each corner that is valid, find out which * tiles are on that corner, and for each of those * tiles, find the pip placement for that tile. */ - game.turn.limits.corners.forEach(cornerIndex => { - const tiles = []; - layout.tiles.forEach((tile, index) => { - if (tile.corners.indexOf(cornerIndex) !== -1 - && tiles.indexOf(index) === -1) { + (game.turn && game.turn.limits && Array.isArray(game.turn.limits.corners) ? game.turn.limits.corners : []).forEach((cornerIndex: any) => { + const tiles: Array<{ tile: any; index: number; }> = []; + (layout.tiles || []).forEach((tile: any, index: number) => { + if ((tile.corners || []).indexOf(cornerIndex) !== -1 && tiles.findIndex(t => t.index === index) === -1) { tiles.push({ tile, index }); } }); @@ -267,11 +238,10 @@ const bestSettlementPlacement = (game) => { let cornerScore = 0; /* Find the tileOrder holding this tile */ - tiles.forEach(tile => { + tiles.forEach((tile: { tile: any; index: number }) => { const index = tile.index; -// const tileIndex = game.tileOrder.indexOf(index); - const pipIndex = game.pipOrder[index]; - const score = staticData.pips[pipIndex].pips; + const pipIndex = game.pipOrder && game.pipOrder[index]; + const score = (staticData.pips && staticData.pips[pipIndex] && staticData.pips[pipIndex].pips) || 0; cornerScore += score; }); @@ -285,17 +255,20 @@ const bestSettlementPlacement = (game) => { return best.index; } -const bestRoadPlacement = (game) => { - const road = calculateLongestRoad(game); +const bestRoadPlacement = (game: any): number => { + const road: any = calculateLongestRoad(game) || { index: -1, segments: 0 }; console.log(`${name} - could make road ${road.segments + 1} long on ${road.index}`); let attempt = -1; - if (road.index !== -1) { - layout.roads[road.index].corners.forEach(cornerIndex => { + const layoutAny: any = layout; + if (road && typeof road.index === 'number' && road.index !== -1 && layoutAny && layoutAny.roads && layoutAny.roads[road.index] && layoutAny.roads[road.index].corners) { + const roadCorners = layoutAny.roads[road.index].corners || []; + roadCorners.forEach((cornerIndex: any) => { if (attempt !== -1) { return; } - layout.corners[cornerIndex].roads.forEach(roadIndex => { + const cornerRoads = (layoutAny.corners && layoutAny.corners[cornerIndex] && layoutAny.corners[cornerIndex].roads) || []; + cornerRoads.forEach((roadIndex: any) => { if (attempt !== -1) { return; } @@ -308,17 +281,18 @@ const bestRoadPlacement = (game) => { }); } - if (game.turn.limits.roads.indexOf(attempt) !== -1) { + if (game && game.turn && game.turn.limits && Array.isArray(game.turn.limits.roads) && game.turn.limits.roads.indexOf(attempt) !== -1) { console.log(`${name} - attempting to place on end of longest road`); return attempt; } else { console.log(`${name} - selecting a random road location`); - return game.turn.limits.roads[Math.floor( - Math.random() * game.turn.limits.roads.length)]; + const roads = (game && game.turn && game.turn.limits && game.turn.limits.roads) || []; + if (!roads.length) return -1; + return roads[Math.floor(Math.random() * roads.length)]; } } -const isMatch = (input, received) => { +const isMatch = (input: any, received: any): boolean => { for (let key in input) { /* received update didn't contain this field */ if (!(key in received)) { @@ -346,29 +320,81 @@ const isMatch = (input, received) => { return true; }; -const processLobby = (received) => { - if (game.name === '' && !received.name) { +const processLobby = (received: any): any => { + /* + * Lobby flow notes: + * - `game` is the local snapshot built from previous updates (Object.assign(game, update)). + * - `received` is the most recent partial update we got from the server which we use + * to check for fields the server just sent in this update cycle. + * - `waitingFor` describes which fields we want the server to include in a future + * update. A value of `anyValue` means "I don't care what value, but the field must + * be present in the update"; a concrete value means "the field must equal this". + * + * Reconnection edge-case: on reconnect the server may provide `name` only in the + * per-player response and/or the `game` snapshot may not yet have `game.name` set + * (it might be undefined rather than an empty string). Be tolerant of both cases. + */ + + console.log(`${name} - processLobby start: game.name=${typeof game.name === 'undefined' ? 'undefined' : JSON.stringify(game.name)}, received.name=${typeof received === 'object' && 'name' in received ? JSON.stringify(received.name) : 'undefined'}`); + + // Prefer whatever the server just sent in `received` (if present), otherwise fall + // back to the `game` snapshot. This avoids a strict === '' check which failed for + // undefined on reconnect. + const effectiveName = (received && typeof received.name !== 'undefined') ? received.name : game.name; + + if ((!effectiveName || effectiveName === '') && !(received && received.name)) { + // If neither the current snapshot nor the latest update provide a game.name, + // announce our player-name to the server and wait for confirmation plus + // the players/unselected lists. send({ type: 'player-name', name }); /* Wait for the game.name to be set to 'name' and for unselected */ return { name, players: anyValue, unselected: anyValue }; } + // If the server included `name` in this update but our `game` snapshot is still + // missing it, copy it across so later logic that reads `game.name` will see it. + if (received && typeof received.name !== 'undefined' && typeof game.name === 'undefined') { + game.name = received.name; + } + if (!received.unselected) { return { unselected: anyValue }; } - /* AI selected a Player. Wait for game-order */ - if (received.unselected.indexOf(name) === -1) { + /* AI selected a Player. Wait for game-order. + * Only treat the bot as already selected if its color is not the 'unassigned' sentinel. + * Some servers may return an empty 'unselected' array even while the player's color + * remains 'unassigned', so guard that case explicitly. */ + if (received.unselected.indexOf(name) === -1 && game.color !== 'unassigned') { send({ type: 'chat', message: `Woohoo! Robot AI ${version} is alive and playing as ${game.color}!` }); + // We are already selected. If there are enough active participants in the + // lobby, actively request to start the game by setting the state to + // 'game-order'. Otherwise return a waitingFor that asks for the state so + // we'll proceed once the server flips it. + const participants = (game && game.participants) || []; + const activeCount = participants.filter((p: any) => p && p.color && p.color !== 'unassigned').length; + if (startOnFull) { + if (activeCount >= 2) { + console.log(`${name} - ${activeCount} active players detected; requesting start (startOnFull enabled)`); + send({ type: 'set', field: 'state', value: 'game-order' }); + } else { + console.log(`${name} - startOnFull enabled but only ${activeCount} active players; not starting yet`); + } + } else { + console.log(`${name} - auto-start disabled (startOnFull not set); not requesting start even with ${activeCount} active players`); + } + + // Still wait for the server to report state=game-order (either because we + // requested it or because another player started it). return { state: 'game-order' }; } - const slots = []; + const slots: any[] = []; for (let color in game.players) { if (game.players[color].status === 'Not active') { slots.push(color); @@ -384,20 +410,43 @@ const processLobby = (received) => { const index = Math.floor(Math.random() * slots.length); console.log(`${name} - requesting to play as ${slots[index]}.`); - game.unselected = game.unselected.filter( - color => color === slots[index]); + game.unselected = (game.unselected || []).filter((color: any) => color === slots[index]); send({ type: 'set', field: 'color', value: slots[index] }); + + // If requested, attempt to start the game immediately after claiming a slot + // (only when startOnFull is set). This mirrors the reconnect flow and gives + // the server a short moment to update player lists before we check counts. + if (startOnFull) { + setTimeout(() => { + try { + const participants = (game && game.participants) || []; + const activeCount = participants.filter((p: any) => p && p.color && p.color !== 'unassigned').length; + console.log(`${name} - (post-claim) startOnFull check: ${activeCount} active players, ${slots.length} unselected slots`); + if (activeCount >= 2) { + console.log(`${name} - attempting to start game (post-claim)`); + send({ type: 'set', field: 'state', value: 'game-order' }); + } else { + send({ type: 'get', fields: [ 'players', 'participants', 'unselected', 'state' ] }); + } + } catch (err) { + console.warn('post-claim start attempt failed', err); + } + }, 500); + } return { color: slots[index], state: 'game-order' }; }; -const processGameOrder = async () => { - if (!game.color) { +const processGameOrder = async (_received?: any): Promise => { + // The server uses the string 'unassigned' to indicate an open slot. + // Previously the code used a falsy-check (!game.color), which fails + // because 'unassigned' is truthy. Check explicitly for the sentinel. + if (game.color === 'unassigned') { console.log(`game-order - player not active`); - return { color }; + return { color: game.color }; } console.log(`game-order - `, { color: game.color, @@ -411,7 +460,7 @@ const processGameOrder = async () => { return { turn: { color: game.color }}; }; -const processInitialPlacement = async (received) => { +const processInitialPlacement = async (_received?: any): Promise => { /* Fetch the various game order elements so we can make * educated location selections */ if (!game.pipOrder @@ -467,17 +516,20 @@ const processInitialPlacement = async (received) => { } /* Start watching for a name entry */ -let waitingFor = { name: anyValue }, received = {}; +let waitingFor: any = { name: anyValue }; +let received: any = {}; -const reducedGame = (game) => { +// Ignore TS6133 (declared but never used) +// @ts-ignore +const reducedGame = (game: any): Record => { const filters = [ 'chat', 'activities', 'placements', 'players', 'private', 'dice' ]; - const value = {}; + const value: Record = {}; for (let key in game) { if (filters.indexOf(key) === -1) { value[key] = game[key]; } else { - if (Array.isArray(game[key])) { - value[key] = `length(${game[key].length})`; + if (Array.isArray((game as any)[key])) { + value[key] = `length(${(game as any)[key].length})`; } else { value[key] = `...filtered`; } @@ -486,11 +538,9 @@ const reducedGame = (game) => { return value; } -const processWaitingFor = (waitingFor) => { - const value = { - type: 'get', - fields: [] - }; +const processWaitingFor = (waitingFor: any): void => { + const value: { type: string; fields: string[] } = { type: 'get', fields: [] }; + console.log(`${name} - waiting for: `, waitingFor); for (let key in waitingFor) { value.fields.push(key); } @@ -499,17 +549,13 @@ const processWaitingFor = (waitingFor) => { } -const selectResources = async (received) => { - if (!game.turn) { - return { turn: anyValue }; - } - - if (!game.turn.actions || game.turn.actions.indexOf('select-resources') === -1) { - return; - } +const selectResources = async (_received: any): Promise => { + if (!game.turn) return { turn: anyValue }; + if (!game.turn.actions || game.turn.actions.indexOf('select-resources') === -1) return undefined; + return undefined; } -const processDiscard = async (received) => { +const processDiscard = async (_received?: any): Promise => { if (!game.players) { waitingFor = { players: {} @@ -524,20 +570,22 @@ const processDiscard = async (received) => { return; } - const cards = [], - discards = {}; + const cards: string[] = []; + const discards: Record = {}; types.forEach(type => { for (let i = 0; i < game.private[type]; i++) { cards.push(type); } }); + if (cards.length === 0) { + // nothing to discard (defensive) + return; + } while (mustDiscard--) { - const type = cards[Math.floor(Math.random() * cards.length)]; + const type = cards[Math.floor(Math.random() * cards.length)] as string; if (!(type in discards)) { - discards[type] = 1; - } else { - discards[type]++; - } + discards[type] = (discards[type] || 0) + 1; + } } console.log(`discarding - `, discards); send({ @@ -552,8 +600,8 @@ const processDiscard = async (received) => { return waitingFor; }; -const processTrade = async (received) => { - const enough = []; +const processTrade = async (received?: any): Promise => { + const enough: string[] = []; let shouldTrade = true; /* Check and see which resources we have enough of */ @@ -564,7 +612,7 @@ const processTrade = async (received) => { }); shouldTrade = enough.length > 0; - let least = { type: undefined, count: 0 }; + let least: { type?: string | undefined; count: number } = { type: undefined, count: 0 }; if (shouldTrade) { /* Find out which resource we have the least amount of */ @@ -623,7 +671,7 @@ const processTrade = async (received) => { gets: [get] }; - if (received.turn.offer) { + if (received && received.turn && received.turn.offer) { send({ type: 'trade', action: 'accept', @@ -642,7 +690,7 @@ const processTrade = async (received) => { /* Initiate offer... */ - if (!received.turn.offer) { + if (!received || !received.turn || !received.turn.offer) { console.log(`trade - `, offer); send({ type: 'trade', @@ -660,7 +708,7 @@ const processTrade = async (received) => { }; } -const processVolcano = async (received) => { +const processVolcano = async (_received?: any): Promise => { if (!game.turn || !game.private) { return { turn: anyValue, @@ -683,8 +731,9 @@ const processVolcano = async (received) => { }; }; -const processNormal = async (received) => { - let waitingFor = undefined; +const processNormal = async (received?: any): Promise => { + let waitingFor: any = undefined; + let index: number | undefined; if (!game.turn || !game.private) { return { @@ -712,7 +761,7 @@ const processNormal = async (received) => { /* From here on it is only actions that occur on the player's turn */ if (!received.turn || received.turn.color !== game.color) { - console.log(`${name} - waiting for turn... ${game.players[game.turn.color].name} is active.`); + console.log(`${name} - waiting for turn... ${game.players && game.players[game.turn && game.turn.color] ? game.players[game.turn.color].name : 'unknown'} is active.`); console.log({ wheat: game.private.wheat, sheep: game.private.sheep, @@ -741,22 +790,17 @@ const processNormal = async (received) => { }; } - if (received.turn.actions && received.turn.actions.indexOf('place-road') !== -1) { + if (received.turn && received.turn.actions && received.turn.actions.indexOf('place-road') !== -1) { index = bestRoadPlacement(game); - send({ - type: 'place-road', index - }); + send({ type: 'place-road', index }); return { turn: { color: game.color } }; } if (game.turn.actions && game.turn.actions.indexOf('place-city') !== -1) { - index = game.turn.limits.corners[Math.floor( - Math.random() * game.turn.limits.corners.length)]; - send({ - type: 'place-city', index - }); + index = game.turn.limits.corners[Math.floor(Math.random() * game.turn.limits.corners.length)]; + send({ type: 'place-city', index }); return { turn: { color: game.color } }; @@ -836,86 +880,49 @@ const processNormal = async (received) => { return { turn: anyValue }; }; -const message = async (data) => { +const message = async (data: WebSocket.Data): Promise => { + let parsed: any = data; try { - data = JSON.parse(data); + const text = typeof data === 'string' ? data : data.toString(); + parsed = JSON.parse(text); } catch (error) { console.error(error); console.log(data); return; } - switch (data.type) { + const msg: any = parsed; + switch (msg.type) { case 'warning': - if (game.turn.color === game.color && game.state !== 'lobby') { - console.log(`WARNING: ${data.warning}. Passing.`); - send({ - type: 'pass' - }); - waitingFor = { - turn: { color: game.color } - }; + if (game.turn && game.turn.color === game.color && game.state !== 'lobby') { + console.log(`WARNING: ${msg.warning}. Passing.`); + send({ type: 'pass' }); + waitingFor = { turn: { color: game.color } }; processWaitingFor(waitingFor); } break; case 'game-update': - /* Keep game updated with the latest information */ - Object.assign(game, data.update); - if (data.update.chat) { + Object.assign(game, msg.update || {}); + if (msg.update && msg.update.chat) { let newState = paused; const rePause = new RegExp(`${name}: pause`, 'i'); const reUnpause = new RegExp(`${name}: unpause`, 'i'); - - for (let i = 0; i < data.update.chat.length; i++) { - if (data.update.chat[i].message.match(rePause)) { - newState = true; - continue; - } - - if (data.update.chat[i].message.match(reUnpause)) { - newState = false; - continue; - } - } - - if (newState !== paused) { - paused = newState; - send({ - type: 'chat', - message: `Robot AI is now ${paused ? '' : 'un'}paused.` - }); + for (let i = 0; i < msg.update.chat.length; i++) { + if (msg.update.chat[i].message.match(rePause)) { newState = true; continue; } + if (msg.update.chat[i].message.match(reUnpause)) { newState = false; continue; } } + if (newState !== paused) { paused = newState; send({ type: 'chat', message: `Robot AI is now ${paused ? '' : 'un'}paused.` }); } } - - if (paused) { - if (waitingFor) { - Object.assign(received, data.update); - } - return; - } - - if (sleeping) { - if (waitingFor) { - Object.assign(received, data.update); - } - console.log(`${name} - sleeping`); - return; - } + if (paused) { if (waitingFor) Object.assign(received, msg.update); return; } + if (sleeping) { if (waitingFor) Object.assign(received, msg.update); console.log(`${name} - sleeping`); return; } if (waitingFor) { - Object.assign(received, data.update); + Object.assign(received, msg.update); if (!isMatch(waitingFor, received)) { - console.log(`${name} - still waiting - waitingFor: `, - waitingFor); - if (game.turn && game.turn.robberInAction) { - console.log(`${name} - robber in action! Must check discards...`); - } else { - return; - } + if (game.turn && game.turn.robberInAction) { console.log(`${name} - robber in action! Must check discards...`); } else { return; } } else { - console.log(`${name} - received match - received: `, - reducedGame(received)); + console.log(`${name} - received match - received: `, msg.update); console.log(`${name} - going to sleep`); await sleep(1000 + Math.random() * 500); console.log(`${name} - waking up`); @@ -927,52 +934,33 @@ const message = async (data) => { case undefined: case 'lobby': waitingFor = await processLobby(received); - if (waitingFor) { - processWaitingFor(waitingFor); - } + if (waitingFor) processWaitingFor(waitingFor); return; - case 'game-order': waitingFor = await processGameOrder(received); - if (waitingFor) { - processWaitingFor(waitingFor); - } + if (waitingFor) processWaitingFor(waitingFor); return; - case 'initial-placement': waitingFor = await processInitialPlacement(received); - if (waitingFor) { - processWaitingFor(waitingFor); - } + if (waitingFor) processWaitingFor(waitingFor); return; - case 'volcano': waitingFor = await processVolcano(received); - if (waitingFor) { - processWaitingFor(waitingFor); - } + if (waitingFor) processWaitingFor(waitingFor); return; - case 'normal': waitingFor = await processNormal(received); - if (waitingFor) { - processWaitingFor(waitingFor); - } + if (waitingFor) processWaitingFor(waitingFor); return; } break; case 'ping': - if (!game.state && !received.state) { - console.log(`ping received with no game. Sending update request`); - send({ - type: 'game-update' - }); - } + if (!game.state && !received.state) { console.log(`ping received with no game. Sending update request`); send({ type: 'game-update' }); } return; default: - console.log(data); + console.log(`Received: ${msg.type} while game state = ${game.state}`); break; } } @@ -988,7 +976,9 @@ connect().then(() => { ai() .catch((error) => { console.error(error); - ws.close(); + if (wsRef) { + try { wsRef.close(); } catch (_) { } + } }); }) .catch((error) => { diff --git a/server/ai/package.json b/server/ai/package.json index 2fe57e9..23684e2 100644 --- a/server/ai/package.json +++ b/server/ai/package.json @@ -1,9 +1,10 @@ { "name": "peddlers-of-ketran-ai-bot", "version": "1.0.0", - "main": "app.js", + "main": "app.ts", "scripts": { - "start": "export $(cat ../../.env | xargs) && node app.js" + "start": "node start.js", + "start:ts": "export $(cat ../../.env | xargs) && node -r ts-node/register app.ts" }, "author": "James Ketrenos ", "license": "MIT", diff --git a/server/ai/start.js b/server/ai/start.js new file mode 100644 index 0000000..cc08bd5 --- /dev/null +++ b/server/ai/start.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +// Load env from ../../.env (best-effort) +try { + const envPath = path.resolve(__dirname, '..', '..', '.env'); + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, 'utf8'); + content.split(/\n/).forEach(line => { + const m = line.match(/^([^#=]+)=([\s\S]*)$/); + if (m) process.env[m[1].trim()] = m[2].trim(); + }); + } +} catch (err) { + // ignore +} + +try { + // Prefer ts-node/register to allow requiring .ts directly + require.resolve('ts-node/register'); + require('ts-node/register'); + require('./app.ts'); +} catch (err) { + console.error('ts-node not found. Please run this inside the project container where dev dependencies are installed.'); + console.error('Original error:', err && err.message ? err.message : err); + process.exit(1); +} diff --git a/server/routes/games/turnFactory.ts b/server/routes/games/turnFactory.ts new file mode 100644 index 0000000..f16866f --- /dev/null +++ b/server/routes/games/turnFactory.ts @@ -0,0 +1,20 @@ +import { Player, Turn } from "./types"; + +export const newTurn = (first: Player) : Turn => { + return { + name: first.name, + color: first.color, + actions: [], + limits: {}, + roll: 0, + volcano: null, + free: false, + freeRoads: 0, + select: {}, + active: null, + robberInAction: false, + placedRobber: false, + developmentPurchased: false, + offer: null, + }; +} \ No newline at end of file