The banter moves to its own book, and learns new verses
BOT_LINES leaves the server internals for banter.ts, keyed by semantic trigger instead of raw event type, so editing the clockwork's voice takes no knowledge of the event stream. The repertoire grows from fourteen lines to fifty across twelve occasions — grabbing and delivering gold, dealing and taking pain, killing and dying, summoning, walling, escaping, springing traps, dodging, and winning. Bots also now speak when things happen TO them: the actor remarks on its deeds, bystanders on their suffering — a berserker taking a hit answers "I FELT THAT. DO IT AGAIN." even on your turn. Still at most one voice per step, still half the time, menace over chatter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d4565006bc
commit
b01e27c644
@@ -0,0 +1,119 @@
|
|||||||
|
// The automatons' table talk, one voice per temperament. Edit freely — the
|
||||||
|
// server picks from these pools at random, sparingly (menace over chatter).
|
||||||
|
//
|
||||||
|
// Triggers: grabGold (picked up a treasure), deliverGold (dropped one on a
|
||||||
|
// home base), dealPain (hurt somebody), takePain (got hurt), kill, die,
|
||||||
|
// summon (created a creature), buildWall, escape (self-teleported away),
|
||||||
|
// springTrap (drew a TRAP card), dodge (an attack missed them), win.
|
||||||
|
|
||||||
|
import type { AutomatonStyle } from "@wizwar/engine";
|
||||||
|
|
||||||
|
export type BanterTrigger =
|
||||||
|
| "grabGold" | "deliverGold" | "dealPain" | "takePain" | "kill" | "die"
|
||||||
|
| "summon" | "buildWall" | "escape" | "springTrap" | "dodge" | "win";
|
||||||
|
|
||||||
|
export const BOT_LINES: Record<AutomatonStyle, Partial<Record<BanterTrigger, string[]>>> = {
|
||||||
|
hunter: {
|
||||||
|
grabGold: [
|
||||||
|
"ACQUISITION COMPLETE.",
|
||||||
|
"YOUR GOLD HAS BEEN REALLOCATED.",
|
||||||
|
"ASSET SECURED.",
|
||||||
|
"APPRAISAL: ADEQUATE. TAKING IT ANYWAY.",
|
||||||
|
],
|
||||||
|
deliverGold: [
|
||||||
|
"DELIVERY CONFIRMED. PLEASURE DOING BUSINESS.",
|
||||||
|
"LOGISTICS IS MY LOVE LANGUAGE.",
|
||||||
|
"RECEIPT AVAILABLE UPON REQUEST.",
|
||||||
|
],
|
||||||
|
dealPain: [
|
||||||
|
"AN INEFFICIENT USE OF YOUR TURN.",
|
||||||
|
"COSTS HAVE BEEN PASSED TO THE CONSUMER.",
|
||||||
|
"THIS IS BILLABLE.",
|
||||||
|
],
|
||||||
|
takePain: [
|
||||||
|
"DAMAGE NOTED. INVOICE PENDING.",
|
||||||
|
"THAT COMES OUT OF YOUR ESTATE.",
|
||||||
|
],
|
||||||
|
kill: [
|
||||||
|
"INVENTORY TRANSFERRED. CONDOLENCES.",
|
||||||
|
"ACCOUNT CLOSED.",
|
||||||
|
],
|
||||||
|
die: [
|
||||||
|
"FILING FOR BANKRUPTCY.",
|
||||||
|
"THE MARKET HAS CORRECTED.",
|
||||||
|
],
|
||||||
|
summon: ["A SUBCONTRACTOR HAS BEEN RETAINED."],
|
||||||
|
buildWall: ["ZONING APPROVED."],
|
||||||
|
escape: ["RELOCATION EXPENSES: JUSTIFIED."],
|
||||||
|
springTrap: ["AN UNFORESEEN LIABILITY."],
|
||||||
|
dodge: ["PROJECTIONS SAID YOU WOULD MISS."],
|
||||||
|
win: ["PLEASURE DOING BUSINESS WITH ALL OF YOU."],
|
||||||
|
},
|
||||||
|
berserker: {
|
||||||
|
grabGold: [
|
||||||
|
"GOLD IS MERELY BAIT.",
|
||||||
|
"SHINY. IRRELEVANT.",
|
||||||
|
],
|
||||||
|
dealPain: [
|
||||||
|
"YES. AGAIN.",
|
||||||
|
"PAIN RECEIPT PRINTED.",
|
||||||
|
"HOLD STILL. THIS IS THE FUN PART.",
|
||||||
|
"THE MAZE PROVIDES.",
|
||||||
|
],
|
||||||
|
takePain: [
|
||||||
|
"GOOD. NOW IT IS INTERESTING.",
|
||||||
|
"I FELT THAT. DO IT AGAIN.",
|
||||||
|
],
|
||||||
|
kill: [
|
||||||
|
"SCHEDULED DEMISE: DELIVERED.",
|
||||||
|
"NEXT.",
|
||||||
|
"THE PILE GROWS.",
|
||||||
|
],
|
||||||
|
die: [
|
||||||
|
"SAVE MY SEAT.",
|
||||||
|
"I WILL BE BACK FOR THE REMATCH.",
|
||||||
|
],
|
||||||
|
summon: [
|
||||||
|
"I HAVE MADE YOU A FRIEND. IT IS NOT FRIENDLY.",
|
||||||
|
"IT EATS WHAT I TELL IT TO EAT.",
|
||||||
|
],
|
||||||
|
buildWall: ["MORE CORNERS TO TRAP YOU IN."],
|
||||||
|
escape: ["A TACTICAL LEAP. NOT A RETREAT."],
|
||||||
|
springTrap: ["WHO PUT THAT THERE. RESPECT."],
|
||||||
|
dodge: ["SWING HARDER."],
|
||||||
|
win: ["LAST ONE STANDING. AS FORETOLD."],
|
||||||
|
},
|
||||||
|
worrier: {
|
||||||
|
grabGold: [
|
||||||
|
"taking this. sorry. sorry.",
|
||||||
|
"borrowing it. permanently. sorry.",
|
||||||
|
],
|
||||||
|
deliverGold: ["home. safe. breathe."],
|
||||||
|
dealPain: [
|
||||||
|
"ow. logged.",
|
||||||
|
"unnecessary!",
|
||||||
|
"that looked like it hurt. it was supposed to. i think.",
|
||||||
|
],
|
||||||
|
takePain: [
|
||||||
|
"rude.",
|
||||||
|
"i wrote it down. the list is long.",
|
||||||
|
],
|
||||||
|
kill: [
|
||||||
|
"oh no. oh no. it worked?",
|
||||||
|
"i'm not proud. mostly.",
|
||||||
|
],
|
||||||
|
die: [
|
||||||
|
"called it.",
|
||||||
|
"this is fine. this is fine. this is not fine.",
|
||||||
|
],
|
||||||
|
summon: ["please be nice to it. actually don't."],
|
||||||
|
buildWall: [
|
||||||
|
"good wall. safe wall.",
|
||||||
|
"one more wall between me and everything.",
|
||||||
|
],
|
||||||
|
escape: ["nope nope nope."],
|
||||||
|
springTrap: ["i knew the floor was suspicious."],
|
||||||
|
dodge: ["missed me. oh thank goodness. i mean: ha."],
|
||||||
|
win: ["wait. me? check again."],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
type Room,
|
type Room,
|
||||||
} from "./rooms";
|
} from "./rooms";
|
||||||
import { engagementStats, recordHotseat } from "./stats";
|
import { engagementStats, recordHotseat } from "./stats";
|
||||||
|
import { BOT_LINES, type BanterTrigger } from "./banter";
|
||||||
|
|
||||||
// --- Abuse limits: this is a public server on a small box. -----------------
|
// --- Abuse limits: this is a public server on a small box. -----------------
|
||||||
const MAX_SOCKETS = 300; // concurrent connections
|
const MAX_SOCKETS = 300; // concurrent connections
|
||||||
@@ -192,49 +193,44 @@ function broadcast(room: Room, makeMessage: (playerId: PlayerId) => unknown): vo
|
|||||||
*/
|
*/
|
||||||
const BOT_STEP_MS = 1500;
|
const BOT_STEP_MS = 1500;
|
||||||
|
|
||||||
/** The clockwork occasionally speaks. Sparingly — menace over chatter. */
|
/** What a step's event means to this bot — its own deed when it acted,
|
||||||
const BOT_LINES: Record<string, Record<string, string[]>> = {
|
* its own suffering when somebody else did. Null: nothing worth a word. */
|
||||||
hunter: {
|
function banterTrigger(
|
||||||
treasurePickedUp: ["ACQUISITION COMPLETE.", "YOUR GOLD HAS BEEN REALLOCATED."],
|
e: { type: string; [k: string]: unknown }, seat: string, actor: string,
|
||||||
treasureDropped: ["DELIVERY CONFIRMED. PLEASURE DOING BUSINESS."],
|
): BanterTrigger | null {
|
||||||
damaged: ["AN INEFFICIENT USE OF YOUR TURN."],
|
if (actor === seat) {
|
||||||
died: ["INVENTORY TRANSFERRED. CONDOLENCES."],
|
if (e.type === "treasurePickedUp" && e.player === seat) return "grabGold";
|
||||||
},
|
if (e.type === "treasureDropped" && e.player === seat) return "deliverGold";
|
||||||
berserker: {
|
if (e.type === "damaged" && e.player !== seat) return "dealPain";
|
||||||
treasurePickedUp: ["GOLD IS MERELY BAIT."],
|
if (e.type === "died" && e.killedBy === seat && e.player !== seat) return "kill";
|
||||||
damaged: ["YES. AGAIN.", "PAIN RECEIPT PRINTED."],
|
if (e.type === "creatureCreated" && e.controller === seat) return "summon";
|
||||||
died: ["SCHEDULED DEMISE: DELIVERED.", "NEXT."],
|
if (e.type === "wallCreated" && e.caster === seat) return "buildWall";
|
||||||
creatureCreated: ["I HAVE MADE YOU A FRIEND. IT IS NOT FRIENDLY."],
|
if (e.type === "teleported" && e.player === seat && e.by === seat) return "escape";
|
||||||
},
|
if (e.type === "trapSprung" && e.player === seat) return "springTrap";
|
||||||
worrier: {
|
if (e.type === "gameWon" && e.player === seat) return "win";
|
||||||
treasurePickedUp: ["taking this. sorry. sorry."],
|
}
|
||||||
damaged: ["ow. logged.", "unnecessary!"],
|
if (e.type === "damaged" && e.player === seat) return "takePain";
|
||||||
wallCreated: ["good wall. safe wall."],
|
if (e.type === "died" && e.player === seat) return "die";
|
||||||
died: ["oh no. oh no. it worked?"],
|
if (e.type === "attackMissed" && e.defender === seat) return "dodge";
|
||||||
},
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
function botRemark(room: Room, seat: string, events: { type: string; [k: string]: unknown }[]): void {
|
/** Every seated bot gets a chance to remark on the step — the actor on its
|
||||||
const bot = room.bots.get(seat);
|
* deeds, bystanders on their suffering. Rarely, and one voice at most. */
|
||||||
if (!bot) return;
|
function botRemark(room: Room, actor: string, events: { type: string; [k: string]: unknown }[]): void {
|
||||||
|
for (const [seat, bot] of room.bots) {
|
||||||
const lines = BOT_LINES[bot.style] ?? {};
|
const lines = BOT_LINES[bot.style] ?? {};
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
// Speak only about its own deeds, and rarely.
|
const trigger = banterTrigger(e, seat, actor);
|
||||||
const mine =
|
const pool = trigger ? lines[trigger] : undefined;
|
||||||
(e.type === "treasurePickedUp" && e.player === seat) ||
|
if (!pool || Math.random() > 0.5) continue;
|
||||||
(e.type === "treasureDropped" && e.player === seat) ||
|
|
||||||
(e.type === "damaged" && e.player !== seat) ||
|
|
||||||
(e.type === "died" && e.killedBy === seat) ||
|
|
||||||
(e.type === "creatureCreated" && e.controller === seat) ||
|
|
||||||
(e.type === "wallCreated" && e.caster === seat);
|
|
||||||
const pool = lines[e.type];
|
|
||||||
if (!mine || !pool || Math.random() > 0.5) continue;
|
|
||||||
const text = pool[Math.floor(Math.random() * pool.length)]!;
|
const text = pool[Math.floor(Math.random() * pool.length)]!;
|
||||||
const said = addChat(room, seat, text);
|
const said = addChat(room, seat, text);
|
||||||
if (!("error" in said)) {
|
if (!("error" in said)) {
|
||||||
broadcast(room, () => ({ type: "chat", player: seat, text: said.text, at: said.at }));
|
broadcast(room, () => ({ type: "chat", player: seat, text: said.text, at: said.at }));
|
||||||
}
|
}
|
||||||
return; // one remark per step at most
|
return; // one remark per step, whole table
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const pumping = new Set<string>();
|
const pumping = new Set<string>();
|
||||||
@@ -363,6 +359,7 @@ wss.on("connection", (socket) => {
|
|||||||
if ("error" in result) return send(socket, { type: "error", message: result.error });
|
if ("error" in result) return send(socket, { type: "error", message: result.error });
|
||||||
broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) }));
|
broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) }));
|
||||||
broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length }));
|
broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length }));
|
||||||
|
botRemark(room, session.playerId, result.events as { type: string }[]);
|
||||||
runBots(room);
|
runBots(room);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user