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:
co-authored by
Claude Fable 5
parent
a6ede6ca1e
commit
b9c784a10c
@@ -6,7 +6,7 @@ import {
|
||||
type PlayerId,
|
||||
} from "../src/game";
|
||||
import { viewFor } from "../src/view";
|
||||
import { automatonCommand, automatonFallback, type AutomatonStyle } from "../src/automaton";
|
||||
import { automatonCommand, automatonFallback, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
|
||||
|
||||
/** Whose input does the maze want right now? */
|
||||
function actingSeat(state: GameState): PlayerId {
|
||||
@@ -20,9 +20,13 @@ function actingSeat(state: GameState): PlayerId {
|
||||
}
|
||||
|
||||
/** Drive a full bot-vs-bot game; returns the final state and command count. */
|
||||
function playOut(seed: number, players: number, expansion = true, styles: AutomatonStyle[] = []) {
|
||||
function playOut(
|
||||
seed: number, players: number, expansion = true,
|
||||
styles: AutomatonStyle[] = [], tiers: AutomatonTier[] = [],
|
||||
) {
|
||||
const ids = Array.from({ length: players }, (_, i) => `bot${i + 1}`);
|
||||
const styleOf = new Map(ids.map((id, i) => [id, styles[i] ?? "hunter"]));
|
||||
const tierOf = new Map(ids.map((id, i) => [id, tiers[i] ?? "archmage"]));
|
||||
let { state } = createGame({
|
||||
playerIds: ids,
|
||||
seed,
|
||||
@@ -35,7 +39,7 @@ function playOut(seed: number, players: number, expansion = true, styles: Automa
|
||||
while (state.phase === "playing" && commands < CAP) {
|
||||
const seat = actingSeat(state);
|
||||
const view = viewFor(state, seat);
|
||||
const cmd = automatonCommand(view, styleOf.get(seat)) ?? automatonFallback(view);
|
||||
const cmd = automatonCommand(view, styleOf.get(seat), tierOf.get(seat)) ?? automatonFallback(view);
|
||||
let r = applyCommand(state, seat, cmd);
|
||||
if (!r.ok) {
|
||||
const fb = automatonFallback(view);
|
||||
@@ -85,6 +89,18 @@ describe("automaton vs automaton", () => {
|
||||
expect(state.phase).toBe("finished");
|
||||
});
|
||||
|
||||
it("the apprentice handicap bites where cards decide: combat mirrors", () => {
|
||||
// Deterministic across these seeds: same brains, same dice.
|
||||
let arch = 0, appr = 0;
|
||||
for (const seed of [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233]) {
|
||||
const { state } = playOut(seed, 2, true,
|
||||
["berserker", "berserker"], ["archmage", "apprentice"]);
|
||||
if (state.winner === "bot1") arch++;
|
||||
if (state.winner === "bot2") appr++;
|
||||
}
|
||||
expect(arch).toBeGreaterThan(appr);
|
||||
});
|
||||
|
||||
it("every temperament finishes its wars", () => {
|
||||
for (const styles of [
|
||||
["berserker", "hunter"], ["worrier", "hunter"], ["berserker", "worrier"],
|
||||
|
||||
Reference in New Issue
Block a user