The engine seats any number of wizards
Seats are a list on the state; a spell with no named target goes at the first opponent still standing; a wizard who dies or surrenders is out and their creatures go with them; the duel ends when one wizard stands, or when a surrender and a death coincide, as the rules decide. The bot attacks the weakest opponent and reads threats from all of them. The two-seat duel is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
cf58193aac
commit
95a1915cae
+40
-27
@@ -22,6 +22,7 @@ import {
|
||||
MAX_HP,
|
||||
enemyOf,
|
||||
isEnchanted,
|
||||
opponentsOf,
|
||||
visibleHistory,
|
||||
type CastChoice,
|
||||
type GameState,
|
||||
@@ -90,40 +91,52 @@ function hash01(text: string): number {
|
||||
return ((h >>> 0) % 1000) / 1000;
|
||||
}
|
||||
|
||||
/** The opponent to attack: the weakest still standing. */
|
||||
function pickFoe(state: GameState, me: WizardId): WizardId {
|
||||
const standing = opponentsOf(state, me);
|
||||
if (standing.length === 0) return enemyOf(state, me);
|
||||
return [...standing].sort((a, b) => state.wizards[a].hp - state.wizards[b].hp)[0];
|
||||
}
|
||||
|
||||
function assess(state: GameState, me: WizardId, rng: () => number): Situation {
|
||||
const foe = enemyOf(me);
|
||||
const foeW = state.wizards[foe];
|
||||
const hist = visibleHistory(state, me, foe);
|
||||
const foeSeq: Record<Hand, HandTurn[]> = {
|
||||
left: handSequence(hist, 'left'),
|
||||
right: handSequence(hist, 'right')
|
||||
const foe = pickFoe(state, me);
|
||||
const foes = opponentsOf(state, me);
|
||||
const seqOf = (id: WizardId): Record<Hand, HandTurn[]> => {
|
||||
const hist = visibleHistory(state, me, id);
|
||||
return { left: handSequence(hist, 'left'), right: handSequence(hist, 'right') };
|
||||
};
|
||||
const foeSeq = seqOf(foe);
|
||||
let threatNow = 0;
|
||||
let threatNowCounterable = 0;
|
||||
let threatNext = 0;
|
||||
let missileNow = false;
|
||||
let foeFirePlan = false;
|
||||
let foeIcePlan = false;
|
||||
for (const hand of HANDS) {
|
||||
for (const c of completableIn(foeSeq[hand], foeW.sequenceStart, 1)) {
|
||||
if (!HOSTILE_SPELLS.includes(c.spell.id)) continue;
|
||||
const v = THREAT_VALUE[c.spell.id] ?? 1;
|
||||
threatNow = Math.max(threatNow, v);
|
||||
if (c.spell.id !== 'finger_of_death') threatNowCounterable = Math.max(threatNowCounterable, v);
|
||||
if (c.spell.id === 'missile') missileNow = true;
|
||||
}
|
||||
for (const c of completableIn(foeSeq[hand], foeW.sequenceStart, 2)) {
|
||||
if (!HOSTILE_SPELLS.includes(c.spell.id)) continue;
|
||||
threatNext = Math.max(threatNext, THREAT_VALUE[c.spell.id] ?? 1);
|
||||
}
|
||||
for (const spell of SPELLS) {
|
||||
const k = progressToward(spell.tokens[0], foeSeq[hand], foeW.sequenceStart);
|
||||
if (k >= 2 && (spell.id === 'fireball' || spell.id === 'fire_storm')) foeFirePlan = true;
|
||||
if (k >= 2 && spell.id === 'ice_storm') foeIcePlan = true;
|
||||
// Danger comes from every opponent, not only the one being attacked.
|
||||
for (const id of foes.length ? foes : [foe]) {
|
||||
const seq = id === foe ? foeSeq : seqOf(id);
|
||||
const start = state.wizards[id].sequenceStart;
|
||||
for (const hand of HANDS) {
|
||||
for (const c of completableIn(seq[hand], start, 1)) {
|
||||
if (!HOSTILE_SPELLS.includes(c.spell.id)) continue;
|
||||
const v = THREAT_VALUE[c.spell.id] ?? 1;
|
||||
threatNow = Math.max(threatNow, v);
|
||||
if (c.spell.id !== 'finger_of_death') threatNowCounterable = Math.max(threatNowCounterable, v);
|
||||
if (c.spell.id === 'missile') missileNow = true;
|
||||
}
|
||||
for (const c of completableIn(seq[hand], start, 2)) {
|
||||
if (!HOSTILE_SPELLS.includes(c.spell.id)) continue;
|
||||
threatNext = Math.max(threatNext, THREAT_VALUE[c.spell.id] ?? 1);
|
||||
}
|
||||
for (const spell of SPELLS) {
|
||||
const k = progressToward(spell.tokens[0], seq[hand], start);
|
||||
if (k >= 2 && (spell.id === 'fireball' || spell.id === 'fire_storm')) foeFirePlan = true;
|
||||
if (k >= 2 && spell.id === 'ice_storm') foeIcePlan = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const foeMonsterDamage = state.monsters
|
||||
.filter((m) => m.owner === foe && !isElemental(m.kind))
|
||||
.filter((m) => m.owner !== me && !isElemental(m.kind))
|
||||
.reduce((sum, m) => sum + m.attack, 0);
|
||||
return {
|
||||
state,
|
||||
@@ -147,7 +160,7 @@ function assess(state: GameState, me: WizardId, rng: () => number): Situation {
|
||||
function baseValue(s: Situation, spell: Spell): number {
|
||||
const meW = s.state.wizards[s.me];
|
||||
const foeW = s.state.wizards[s.foe];
|
||||
const foeMonsters = s.state.monsters.filter((m) => m.owner === s.foe);
|
||||
const foeMonsters = s.state.monsters.filter((m) => m.owner !== s.me);
|
||||
const foeEnchanted = isEnchanted(foeW);
|
||||
switch (spell.id) {
|
||||
case 'fireball':
|
||||
@@ -394,7 +407,7 @@ function choosePair(s: Situation, history: TurnGestures[], timeStopped: boolean)
|
||||
if (left === 'P' && right === 'P') score -= 100;
|
||||
if (left === '>' && right === '>') score -= 50;
|
||||
if (left === '-' && right === '-') score -= 3;
|
||||
if (left === '>' || right === '>') score += s.state.monsters.some((m) => m.owner === foe && m.hp === 1) ? 1.5 : 0.15;
|
||||
if (left === '>' || right === '>') score += s.state.monsters.some((m) => m.owner !== s.me && m.hp === 1) ? 1.5 : 0.15;
|
||||
score += s.rng() * 0.5;
|
||||
if (!bestPair || score > bestPair.score) bestPair = { left, right, score };
|
||||
}
|
||||
@@ -415,7 +428,7 @@ export function chooseBotTurn(state: GameState, me: WizardId, rng: () => number
|
||||
const timeStopped = state.timeStops[0] === me;
|
||||
|
||||
const first = choosePair(s, meW.history, timeStopped);
|
||||
const weakFoeMonster = state.monsters.find((m) => m.owner === foe && m.hp === 1);
|
||||
const weakFoeMonster = state.monsters.find((m) => m.owner !== me && m.hp === 1);
|
||||
const monsterOrders: Record<string, TargetId> = {};
|
||||
for (const m of state.monsters) if (m.owner === me) monsterOrders[m.id] = foe;
|
||||
|
||||
@@ -440,6 +453,6 @@ export function chooseBotTurn(state: GameState, me: WizardId, rng: () => number
|
||||
input.release = { target: choice.target, monsterTarget: choice.monsterTarget };
|
||||
}
|
||||
}
|
||||
if (state.wizards[foe].constraints.charmed?.by === me) input.charmGesture = charmGesture(s);
|
||||
if (state.seats.some((id) => state.wizards[id].constraints.charmed?.by === me)) input.charmGesture = charmGesture(s);
|
||||
return input;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user