// The spell book for Waving Hands (Richard Bartle, 1977), as transcribed in // docs/waving-hands-rules.txt. Gesture notation: F fingers, P palm, S snap, // W wave, D digit point, C clap (always both hands). A lower-case letter in // parentheses, e.g. "(w", means both hands make that gesture at once. export type Gesture = 'F' | 'P' | 'S' | 'W' | 'D' | 'C' | '>' | '-'; export type MagicGesture = 'F' | 'P' | 'S' | 'W' | 'D' | 'C'; export type Hand = 'left' | 'right'; export const HANDS: Hand[] = ['left', 'right']; export const GESTURES: Gesture[] = ['F', 'P', 'S', 'W', 'D', 'C', '>', '-']; export const MAGIC_GESTURES: MagicGesture[] = ['F', 'P', 'S', 'W', 'D', 'C']; export const GESTURE_NAMES: Record = { F: 'wriggled fingers', P: 'proffered palm', S: 'snap', W: 'wave', D: 'pointed digit', C: 'clap', '>': 'stab', '-': 'nothing' }; export interface Token { gesture: MagicGesture; /** Both hands must make this gesture on the same turn. */ both: boolean; } export type SpellCategory = 'protection' | 'summons' | 'damage' | 'enchantment'; export type SpellId = | 'shield' | 'remove_enchantment' | 'magic_mirror' | 'counter_spell' | 'dispel_magic' | 'raise_dead' | 'cure_light_wounds' | 'cure_heavy_wounds' | 'summon_goblin' | 'summon_ogre' | 'summon_troll' | 'summon_giant' | 'summon_elemental' | 'missile' | 'finger_of_death' | 'lightning_bolt' | 'lightning_bolt_quick' | 'cause_light_wounds' | 'cause_heavy_wounds' | 'fireball' | 'fire_storm' | 'ice_storm' | 'amnesia' | 'confusion' | 'charm_person' | 'charm_monster' | 'paralysis' | 'fear' | 'anti_spell' | 'protection_from_evil' | 'resist_heat' | 'resist_cold' | 'disease' | 'poison' | 'blindness' | 'invisibility' | 'haste' | 'time_stop' | 'delayed_effect' | 'permanency'; export interface Spell { id: SpellId; name: string; category: SpellCategory; /** Gesture sequences that complete the spell, in Bartle's notation. */ sequences: string[]; tokens: Token[][]; usualTarget: 'self' | 'enemy'; summary: string; } export function parseSequence(sequence: string): Token[] { return sequence.split('-').map((part) => { if (part.startsWith('(')) { return { gesture: part[1].toUpperCase() as MagicGesture, both: true }; } return { gesture: part as MagicGesture, both: part === 'C' }; }); } function spell( id: SpellId, name: string, category: SpellCategory, sequences: string[], usualTarget: 'self' | 'enemy', summary: string ): Spell { return { id, name, category, sequences, tokens: sequences.map(parseSequence), usualTarget, summary }; } export const SPELLS: Spell[] = [ // Protection spell('shield', 'Shield', 'protection', ['P'], 'self', 'Blocks monsters, missiles and stabs against the subject this turn.'), spell('remove_enchantment', 'Remove enchantment', 'protection', ['P-D-W-P'], 'enemy', 'Ends every enchantment on the subject. Destroys a monster after it attacks.'), spell('magic_mirror', 'Magic mirror', 'protection', ['C-(w'], 'self', 'Spells cast at the subject this turn rebound on their caster. Undone by a counter-spell.'), spell('counter_spell', 'Counter-spell', 'protection', ['W-P-P', 'W-W-S'], 'self', 'Other spells cast at the subject this turn fail, and the subject is shielded. Cannot stop finger of death or dispel magic.'), spell('dispel_magic', 'Dispel magic', 'protection', ['C-D-P-W'], 'self', 'Every other spell this turn fails, all enchantments end, all monsters die after attacking. Shields the subject.'), spell('raise_dead', 'Raise dead', 'protection', ['D-W-W-F-W-C'], 'self', 'Heals 5 damage and removes all enchantments from the subject.'), spell('cure_light_wounds', 'Cure light wounds', 'protection', ['D-F-W'], 'self', 'Heals 1 damage.'), spell('cure_heavy_wounds', 'Cure heavy wounds', 'protection', ['D-F-P-W'], 'self', 'Heals 2 damage and cures disease.'), // Summons spell('summon_goblin', 'Summon goblin', 'summons', ['S-F-W'], 'self', 'A goblin (1 hit point, 1 damage a turn) fights for the subject.'), spell('summon_ogre', 'Summon ogre', 'summons', ['P-S-F-W'], 'self', 'An ogre (2 hit points, 2 damage a turn) fights for the subject.'), spell('summon_troll', 'Summon troll', 'summons', ['F-P-S-F-W'], 'self', 'A troll (3 hit points, 3 damage a turn) fights for the subject.'), spell('summon_giant', 'Summon giant', 'summons', ['W-F-P-S-F-W'], 'self', 'A giant (4 hit points, 4 damage a turn) fights for the subject.'), spell('summon_elemental', 'Summon elemental', 'summons', ['C-S-W-W-S'], 'self', 'A fire or ice elemental (3 hit points) savages everyone not resistant to it for 3 a turn.'), // Damage spell('missile', 'Missile', 'damage', ['S-D'], 'enemy', '1 damage. Stopped by a shield.'), spell('finger_of_death', 'Finger of death', 'damage', ['P-W-P-F-S-S-S-D'], 'enemy', 'Kills the subject outright. Only dispel magic can stop it.'), spell('lightning_bolt', 'Lightning bolt', 'damage', ['D-F-F-D-D'], 'enemy', '5 damage. Shields do not help.'), spell('lightning_bolt_quick', 'Lightning bolt (quick)', 'damage', ['W-D-D-C'], 'enemy', '5 damage. Each wizard may use this short form only once a duel.'), spell('cause_light_wounds', 'Cause light wounds', 'damage', ['W-F-P'], 'enemy', '2 damage. Shields do not help.'), spell('cause_heavy_wounds', 'Cause heavy wounds', 'damage', ['W-P-F-D'], 'enemy', '3 damage. Shields do not help.'), spell('fireball', 'Fireball', 'damage', ['F-S-S-D-D'], 'enemy', '5 damage unless the subject resists heat. Destroys an ice elemental.'), spell('fire_storm', 'Fire storm', 'damage', ['S-W-W-C'], 'enemy', '5 damage to everyone not resistant to heat, the caster included. Cancels with an ice storm or ice elemental.'), spell('ice_storm', 'Ice storm', 'damage', ['W-S-S-C'], 'enemy', '5 damage to everyone not resistant to cold, the caster included. Cancels with a fire storm or fire elemental.'), // Enchantments spell('amnesia', 'Amnesia', 'enchantment', ['D-P-P'], 'enemy', 'Next turn the subject must repeat this turn’s gestures exactly.'), spell('confusion', 'Confusion', 'enchantment', ['D-S-F'], 'enemy', 'Next turn one of the subject’s gestures is replaced at random.'), spell('charm_person', 'Charm person', 'enchantment', ['P-S-D-F'], 'enemy', 'Next turn the caster chooses what one of the subject’s hands does.'), spell('charm_monster', 'Charm monster', 'enchantment', ['P-S-D-D'], 'enemy', 'The caster takes control of the target monster from this turn on.'), spell('paralysis', 'Paralysis', 'enchantment', ['F-F-F'], 'enemy', 'Next turn one of the subject’s hands is frozen in its current gesture (C becomes F, S becomes D, W becomes P). A monster misses its next attack.'), spell('fear', 'Fear', 'enchantment', ['S-W-D'], 'enemy', 'Next turn the subject cannot make a C, D, F or S gesture.'), spell('anti_spell', 'Anti-spell', 'enchantment', ['S-P-F'], 'enemy', 'The subject’s gestures so far can no longer be used in a spell. Every spell must start again.'), spell('protection_from_evil', 'Protection from evil', 'enchantment', ['W-W-P'], 'self', 'Shields the subject this turn and for the next three.'), spell('resist_heat', 'Resist heat', 'enchantment', ['W-W-F-P'], 'self', 'The subject ignores fireballs, fire storms and fire elementals until dispelled. Destroys a fire elemental.'), spell('resist_cold', 'Resist cold', 'enchantment', ['S-S-F-P'], 'self', 'The subject ignores ice storms and ice elementals until dispelled. Destroys an ice elemental.'), spell('disease', 'Disease', 'enchantment', ['D-S-F-F-F-C'], 'enemy', 'The subject dies at the end of the sixth turn unless cured by cure heavy wounds, remove enchantment or dispel magic.'), spell('poison', 'Poison', 'enchantment', ['D-W-W-F-W-D'], 'enemy', 'As disease, but cure heavy wounds does not help.'), spell('blindness', 'Blindness', 'enchantment', ['D-W-F-F-(d'], 'enemy', 'For three turns the subject cannot see the opponent’s gestures. Destroys a monster.'), spell('invisibility', 'Invisibility', 'enchantment', ['P-P-(w-(s'], 'self', 'For three turns the opponent cannot see the subject’s gestures and monsters cannot attack them. Destroys a monster.'), spell('haste', 'Haste', 'enchantment', ['P-W-P-W-W-C'], 'self', 'For the next three turns the subject makes two pairs of gestures a turn, both taking effect together.'), spell('time_stop', 'Time stop', 'enchantment', ['S-P-P-C'], 'self', 'The subject at once takes an extra turn nobody else can see or resist.'), spell('delayed_effect', 'Delayed effect', 'enchantment', ['D-W-S-S-S-P'], 'self', 'The subject’s next spell (this turn or the next three) is banked, to be released whenever they choose.'), spell('permanency', 'Permanency', 'enchantment', ['S-P-F-P-S-D-W'], 'self', 'The subject’s next enchantment (this turn or the next three) lasts for the rest of the duel.') ]; export const SPELL_BY_ID: Record = Object.fromEntries( SPELLS.map((s) => [s.id, s]) ) as Record; /** Enchantments that cancel one another when they hit the same being together. */ export const CONTROL_SPELLS: SpellId[] = [ 'amnesia', 'confusion', 'charm_person', 'charm_monster', 'paralysis', 'fear' ]; /** Enchantments a permanency cannot extend. */ export const PERMANENCY_EXCLUDED: SpellId[] = ['anti_spell', 'disease', 'poison', 'time_stop', 'permanency']; export function permanencyEligible(spell: Spell): boolean { return spell.category === 'enchantment' && !PERMANENCY_EXCLUDED.includes(spell.id); } /** Spells worth warning about, or defending against, when the other wizard's hands approach them. */ export const HOSTILE_SPELLS: SpellId[] = [ 'missile', 'finger_of_death', 'lightning_bolt', 'lightning_bolt_quick', 'cause_light_wounds', 'cause_heavy_wounds', 'fireball', 'fire_storm', 'ice_storm', 'amnesia', 'confusion', 'charm_person', 'charm_monster', 'paralysis', 'fear', 'anti_spell', 'disease', 'poison', 'blindness', 'remove_enchantment', 'summon_goblin', 'summon_ogre', 'summon_troll', 'summon_giant', 'summon_elemental' ]; /** Spells that affect everyone rather than one subject. */ export const BLANKET_SPELLS: SpellId[] = ['fire_storm', 'ice_storm']; export type MonsterKind = 'goblin' | 'ogre' | 'troll' | 'giant' | 'fire_elemental' | 'ice_elemental'; export const MONSTER_STATS: Record = { goblin: { name: 'Goblin', hp: 1, attack: 1 }, ogre: { name: 'Ogre', hp: 2, attack: 2 }, troll: { name: 'Troll', hp: 3, attack: 3 }, giant: { name: 'Giant', hp: 4, attack: 4 }, fire_elemental: { name: 'Fire elemental', hp: 3, attack: 3 }, ice_elemental: { name: 'Ice elemental', hp: 3, attack: 3 } }; export function isElemental(kind: MonsterKind): boolean { return kind === 'fire_elemental' || kind === 'ice_elemental'; } export const SUMMON_KIND: Partial> = { summon_goblin: 'goblin', summon_ogre: 'ogre', summon_troll: 'troll', summon_giant: 'giant' };