The clockwork learns numbers, summons, creatures — and temperament
The automaton now plays number cards where they matter: attached to value-scaled attacks (waterbolt, powerthrust) choosing the biggest for damage, and played for movement when the goal is just out of stride. It raises its monsters when no wizard is in spell range and commands them every turn — creatures march at enemies by the same hazard-shy BFS and maul whoever shares their square. Targets are picked by lowest life. And it has moods. The HUNTER plays the classic game: gold first, violence when convenient. The BERSERKER hunts wizards over treasure and finishes games by last-wizard-standing. The WORRIER paths around enemies, counters at a lower threshold, shields against chaos, and never brawls. Hosts pick the temperament when seating one (or the workshop assigns at random); it persists with the seat, shows in the roster, and the tournament harness proves every pairing finishes. Across ten seeds of berserker versus hunter: six treasure wins to four kills — the moods play genuinely different games. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
01101ca24a
commit
5a407a1e4c
@@ -17,7 +17,12 @@ import {
|
||||
} from "@wizwar/engine";
|
||||
import { appendLine, ensureDataDir, readAllRooms, roomFileExists, type RoomLine } from "./store";
|
||||
import { recordRoom } from "./stats";
|
||||
import { automatonCommand, automatonFallback } from "@wizwar/engine";
|
||||
import {
|
||||
automatonCommand,
|
||||
automatonFallback,
|
||||
AUTOMATON_STYLES,
|
||||
type AutomatonStyle,
|
||||
} from "@wizwar/engine";
|
||||
|
||||
export interface LoggedCommand {
|
||||
seq: number;
|
||||
@@ -42,8 +47,8 @@ export interface Room {
|
||||
events: GameEvent[]; // full history (unredacted — redact per recipient)
|
||||
/** Table talk, persisted with the room (public to all seats). */
|
||||
chat: { player: PlayerId; text: string; at: string }[];
|
||||
/** Seats the server itself plays. */
|
||||
bots: Set<PlayerId>;
|
||||
/** Seats the server itself plays, and each one's temperament. */
|
||||
bots: Map<PlayerId, AutomatonStyle>;
|
||||
}
|
||||
|
||||
const rooms = new Map<string, Room>();
|
||||
@@ -101,7 +106,7 @@ export function createRoom(hostId: PlayerId): { room: Room; token: string } {
|
||||
log: [],
|
||||
events: [],
|
||||
chat: [],
|
||||
bots: new Set(),
|
||||
bots: new Map(),
|
||||
};
|
||||
rooms.set(room.id, room);
|
||||
recordRoom(room);
|
||||
@@ -237,14 +242,17 @@ export function addChat(room: Room, playerId: PlayerId, rawText: string): { text
|
||||
const AUTOMATON_NAMES = ["Automaton", "Automaton II", "Automaton III", "Automaton IV", "Automaton V"];
|
||||
|
||||
/** Seat a clockwork wizard (host's choice, before the game starts). */
|
||||
export function addAutomaton(room: Room): { name: PlayerId } | { error: string } {
|
||||
export function addAutomaton(room: Room, styleWanted?: string): { name: PlayerId } | { error: string } {
|
||||
if (room.state) return { error: "the game has started" };
|
||||
if (room.players.length >= 6) return { error: "room is full" };
|
||||
const name = AUTOMATON_NAMES.find((n) => !room.players.includes(n));
|
||||
if (!name) return { error: "the workshop is empty" };
|
||||
const style: AutomatonStyle = AUTOMATON_STYLES.includes(styleWanted as AutomatonStyle)
|
||||
? (styleWanted as AutomatonStyle)
|
||||
: AUTOMATON_STYLES[randomInt(AUTOMATON_STYLES.length)]!;
|
||||
room.players.push(name);
|
||||
room.bots.add(name);
|
||||
appendLine(room.id, { kind: "join", name, bot: true });
|
||||
room.bots.set(name, style);
|
||||
appendLine(room.id, { kind: "join", name, bot: true, style });
|
||||
return { name };
|
||||
}
|
||||
|
||||
@@ -269,7 +277,7 @@ export function driveOneAutomaton(room: Room): { seat: PlayerId; events: GameEve
|
||||
const seat = actingSeat(room);
|
||||
if (!seat || !room.bots.has(seat)) return null;
|
||||
const view = viewFor(room.state!, seat);
|
||||
const cmd = automatonCommand(view) ?? automatonFallback(view);
|
||||
const cmd = automatonCommand(view, room.bots.get(seat)) ?? automatonFallback(view);
|
||||
let r = runCommand(room, seat, cmd);
|
||||
if ("error" in r) {
|
||||
r = runCommand(room, seat, automatonFallback(view));
|
||||
@@ -480,13 +488,13 @@ export function loadPersistedRooms(): void {
|
||||
log: [],
|
||||
events: [],
|
||||
chat: [],
|
||||
bots: new Set(),
|
||||
bots: new Map(),
|
||||
};
|
||||
for (const line of lines.slice(1)) {
|
||||
if (line.kind === "join") {
|
||||
if (line.bot) {
|
||||
room.players.push(line.name);
|
||||
room.bots.add(line.name);
|
||||
room.bots.set(line.name, (line.style as AutomatonStyle) ?? "hunter");
|
||||
} else {
|
||||
const joinHash = line.tokenHash ?? (line.token ? hashToken(line.token) : null);
|
||||
if (!joinHash) throw new Error("join line has no token");
|
||||
|
||||
Reference in New Issue
Block a user