Easier to make a move, and plainer words around the game

From feedback on the BoardGameGeek thread. The hands are framed and
labelled as yours, so they no longer read as sitting under your
opponent's columns. A move can be typed: the first letter goes to the
left hand, the second to the right, K stabs, a dash rests, Backspace
takes one back and Enter seals. The chosen pair, what it will do, any
warning and the button now sit in one row that stays in view. A clap
with one hand is warned of before the turn is sealed. The button says
what the player does: Seal the turn. The chronicle reads oldest first
and scrolls to the newest line. Headings, tabs and notes lose the
label-and-colon construction and the abstract headings; the chronicle's
imagery stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-10-10 18:29:49 -04:00
co-authored by Claude Opus 5.5
parent db682814ea
commit ee630276b3
9 changed files with 213 additions and 42 deletions
+131 -24
View File
@@ -11,7 +11,7 @@
import { castKey, listText, tokensText, type Duel } from '$lib/game/duel.svelte'; import { castKey, listText, tokensText, type Duel } from '$lib/game/duel.svelte';
import { seatColor } from '$lib/game/colors'; import { seatColor } from '$lib/game/colors';
import { GESTURE_SHORT, glyph } from '$lib/game/glyphs'; import { GESTURE_SHORT, glyph } from '$lib/game/glyphs';
import { MAGIC_GESTURES } from '$lib/game/spells'; import { MAGIC_GESTURES, type Gesture } from '$lib/game/spells';
let { duel }: { duel: Duel } = $props(); let { duel }: { duel: Duel } = $props();
@@ -20,8 +20,37 @@
const won = $derived(outcome?.winner === duel.me); const won = $derived(outcome?.winner === duel.me);
/** Past three wizards the rail lists the others on a line each. */ /** Past three wizards the rail lists the others on a line each. */
const crowded = $derived(duel.others.length >= 3); const crowded = $derived(duel.others.length >= 3);
/** The button's word for what the player is about to do. */
const sealLabel = $derived(
duel.moved ? 'Waiting' : duel.sending ? 'Sending' : duel.surrendering ? 'Surrender' : duel.timeStopped ? 'Act in the stopped moment' : 'Seal the turn'
);
/** Letters typed anywhere on the board fill the hands, left then right; Enter seals the turn. */
const KEYS: Record<string, Gesture> = { f: 'F', p: 'P', s: 'S', w: 'W', d: 'D', c: 'C', k: '>', '>': '>', '-': '-', '.': '-', ' ': '-' };
function onKey(e: KeyboardEvent) {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const el = e.target as HTMLElement | null;
if (el && (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName))) return;
if (outcome || duel.out || duel.moved || duel.spectating) return;
if (e.key === 'Enter') {
if (duel.ready) {
e.preventDefault();
duel.reveal();
}
return;
}
if (e.key === 'Backspace') {
e.preventDefault();
duel.untype();
return;
}
const g = KEYS[e.key.toLowerCase()];
if (g && duel.typeGesture(g)) e.preventDefault();
}
</script> </script>
<svelte:window onkeydown={onKey} />
<main class="board"> <main class="board">
<section class="play"> <section class="play">
<Ledger {duel} /> <Ledger {duel} />
@@ -54,7 +83,7 @@
<h2 id="move-heading">Time stands still. Take your extra turn.</h2> <h2 id="move-heading">Time stands still. Take your extra turn.</h2>
<p class="threats"><span class="muted">{duel.foe.name} cannot move, see this, or resist anything you do. Last turn's enchantments do not bind you.</span></p> <p class="threats"><span class="muted">{duel.foe.name} cannot move, see this, or resist anything you do. Last turn's enchantments do not bind you.</span></p>
{:else} {:else}
<h2 id="move-heading">Turn {duel.turnNumber}: write your gestures{#if duel.hasted}, twice{/if}</h2> <h2 id="move-heading">Your gestures for turn {duel.turnNumber}{#if duel.hasted}, twice over{/if}</h2>
{/if} {/if}
{#if duel.threats.length && !duel.timeStopped} {#if duel.threats.length && !duel.timeStopped}
@@ -111,7 +140,7 @@
</label> </label>
</div> </div>
{#if duel.hasted} {#if duel.hasted}
<p class="haste-note">You are hastened: a second pair of gestures follows the first, and both take effect together.</p> <p class="haste-note">You are hastened, so a second pair of gestures follows the first, and both take effect together.</p>
<div class="hands"> <div class="hands">
<HandPicker {duel} hand="left" set="haste" /> <HandPicker {duel} hand="left" set="haste" />
<HandPicker {duel} hand="right" set="haste" /> <HandPicker {duel} hand="right" set="haste" />
@@ -191,22 +220,35 @@
{#if duel.remote?.error} {#if duel.remote?.error}
<p class="warning">{duel.remote.error}</p> <p class="warning">{duel.remote.error}</p>
{/if} {/if}
{#if duel.intentText && !duel.moved} <div class="commit">
<p class="intent">{duel.intentText}</p> <div class="pair" aria-label="This turn's gestures">
{/if} {#each duel.activeSets as set (set)}
{#if duel.moved} <span class="hand-pair">
<p class="waiting">Your move is in. Waiting for {duel.awaitingText}.</p> <span class="which">left</span> <span class="letter" class:empty={duel.chosenGesture(set, 'left') === null}>{duel.drawn(set, 'left')}</span>
{/if} <span class="which">right</span> <span class="letter" class:empty={duel.chosenGesture(set, 'right') === null}>{duel.drawn(set, 'right')}</span>
{#if duel.surrendering} </span>
<p class="warning">Both palms at once is surrender. You will lose the duel.</p> {/each}
{/if} </div>
{#if duel.conflict} <div class="notes">
<p class="warning">Those two spells share a gesture. A gesture can finish only one spell; let one of them pass.</p> {#if duel.moved}
{/if} <p class="waiting">Your hands are still. Waiting for {duel.awaitingText}.</p>
{:else if duel.intentText}
<button type="button" class="reveal" disabled={!duel.ready} onclick={() => duel.reveal()}> <p class="intent">{duel.intentText}</p>
{duel.moved ? 'Waiting' : duel.sending ? 'Sending' : duel.surrendering ? 'Surrender' : duel.timeStopped ? 'Act in the stopped moment' : 'Reveal gestures'} {/if}
</button> {#if duel.surrendering}
<p class="warning">Both palms at once is surrender. You will lose the duel.</p>
{:else if duel.loneClap && !duel.moved}
<p class="warning">A clap needs both hands; one hand clapping does nothing. Clap with both, or rest the other hand.</p>
{/if}
{#if duel.conflict}
<p class="warning">Those two spells share a gesture, and a gesture can finish only one spell. Let one of them pass.</p>
{/if}
{#if !duel.moved}
<p class="keys muted">Or type: F P S W D C, K to stab, a dash to rest; Backspace undoes, Enter seals.</p>
{/if}
</div>
<button type="button" class="reveal" disabled={!duel.ready} onclick={() => duel.reveal()}>{sealLabel}</button>
</div>
</section> </section>
{/if} {/if}
{/if} {/if}
@@ -378,8 +420,66 @@
margin: 1rem 0 0.6rem; margin: 1rem 0 0.6rem;
} }
.move > .reveal { /* Your hands, framed apart from the ledger's columns above them. */
margin-top: 0.8rem; .hands {
background: var(--slate);
border: 1px solid var(--rule-strong);
border-radius: 6px;
padding: 0.7rem 0.9rem 0.9rem;
}
/* The turn about to be sealed, kept in view while the hands and orders scroll. */
.commit {
position: sticky;
bottom: 0;
z-index: 4;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.4rem 1rem;
margin-top: 0.9rem;
padding: 0.6rem 0.2rem;
background: var(--slate-deep);
border-top: 1px solid var(--rule-strong);
}
.pair {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.hand-pair {
display: flex;
align-items: baseline;
gap: 0.35rem;
}
.which {
font-size: 0.75rem;
font-style: italic;
color: var(--bone-faint);
}
.pair .letter {
font-size: 1.5rem;
min-width: 1ch;
}
.pair .letter.empty {
color: var(--bone-faint);
}
.notes p {
margin: 0;
}
.notes p + p {
margin-top: 0.2rem;
}
.keys {
font-size: 0.8rem;
} }
.verdict { .verdict {
@@ -407,8 +507,16 @@
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }
/* The sticky bar carries the reveal button on phones. */ /* On phones the bar at the foot of the screen carries the pair and the button. */
.move > .reveal { .commit {
position: static;
grid-template-columns: minmax(0, 1fr);
border-top: 0;
}
.commit .pair,
.commit .reveal,
.keys {
display: none; display: none;
} }
@@ -425,7 +533,6 @@
.waiting { .waiting {
color: var(--frost); color: var(--frost);
margin-top: 0.8rem;
} }
.reveal.again { .reveal.again {
+13 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { tick } from 'svelte';
import type { LogEntry } from '$lib/game/state'; import type { LogEntry } from '$lib/game/state';
let { log }: { log: LogEntry[] } = $props(); let { log }: { log: LogEntry[] } = $props();
@@ -9,14 +10,23 @@
if (!groups.has(entry.turn)) groups.set(entry.turn, []); if (!groups.has(entry.turn)) groups.set(entry.turn, []);
groups.get(entry.turn)!.push(entry); groups.get(entry.turn)!.push(entry);
} }
return [...groups.entries()].reverse(); return [...groups.entries()];
});
// Read top to bottom like a conversation; each new turn scrolls into view at the foot.
let box = $state<HTMLElement | null>(null);
$effect(() => {
void log.length;
void tick().then(() => {
if (box) box.scrollTop = box.scrollHeight;
});
}); });
</script> </script>
<section class="chronicle"> <section class="chronicle" bind:this={box}>
<h2>Chronicle</h2> <h2>Chronicle</h2>
{#if turns.length === 0} {#if turns.length === 0}
<p class="muted">Nothing has happened yet. Choose a gesture for each hand and reveal them.</p> <p class="muted">Nothing has happened yet. Choose a gesture for each hand and seal the turn.</p>
{/if} {/if}
{#each turns as [turn, entries] (turn)} {#each turns as [turn, entries] (turn)}
<article> <article>
+2 -2
View File
@@ -264,7 +264,7 @@
{/each} {/each}
</tbody> </tbody>
</table> </table>
<p class="caption">The ledger: every gesture both wizards have made, and the spells they finished.</p> <p class="caption">A few turns of a duel. Each row is a turn, each letter one hand's gesture, and a spell is named under the gesture that finished it.</p>
</div> </div>
{#if seats.length || local} {#if seats.length || local}
@@ -357,7 +357,7 @@
<p>A rule read wrong, a bug, a duel you would like to tell of: write to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon. The keeper of this hall roosts at <a href="https://kestrelsnest.social" target="_blank" rel="noreferrer">kestrelsnest.social</a>.</p> <p>A rule read wrong, a bug, a duel you would like to tell of: write to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon. The keeper of this hall roosts at <a href="https://kestrelsnest.social" target="_blank" rel="noreferrer">kestrelsnest.social</a>.</p>
</div> </div>
{/snippet} {/snippet}
<About bind:open={aboutOpen} title="About Waving Hands" tabs={[{ id: 'game', label: 'The game', body: aboutGame }, { id: 'tally', label: 'The tally', body: aboutTally }, { id: 'family', label: 'More games', body: aboutFamily }, { id: 'word', label: 'Send word', body: aboutWord }]} /> <About bind:open={aboutOpen} title="About Waving Hands" tabs={[{ id: 'game', label: 'Origins', body: aboutGame }, { id: 'tally', label: 'Duels so far', body: aboutTally }, { id: 'family', label: 'More games', body: aboutFamily }, { id: 'word', label: 'Send word', body: aboutWord }]} />
</section> </section>
<style> <style>
+5 -5
View File
@@ -21,20 +21,20 @@
const hiddenPlans = $derived(duel.plans[hand].length - plans.length); const hiddenPlans = $derived(duel.plans[hand].length - plans.length);
const whyForced = $derived( const whyForced = $derived(
duel.you.constraints.amnesia duel.you.constraints.amnesia
? 'Amnesia: you must repeat last turn.' ? 'Amnesia holds this hand to last turn\'s gesture.'
: duel.you.constraints.paralysedHand === hand : duel.you.constraints.paralysedHand === hand
? "Paralysed in last turn's position." ? "Paralysis has locked this hand in last turn's position."
: '' : ''
); );
</script> </script>
<fieldset class="hand"> <fieldset class="hand">
<legend>{hand === 'left' ? 'Left hand' : 'Right hand'}{#if set === 'haste'}<span class="again">, again</span>{/if}</legend> <legend>{hand === 'left' ? 'Your left hand' : 'Your right hand'}{#if set === 'haste'}<span class="again">, again</span>{/if}</legend>
{#if forced} {#if forced}
<p class="locked"><span class="letter big">{glyph(forced, duel.gestureFor(set, other))}</span> {whyForced}</p> <p class="locked"><span class="letter big">{glyph(forced, duel.gestureFor(set, other))}</span> {whyForced}</p>
{:else if charmed} {:else if charmed}
<p class="locked"><span class="letter big">?</span> Charmed: {duel.foe.name} chooses this hand's gesture.</p> <p class="locked"><span class="letter big">?</span> {duel.foe.name} has charmed this hand and chooses its gesture.</p>
{:else} {:else}
<div class="gestures" role="group" aria-label="{hand} hand gesture{set === 'haste' ? ', second pair' : ''}"> <div class="gestures" role="group" aria-label="{hand} hand gesture{set === 'haste' ? ', second pair' : ''}">
{#each GESTURES as g (g)} {#each GESTURES as g (g)}
@@ -53,7 +53,7 @@
{/each} {/each}
</div> </div>
{#if duel.you.constraints.fear && !duel.timeStopped} {#if duel.you.constraints.fear && !duel.timeStopped}
<p class="hint">Fear: no clap, digit, fingers or snap this turn.</p> <p class="hint">Fear keeps this hand from clapping, pointing, wriggling or snapping this turn.</p>
{/if} {/if}
{#if set === 'main' && duel.showPlans} {#if set === 'main' && duel.showPlans}
{#if duel.plans[hand].length === 0} {#if duel.plans[hand].length === 0}
+1 -1
View File
@@ -36,7 +36,7 @@
{/each} {/each}
</div> </div>
<button type="button" class="reveal" disabled={!duel.ready} onclick={() => duel.reveal()}> <button type="button" class="reveal" disabled={!duel.ready} onclick={() => duel.reveal()}>
{duel.out ? 'Out' : duel.moved ? 'Waiting' : duel.surrendering ? 'Surrender' : duel.timeStopped ? 'Act' : 'Reveal'} {duel.out ? 'Out' : duel.moved ? 'Waiting' : duel.surrendering ? 'Surrender' : duel.timeStopped ? 'Act' : 'Seal'}
</button> </button>
{/if} {/if}
</div> </div>
+1 -1
View File
@@ -23,7 +23,7 @@
{/each} {/each}
</div> </div>
{#if details.length} {#if details.length}
<h3>The ledgers in detail</h3> <h3>Every count</h3>
<dl class="ledger"> <dl class="ledger">
{#each details as d (d.label)} {#each details as d (d.label)}
<dt>{shown(d.n)}</dt><dd>{d.label}</dd> <dt>{shown(d.n)}</dt><dd>{d.label}</dd>
+29
View File
@@ -696,6 +696,35 @@ export class Duel {
return list; return list;
} }
/** The hands a typed gesture fills, in the order they are filled: left then right, and the second pair after the first when hastened. */
private typingSlots(): { set: SetName; hand: Hand }[] {
return this.activeSets.flatMap((set) =>
HANDS.filter((h) => !this.forcedFor(set, h) && this.charmedHand !== h).map((hand) => ({ set, hand }))
);
}
/** A gesture typed on the keyboard goes to the first open hand. Returns false when no hand can take it. */
typeGesture(gesture: Gesture): boolean {
const slot = this.typingSlots().find((s) => this.sets[s.set][s.hand].gesture === null);
if (!slot || !this.allowed[slot.hand].includes(gesture)) return false;
this.sets[slot.set][slot.hand].gesture = gesture;
this.syncAll();
return true;
}
/** Backspace clears the last hand filled. */
untype(): void {
const slot = [...this.typingSlots()].reverse().find((s) => this.sets[s.set][s.hand].gesture !== null);
if (!slot) return;
this.sets[slot.set][slot.hand].gesture = null;
this.syncAll();
}
/** A clap with only one hand does nothing; said before the turn is sealed, not after. */
loneClap = $derived(
this.activeSets.some((set) => (this.gestureFor(set, 'left') === 'C') !== (this.gestureFor(set, 'right') === 'C'))
);
choose(set: SetName, hand: Hand, gesture: Gesture): void { choose(set: SetName, hand: Hand, gesture: Gesture): void {
const draft = this.sets[set][hand]; const draft = this.sets[set][hand];
draft.gesture = draft.gesture === gesture ? null : gesture; draft.gesture = draft.gesture === gesture ? null : gesture;
+25
View File
@@ -49,3 +49,28 @@ describe('threats at a crowded table', () => {
expect(duel.targetLabel('C')).toBe('Cass · 8 HP · invisible'); expect(duel.targetLabel('C')).toBe('Cass · 8 HP · invisible');
}); });
}); });
describe('typing a move', () => {
it('fills the left hand, then the right, and backspace takes the last one back', () => {
const duel = new Duel(createGame({ A: 'You', B: 'Foe' }, 1));
expect(duel.typeGesture('S')).toBe(true);
expect(duel.typeGesture('P')).toBe(true);
expect(duel.sets.main.left.gesture).toBe('S');
expect(duel.sets.main.right.gesture).toBe('P');
expect(duel.typeGesture('W')).toBe(false);
duel.untype();
expect(duel.sets.main.right.gesture).toBeNull();
expect(duel.sets.main.left.gesture).toBe('S');
});
it('warns of a clap made with one hand before the turn is sealed', () => {
const duel = new Duel(createGame({ A: 'You', B: 'Foe' }, 1));
duel.typeGesture('C');
duel.typeGesture('W');
expect(duel.loneClap).toBe(true);
expect(duel.ready).toBe(true);
duel.untype();
duel.typeGesture('C');
expect(duel.loneClap).toBe(false);
});
});
+6 -6
View File
@@ -13,7 +13,7 @@
<a href="#ledger">The ledger</a> <a href="#ledger">The ledger</a>
<a href="#hands">Your hands</a> <a href="#hands">Your hands</a>
<a href="#threats">What they may do</a> <a href="#threats">What they may do</a>
<a href="#reveal">The reveal</a> <a href="#reveal">Sealing the turn</a>
<a href="#rail">Health, creatures and the sheet</a> <a href="#rail">Health, creatures and the sheet</a>
<a href="#phone">On a phone</a> <a href="#phone">On a phone</a>
<a href="#gallery">The Peanut Gallery and table talk</a> <a href="#gallery">The Peanut Gallery and table talk</a>
@@ -53,7 +53,7 @@
<figure> <figure>
<img src="/guide/hands.png" alt="Two panels of gesture buttons, left hand and right, each with a list beneath of spells under way and the gesture that continues them." /> <img src="/guide/hands.png" alt="Two panels of gesture buttons, left hand and right, each with a list beneath of spells under way and the gesture that continues them." />
</figure> </figure>
<p>Press a gesture for each hand. Beneath each hand is what that hand is part-way through: <em>D → F-W, Cure light wounds</em> means the digit you made last turn is the first gesture of that spell, and F then W would finish it. Press a line to make its next gesture; the diamond pins a spell so it stays at the top while you work toward it.</p> <p>Press a gesture for each hand, or type them: the first letter you type goes to your left hand and the second to your right, with K for a stab, a dash to rest, Backspace to take one back and Enter to seal the turn. The two letters wait in a row at the foot of the hands, beside the button, however far the page has scrolled. Beneath each hand is what that hand is part-way through: <em>D → F-W, Cure light wounds</em> means the digit you made last turn is the first gesture of that spell, and F then W would finish it. Press a line to make its next gesture; the diamond pins a spell so it stays at the top while you work toward it.</p>
<p>When a gesture finishes a spell, <em>Finishes</em> appears with the spell chosen and <em>at</em> naming the likeliest subject. Change either. A gesture can belong to more than one spell so long as each run is unbroken, which is how F-F-F followed by S-S-D-D casts a Paralysis and then a Fireball; but one gesture may finish only one spell, and the page will tell you when two of your choices would share one.</p> <p>When a gesture finishes a spell, <em>Finishes</em> appears with the spell chosen and <em>at</em> naming the likeliest subject. Change either. A gesture can belong to more than one spell so long as each run is unbroken, which is how F-F-F followed by S-S-D-D casts a Paralysis and then a Fireball; but one gesture may finish only one spell, and the page will tell you when two of your choices would share one.</p>
<p>Some hands are not yours to move. A hand paralysed or charmed by a rival, or a turn under amnesia, shows its forced gesture locked. Both palms at once is surrender, and the page warns before you do it.</p> <p>Some hands are not yours to move. A hand paralysed or charmed by a rival, or a turn under amnesia, shows its forced gesture locked. Both palms at once is surrender, and the page warns before you do it.</p>
</section> </section>
@@ -68,11 +68,11 @@
</section> </section>
<section id="reveal"> <section id="reveal">
<h2>The reveal</h2> <h2>Sealing the turn</h2>
<figure> <figure>
<img src="/guide/result.png" alt="The result strip: each wizard's health before and after the turn, then the events of the turn in a line each." /> <img src="/guide/result.png" alt="The result strip: each wizard's health before and after the turn, then the events of the turn in a line each." />
</figure> </figure>
<p><strong>Reveal gestures</strong> commits your turn. Against the bot the turn resolves at once; at a table it resolves when the last wizard has moved, and until then the button reads <em>Waiting</em>. Everything happens together: a shield made this turn stops a missile made this turn, a creature summoned this turn strikes this turn, and a cure lands in the same breath as the wound.</p> <p><strong>Seal the turn</strong> commits your two gestures; nobody else sees them until every wizard has sealed theirs, and then all hands are shown at once. Against the bot the turn resolves at once; at a table it resolves when the last wizard has moved, and until then the button reads <em>Waiting</em>. Everything happens together: a shield made this turn stops a missile made this turn, a creature summoned this turn strikes this turn, and a cure lands in the same breath as the wound.</p>
<p>The strip above the hands says what changed: each wizard's health, then the events worth knowing, and the first time a rule surprises you, a line explaining it. The chronicle in the rail keeps the whole story.</p> <p>The strip above the hands says what changed: each wizard's health, then the events worth knowing, and the first time a rule surprises you, a line explaining it. The chronicle in the rail keeps the whole story.</p>
</section> </section>
@@ -93,9 +93,9 @@
<section id="phone"> <section id="phone">
<h2>On a phone</h2> <h2>On a phone</h2>
<figure class="narrow"> <figure class="narrow">
<img src="/guide/phone.png" alt="The duel on a phone: the ledger, the result, the hands, and a bar fixed at the foot with every wizard's health, the chosen gestures and the reveal button." /> <img src="/guide/phone.png" alt="The duel on a phone: the ledger, the result, the hands, and a bar fixed at the foot with every wizard's health, the chosen gestures and the button that seals the turn." />
</figure> </figure>
<p>Everything is here, in one column. The bar at the foot keeps every wizard's health, your two chosen gestures and the reveal button in reach; after a reveal the page brings the ledger's last rows and the result into view. Lists are shorter, and <em>More spells</em> opens the rest.</p> <p>Everything is here, in one column. The bar at the foot keeps every wizard's health, your two chosen gestures and the button that seals the turn in reach; after each turn the page brings the ledger's last rows and the result into view. Lists are shorter, and <em>More spells</em> opens the rest.</p>
</section> </section>
<section id="gallery"> <section id="gallery">