@dsh-agentic-roleplay/state 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { DatabaseSync } from "node:sqlite";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { randomUUID } from "node:crypto";
6
+ import { randomInt, randomUUID } from "node:crypto";
7
7
  import { copyFileSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
8
8
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
9
  //#region ../dsh-roleplay-model/src/character.ts
@@ -367,6 +367,31 @@ const TRIGGER_NOTE_KINDS = ["beat", "cue"];
367
367
  /** At most this many triggers in a story. */
368
368
  const MAX_TRIGGERS = 200;
369
369
  //#endregion
370
+ //#region ../dsh-roleplay-model/src/dice.ts
371
+ /** What a band of a check makes of an attempt; the same three results a round's outcome has. */
372
+ const DICE_RESULTS = [
373
+ "succeeded",
374
+ "partial",
375
+ "failed"
376
+ ];
377
+ /** How a roll is read: by its total, or by how many of the dice came up high enough. */
378
+ const DICE_READINGS = ["total", "successes"];
379
+ /**
380
+ * What the value is compared with: `absolute` reads the value itself, `over` the amount by which it beats the target
381
+ * (a difficulty to meet or exceed), `under` the amount by which it stays below the target (a skill to roll under).
382
+ */
383
+ const DICE_COMPARISONS = [
384
+ "absolute",
385
+ "over",
386
+ "under"
387
+ ];
388
+ /** Rolls and checks are limited so that a story cannot grow without bound. */
389
+ const MAX_DICE_CHECKS = 40;
390
+ const MAX_DICE_BANDS = 20;
391
+ const MAX_DICE_PER_ROLL = 100;
392
+ const MAX_DICE_SIDES = 1e3;
393
+ const MAX_DICE_TERMS = 8;
394
+ //#endregion
370
395
  //#region ../dsh-roleplay-model/src/visual.ts
371
396
  const VISUAL_SUBJECT_KINDS = [
372
397
  "character",
@@ -445,6 +470,16 @@ const ZH = {
445
470
  "age: {value}": "年龄:{value}",
446
471
  "gender: {value}": "性别:{value}",
447
472
  "pronouns: {value}": "代词:{value}",
473
+ "- {key} ({name}): {use}": "- {key}({name}):{use}",
474
+ "Rolls {dice}.": "掷 {dice}。",
475
+ "Give a target: the difficulty, the number to meet or beat; set it by how hard the attempt is.": "需给出 target:难度,即要达到或超过的数字;按这次尝试的难易来定。",
476
+ "Give a target: the number to stay under; set it by how hard the attempt is.": "需给出 target:要低于的数字;按这次尝试的难易来定。",
477
+ "It adds the character's {variable}.": "它会加上该角色的 {variable}。",
478
+ "Counts the dice that show {hit} or more.": "统计显示 {hit} 或更高的骰子数。",
479
+ "the player's roll": "玩家的掷骰",
480
+ "The player rolled {dice} ({reason}) and got {total}: dice {faces}. This is a fact, decided by the dice: take it into account when you judge the attempt and decide what happens.": "玩家掷了 {dice}({reason}),得到 {total}:骰面 {faces}。这是由骰子决定的事实:在你判断这次尝试、决定发生什么时把它考虑进去。",
481
+ "The checks of this story:": "本故事的检定:",
482
+ "No checks are defined: use roll_dice only.": "没有定义检定:只用 roll_dice。",
448
483
  "Situation:": "当前情形:",
449
484
  "Now: {time}.": "现在是:{time}。",
450
485
  "Where you are:": "你所在之处:",
@@ -542,7 +577,9 @@ const ZH = {
542
577
  "{where}: say what it took in since that bears on it": "{where}:请写出此后与之相关的经历",
543
578
  "{where}: thing {id} came before the belief was formed; cite what came after": "{where}:经历 {id} 发生在看法形成之前;请引用之后发生的事",
544
579
  "{where}: say what changes": "{where}:请写出有何改变",
545
- "{where}: the text must be there and at most {max} characters": "{where}:必须有文字,且最多 {max} 个字符",
580
+ "{where}: the text must be there and at most {max} characters; leave it out to keep the belief as it is": "{where}:文字不能为空,且最多 {max} 个字符;想保持信念原样就不要写这一项",
581
+ "{where}: the confidence, the importance and the numbers are fields of their own, not part of the text": "{where}:把握程度、重要性和编号是各自独立的字段,不属于文字",
582
+ "{where}: name what speaks against the belief in contradicted_by": "{where}:在 contradicted_by 里写出与该信念相悖的内容的编号",
546
583
  "{where}: a belief that something contradicts must be doubted: lower its confidence": "{where}:被事实相悖的看法必须存疑:请降低其 confidence",
547
584
  "{where}: to doubt a belief, name what contradicts it in contradicted_by": "{where}:要对看法存疑,请在 contradicted_by 中写明与之相悖的经历",
548
585
  "{where}: merge into a belief that stays": "{where}:请合并到保留的看法中",
@@ -595,7 +632,9 @@ const ZH = {
595
632
  "The time you had for \"{goal}\" has passed ({when}; it is {now} now)": "你为“{goal}”预留的时间已过({when};现在是{now})",
596
633
  "You are not with the player's character and nothing of this happens in front of anyone: decide what you do about it.": "你不和玩家的角色在一起,这件事也不会发生在任何人面前:决定你要怎么处理。",
597
634
  "{names} noticed: {text}": "{names}注意到:{text}",
598
- "{name} went to {place}.": "{name}去了{place}。"
635
+ "{name} went to {place}.": "{name}去了{place}。",
636
+ "## MCP resource servers\n\nUse {tools} with the server argument naming an MCP server.": "## MCP 资源服务器\n\n使用 {tools},server 参数填 MCP 服务器的名称。",
637
+ "## MCP resource servers\n\nUse {tools} with one of these names as the server argument: {servers}.": "## MCP 资源服务器\n\n使用 {tools},server 参数填下列名称之一:{servers}。"
599
638
  };
600
639
  //#endregion
601
640
  //#region ../dsh-roleplay-model/src/messages.ts
@@ -1084,7 +1123,7 @@ function seasonOf(day, hemisphere = "north") {
1084
1123
  return north === "winter" ? "summer" : north === "summer" ? "winter" : north === "spring" ? "autumn" : "spring";
1085
1124
  }
1086
1125
  const key = (month, day) => month * 100 + day;
1087
- function within(day, from, to) {
1126
+ function within$1(day, from, to) {
1088
1127
  if (from.year !== void 0 && to.year !== void 0) {
1089
1128
  const at = day.year * 1e4 + key(day.month, day.day);
1090
1129
  return at >= from.year * 1e4 + key(from.month, from.day) && at <= to.year * 1e4 + key(to.month, to.day);
@@ -1099,7 +1138,7 @@ function matches(token, day, calendar) {
1099
1138
  case "weekday": return day.weekday === token.weekday;
1100
1139
  case "month": return day.month === token.month;
1101
1140
  case "date": return day.month === token.month && day.day === token.day && (token.year === void 0 || day.year === token.year);
1102
- case "span": return within(day, token.from, token.to);
1141
+ case "span": return within$1(day, token.from, token.to);
1103
1142
  case "dom": return token.day === "last" ? day.day === daysIn(day.year, day.month) : day.day === token.day;
1104
1143
  case "nth": return day.weekday === token.weekday && (token.n === "last" ? day.day + 7 > daysIn(day.year, day.month) : Math.ceil(day.day / 7) === token.n);
1105
1144
  case "season": return seasonOf(day, calendar.hemisphere) === token.season;
@@ -7538,6 +7577,108 @@ var SqliteTriggerRepository = class {
7538
7577
  }
7539
7578
  };
7540
7579
  //#endregion
7580
+ //#region src/sqlite/dice-repository.ts
7581
+ const MAX_ROLLS_LISTED = 200;
7582
+ function parseDefinition(text) {
7583
+ try {
7584
+ return JSON.parse(text);
7585
+ } catch (error) {
7586
+ throw new CorruptDataError("a dice check's definition is not valid JSON", { cause: error });
7587
+ }
7588
+ }
7589
+ var SqliteDiceRepository = class {
7590
+ db;
7591
+ clock;
7592
+ constructor(db, clock) {
7593
+ this.db = db;
7594
+ this.clock = clock;
7595
+ }
7596
+ define(definition) {
7597
+ if ((this.db.get("SELECT count(*) AS count FROM dice_checks")?.count ?? 0) >= 40) throw new InvalidValueError(`a story has at most 40 dice checks`);
7598
+ const { key, name, use, ...rest } = definition;
7599
+ return guard(() => {
7600
+ const { lastInsertRowid } = this.db.run("INSERT INTO dice_checks (key, name, use_text, definition) VALUES (?, ?, ?, ?)", [
7601
+ key,
7602
+ name,
7603
+ use,
7604
+ toJson(rest)
7605
+ ]);
7606
+ return this.require(Number(lastInsertRowid));
7607
+ });
7608
+ }
7609
+ checks() {
7610
+ return this.db.all("SELECT * FROM dice_checks ORDER BY id").map((row) => this.toCheck(row));
7611
+ }
7612
+ check(key) {
7613
+ const row = this.db.get("SELECT * FROM dice_checks WHERE key = ?", [key]);
7614
+ return row === void 0 ? void 0 : this.toCheck(row);
7615
+ }
7616
+ removeCheck(key) {
7617
+ if (this.db.run("DELETE FROM dice_checks WHERE key = ?", [key]).changes === 0) throw new NotFoundError(`there is no dice check "${key}"`);
7618
+ }
7619
+ record(roll) {
7620
+ const at = this.clock().toISOString();
7621
+ const { lastInsertRowid } = this.db.run("INSERT INTO dice_rolls (turn, check_key, expression, faces, kept, modifier, target, total, value, readout, reason, character, by, at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
7622
+ roll.turn,
7623
+ roll.check ?? null,
7624
+ roll.expression,
7625
+ toJson(roll.faces),
7626
+ toJson(roll.kept),
7627
+ roll.modifier,
7628
+ roll.target ?? null,
7629
+ roll.total,
7630
+ roll.value,
7631
+ roll.readout === void 0 ? null : toJson(roll.readout),
7632
+ roll.reason,
7633
+ roll.character ?? null,
7634
+ roll.by,
7635
+ at
7636
+ ]);
7637
+ return {
7638
+ ...roll,
7639
+ id: Number(lastInsertRowid),
7640
+ at
7641
+ };
7642
+ }
7643
+ rolls(filter = {}) {
7644
+ const limit = Math.min(filter.limit ?? MAX_ROLLS_LISTED, MAX_ROLLS_LISTED);
7645
+ return (filter.turn === void 0 ? this.db.all("SELECT * FROM (SELECT * FROM dice_rolls ORDER BY id DESC LIMIT ?) ORDER BY id", [limit]) : this.db.all("SELECT * FROM dice_rolls WHERE turn = ? ORDER BY id LIMIT ?", [filter.turn, limit])).map((row) => this.toRoll(row));
7646
+ }
7647
+ require(id) {
7648
+ const row = this.db.get("SELECT * FROM dice_checks WHERE id = ?", [id]);
7649
+ if (row === void 0) throw new NotFoundError(`there is no dice check ${id}`);
7650
+ return this.toCheck(row);
7651
+ }
7652
+ toCheck(row) {
7653
+ return {
7654
+ id: row.id,
7655
+ key: row.key,
7656
+ name: row.name,
7657
+ use: row.use_text,
7658
+ ...parseDefinition(row.definition)
7659
+ };
7660
+ }
7661
+ toRoll(row) {
7662
+ return {
7663
+ id: row.id,
7664
+ turn: row.turn,
7665
+ expression: row.expression,
7666
+ faces: parseNumberList(row.faces, "dice_rolls.faces"),
7667
+ kept: parseNumberList(row.kept, "dice_rolls.kept"),
7668
+ modifier: row.modifier,
7669
+ total: row.total,
7670
+ value: row.value,
7671
+ reason: row.reason,
7672
+ by: row.by,
7673
+ at: row.at,
7674
+ ...row.check_key === null ? {} : { check: row.check_key },
7675
+ ...row.target === null ? {} : { target: row.target },
7676
+ ...row.readout === null ? {} : { readout: JSON.parse(row.readout) },
7677
+ ...row.character === null ? {} : { character: row.character }
7678
+ };
7679
+ }
7680
+ };
7681
+ //#endregion
7541
7682
  //#region src/sqlite/clock-repository.ts
7542
7683
  const toChange = (row) => ({
7543
7684
  id: row.id,
@@ -8681,7 +8822,7 @@ var SqliteMetadataRepository = class {
8681
8822
  //#region src/sqlite/schema.ts
8682
8823
  /**
8683
8824
  * Migrations are applied in order and never edited once released (0.1.0 released
8684
- * versions 1 to 6; tests/sqlite/migrate.spec.ts pins them): a change to the schema
8825
+ * versions 1 to 6; tests/sqlite/migrate.spec.ts pins the released ones): a change to the schema
8685
8826
  * is a new entry. Enumerations are spelled out so that a later change to the model
8686
8827
  * cannot alter what an old migration did.
8687
8828
  */
@@ -9260,6 +9401,36 @@ const MIGRATIONS = [
9260
9401
  ) STRICT;
9261
9402
  CREATE INDEX memory_evidence_perception ON memory_evidence (perception_id);
9262
9403
  ALTER TABLE routine_windows ADD COLUMN filters TEXT NOT NULL DEFAULT '[]';
9404
+ `
9405
+ },
9406
+ {
9407
+ version: 8,
9408
+ sql: `
9409
+ CREATE TABLE dice_checks (
9410
+ id INTEGER PRIMARY KEY,
9411
+ key TEXT NOT NULL UNIQUE,
9412
+ name TEXT NOT NULL,
9413
+ use_text TEXT NOT NULL,
9414
+ definition TEXT NOT NULL
9415
+ ) STRICT;
9416
+ CREATE TABLE dice_rolls (
9417
+ id INTEGER PRIMARY KEY,
9418
+ turn INTEGER NOT NULL,
9419
+ check_key TEXT,
9420
+ expression TEXT NOT NULL,
9421
+ faces TEXT NOT NULL,
9422
+ kept TEXT NOT NULL,
9423
+ modifier INTEGER NOT NULL,
9424
+ target INTEGER,
9425
+ total INTEGER NOT NULL,
9426
+ value INTEGER NOT NULL,
9427
+ readout TEXT,
9428
+ reason TEXT NOT NULL,
9429
+ character TEXT,
9430
+ by TEXT NOT NULL CHECK (by IN ('agent', 'player')),
9431
+ at TEXT NOT NULL
9432
+ ) STRICT;
9433
+ CREATE INDEX dice_rolls_turn ON dice_rolls (turn, id);
9263
9434
  `
9264
9435
  }
9265
9436
  ];
@@ -10008,6 +10179,7 @@ var SqliteSessionStore = class {
10008
10179
  routines;
10009
10180
  engagements;
10010
10181
  triggers;
10182
+ dice;
10011
10183
  recallIndex;
10012
10184
  eventRepository;
10013
10185
  constructor(db, clock, vectors) {
@@ -10038,6 +10210,7 @@ var SqliteSessionStore = class {
10038
10210
  this.routines = new SqliteRoutineRepository(db);
10039
10211
  this.engagements = new SqliteEngagementRepository(db);
10040
10212
  this.triggers = new SqliteTriggerRepository(db);
10213
+ this.dice = new SqliteDiceRepository(db, clock);
10041
10214
  this.eventRepository = new SqliteEventRepository(db, clock, this.recallIndex, (id) => this.locations.get(id)?.name, (id) => this.items.get(id)?.name);
10042
10215
  this.events = this.eventRepository;
10043
10216
  this.memories = new SqliteMemoryRepository(db, clock, this.recallIndex);
@@ -10444,6 +10617,375 @@ function identityOf(database, storeOptions) {
10444
10617
  }
10445
10618
  }
10446
10619
  //#endregion
10620
+ //#region src/records/dice.ts
10621
+ const systemRoller = (sides) => randomInt(1, sides + 1);
10622
+ const TERM = /^(?:(\d*)d(\d+|%)(?:(kh|kl|dh|dl)(\d*))?|(\d+))$/;
10623
+ /** Reads dice notation: `2d6+1`, `d20`, `4d6kh3`, `d%`, `1d8+1d4-1`. @throws {InvalidValueError} with what is wrong. */
10624
+ function parseNotation(input) {
10625
+ const source = input.toLowerCase().replace(/\s+/g, "");
10626
+ if (source === "") throw new InvalidValueError("dice notation is empty: write it like 2d6+1");
10627
+ const parts = source.split(/(?=[+-])/);
10628
+ if (parts.length > 8) throw new InvalidValueError(`dice notation has more than 8 terms`);
10629
+ const terms = parts.map((part, index) => {
10630
+ const sign = part.startsWith("-") ? -1 : 1;
10631
+ const body = part.replace(/^[+-]/, "");
10632
+ if (index > 0 && !/^[+-]/.test(part)) throw new InvalidValueError(`"${input}" is not dice notation: write it like 2d6+1`);
10633
+ const match = TERM.exec(body);
10634
+ if (match === null) throw new InvalidValueError(`"${part}" is not dice notation: write it like 2d6+1, 4d6kh3 or d100`);
10635
+ if (match[5] !== void 0) return {
10636
+ sign,
10637
+ constant: Number(match[5])
10638
+ };
10639
+ const count = match[1] === "" || match[1] === void 0 ? 1 : Number(match[1]);
10640
+ const sides = match[2] === "%" ? 100 : Number(match[2]);
10641
+ if (count < 1 || count > 100) throw new InvalidValueError(`"${part}": roll from 1 to 100 dice at a time`);
10642
+ if (sides < 2 || sides > 1e3) throw new InvalidValueError(`"${part}": a die has from 2 to ${MAX_DICE_SIDES} sides`);
10643
+ const mode = match[3];
10644
+ if (mode === void 0) return {
10645
+ sign,
10646
+ count,
10647
+ sides
10648
+ };
10649
+ const n = match[4] === "" || match[4] === void 0 ? 1 : Number(match[4]);
10650
+ if (n < 1 || n >= count) throw new InvalidValueError(`"${part}": ${mode.startsWith("k") ? "keep" : "drop"} at least 1 and fewer than the ${count} dice`);
10651
+ return {
10652
+ sign,
10653
+ count,
10654
+ sides,
10655
+ keep: {
10656
+ mode,
10657
+ n
10658
+ }
10659
+ };
10660
+ });
10661
+ if (!terms.some((term) => "sides" in term)) throw new InvalidValueError(`"${input}" has no dice: write it like 2d6+1`);
10662
+ return {
10663
+ text: source,
10664
+ terms
10665
+ };
10666
+ }
10667
+ /** The lowest and highest total the dice and constants can give. */
10668
+ function notationRange(notation) {
10669
+ let min = 0;
10670
+ let max = 0;
10671
+ for (const term of notation.terms) {
10672
+ if ("constant" in term) {
10673
+ min += term.sign * term.constant;
10674
+ max += term.sign * term.constant;
10675
+ continue;
10676
+ }
10677
+ const kept = term.keep === void 0 ? term.count : term.keep.mode.startsWith("k") ? term.keep.n : term.count - term.keep.n;
10678
+ const low = kept;
10679
+ const high = kept * term.sides;
10680
+ min += term.sign === 1 ? low : -high;
10681
+ max += term.sign === 1 ? high : -low;
10682
+ }
10683
+ return {
10684
+ min,
10685
+ max
10686
+ };
10687
+ }
10688
+ function keepOf(faces, keep) {
10689
+ const ordered = faces.map((face, index) => ({
10690
+ face,
10691
+ index
10692
+ })).sort((a, b) => a.face - b.face || a.index - b.index);
10693
+ const count = keep.mode.startsWith("k") ? keep.n : faces.length - keep.n;
10694
+ return (keep.mode === "kh" || keep.mode === "dl" ? ordered.slice(ordered.length - count) : ordered.slice(0, count)).sort((a, b) => a.index - b.index).map((entry) => entry.face);
10695
+ }
10696
+ function rollNotation(notation, roll = systemRoller) {
10697
+ const faces = [];
10698
+ const kept = [];
10699
+ let constant = 0;
10700
+ let total = 0;
10701
+ for (const term of notation.terms) {
10702
+ if ("constant" in term) {
10703
+ constant += term.sign * term.constant;
10704
+ total += term.sign * term.constant;
10705
+ continue;
10706
+ }
10707
+ const thrown = Array.from({ length: term.count }, () => roll(term.sides));
10708
+ const counted = term.keep === void 0 ? thrown : keepOf(thrown, term.keep);
10709
+ faces.push(...thrown);
10710
+ kept.push(...counted);
10711
+ total += term.sign * counted.reduce((sum, face) => sum + face, 0);
10712
+ }
10713
+ return {
10714
+ faces,
10715
+ kept,
10716
+ constant,
10717
+ total
10718
+ };
10719
+ }
10720
+ const within = (band, value) => (band.min === void 0 || value >= band.min) && (band.max === void 0 || value <= band.max);
10721
+ /** What the bands of a check make of a value. @throws {InvalidValueError} when no band holds it. */
10722
+ function readBands(bands, value) {
10723
+ const band = bands.find((candidate) => within(candidate, value));
10724
+ if (band === void 0) throw new InvalidValueError(`no band of this check holds ${value}: the check's bands leave a gap`);
10725
+ return {
10726
+ label: band.label,
10727
+ result: band.result,
10728
+ critical: band.critical === true
10729
+ };
10730
+ }
10731
+ /**
10732
+ * Rolls a check: the dice, the character's modifier and any bonus added, then the value read against the bands. With the reading
10733
+ * `successes` the total is the number of dice that hit. A check read against a target needs one.
10734
+ * @throws {InvalidValueError} for a target a check needs and is not given, or bands that leave the value unread.
10735
+ */
10736
+ function rollCheck(check, input, roll = systemRoller) {
10737
+ const rolled = rollNotation(parseNotation(check.dice), roll);
10738
+ const comparison = check.compare ?? "absolute";
10739
+ if (comparison !== "absolute" && input.target === void 0) throw new InvalidValueError(`the check "${check.key}" is read against a target (${comparison === "over" ? "the number to meet or beat" : "the number to stay under"}): give one`);
10740
+ const added = (input.modifier ?? 0) + (input.bonus ?? 0);
10741
+ const total = check.reading === "successes" ? rolled.kept.filter((face) => face >= (check.hit ?? 0)).length + (input.bonus ?? 0) : rolled.total + added;
10742
+ const value = comparison === "over" ? total - (input.target ?? 0) : comparison === "under" ? (input.target ?? 0) - total : total;
10743
+ return {
10744
+ faces: rolled.faces,
10745
+ kept: rolled.kept,
10746
+ modifier: check.reading === "successes" ? input.bonus ?? 0 : rolled.constant + added,
10747
+ ...input.target === void 0 ? {} : { target: input.target },
10748
+ total,
10749
+ value,
10750
+ readout: readBands(check.bands, value)
10751
+ };
10752
+ }
10753
+ /** What is wrong with a check as a world defines it; nothing when it is sound. */
10754
+ function checkProblems(check) {
10755
+ const problems = [];
10756
+ if (!/^[a-z][a-z0-9_-]{0,39}$/.test(check.key)) problems.push(`the key "${check.key}" must be lower-case letters, digits, - or _, starting with a letter`);
10757
+ if (check.name.trim() === "") problems.push("it needs a name");
10758
+ if (check.use.trim() === "") problems.push("it needs a \"use\": when the game master rolls it");
10759
+ if (check.compare !== void 0 && !DICE_COMPARISONS.includes(check.compare)) problems.push(`"compare" must be one of: ${DICE_COMPARISONS.join(", ")}`);
10760
+ if (check.reading !== void 0 && !DICE_READINGS.includes(check.reading)) problems.push(`"reading" must be one of: ${DICE_READINGS.join(", ")}`);
10761
+ let notation;
10762
+ try {
10763
+ notation = parseNotation(check.dice);
10764
+ } catch (error) {
10765
+ problems.push(`"dice": ${error instanceof Error ? error.message : String(error)}`);
10766
+ }
10767
+ if (check.bands.length === 0) problems.push("it needs at least one band");
10768
+ if (check.bands.length > 20) problems.push(`it has more than 20 bands`);
10769
+ check.bands.forEach((band, index) => {
10770
+ if (band.label.trim() === "") problems.push(`band ${index + 1} needs a label`);
10771
+ if (!DICE_RESULTS.includes(band.result)) problems.push(`band ${index + 1}: "result" must be one of: ${DICE_RESULTS.join(", ")}`);
10772
+ if (band.min !== void 0 && band.max !== void 0 && band.min > band.max) problems.push(`band ${index + 1}: min is above max`);
10773
+ });
10774
+ if (check.reading === "successes") {
10775
+ const sides = notation?.terms.flatMap((term) => "sides" in term ? [term.sides] : []) ?? [];
10776
+ if (check.hit === void 0 || !Number.isInteger(check.hit) || check.hit < 1 || sides.length > 0 && check.hit > Math.max(...sides)) problems.push("the reading \"successes\" needs \"hit\": the lowest face that counts");
10777
+ }
10778
+ if (notation !== void 0 && problems.length === 0 && (check.compare ?? "absolute") === "absolute" && check.modifier === void 0) {
10779
+ const { min, max } = check.reading === "successes" ? {
10780
+ min: 0,
10781
+ max: notation.terms.reduce((sum, term) => sum + ("count" in term ? term.count : 0), 0)
10782
+ } : notationRange(notation);
10783
+ for (let value = min; value <= max; value += 1) if (!check.bands.some((band) => within(band, value))) {
10784
+ problems.push(`no band holds ${value}, which the dice can give`);
10785
+ break;
10786
+ }
10787
+ }
10788
+ return problems;
10789
+ }
10790
+ function rollPlain(expression, roll = systemRoller) {
10791
+ const notation = parseNotation(expression);
10792
+ const rolled = rollNotation(notation, roll);
10793
+ return {
10794
+ expression: notation.text,
10795
+ faces: rolled.faces,
10796
+ kept: rolled.kept,
10797
+ modifier: rolled.constant,
10798
+ total: rolled.total
10799
+ };
10800
+ }
10801
+ //#endregion
10802
+ //#region src/tools/dice.ts
10803
+ const ROLL_DICE_TOOL = "roll_dice";
10804
+ const ROLL_CHECK_TOOL = "roll_check";
10805
+ const LIST_CHECKS_TOOL = "list_checks";
10806
+ const sign = (value) => value >= 0 ? `+${value}` : String(value);
10807
+ /** How a roll reads to the agent that asked for it: what fell, what it adds up to, and what the world's check makes of it. */
10808
+ function describeRoll(roll, check) {
10809
+ const dice = `[${roll.faces.join(", ")}]${roll.kept.length < roll.faces.length ? ` keeping [${roll.kept.join(", ")}]` : ""}`;
10810
+ const lines = [`Roll ${roll.id}: ${roll.expression}${check === void 0 ? "" : ` for the check "${check.name}"`}${roll.character === void 0 ? "" : ` (${roll.character})`} - ${dice}${roll.modifier === 0 ? "" : ` ${sign(roll.modifier)}`} = ${roll.total}${check?.reading === "successes" ? " successes" : ""}.`];
10811
+ if (roll.target !== void 0) lines.push(`Against ${roll.target}: ${roll.value >= 0 ? "+" : ""}${roll.value}.`);
10812
+ if (roll.readout !== void 0) lines.push(`That is: ${roll.readout.label}${roll.readout.critical ? " (critical)" : ""}; the result is ${roll.readout.result}. This is decided by the dice: do not change it, narrate it.${check === void 0 || roll.character === void 0 ? "" : ` In resolve_round give ${roll.character}'s outcome this result and cite the roll: roll ${roll.id}.`}`);
10813
+ return lines.join(" ");
10814
+ }
10815
+ const rolled = (_args, value) => [{
10816
+ type: "text",
10817
+ text: value.text
10818
+ }];
10819
+ /**
10820
+ * Tools that roll dice for an agent, so that numbers are not made up. `roll_check` rolls a check the story defines and reads
10821
+ * the result with the check's own bands; `roll_dice` rolls plain notation for what has no check. The roller is the system's
10822
+ * source of randomness unless one is given.
10823
+ */
10824
+ function createDiceTools(resolve, options = {}) {
10825
+ const roller = options.roller ?? systemRoller;
10826
+ const linkOf = (agent) => {
10827
+ const found = agent === void 0 ? void 0 : resolve(agent.session.id);
10828
+ if (found === void 0) throw new NotFoundError("this agent is not working in a roleplay session, so there are no dice to roll");
10829
+ return found;
10830
+ };
10831
+ return [
10832
+ defineTool({
10833
+ name: LIST_CHECKS_TOOL,
10834
+ description: "List the checks this story defines for settling uncertain attempts: when each is used, the dice it rolls and how its result is read.",
10835
+ parameters: {},
10836
+ output: {
10837
+ schema: { type: "json" },
10838
+ render: (_args, value) => [{
10839
+ type: "text",
10840
+ text: JSON.stringify(value, null, 2)
10841
+ }]
10842
+ },
10843
+ isConcurrencySafe: () => true,
10844
+ execute(_args, exec) {
10845
+ const { session } = linkOf(exec.agent);
10846
+ return Promise.resolve(session.dice.checks().map((check) => ({
10847
+ key: check.key,
10848
+ name: check.name,
10849
+ use: check.use,
10850
+ dice: check.dice,
10851
+ compare: check.compare ?? "absolute",
10852
+ ...check.modifier === void 0 ? {} : { modifier: `the character's ${check.modifier}` },
10853
+ bands: check.bands.map((band) => ({
10854
+ ...band.min === void 0 ? {} : { from: band.min },
10855
+ ...band.max === void 0 ? {} : { to: band.max },
10856
+ label: band.label,
10857
+ result: band.result
10858
+ }))
10859
+ })));
10860
+ }
10861
+ }),
10862
+ defineTool({
10863
+ name: ROLL_CHECK_TOOL,
10864
+ description: "Roll one of the story's checks for an attempt whose outcome is uncertain and matters: the dice are rolled for you and the result is read with the check's own bands, so you never choose or make up a number. Use the check whose \"use\" fits (list_checks shows them). Roll once per attempt, before you decide what happens, and let the result stand: a failed roll is a failure, whatever the story would like.",
10865
+ parameters: {
10866
+ check: {
10867
+ type: "string",
10868
+ required: true,
10869
+ description: "The key of the check, as list_checks gives it."
10870
+ },
10871
+ reason: {
10872
+ type: "string",
10873
+ required: true,
10874
+ description: "What is attempted, by whom, in a sentence."
10875
+ },
10876
+ character: {
10877
+ type: "string",
10878
+ required: true,
10879
+ description: "The key of the character the attempt is for (the player's character included): the roll is theirs, and their outcome in resolve_round takes its result from it."
10880
+ },
10881
+ target: {
10882
+ type: "integer",
10883
+ description: "When the check is read against a target (list_checks says compare over or under): the difficulty, the number the total must meet or beat (or stay under). Set it by how hard the attempt is, using what the check's \"use\" says about difficulty; never 0 to make it easy."
10884
+ },
10885
+ bonus: {
10886
+ type: "integer",
10887
+ description: "What the circumstances add to or take from the roll, from -10 to 10; leave out for none."
10888
+ }
10889
+ },
10890
+ output: {
10891
+ schema: { type: "json" },
10892
+ render: rolled
10893
+ },
10894
+ isConcurrencySafe: () => false,
10895
+ execute(args, exec) {
10896
+ const { session, turn } = linkOf(exec.agent);
10897
+ const check = session.dice.check(args.check);
10898
+ if (check === void 0) throw new NotFoundError(`there is no check "${args.check}"; list_checks shows the story's checks`);
10899
+ if (session.characters.list().every((entry) => characterKey(entry) !== args.character)) throw new NotFoundError(`there is no character "${args.character}"; list_characters shows the keys`);
10900
+ let modifier;
10901
+ if (check.modifier !== void 0) {
10902
+ const who = session.characters.list().find((entry) => characterKey(entry) === args.character);
10903
+ if (who === void 0) throw new NotFoundError(`there is no character "${args.character}"; list_characters shows the keys`);
10904
+ const value = session.variables.value(check.modifier, "character", who.id);
10905
+ if (typeof value !== "number") throw new NotFoundError(`the character's ${check.modifier} is not a number, so the check cannot add it`);
10906
+ modifier = value;
10907
+ }
10908
+ const bonus = Math.max(-10, Math.min(10, args.bonus ?? 0));
10909
+ const made = rollCheck(check, {
10910
+ ...modifier === void 0 ? {} : { modifier },
10911
+ bonus,
10912
+ ...args.target === void 0 ? {} : { target: args.target }
10913
+ }, roller);
10914
+ const roll = session.dice.record({
10915
+ turn,
10916
+ check: check.key,
10917
+ expression: check.dice,
10918
+ faces: made.faces,
10919
+ kept: made.kept,
10920
+ modifier: made.modifier,
10921
+ ...made.target === void 0 ? {} : { target: made.target },
10922
+ total: made.total,
10923
+ value: made.value,
10924
+ readout: made.readout,
10925
+ reason: args.reason,
10926
+ character: args.character,
10927
+ by: options.by ?? "agent"
10928
+ });
10929
+ return Promise.resolve({
10930
+ roll: roll.id,
10931
+ check: check.key,
10932
+ total: roll.total,
10933
+ result: made.readout.result,
10934
+ label: made.readout.label,
10935
+ critical: made.readout.critical,
10936
+ text: describeRoll(roll, check)
10937
+ });
10938
+ }
10939
+ }),
10940
+ defineTool({
10941
+ name: ROLL_DICE_TOOL,
10942
+ description: "Roll dice for something the story has no check for (damage, a random pick, who goes first, a rumour table): the dice are rolled for you, so you never make up a number. Write the notation like 1d6, 2d4+1, 4d6kh3 (keep the highest 3) or d100. What the number means is for you to decide from the rules you were given; when a check fits the attempt, use roll_check instead.",
10943
+ parameters: {
10944
+ expression: {
10945
+ type: "string",
10946
+ required: true,
10947
+ description: "Dice notation, for example 2d6+1."
10948
+ },
10949
+ reason: {
10950
+ type: "string",
10951
+ required: true,
10952
+ description: "What the roll is for, in a sentence."
10953
+ },
10954
+ character: {
10955
+ type: "string",
10956
+ description: "The key of the character it is for, when it is for one."
10957
+ }
10958
+ },
10959
+ output: {
10960
+ schema: { type: "json" },
10961
+ render: rolled
10962
+ },
10963
+ isConcurrencySafe: () => false,
10964
+ execute(args, exec) {
10965
+ const { session, turn } = linkOf(exec.agent);
10966
+ const made = rollPlain(args.expression, roller);
10967
+ const roll = session.dice.record({
10968
+ turn,
10969
+ expression: made.expression,
10970
+ faces: made.faces,
10971
+ kept: made.kept,
10972
+ modifier: made.modifier,
10973
+ total: made.total,
10974
+ value: made.total,
10975
+ reason: args.reason,
10976
+ ...args.character === void 0 ? {} : { character: args.character },
10977
+ by: options.by ?? "agent"
10978
+ });
10979
+ return Promise.resolve({
10980
+ roll: roll.id,
10981
+ total: roll.total,
10982
+ text: describeRoll(roll)
10983
+ });
10984
+ }
10985
+ })
10986
+ ];
10987
+ }
10988
+ //#endregion
10447
10989
  //#region src/records/lint.ts
10448
10990
  /**
10449
10991
  * What is inconsistent or unfinished in the world's graph of places and characters. Things made up in
@@ -12031,7 +12573,8 @@ function createTools(resolve, options) {
12031
12573
  //#region src/plugin.ts
12032
12574
  const Config = z.object({
12033
12575
  journalMode: z.union(["delete", "wal"]).default("delete"),
12034
- writeTools: z.boolean().default(false)
12576
+ writeTools: z.boolean().default(false),
12577
+ diceTools: z.boolean().default(false)
12035
12578
  });
12036
12579
  /**
12037
12580
  * Roleplay session state for other plugins. It creates and opens sessions
@@ -12057,6 +12600,7 @@ var RoleplayState = class extends Service {
12057
12600
  };
12058
12601
  ctx.inject(["tools"], (scope) => {
12059
12602
  for (const tool of createTools(resolve, { write: config.writeTools ?? false })) scope.tools.register(tool);
12603
+ if (config.diceTools === true) for (const tool of createDiceTools(resolve)) scope.tools.register(tool);
12060
12604
  });
12061
12605
  }
12062
12606
  /**
@@ -12192,6 +12736,7 @@ var RoleplayState = class extends Service {
12192
12736
  routines: store.routines,
12193
12737
  engagements: store.engagements,
12194
12738
  triggers: store.triggers,
12739
+ dice: store.dice,
12195
12740
  planMove: (characterId, move) => store.planMove(characterId, move),
12196
12741
  planKnownMove: (characterId, move) => store.planKnownMove(characterId, move),
12197
12742
  move: (characterId, move) => store.move(characterId, move),
@@ -12461,6 +13006,6 @@ function createCharacterTools(link, characterId) {
12461
13006
  ];
12462
13007
  }
12463
13008
  //#endregion
12464
- export { ACCURACIES, CHARACTER_CONTROLS, CHARACTER_KINDS, CHARACTER_ROLES, CHARACTER_TIERS, CLOCK_MODES, COMPARISONS, CONSOLIDATION_KINDS, CONTAINER_STATES, Config, ConflictError, CorruptDataError, DAY, DAY_PARTS, DEFAULT_CONFIDENCE, DEFAULT_MEMORY_POLICY, DEFAULT_SLOTS, EMOTION_VARIABLES, ENGAGEMENT_GRACE, ENGAGEMENT_STATUSES, EVIDENCE_RELATIONS, EXIT_STATES, HOUR, IMPORTANCE_MAX, IMPORTANCE_MIN, INTENT_HORIZONS, INTENT_STATUSES, INTENT_TYPES, ITEM_ACTIONS, InvalidEditError, InvalidMoveError, InvalidValueError, KNOWLEDGE_KINDS, LANGUAGES, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_LOCATION_FEATURES, MAX_ROUTINE_WINDOWS, MAX_SHOWN, MAX_TRIGGERS, MAX_VARIABLES, MEMBERSHIP_STATUSES, MEMORY_CONFIDENCES, MEMORY_KINDS, MIGRATION_BACKUP_FOLDER, MINUTE, MONTH_NAMES, NO_CALENDAR, NotFoundError, PERCEPTION_MODES, PLACEABLE_KINDS, PLOT_NUDGE_MODES, PLOT_STATUSES, RECALL_ITEM_KINDS, RoleplayState, LATEST_VERSION as SCHEMA_VERSION, SEASONS, SECOND, SELF, SESSION_DATABASE_FILE, SLOT_ID, SUMMARY_LEVELS, SchemaError, StateError, TEMPLATE_KINDS, THREAD_STATUSES, TIME_CHANGE_KINDS, TRIGGER_NOTE_KINDS, TRIGGER_STATUSES, UnwritableError, VARIABLE_KEY, VARIABLE_KINDS, VARIABLE_SUBJECTS, VARIABLE_VISIBILITIES, VISUAL_SUBJECT_KINDS, WEEKDAY_NAMES, addLocalDays, agendaAt, announcements, apply, applyItemPlan, applyRecordsEdit, audienceIds, catchUpPlan, changeItem, changeVariable, characterKey, clarityAt, clockTime, conditionHolds, createCharacterTools, createSession, createSessionStore, createTools, dayMatches, dayPart, dayTokenProblems, deleteSession, deliverNotes, denseScore, describeDuration, describeHolder, describeInstant, describeValue, finishRecall, fuseScores, holidaysOn, inAudience, inWindow, isSessionId, isTimeZone, itemKey, jitter, judgedStatements, languageOf, lapseNotes, lintWorld, listSessions, localIso, localTime, locationKey, mentioned, migrationBackupPath, moveCharacter, name, nextOccurrence, nextTimeOfDay, nodeSqliteDriver, occurrenceAt, openSession, openSessionStore, overlap, parseDayToken, parseInstant, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, placeholdersOf, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readCalendar, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, renderWords, retention, runTriggers, seasonOf, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemTimeZone, tokens, translated, translation, translator, triggerReferenceProblems, valueIssue, writeCalendar, zonedInstant };
13009
+ export { ACCURACIES, CHARACTER_CONTROLS, CHARACTER_KINDS, CHARACTER_ROLES, CHARACTER_TIERS, CLOCK_MODES, COMPARISONS, CONSOLIDATION_KINDS, CONTAINER_STATES, Config, ConflictError, CorruptDataError, DAY, DAY_PARTS, DEFAULT_CONFIDENCE, DEFAULT_MEMORY_POLICY, DEFAULT_SLOTS, DICE_COMPARISONS, DICE_READINGS, DICE_RESULTS, EMOTION_VARIABLES, ENGAGEMENT_GRACE, ENGAGEMENT_STATUSES, EVIDENCE_RELATIONS, EXIT_STATES, HOUR, IMPORTANCE_MAX, IMPORTANCE_MIN, INTENT_HORIZONS, INTENT_STATUSES, INTENT_TYPES, ITEM_ACTIONS, InvalidEditError, InvalidMoveError, InvalidValueError, KNOWLEDGE_KINDS, LANGUAGES, LIST_CHECKS_TOOL, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_DICE_BANDS, MAX_DICE_CHECKS, MAX_DICE_PER_ROLL, MAX_DICE_SIDES, MAX_DICE_TERMS, MAX_LOCATION_FEATURES, MAX_ROUTINE_WINDOWS, MAX_SHOWN, MAX_TRIGGERS, MAX_VARIABLES, MEMBERSHIP_STATUSES, MEMORY_CONFIDENCES, MEMORY_KINDS, MIGRATION_BACKUP_FOLDER, MINUTE, MONTH_NAMES, NO_CALENDAR, NotFoundError, PERCEPTION_MODES, PLACEABLE_KINDS, PLOT_NUDGE_MODES, PLOT_STATUSES, RECALL_ITEM_KINDS, ROLL_CHECK_TOOL, ROLL_DICE_TOOL, RoleplayState, LATEST_VERSION as SCHEMA_VERSION, SEASONS, SECOND, SELF, SESSION_DATABASE_FILE, SLOT_ID, SUMMARY_LEVELS, SchemaError, StateError, TEMPLATE_KINDS, THREAD_STATUSES, TIME_CHANGE_KINDS, TRIGGER_NOTE_KINDS, TRIGGER_STATUSES, UnwritableError, VARIABLE_KEY, VARIABLE_KINDS, VARIABLE_SUBJECTS, VARIABLE_VISIBILITIES, VISUAL_SUBJECT_KINDS, WEEKDAY_NAMES, addLocalDays, agendaAt, announcements, apply, applyItemPlan, applyRecordsEdit, audienceIds, catchUpPlan, changeItem, changeVariable, characterKey, checkProblems, clarityAt, clockTime, conditionHolds, createCharacterTools, createDiceTools, createSession, createSessionStore, createTools, dayMatches, dayPart, dayTokenProblems, deleteSession, deliverNotes, denseScore, describeDuration, describeHolder, describeInstant, describeRoll, describeValue, finishRecall, fuseScores, holidaysOn, inAudience, inWindow, isSessionId, isTimeZone, itemKey, jitter, judgedStatements, languageOf, lapseNotes, lintWorld, listSessions, localIso, localTime, locationKey, mentioned, migrationBackupPath, moveCharacter, name, nextOccurrence, nextTimeOfDay, nodeSqliteDriver, notationRange, occurrenceAt, openSession, openSessionStore, overlap, parseDayToken, parseInstant, parseNotation, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, placeholdersOf, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readBands, readCalendar, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, renderWords, retention, rollCheck, rollNotation, rollPlain, runTriggers, seasonOf, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemRoller, systemTimeZone, tokens, translated, translation, translator, triggerReferenceProblems, valueIssue, writeCalendar, zonedInstant };
12465
13010
 
12466
13011
  //# sourceMappingURL=index.js.map