run-dmcp 0.9.0 → 0.10.0

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.
@@ -0,0 +1,60 @@
1
+ import { type T } from "./t.js";
2
+ /**
3
+ * A principal is due to act (GitHub issue #40). The issue draws the line:
4
+ * "The engine records whose turn it is and refuses out-of-turn proposals if
5
+ * a game declares turns. It never runs the turn itself." So this module holds
6
+ * a DECLARATION -- which entities act, in what cycle, from which t -- and
7
+ * answers "who is due at t" for any t, past included. It never schedules,
8
+ * waits, notifies or calls anything: an MCP server answers calls, so
9
+ * whatever runs minds asks (pull), moves the clock with `advanceTurn`, and
10
+ * proposes as that principal. `resolve()` refuses a proposal whose `actor` is
11
+ * not the one due (resolve.ts, `out-of-turn`).
12
+ *
13
+ * WHY A COUNTER AXIS ONLY. A turn is a count of things that happened, which is
14
+ * exactly what `counter` means (t.ts). On `sequence` the engine's own writes
15
+ * advance t, so "one principal per t" would hand the turn over mid-action;
16
+ * on `elapsed` t is a measure, and turns-per-second is a policy this engine
17
+ * has no business guessing.
18
+ *
19
+ * WHY APPEND-ONLY. `dueAt(t)` for a past t is history, and this engine can
20
+ * always say what the world looked like at any point. A changed order is a
21
+ * NEW declaration from a later t; the old one keeps answering for the t it
22
+ * covered. The `turn_orders` triggers (schema.ts) make an edit
23
+ * unconstructable rather than discouraged.
24
+ *
25
+ * A principal is an entity of the game (DESIGN.md §8's admission criterion:
26
+ * it resolves to an entity the engine already has). The engine never learns
27
+ * what a principal is for.
28
+ */
29
+ export interface TurnDue {
30
+ t: T;
31
+ principal: string;
32
+ /** 1-based: which pass through the declared cycle this t belongs to. */
33
+ round: number;
34
+ /** 0-based position in the cycle. */
35
+ position: number;
36
+ /** The declaration this answer comes from. */
37
+ fromT: T;
38
+ }
39
+ export declare function declareTurnOrder(params: {
40
+ gameId: string;
41
+ principals: readonly string[];
42
+ fromT: T;
43
+ }): {
44
+ fromT: T;
45
+ principals: string[];
46
+ };
47
+ /** Who is due at `t`, or `null`: no declaration covers `t`, or `t` is not a
48
+ * whole step on from the declaration's `fromT`. */
49
+ export declare function dueAt(params: {
50
+ gameId: string;
51
+ t: T;
52
+ }): TurnDue | null;
53
+ /** Moves the clock to the next turn -- one step on, or to the first
54
+ * declaration's `fromT` if the clock has not reached it -- and says who is
55
+ * due there. Moves time only; runs nothing. */
56
+ export declare function advanceTurn(params: {
57
+ gameId: string;
58
+ }): TurnDue;
59
+ /** Whether the game has declared any turn order at all. */
60
+ export declare function hasTurnOrder(gameId: string): boolean;
@@ -0,0 +1,93 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { currentStoryTime, setStoryTime } from "./clock.js";
4
+ import { assertT } from "./t.js";
5
+ function declarationAt(gameId, t) {
6
+ return getDatabase()
7
+ .prepare(`SELECT from_t, principals FROM turn_orders WHERE game_id = ? AND from_t <= ? ORDER BY from_t DESC LIMIT 1`)
8
+ .get(gameId, t);
9
+ }
10
+ function latestDeclaration(gameId) {
11
+ return getDatabase()
12
+ .prepare(`SELECT from_t, principals FROM turn_orders WHERE game_id = ? ORDER BY from_t DESC LIMIT 1`)
13
+ .get(gameId);
14
+ }
15
+ export function declareTurnOrder(params) {
16
+ const { gameId, fromT } = params;
17
+ assertT(fromT);
18
+ const principals = [...params.principals];
19
+ const story = currentStoryTime(gameId);
20
+ if (!story || story.axis.kind !== "counter") {
21
+ throw new Error(`declareTurnOrder: game '${gameId}' must be on a counter axis -- a turn is a count of things that happened ` +
22
+ `(declare_time_axis with kind 'counter')`);
23
+ }
24
+ if (!Number.isInteger(fromT))
25
+ throw new Error(`declareTurnOrder: fromT must be an integer t on the counter axis, got ${fromT}`);
26
+ if (fromT < story.t) {
27
+ throw new Error(`declareTurnOrder: fromT ${fromT} is before the game's current t (${story.t}); turns already taken cannot be reordered`);
28
+ }
29
+ const latest = latestDeclaration(gameId);
30
+ // The current t may already be someone's turn (and a resolution may
31
+ // already record them acting in it); reassigning it would rewrite that.
32
+ if (latest && latest.from_t <= story.t && fromT <= story.t) {
33
+ throw new Error(`declareTurnOrder: t=${story.t} is already covered by the declaration from ${latest.from_t}; a new order can start at t=${story.t + 1} at the earliest`);
34
+ }
35
+ if (latest && fromT <= latest.from_t) {
36
+ throw new Error(`declareTurnOrder: fromT ${fromT} must come after the latest declaration's fromT (${latest.from_t})`);
37
+ }
38
+ if (principals.length === 0)
39
+ throw new Error(`declareTurnOrder: a turn order needs at least one principal`);
40
+ const seen = new Set();
41
+ const alive = getDatabase().prepare(`SELECT 1 FROM entities WHERE id = ? AND game_id = ? AND created_at_t <= ? AND (destroyed_at_t IS NULL OR destroyed_at_t > ?)`);
42
+ for (const principal of principals) {
43
+ if (typeof principal !== "string")
44
+ throw new Error(`declareTurnOrder: a principal must be an entity id, got ${JSON.stringify(principal)}`);
45
+ if (seen.has(principal))
46
+ throw new Error(`declareTurnOrder: principal '${principal}' appears twice in one cycle`);
47
+ seen.add(principal);
48
+ if (!alive.get(principal, gameId, fromT, fromT)) {
49
+ throw new Error(`declareTurnOrder: '${principal}' is not an entity of this game alive at t=${fromT}`);
50
+ }
51
+ }
52
+ getDatabase()
53
+ .prepare(`INSERT INTO turn_orders (id, game_id, from_t, principals, declared_at) VALUES (?, ?, ?, ?, ?)`)
54
+ .run(uuidv4(), gameId, fromT, JSON.stringify(principals), new Date().toISOString());
55
+ return { fromT, principals };
56
+ }
57
+ /** Who is due at `t`, or `null`: no declaration covers `t`, or `t` is not a
58
+ * whole step on from the declaration's `fromT`. */
59
+ export function dueAt(params) {
60
+ const { gameId, t } = params;
61
+ assertT(t);
62
+ const declaration = declarationAt(gameId, t);
63
+ if (!declaration)
64
+ return null;
65
+ const offset = t - declaration.from_t;
66
+ if (!Number.isInteger(offset))
67
+ return null;
68
+ const principals = JSON.parse(declaration.principals);
69
+ const position = offset % principals.length;
70
+ return { t, principal: principals[position], round: Math.floor(offset / principals.length) + 1, position, fromT: declaration.from_t };
71
+ }
72
+ /** Moves the clock to the next turn -- one step on, or to the first
73
+ * declaration's `fromT` if the clock has not reached it -- and says who is
74
+ * due there. Moves time only; runs nothing. */
75
+ export function advanceTurn(params) {
76
+ const { gameId } = params;
77
+ const story = currentStoryTime(gameId);
78
+ const first = getDatabase()
79
+ .prepare(`SELECT from_t FROM turn_orders WHERE game_id = ? ORDER BY from_t ASC LIMIT 1`)
80
+ .get(gameId);
81
+ if (!story || !first)
82
+ throw new Error(`advanceTurn: game '${gameId}' has no turn order declared (declare_turn_order)`);
83
+ const next = story.t < first.from_t ? first.from_t : Math.floor(story.t) + 1;
84
+ const due = dueAt({ gameId, t: next });
85
+ if (!due)
86
+ throw new Error(`advanceTurn: nobody is due at t=${next}`);
87
+ setStoryTime({ gameId, t: next });
88
+ return due;
89
+ }
90
+ /** Whether the game has declared any turn order at all. */
91
+ export function hasTurnOrder(gameId) {
92
+ return getDatabase().prepare(`SELECT 1 FROM turn_orders WHERE game_id = ? LIMIT 1`).get(gameId) !== undefined;
93
+ }
@@ -255,14 +255,14 @@ export declare const characterOutputSchema: {
255
255
  condition: z.ZodOptional<z.ZodString>;
256
256
  glowOrEffects: z.ZodOptional<z.ZodString>;
257
257
  }, "strip", z.ZodTypeAny, {
258
+ condition?: string | undefined;
258
259
  size?: string | undefined;
259
260
  material?: string | undefined;
260
- condition?: string | undefined;
261
261
  glowOrEffects?: string | undefined;
262
262
  }, {
263
+ condition?: string | undefined;
263
264
  size?: string | undefined;
264
265
  material?: string | undefined;
265
- condition?: string | undefined;
266
266
  glowOrEffects?: string | undefined;
267
267
  }>>;
