diff --git a/server/ai/app.ts b/server/ai/app.ts index a208b25..0f5587f 100644 --- a/server/ai/app.ts +++ b/server/ai/app.ts @@ -566,26 +566,72 @@ const processDiscard = async (_received?: any): Promise => { let mustDiscard = game.players[game.color].mustDiscard; + // Some servers may not explicitly set `mustDiscard` in the players object; + // derive it from the player's resource count when possible. According to + // rules: if a player has >7 resources they must discard floor(resources/2). + if (!mustDiscard) { + const playerResources = Number(game.players[game.color].resources || 0); + const privateTotal = types.reduce((s, t) => s + (Number(game.private && game.private[t]) || 0), 0); + const totalResources = playerResources || privateTotal; + if (totalResources > 7) { + mustDiscard = Math.floor(totalResources / 2); + console.log(`${name} - computed mustDiscard=${mustDiscard} from totalResources=${totalResources} (playerResources=${playerResources}, privateTotal=${privateTotal})`); + // Store it so subsequent logic can observe it if desired + try { + game.players[game.color].mustDiscard = mustDiscard; + } catch (e) { + // ignore if assignment fails on read-only snapshot + } + } + } + if (!mustDiscard) { return; } - const cards: string[] = []; + // Intelligent discard heuristic: + // - Compute counts for each resource type from our private hand. + // - Repeatedly discard from the type we have the most of (most-abundant first). + // - Tie-breaker: prefer to discard sheep first, then brick/wood, then wheat/stone + // (sheep is typically least critical; wheat/stone useful for cities/devs). const discards: Record = {}; - types.forEach(type => { - for (let i = 0; i < game.private[type]; i++) { - cards.push(type); - } - }); - if (cards.length === 0) { + const counts: Record = {}; + types.forEach(type => { counts[type] = Math.max(0, Number(game.private[type]) || 0); }); + + let totalCards = types.reduce((s, t) => s + (counts[t] || 0), 0); + if (totalCards === 0) { // nothing to discard (defensive) return; } - while (mustDiscard--) { - const type = cards[Math.floor(Math.random() * cards.length)] as string; - if (!(type in discards)) { - discards[type] = (discards[type] || 0) + 1; + + const tieOrder = [ 'sheep', 'brick', 'wood', 'wheat', 'stone' ]; + while (mustDiscard-- > 0 && totalCards > 0) { + // pick the resource with the highest count; tie-break using tieOrder + let bestType: string | null = null; + let bestCount = -1; + for (const t of types) { + const c = counts[t] || 0; + if (c > bestCount) { + bestCount = c; + bestType = t; + } else if (c === bestCount && c > 0 && bestType) { + // tie-breaker by tieOrder index (lower index == prefer to discard) + const curTie = tieOrder.indexOf(t); + const bestTie = tieOrder.indexOf(bestType); + if (curTie !== -1 && bestTie !== -1 && curTie < bestTie) { + bestType = t; + } } + } + + if (!bestType || (counts[bestType] || 0) <= 0) { + // no selectable resource left + break; + } + + discards[bestType] = (discards[bestType] || 0) + 1; + counts[bestType] = (counts[bestType] || 0) - 1; + totalCards--; } console.log(`discarding - `, discards); send({ @@ -775,6 +821,31 @@ const processNormal = async (received?: any): Promise => { }; } + // If the robber is active on our turn, ensure we have player information + // only when a discard might be required. Some servers omit `mustDiscard`; + // in that case request `players` only if the known resource total > 7. + if (game.turn && game.turn.robberInAction) { + if (!game.players) { + console.log(`${name} - robber in action and players missing; requesting players`); + return { players: anyValue }; + } + + const playerObj = game.players[game.color]; + const playerResources = (playerObj && typeof playerObj.resources !== 'undefined') ? Number(playerObj.resources) : undefined; + const privateTotal = types.reduce((s, t) => s + (Number(game.private && game.private[t]) || 0), 0); + const totalResources = (typeof playerResources !== 'undefined') ? playerResources : privateTotal; + + if (typeof game.players[game.color].mustDiscard === 'undefined') { + if (totalResources > 7) { + console.log(`${name} - robber in action but mustDiscard unknown and totalResources=${totalResources} > 7; requesting players`); + return { players: anyValue }; + } else { + // No discard required (totalResources <= 7); proceed with turn actions. + console.log(`${name} - robber in action but no discard required (totalResources=${totalResources}); proceeding`); + } + } + } + console.log(`${name}'s turn. Processing...`); if (!game.dice) { @@ -895,10 +966,19 @@ const message = async (data: WebSocket.Data): Promise => { switch (msg.type) { case 'warning': 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); + console.log(`WARNING: ${msg.warning}.`); + // If robber is in action or we may need to discard, do not blindly pass. + if (game.turn.robberInAction) { + console.log(`${name} - WARNING received while robber in action; ensuring players data`); + // Request players so we learn mustDiscard fields and can act accordingly. + waitingFor = { players: anyValue }; + processWaitingFor(waitingFor); + } else { + console.log(`${name} - not in robber action; passing.`); + send({ type: 'pass' }); + waitingFor = { turn: { color: game.color } }; + processWaitingFor(waitingFor); + } } break;