Difficulty without stupidity: apprentice, adept, archmage

Tiers degrade resources and repertoire, never judgment — the design
constraint was that no tier may ever look dumb. The APPRENTICE draws
one card a turn instead of two (a poorer wizard, not a worse one),
spends counters only on heavy hits (thrift, not blindness), and
carries a modest spellbook: damage, summons, stones, keys, and
treasure play, with no afflictions, amplifies, ambushes, guarding
tricks, or deja-vu. The ADEPT draws fully and knows everything except
ambushes and amplify. The ARCHMAGE is the full curriculum. Every card
any tier plays, it plays correctly.

The lobby workshop gains a tier picker beside the temperaments
(default adept); the tier persists with the seat, shows in roster and
scoresheet ("⚙ apprentice mystery"), and rides the drive loop.
Measured where it should matter: in berserker combat mirrors the
archmage beats the apprentice two to one, while pure treasure races
stay honest — the handicap lives in the card exchanges a human
actually feels, pinned deterministically in the tournament suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 17:59:11 -04:00
co-authored by Claude Fable 5
parent a6ede6ca1e
commit b9c784a10c
7 changed files with 115 additions and 36 deletions
+19 -5
View File
@@ -36,6 +36,7 @@
/** Leafing through the face-up discard pile. */
let showDiscards = $state(false);
let chatDraft = $state("");
let botTier = $state("adept");
/** Card whose official FAQ rulings are open. */
let faqCardId = $state<string | null>(null);
/** A discard-pile card enlarged above the pile. */
@@ -1051,11 +1052,16 @@
{#if net.you === net.hostId}
{#if net.players.length < 6}
<span class="bot-row">
⚙ seat an automaton:
<button class="stamp tiny" onclick={() => net.addBot("hunter")}>hunter</button>
<button class="stamp tiny" onclick={() => net.addBot("berserker")}>berserker</button>
<button class="stamp tiny" onclick={() => net.addBot("worrier")}>worrier</button>
<button class="stamp tiny" onclick={() => net.addBot()}>mystery</button>
⚙ seat a
<select class="tier-pick" bind:value={botTier} aria-label="automaton difficulty">
<option value="apprentice">apprentice</option>
<option value="adept">adept</option>
<option value="archmage">archmage</option>
</select>
<button class="stamp tiny" onclick={() => net.addBot("hunter", botTier)}>hunter</button>
<button class="stamp tiny" onclick={() => net.addBot("berserker", botTier)}>berserker</button>
<button class="stamp tiny" onclick={() => net.addBot("worrier", botTier)}>worrier</button>
<button class="stamp tiny" onclick={() => net.addBot(undefined, botTier)}>mystery</button>
</span>
{/if}
<label class="check">
@@ -2012,6 +2018,14 @@
.big-peek :global(.card:hover) { transform: scale(2.1); }
.big-peek { display: flex; flex-direction: column; align-items: center; }
.big-peek-note { margin-top: 6.8rem; max-width: 15rem; font-size: 0.8rem; }
.tier-pick {
background: #f6f0df;
border: 1px solid #b3a687;
border-radius: 3px;
font: inherit;
color: #43331f;
padding: 0.1rem 0.25rem;
}
.bot-row {
display: inline-flex;
align-items: center;
+2 -2
View File
@@ -428,8 +428,8 @@ class Net {
}
/** Ask the server how all our games are doing. */
addBot(style?: string): void {
this.send({ type: "addBot", ...(style ? { style } : {}) });
addBot(style?: string, tier?: string): void {
this.send({ type: "addBot", ...(style ? { style } : {}), ...(tier ? { tier } : {}) });
}
rollTableDie(): void {