268
268
  pose: z.ZodOptional<z.ZodString>;
@@ -299,9 +299,9 @@ export declare const characterOutputSchema: {
299
299
  vegetation?: string | undefined;
300
300
  } | undefined;
301
301
  objectDetails?: {
302
+ condition?: string | undefined;
302
303
  size?: string | undefined;
303
304
  material?: string | undefined;
304
- condition?: string | undefined;
305
305
  glowOrEffects?: string | undefined;
306
306
  } | undefined;
307
307
  pose?: string | undefined;
@@ -338,9 +338,9 @@ export declare const characterOutputSchema: {
338
338
  vegetation?: string | undefined;
339
339
  } | undefined;
340
340
  objectDetails?: {
341
+ condition?: string | undefined;
341
342
  size?: string | undefined;
342
343
  material?: string | undefined;
343
- condition?: string | undefined;
344
344
  glowOrEffects?: string | undefined;
345
345
  } | undefined;
346
346
  pose?: string | undefined;
@@ -587,9 +587,9 @@ export declare const characterOutputSchema: {
587
587
  vegetation?: string | undefined;
588
588
  } | undefined;
589
589
  objectDetails?: {
590
+ condition?: string | undefined;
590
591
  size?: string | undefined;
591
592
  material?: string | undefined;
592
- condition?: string | undefined;
593
593
  glowOrEffects?: string | undefined;
594
594
  } | undefined;
595
595
  pose?: string | undefined;
@@ -683,9 +683,9 @@ export declare const characterOutputSchema: {
683
683
  vegetation?: string | undefined;
684
684
  } | undefined;
685
685
  objectDetails?: {
686
+ condition?: string | undefined;
686
687
  size?: string | undefined;
687
688
  material?: string | undefined;
688
- condition?: string | undefined;
689
689
  glowOrEffects?: string | undefined;
690
690
  } | undefined;
691
691
  pose?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run-dmcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "An MCP server for LLM-run interactive fiction where the server owns what is true - including when it was true. A continuation of DMCP.",
5
5
  "license": "MIT",
6
6
  "author": "Derek Ferguson",