A table for a dozen wizards, begun by its host

Rooms take up to twelve seats and the host begins the duel with whoever
is seated; a full table begins by itself. The lobby is a roster with a
bot seat and the host's begin button. The ledger pins the turn and your
own columns and scrolls sideways to the rest; the phone bar shows every
wizard's health; a wizard who is out watches the rest; each charmed
wizard gets their own gesture; an unnamed spell goes at whoever hurt you
last, else the weakest opponent; a stab has a target from the start.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-09-23 09:34:21 -04:00
co-authored by Claude Fable 5.1
parent 79947e31ba
commit 58d7db106e
15 changed files with 299 additions and 61 deletions
+15 -11
View File
@@ -214,7 +214,8 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
return [prev.left, prev.right];
}
if (c.charmed) {
const g = c.charmedGesture ?? input(c.charmed.by).charmGesture ?? '-';
const charmer = input(c.charmed.by);
const g = c.charmedGesture ?? charmer.charmGestures?.[w.id] ?? charmer.charmGesture ?? '-';
if (w.permanent.charmed && !w.permanent.charmedGesture) w.permanent.charmedGesture = g;
if (c.charmed.hand === 'left') left = g;
else right = g;
@@ -495,7 +496,8 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
};
state.monsters.push(m);
orders.set(m.id, c.choice.monsterTarget ?? enemyOf(state, controller));
log(`A ${stats.name.toLowerCase()} answers ${W[controller].name}'s summons.`, 'spell');
const creature = stats.name.toLowerCase();
log(`${/^[aeiou]/.test(creature) ? 'An' : 'A'} ${creature} answers ${W[controller].name}'s summons.`, 'spell');
}
let fireStorm = live().some((c) => c.spell.id === 'fire_storm');
@@ -727,8 +729,9 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
const damage = new Map<TargetId, number>();
const healing = new Map<TargetId, number>();
const slain = new Set<TargetId>();
const hurt = (id: TargetId, n: number, why: string) => {
const hurt = (id: TargetId, n: number, why: string, by?: WizardId) => {
damage.set(id, (damage.get(id) ?? 0) + n);
if (isWizard(id) && by && by !== id) W[id].lastHurtBy = by;
note(id, tidy(why), -n);
log(`${beingName(state, id)} takes ${n} from ${why}.`, 'damage');
};
@@ -743,31 +746,32 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
if (isShielded(t)) {
facts.shieldBlocked.add(t);
log(`${guard(t)} turns the missile aside.`, 'fizzle');
} else hurt(t, 1, 'a missile');
} else hurt(t, 1, 'a missile', c.caster);
break;
case 'finger_of_death':
slain.add(t);
if (isWizard(t)) W[t].lastHurtBy = c.caster;
note(t, 'Finger of death', -(isWizard(t) ? W[t].hp : 0));
log(`The finger of death touches ${beingName(state, t)}.`, 'death');
break;
case 'lightning_bolt':
case 'lightning_bolt_quick':
if (isShielded(t)) facts.throughShield.push({ target: t, spell: 'a lightning bolt' });
hurt(t, 5, 'a lightning bolt');
hurt(t, 5, 'a lightning bolt', c.caster);
break;
case 'cause_light_wounds':
if (isShielded(t)) facts.throughShield.push({ target: t, spell: 'cause light wounds' });
hurt(t, 2, 'cause light wounds');
hurt(t, 2, 'cause light wounds', c.caster);
break;
case 'cause_heavy_wounds':
if (isShielded(t)) facts.throughShield.push({ target: t, spell: 'cause heavy wounds' });
hurt(t, 3, 'cause heavy wounds');
hurt(t, 3, 'cause heavy wounds', c.caster);
break;
case 'fireball':
if (resists(t, 'heat')) log(`${beingName(state, t)} shrugs off the fireball.`, 'fizzle');
else {
if (isShielded(t)) facts.throughShield.push({ target: t, spell: 'a fireball' });
hurt(t, 5, 'a fireball');
hurt(t, 5, 'a fireball', c.caster);
}
break;
case 'cure_light_wounds':
@@ -825,7 +829,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
if (isShielded(id)) continue;
if (isWizard(id) && W[id].invisibleTurns > 0) continue;
}
hurt(id, m.attack, `the ${m.name.toLowerCase()}`);
hurt(id, m.attack, `the ${m.name.toLowerCase()}`, m.owner);
}
continue;
}
@@ -842,7 +846,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
log(`${m.name} flails at the air where ${W[target].name} used to be.`, 'fizzle');
} else {
if (m.createdTurn === turn && phase === 'main') facts.newMonsterAttacked = m.name;
hurt(target, m.attack, `the ${m.name.toLowerCase()}`);
hurt(target, m.attack, `the ${m.name.toLowerCase()}`, m.owner);
}
}
@@ -859,7 +863,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
facts.shieldBlocked.add(target);
log(`${w.name}'s knife skids off ${guard(target)}.`, 'fizzle');
} else {
hurt(target, 1, `${w.name}'s knife`);
hurt(target, 1, `${w.name}'s knife`, id);
}
});
}