AI playing better
This commit is contained in:
parent
e092bd5d01
commit
b541dd49e2
110
server/ai/app.ts
110
server/ai/app.ts
@ -566,26 +566,72 @@ const processDiscard = async (_received?: any): Promise<any> => {
|
|||||||
|
|
||||||
let mustDiscard = game.players[game.color].mustDiscard;
|
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) {
|
if (!mustDiscard) {
|
||||||
return;
|
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<string, number> = {};
|
const discards: Record<string, number> = {};
|
||||||
types.forEach(type => {
|
const counts: Record<string, number> = {};
|
||||||
for (let i = 0; i < game.private[type]; i++) {
|
types.forEach(type => { counts[type] = Math.max(0, Number(game.private[type]) || 0); });
|
||||||
cards.push(type);
|
|
||||||
}
|
let totalCards = types.reduce((s, t) => s + (counts[t] || 0), 0);
|
||||||
});
|
if (totalCards === 0) {
|
||||||
if (cards.length === 0) {
|
|
||||||
// nothing to discard (defensive)
|
// nothing to discard (defensive)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (mustDiscard--) {
|
|
||||||
const type = cards[Math.floor(Math.random() * cards.length)] as string;
|
const tieOrder = [ 'sheep', 'brick', 'wood', 'wheat', 'stone' ];
|
||||||
if (!(type in discards)) {
|
while (mustDiscard-- > 0 && totalCards > 0) {
|
||||||
discards[type] = (discards[type] || 0) + 1;
|
// 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);
|
console.log(`discarding - `, discards);
|
||||||
send({
|
send({
|
||||||
@ -775,6 +821,31 @@ const processNormal = async (received?: any): Promise<any> => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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...`);
|
console.log(`${name}'s turn. Processing...`);
|
||||||
|
|
||||||
if (!game.dice) {
|
if (!game.dice) {
|
||||||
@ -895,10 +966,19 @@ const message = async (data: WebSocket.Data): Promise<void> => {
|
|||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'warning':
|
case 'warning':
|
||||||
if (game.turn && game.turn.color === game.color && game.state !== 'lobby') {
|
if (game.turn && game.turn.color === game.color && game.state !== 'lobby') {
|
||||||
console.log(`WARNING: ${msg.warning}. Passing.`);
|
console.log(`WARNING: ${msg.warning}.`);
|
||||||
send({ type: 'pass' });
|
// If robber is in action or we may need to discard, do not blindly pass.
|
||||||
waitingFor = { turn: { color: game.color } };
|
if (game.turn.robberInAction) {
|
||||||
processWaitingFor(waitingFor);
|
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;
|
break;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user