The target dropdown starts on the likeliest subject
One module decides where a spell most likely goes given the field (a fireball at an ice elemental, charm monster at their strongest creature, remove enchantment at the wizard only if enchanted); the bot and the dropdown both use it. The dropdown lists that subject first and marks creatures by whose they are. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
dd6ff93606
commit
81acbd2172
+4
-35
@@ -16,6 +16,7 @@ import {
|
|||||||
type TurnGestures
|
type TurnGestures
|
||||||
} from './gestures';
|
} from './gestures';
|
||||||
import { allowedGestures, forcedGesture } from './resolve';
|
import { allowedGestures, forcedGesture } from './resolve';
|
||||||
|
import { likelyTarget } from './targets';
|
||||||
import { GESTURES, HANDS, HOSTILE_SPELLS, MAGIC_GESTURES, 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 {
|
import {
|
||||||
MAX_HP,
|
MAX_HP,
|
||||||
@@ -330,41 +331,9 @@ function foeDangerousHand(s: Situation): Hand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function targetFor(s: Situation, spell: Spell, choice: CastChoice): void {
|
function targetFor(s: Situation, spell: Spell, choice: CastChoice): void {
|
||||||
const meW = s.state.wizards[s.me];
|
choice.target = likelyTarget(s.state, s.me, spell);
|
||||||
const foeMonsters = s.state.monsters.filter((m) => m.owner === s.foe);
|
if (spell.id === 'summon_elemental') choice.elemental = s.state.wizards[s.me].resistCold ? 'ice' : 'fire';
|
||||||
const strongestFoeMonster = [...foeMonsters].sort((a, b) => b.attack - a.attack)[0];
|
if (spell.id === 'paralysis' || spell.id === 'charm_person') choice.chosenHand = foeDangerousHand(s);
|
||||||
const iceEl = s.state.monsters.find((m) => m.kind === 'ice_elemental');
|
|
||||||
const fireEl = s.state.monsters.find((m) => m.kind === 'fire_elemental');
|
|
||||||
switch (spell.id) {
|
|
||||||
case 'fireball':
|
|
||||||
choice.target = iceEl ? iceEl.id : s.foe;
|
|
||||||
break;
|
|
||||||
case 'resist_cold':
|
|
||||||
choice.target = iceEl ? iceEl.id : s.me;
|
|
||||||
break;
|
|
||||||
case 'resist_heat':
|
|
||||||
choice.target = fireEl ? fireEl.id : s.me;
|
|
||||||
break;
|
|
||||||
case 'charm_monster':
|
|
||||||
choice.target = strongestFoeMonster?.id ?? s.foe;
|
|
||||||
break;
|
|
||||||
case 'remove_enchantment': {
|
|
||||||
const foeW = s.state.wizards[s.foe];
|
|
||||||
choice.target = isEnchanted(foeW) || !strongestFoeMonster ? s.foe : strongestFoeMonster.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'summon_elemental':
|
|
||||||
choice.target = s.me;
|
|
||||||
choice.elemental = meW.resistCold ? 'ice' : 'fire';
|
|
||||||
break;
|
|
||||||
case 'paralysis':
|
|
||||||
case 'charm_person':
|
|
||||||
choice.target = s.foe;
|
|
||||||
choice.chosenHand = foeDangerousHand(s);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
choice.target = spell.usualTarget === 'self' ? s.me : s.foe;
|
|
||||||
}
|
|
||||||
if (spell.category === 'summons') choice.monsterTarget = s.foe;
|
if (spell.category === 'summons') choice.monsterTarget = s.foe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from './gestures';
|
} from './gestures';
|
||||||
import { glyph } from './glyphs';
|
import { glyph } from './glyphs';
|
||||||
import { allowedGestures, forcedGesture, resolveTurn } from './resolve';
|
import { allowedGestures, forcedGesture, resolveTurn } from './resolve';
|
||||||
|
import { likelyTarget } from './targets';
|
||||||
import {
|
import {
|
||||||
GESTURES,
|
GESTURES,
|
||||||
HANDS,
|
HANDS,
|
||||||
@@ -72,11 +73,6 @@ export interface PlannedCast {
|
|||||||
turnCount: number;
|
turnCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultTarget(spell: Spell | undefined): TargetId | '' {
|
|
||||||
if (!spell) return '';
|
|
||||||
return spell.usualTarget === 'self' ? YOU : FOE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function blankDraft(): HandDraft {
|
function blankDraft(): HandDraft {
|
||||||
return { gesture: null, spellId: '', target: '', elemental: 'fire', chosenHand: 'left' };
|
return { gesture: null, spellId: '', target: '', elemental: 'fire', chosenHand: 'left' };
|
||||||
}
|
}
|
||||||
@@ -541,12 +537,22 @@ export class Duel {
|
|||||||
);
|
);
|
||||||
bankedSpell = $derived(this.you.banked ? SPELL_BY_ID[this.you.banked.spellId] : undefined);
|
bankedSpell = $derived(this.you.banked ? SPELL_BY_ID[this.you.banked.spellId] : undefined);
|
||||||
|
|
||||||
|
/** Where a spell goes unless the player says otherwise. */
|
||||||
|
defaultTarget(spell: Spell | undefined): TargetId | '' {
|
||||||
|
return spell ? likelyTarget(this.state, YOU, spell) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every possible subject, the likeliest first, creatures marked by whose they are. */
|
||||||
targetsFor(spell: Spell | undefined): { id: TargetId; label: string }[] {
|
targetsFor(spell: Spell | undefined): { id: TargetId; label: string }[] {
|
||||||
const list: { id: TargetId; label: string }[] = [
|
const list: { id: TargetId; label: string }[] = [
|
||||||
{ id: YOU, label: 'yourself' },
|
{ id: YOU, label: 'yourself' },
|
||||||
{ id: FOE, label: this.foe.name }
|
{ id: FOE, label: this.foe.name }
|
||||||
];
|
];
|
||||||
for (const m of this.state.monsters) list.push({ id: m.id, label: m.name });
|
for (const m of this.state.monsters) {
|
||||||
|
list.push({ id: m.id, label: `${m.name} (${m.owner === YOU ? 'yours' : `${this.foe.name}'s`})` });
|
||||||
|
}
|
||||||
|
const likely = this.defaultTarget(spell);
|
||||||
|
list.sort((a, b) => Number(b.id === likely) - Number(a.id === likely));
|
||||||
if (spell && spell.id !== 'fire_storm' && spell.id !== 'ice_storm') list.push({ id: NOWHERE, label: 'nowhere (let it fade)' });
|
if (spell && spell.id !== 'fire_storm' && spell.id !== 'ice_storm') list.push({ id: NOWHERE, label: 'nowhere (let it fade)' });
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -563,13 +569,13 @@ export class Duel {
|
|||||||
const options = this.completions[set][hand];
|
const options = this.completions[set][hand];
|
||||||
if (!options.some((c) => c.spell.id === draft.spellId)) {
|
if (!options.some((c) => c.spell.id === draft.spellId)) {
|
||||||
draft.spellId = options[0]?.spell.id ?? '';
|
draft.spellId = options[0]?.spell.id ?? '';
|
||||||
draft.target = defaultTarget(options[0]?.spell);
|
draft.target = this.defaultTarget(options[0]?.spell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpell(set: SetName, hand: Hand, id: SpellId | ''): void {
|
setSpell(set: SetName, hand: Hand, id: SpellId | ''): void {
|
||||||
this.sets[set][hand].spellId = id;
|
this.sets[set][hand].spellId = id;
|
||||||
this.sets[set][hand].target = defaultTarget(id ? SPELL_BY_ID[id] : undefined);
|
this.sets[set][hand].target = this.defaultTarget(id ? SPELL_BY_ID[id] : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private castFor(p: PlannedCast): CastChoice {
|
private castFor(p: PlannedCast): CastChoice {
|
||||||
@@ -578,7 +584,7 @@ export class Duel {
|
|||||||
hand: p.hand,
|
hand: p.hand,
|
||||||
spellId: spell.id,
|
spellId: spell.id,
|
||||||
seqIndex: p.completion.seqIndex,
|
seqIndex: p.completion.seqIndex,
|
||||||
target: p.draft.target || defaultTarget(spell)
|
target: p.draft.target || this.defaultTarget(spell)
|
||||||
};
|
};
|
||||||
if (spell.id === 'summon_elemental') cast.elemental = p.draft.elemental;
|
if (spell.id === 'summon_elemental') cast.elemental = p.draft.elemental;
|
||||||
if (spell.id === 'paralysis' || spell.id === 'charm_person') cast.chosenHand = p.draft.chosenHand;
|
if (spell.id === 'paralysis' || spell.id === 'charm_person') cast.chosenHand = p.draft.chosenHand;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { SPELL_BY_ID } from './spells';
|
||||||
|
import { createGame } from './state';
|
||||||
|
import { likelyTarget } from './targets';
|
||||||
|
import { runSequence } from './test-helpers';
|
||||||
|
|
||||||
|
describe('the likeliest target', () => {
|
||||||
|
it('sends a fireball and resist cold at an ice elemental when one is out', () => {
|
||||||
|
let s = createGame({ A: 'Black', B: 'White' }, 1);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.fireball)).toBe('B');
|
||||||
|
s = runSequence(s, 'B', 'CSWWS', 'C----', { casts: [{ hand: 'left', spellId: 'summon_elemental', elemental: 'ice' }] });
|
||||||
|
const elemental = s.monsters.find((m) => m.kind === 'ice_elemental')!;
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.fireball)).toBe(elemental.id);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.resist_cold)).toBe(elemental.id);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.resist_heat)).toBe('A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aims remove enchantment at the wizard only when they carry an enchantment', () => {
|
||||||
|
let s = createGame({ A: 'Black', B: 'White' }, 1);
|
||||||
|
s = runSequence(s, 'B', 'SFW', '---', { casts: [{ hand: 'left', spellId: 'summon_goblin' }] });
|
||||||
|
const goblin = s.monsters[0];
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.remove_enchantment)).toBe(goblin.id);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.charm_monster)).toBe(goblin.id);
|
||||||
|
s = runSequence(s, 'B', 'WWFP', '----', { casts: [{ hand: 'left', spellId: 'resist_heat' }] });
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.remove_enchantment)).toBe('B');
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.counter_spell)).toBe('A');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Where a spell most likely goes, given what is on the field. Both the bot and
|
||||||
|
// the target dropdown start from this; the player can always aim elsewhere.
|
||||||
|
|
||||||
|
import { isElemental, type Spell } from './spells';
|
||||||
|
import { enemyOf, isEnchanted, type GameState, type TargetId, type WizardId } from './state';
|
||||||
|
|
||||||
|
export function likelyTarget(state: GameState, me: WizardId, spell: Spell): TargetId {
|
||||||
|
const foe = enemyOf(me);
|
||||||
|
const monsters = state.monsters;
|
||||||
|
const foeMonsters = monsters.filter((m) => m.owner === foe && !isElemental(m.kind));
|
||||||
|
const strongestFoeMonster = [...foeMonsters].sort((a, b) => b.attack - a.attack)[0];
|
||||||
|
const iceElemental = monsters.find((m) => m.kind === 'ice_elemental');
|
||||||
|
const fireElemental = monsters.find((m) => m.kind === 'fire_elemental');
|
||||||
|
switch (spell.id) {
|
||||||
|
case 'fireball':
|
||||||
|
return iceElemental?.id ?? foe;
|
||||||
|
case 'resist_cold':
|
||||||
|
return iceElemental?.id ?? me;
|
||||||
|
case 'resist_heat':
|
||||||
|
return fireElemental?.id ?? me;
|
||||||
|
case 'charm_monster':
|
||||||
|
return strongestFoeMonster?.id ?? foe;
|
||||||
|
case 'remove_enchantment':
|
||||||
|
return isEnchanted(state.wizards[foe]) || !strongestFoeMonster ? foe : strongestFoeMonster.id;
|
||||||
|
default:
|
||||||
|
return spell.usualTarget === 'self' ? me : foe;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user