Credibility pass: one conflict check, one hostile list, one set of chrome
Duplicated logic and styling that had drifted apart is unified: the gesture-sharing check, the hostile spell list, the enchanted test, the plan limits, and the shared button, link, field and disabled styles. Dead plumbing is gone: the always-true implemented flag, the unread cast length, an unreachable guard, an impossible time-stop condition, the unused sequence formatter and utility class, the template placeholder. The counter-spell tests now put a counter-spell in front of a spell, the test helpers live in one file, and the resolver's sections are numbered in order. The deploy script's cache headers now do what its comment says, and the README describes the screen rather than listing features in the order they arrived. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
527820ee95
commit
5de352db43
+23
-49
@@ -5,6 +5,7 @@
|
||||
// and each named opponent weights the spell book a little differently.
|
||||
|
||||
import {
|
||||
cellsOverlap,
|
||||
completableIn,
|
||||
completedSpells,
|
||||
handSequence,
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
type TurnGestures
|
||||
} from './gestures';
|
||||
import { allowedGestures, forcedGesture } from './resolve';
|
||||
import { CONTROL_SPELLS, SPELLS, SPELL_BY_ID, isElemental, type Gesture, type Hand, type Spell, type SpellId } from './spells';
|
||||
import { GESTURES, HANDS, HOSTILE_SPELLS, MAGIC_GESTURES, SPELLS, SPELL_BY_ID, isElemental, type Gesture, type Hand, type Spell, type SpellId } from './spells';
|
||||
import {
|
||||
MAX_HP,
|
||||
enemyOf,
|
||||
isEnchanted,
|
||||
visibleHistory,
|
||||
type CastChoice,
|
||||
type GameState,
|
||||
@@ -27,27 +29,6 @@ import {
|
||||
type WizardId
|
||||
} from './state';
|
||||
|
||||
const HOSTILE: SpellId[] = [
|
||||
'missile',
|
||||
'finger_of_death',
|
||||
'lightning_bolt',
|
||||
'lightning_bolt_quick',
|
||||
'cause_light_wounds',
|
||||
'cause_heavy_wounds',
|
||||
'fireball',
|
||||
'fire_storm',
|
||||
'ice_storm',
|
||||
...CONTROL_SPELLS,
|
||||
'anti_spell',
|
||||
'disease',
|
||||
'poison',
|
||||
'blindness',
|
||||
'summon_goblin',
|
||||
'summon_ogre',
|
||||
'summon_troll',
|
||||
'summon_giant',
|
||||
'summon_elemental'
|
||||
];
|
||||
|
||||
const THREAT_VALUE: Partial<Record<SpellId, number>> = {
|
||||
missile: 1,
|
||||
@@ -122,16 +103,16 @@ function assess(state: GameState, me: WizardId, rng: () => number): Situation {
|
||||
let missileNow = false;
|
||||
let foeFirePlan = false;
|
||||
let foeIcePlan = false;
|
||||
for (const hand of ['left', 'right'] as Hand[]) {
|
||||
for (const hand of HANDS) {
|
||||
for (const c of completableIn(foeSeq[hand], foeW.sequenceStart, 1)) {
|
||||
if (!HOSTILE.includes(c.spell.id)) continue;
|
||||
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.includes(c.spell.id)) continue;
|
||||
if (!HOSTILE_SPELLS.includes(c.spell.id)) continue;
|
||||
threatNext = Math.max(threatNext, THREAT_VALUE[c.spell.id] ?? 1);
|
||||
}
|
||||
for (const spell of SPELLS) {
|
||||
@@ -166,8 +147,7 @@ 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 foeEnchanted =
|
||||
foeW.resistHeat || foeW.resistCold || foeW.protectionTurns > 0 || foeW.invisibleTurns > 0 || foeW.hasteTurns > 0;
|
||||
const foeEnchanted = isEnchanted(foeW);
|
||||
switch (spell.id) {
|
||||
case 'fireball':
|
||||
if (s.iceElemental) return 6;
|
||||
@@ -208,7 +188,7 @@ function baseValue(s: Situation, spell: Spell): number {
|
||||
return foeMonsters.some((m) => !isElemental(m.kind)) ? 3.5 : 0;
|
||||
case 'anti_spell': {
|
||||
let longest = 0;
|
||||
for (const hand of ['left', 'right'] as Hand[]) {
|
||||
for (const hand of HANDS) {
|
||||
for (const sp of SPELLS) {
|
||||
longest = Math.max(longest, progressToward(sp.tokens[0], s.foeSeq[hand], foeW.sequenceStart));
|
||||
}
|
||||
@@ -296,10 +276,9 @@ function evaluateHand(s: Situation, seq: HandTurn[], start: number, otherPlanned
|
||||
for (const spell of SPELLS) {
|
||||
const value = spellValue(s, spell);
|
||||
if (value <= 0) continue;
|
||||
spell.tokens.forEach((tokens, i) => {
|
||||
if (spell.id === 'lightning_bolt_quick' && i > 0) return;
|
||||
for (const tokens of spell.tokens) {
|
||||
const k = progressToward(tokens, seq, start);
|
||||
if (k === 0) return;
|
||||
if (k === 0) continue;
|
||||
let v = value * Math.pow(k / tokens.length, 1.3);
|
||||
if (k >= 2) v += 0.3;
|
||||
// Being one gesture from a counter-spell is worth a lot when a blow is coming next turn.
|
||||
@@ -309,14 +288,14 @@ function evaluateHand(s: Situation, seq: HandTurn[], start: number, otherPlanned
|
||||
progress = v;
|
||||
planned = spell.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return { score: best + progress * 0.9, planned };
|
||||
}
|
||||
|
||||
function bestCasts(s: Situation, completions: Record<Hand, Completion[]>, turnCount: number): CastChoice[] {
|
||||
const options: { hand: Hand; completion: Completion; value: number }[] = [];
|
||||
for (const hand of ['left', 'right'] as Hand[]) {
|
||||
for (const hand of HANDS) {
|
||||
for (const completion of completions[hand]) {
|
||||
const value = spellValue(s, completion.spell);
|
||||
if (value > 0.25) options.push({ hand, completion, value });
|
||||
@@ -327,11 +306,7 @@ function bestCasts(s: Situation, completions: Record<Hand, Completion[]>, turnCo
|
||||
for (const o of options) {
|
||||
if (chosen.some((c) => c.hand === o.hand)) continue;
|
||||
const cells = usedCells(o.completion, o.hand, turnCount);
|
||||
const clash = chosen.some((c) => {
|
||||
for (const cell of usedCells(c.completion, c.hand, turnCount)) if (cells.has(cell)) return true;
|
||||
return false;
|
||||
});
|
||||
if (clash) continue;
|
||||
if (chosen.some((c) => cellsOverlap(cells, usedCells(c.completion, c.hand, turnCount)))) continue;
|
||||
chosen.push(o);
|
||||
}
|
||||
return chosen.map((o) => toChoice(s, o.hand, o.completion));
|
||||
@@ -341,9 +316,9 @@ function foeDangerousHand(s: Situation): Hand {
|
||||
const foeW = s.state.wizards[s.foe];
|
||||
let bestHand: Hand = 'left';
|
||||
let bestK = -1;
|
||||
for (const hand of ['left', 'right'] as Hand[]) {
|
||||
for (const hand of HANDS) {
|
||||
for (const spell of SPELLS) {
|
||||
if (!HOSTILE.includes(spell.id)) continue;
|
||||
if (!HOSTILE_SPELLS.includes(spell.id)) continue;
|
||||
const k = progressToward(spell.tokens[0], s.foeSeq[hand], foeW.sequenceStart);
|
||||
if (k > bestK) {
|
||||
bestK = k;
|
||||
@@ -375,8 +350,7 @@ function targetFor(s: Situation, spell: Spell, choice: CastChoice): void {
|
||||
break;
|
||||
case 'remove_enchantment': {
|
||||
const foeW = s.state.wizards[s.foe];
|
||||
const enchanted = foeW.resistHeat || foeW.resistCold || foeW.protectionTurns > 0 || foeW.invisibleTurns > 0 || foeW.hasteTurns > 0;
|
||||
choice.target = enchanted || !strongestFoeMonster ? s.foe : strongestFoeMonster.id;
|
||||
choice.target = isEnchanted(foeW) || !strongestFoeMonster ? s.foe : strongestFoeMonster.id;
|
||||
break;
|
||||
}
|
||||
case 'summon_elemental':
|
||||
@@ -406,11 +380,11 @@ function charmGesture(s: Situation): Gesture {
|
||||
const charmedHand = foeW.constraints.charmed?.hand ?? 'left';
|
||||
let best: Gesture = 'P';
|
||||
let bestScore = Infinity;
|
||||
for (const g of ['C', 'D', 'F', 'P', 'S', 'W'] as Gesture[]) {
|
||||
for (const g of MAGIC_GESTURES) {
|
||||
const seq = [...s.foeSeq[charmedHand], { own: g, other: '-' as Gesture }];
|
||||
let score = 0;
|
||||
for (const spell of SPELLS) {
|
||||
if (!HOSTILE.includes(spell.id)) continue;
|
||||
if (!HOSTILE_SPELLS.includes(spell.id)) continue;
|
||||
score += progressToward(spell.tokens[0], seq, foeW.sequenceStart);
|
||||
}
|
||||
if (score < bestScore || (score === bestScore && s.rng() < 0.5)) {
|
||||
@@ -428,11 +402,11 @@ interface PairChoice {
|
||||
}
|
||||
|
||||
/** Pick one pair of gestures given the history so far, and what to cast with them. */
|
||||
function choosePair(s: Situation, history: TurnGestures[], rng: () => number, timeStopped: boolean): PairChoice {
|
||||
function choosePair(s: Situation, history: TurnGestures[], timeStopped: boolean): PairChoice {
|
||||
const meW = s.state.wizards[s.me];
|
||||
const foe = s.foe;
|
||||
const optionsFor = (hand: Hand): Gesture[] => {
|
||||
if (timeStopped) return ['F', 'P', 'S', 'W', 'D', 'C', '>', '-'];
|
||||
if (timeStopped) return GESTURES;
|
||||
const forced = forcedGesture(meW, hand);
|
||||
if (forced) return [forced];
|
||||
const allowed = allowedGestures(meW, hand);
|
||||
@@ -452,7 +426,7 @@ function choosePair(s: Situation, history: TurnGestures[], rng: () => number, ti
|
||||
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;
|
||||
score += rng() * 0.5;
|
||||
score += s.rng() * 0.5;
|
||||
if (!bestPair || score > bestPair.score) bestPair = { left, right, score };
|
||||
}
|
||||
}
|
||||
@@ -471,7 +445,7 @@ export function chooseBotTurn(state: GameState, me: WizardId, rng: () => number
|
||||
const foe = s.foe;
|
||||
const timeStopped = state.timeStops[0] === me;
|
||||
|
||||
const first = choosePair(s, meW.history, rng, timeStopped);
|
||||
const first = choosePair(s, meW.history, timeStopped);
|
||||
const weakFoeMonster = state.monsters.find((m) => m.owner === foe && m.hp === 1);
|
||||
const monsterOrders: Record<string, TargetId> = {};
|
||||
for (const m of state.monsters) if (m.owner === me) monsterOrders[m.id] = foe;
|
||||
@@ -484,7 +458,7 @@ export function chooseBotTurn(state: GameState, me: WizardId, rng: () => number
|
||||
monsterOrders
|
||||
};
|
||||
if (meW.hasteTurns > 0 && !timeStopped) {
|
||||
const second = choosePair(s, [...meW.history, { left: first.left, right: first.right }], rng, timeStopped);
|
||||
const second = choosePair(s, [...meW.history, { left: first.left, right: first.right }], timeStopped);
|
||||
input.second = { left: second.left, right: second.right, casts: second.casts, stabTarget: input.stabTarget };
|
||||
}
|
||||
if (meW.banked) {
|
||||
|
||||
Reference in New Issue
Block a user