run-dmcp 0.1.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.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +79 -0
  3. package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
  4. package/dist/__tests__/engineVocabulary.test.js +147 -0
  5. package/dist/db/__tests__/connection.test.d.ts +1 -0
  6. package/dist/db/__tests__/connection.test.js +72 -0
  7. package/dist/db/__tests__/testDb.d.ts +33 -0
  8. package/dist/db/__tests__/testDb.js +41 -0
  9. package/dist/db/connection.d.ts +22 -0
  10. package/dist/db/connection.js +107 -0
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/db/schema.js +725 -0
  13. package/dist/events/emitter.d.ts +22 -0
  14. package/dist/events/emitter.js +71 -0
  15. package/dist/http/server.d.ts +3 -0
  16. package/dist/http/server.js +649 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +92 -0
  19. package/dist/register/abilities.d.ts +2 -0
  20. package/dist/register/abilities.js +165 -0
  21. package/dist/register/audio.d.ts +2 -0
  22. package/dist/register/audio.js +326 -0
  23. package/dist/register/batch.d.ts +2 -0
  24. package/dist/register/batch.js +343 -0
  25. package/dist/register/character.d.ts +2 -0
  26. package/dist/register/character.js +324 -0
  27. package/dist/register/combat.d.ts +2 -0
  28. package/dist/register/combat.js +207 -0
  29. package/dist/register/core.d.ts +2 -0
  30. package/dist/register/core.js +1040 -0
  31. package/dist/register/display.d.ts +2 -0
  32. package/dist/register/display.js +263 -0
  33. package/dist/register/factions.d.ts +2 -0
  34. package/dist/register/factions.js +186 -0
  35. package/dist/register/images.d.ts +2 -0
  36. package/dist/register/images.js +400 -0
  37. package/dist/register/inventory.d.ts +2 -0
  38. package/dist/register/inventory.js +115 -0
  39. package/dist/register/mcp-prompts.d.ts +2 -0
  40. package/dist/register/mcp-prompts.js +684 -0
  41. package/dist/register/mcp-resources.d.ts +2 -0
  42. package/dist/register/mcp-resources.js +335 -0
  43. package/dist/register/narrative.d.ts +2 -0
  44. package/dist/register/narrative.js +242 -0
  45. package/dist/register/notes.d.ts +2 -0
  46. package/dist/register/notes.js +170 -0
  47. package/dist/register/pause.d.ts +2 -0
  48. package/dist/register/pause.js +580 -0
  49. package/dist/register/quests.d.ts +2 -0
  50. package/dist/register/quests.js +118 -0
  51. package/dist/register/relationships.d.ts +2 -0
  52. package/dist/register/relationships.js +147 -0
  53. package/dist/register/resources.d.ts +2 -0
  54. package/dist/register/resources.js +277 -0
  55. package/dist/register/secrets.d.ts +2 -0
  56. package/dist/register/secrets.js +192 -0
  57. package/dist/register/status.d.ts +2 -0
  58. package/dist/register/status.js +130 -0
  59. package/dist/register/tables.d.ts +2 -0
  60. package/dist/register/tables.js +146 -0
  61. package/dist/register/tags.d.ts +2 -0
  62. package/dist/register/tags.js +114 -0
  63. package/dist/register/time.d.ts +2 -0
  64. package/dist/register/time.js +281 -0
  65. package/dist/register/world.d.ts +2 -0
  66. package/dist/register/world.js +127 -0
  67. package/dist/schemas/index.d.ts +921 -0
  68. package/dist/schemas/index.js +121 -0
  69. package/dist/test-setup.d.ts +1 -0
  70. package/dist/test-setup.js +13 -0
  71. package/dist/tools/__tests__/audio.test.d.ts +1 -0
  72. package/dist/tools/__tests__/audio.test.js +59 -0
  73. package/dist/tools/__tests__/conserved.test.d.ts +1 -0
  74. package/dist/tools/__tests__/conserved.test.js +488 -0
  75. package/dist/tools/__tests__/constraint.test.d.ts +1 -0
  76. package/dist/tools/__tests__/constraint.test.js +212 -0
  77. package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
  78. package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
  79. package/dist/tools/__tests__/images.test.d.ts +1 -0
  80. package/dist/tools/__tests__/images.test.js +59 -0
  81. package/dist/tools/__tests__/relationship.test.d.ts +1 -0
  82. package/dist/tools/__tests__/relationship.test.js +132 -0
  83. package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
  84. package/dist/tools/__tests__/resource-constraints.test.js +131 -0
  85. package/dist/tools/__tests__/resource.test.d.ts +1 -0
  86. package/dist/tools/__tests__/resource.test.js +190 -0
  87. package/dist/tools/__tests__/time.test.d.ts +1 -0
  88. package/dist/tools/__tests__/time.test.js +404 -0
  89. package/dist/tools/__tests__/timers.test.d.ts +1 -0
  90. package/dist/tools/__tests__/timers.test.js +426 -0
  91. package/dist/tools/__tests__/world.test.d.ts +1 -0
  92. package/dist/tools/__tests__/world.test.js +70 -0
  93. package/dist/tools/ability.d.ts +48 -0
  94. package/dist/tools/ability.js +238 -0
  95. package/dist/tools/audio.d.ts +24 -0
  96. package/dist/tools/audio.js +365 -0
  97. package/dist/tools/character.d.ts +70 -0
  98. package/dist/tools/character.js +309 -0
  99. package/dist/tools/combat.d.ts +13 -0
  100. package/dist/tools/combat.js +195 -0
  101. package/dist/tools/constraint.d.ts +132 -0
  102. package/dist/tools/constraint.js +269 -0
  103. package/dist/tools/dice.d.ts +23 -0
  104. package/dist/tools/dice.js +111 -0
  105. package/dist/tools/display.d.ts +120 -0
  106. package/dist/tools/display.js +528 -0
  107. package/dist/tools/faction.d.ts +61 -0
  108. package/dist/tools/faction.js +269 -0
  109. package/dist/tools/game.d.ts +96 -0
  110. package/dist/tools/game.js +526 -0
  111. package/dist/tools/image-prompt.d.ts +49 -0
  112. package/dist/tools/image-prompt.js +479 -0
  113. package/dist/tools/images.d.ts +47 -0
  114. package/dist/tools/images.js +449 -0
  115. package/dist/tools/inventory.d.ts +20 -0
  116. package/dist/tools/inventory.js +145 -0
  117. package/dist/tools/narrative.d.ts +58 -0
  118. package/dist/tools/narrative.js +237 -0
  119. package/dist/tools/notes.d.ts +41 -0
  120. package/dist/tools/notes.js +220 -0
  121. package/dist/tools/pause.d.ts +110 -0
  122. package/dist/tools/pause.js +1254 -0
  123. package/dist/tools/quest.d.ts +34 -0
  124. package/dist/tools/quest.js +164 -0
  125. package/dist/tools/relationship.d.ts +74 -0
  126. package/dist/tools/relationship.js +324 -0
  127. package/dist/tools/resource.d.ts +93 -0
  128. package/dist/tools/resource.js +374 -0
  129. package/dist/tools/rules.d.ts +4 -0
  130. package/dist/tools/rules.js +30 -0
  131. package/dist/tools/secrets.d.ts +49 -0
  132. package/dist/tools/secrets.js +195 -0
  133. package/dist/tools/status.d.ts +36 -0
  134. package/dist/tools/status.js +218 -0
  135. package/dist/tools/tables.d.ts +33 -0
  136. package/dist/tools/tables.js +209 -0
  137. package/dist/tools/tags.d.ts +52 -0
  138. package/dist/tools/tags.js +176 -0
  139. package/dist/tools/time.d.ts +33 -0
  140. package/dist/tools/time.js +276 -0
  141. package/dist/tools/timers.d.ts +41 -0
  142. package/dist/tools/timers.js +215 -0
  143. package/dist/tools/world.d.ts +78 -0
  144. package/dist/tools/world.js +331 -0
  145. package/dist/types/index.d.ts +969 -0
  146. package/dist/types/index.js +1 -0
  147. package/dist/utils/__tests__/json.test.d.ts +1 -0
  148. package/dist/utils/__tests__/json.test.js +55 -0
  149. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  150. package/dist/utils/__tests__/validation.test.js +90 -0
  151. package/dist/utils/errors.d.ts +44 -0
  152. package/dist/utils/errors.js +121 -0
  153. package/dist/utils/json.d.ts +9 -0
  154. package/dist/utils/json.js +23 -0
  155. package/dist/utils/logger.d.ts +7 -0
  156. package/dist/utils/logger.js +50 -0
  157. package/dist/utils/output-schemas.d.ts +594 -0
  158. package/dist/utils/output-schemas.js +331 -0
  159. package/dist/utils/tool-annotations.d.ts +147 -0
  160. package/dist/utils/tool-annotations.js +98 -0
  161. package/dist/utils/validation.d.ts +34 -0
  162. package/dist/utils/validation.js +52 -0
  163. package/dist/utils/verbosity.d.ts +57 -0
  164. package/dist/utils/verbosity.js +67 -0
  165. package/dist/utils/webui.d.ts +20 -0
  166. package/dist/utils/webui.js +35 -0
  167. package/package.json +75 -0
@@ -0,0 +1,269 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { validateGameExists } from "./game.js";
4
+ import { getResource } from "./resource.js";
5
+ /**
6
+ * Declarative, opt-in, server-enforced invariants on `resources` rows.
7
+ *
8
+ * A resource with no declared constraint behaves exactly as it always has --
9
+ * out-of-bounds writes are silently clamped by clampValue() in resource.ts.
10
+ * Declaring a constraint here changes that contract for that one resource
11
+ * (or set of resources, for 'conserved') only.
12
+ *
13
+ * SCOPE NOTE: this layer covers the `resources` table only. Three other
14
+ * numeric write paths bypass it entirely and are NOT covered:
15
+ * - factions.resources (JSON blob; see modifyFactionResource/setFactionResource
16
+ * in src/tools/faction.ts -- no bounds, no history, silently deletes an
17
+ * entry when it hits <= 0)
18
+ * - characters.attributes (arbitrary numeric JSON, unvalidated)
19
+ * - relationships.value (its own separate system)
20
+ * A game author who needs a server-enforced invariant on a quantity must
21
+ * model that quantity as a `resources` row, not one of the above.
22
+ *
23
+ * 'conserved' is fully enforced: a resource that is a member of a declared
24
+ * 'conserved' constraint can no longer be written directly through
25
+ * update_resource_value (see checkResourceConstraints() below) -- it must go
26
+ * through transferResourceValue() in resource.ts, which moves value between
27
+ * exactly two members of the same set atomically. See the comment on
28
+ * transferResourceValue() for why an explicit transfer, rather than a
29
+ * balanced multi-resource write, was chosen.
30
+ */
31
+ /** Absolute tolerance for floating-point sum comparisons on 'conserved'
32
+ * constraints. IEEE 754 doubles cannot represent values like 0.1 exactly,
33
+ * so repeated addition/subtraction across many transfers can drift by a
34
+ * few ULPs. This is large enough to absorb that drift over realistic
35
+ * transfer volumes while still catching an actual logic bug (which would
36
+ * typically desync the sum by a whole `amount`, not a fraction of one). */
37
+ export const CONSERVED_SUM_EPSILON = 1e-6;
38
+ export class ConstraintViolationError extends Error {
39
+ constraintKind;
40
+ resourceId;
41
+ constructor(constraintKind, resourceId, message) {
42
+ super(message);
43
+ this.constraintKind = constraintKind;
44
+ this.resourceId = resourceId;
45
+ this.name = "ConstraintViolationError";
46
+ }
47
+ }
48
+ function memberIdsFor(constraintId) {
49
+ const db = getDatabase();
50
+ const rows = db
51
+ .prepare(`SELECT resource_id FROM resource_constraint_members WHERE constraint_id = ? ORDER BY rowid`)
52
+ .all(constraintId);
53
+ return rows.map((r) => r.resource_id);
54
+ }
55
+ function rowToConstraint(row) {
56
+ return {
57
+ id: row.id,
58
+ gameId: row.game_id,
59
+ kind: row.kind,
60
+ resourceIds: memberIdsFor(row.id),
61
+ direction: row.direction,
62
+ total: row.total,
63
+ createdAt: row.created_at,
64
+ };
65
+ }
66
+ function insertConstraint(gameId, kind, resourceIds, direction, total) {
67
+ const db = getDatabase();
68
+ const id = uuidv4();
69
+ const now = new Date().toISOString();
70
+ db.prepare(`INSERT INTO resource_constraints (id, game_id, kind, direction, total, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(id, gameId, kind, direction, total, now);
71
+ const memberStmt = db.prepare(`INSERT INTO resource_constraint_members (constraint_id, resource_id) VALUES (?, ?)`);
72
+ for (const resourceId of resourceIds) {
73
+ memberStmt.run(id, resourceId);
74
+ }
75
+ return {
76
+ id,
77
+ gameId,
78
+ kind,
79
+ resourceIds,
80
+ direction,
81
+ total,
82
+ createdAt: now,
83
+ };
84
+ }
85
+ /**
86
+ * Declare a 'bounded' constraint: the resource's value must stay within its
87
+ * existing minValue/maxValue (set via create_resource/update_resource).
88
+ * Once declared, out-of-bounds writes through updateResourceValue() are
89
+ * REJECTED instead of silently clamped -- see checkResourceConstraints().
90
+ */
91
+ export function declareBoundedConstraint(params) {
92
+ validateGameExists(params.gameId);
93
+ const resource = getResource(params.resourceId);
94
+ if (!resource) {
95
+ throw new Error(`Resource '${params.resourceId}' not found.`);
96
+ }
97
+ if (resource.minValue === null && resource.maxValue === null) {
98
+ throw new Error(`Resource '${params.resourceId}' has neither minValue nor maxValue set. ` +
99
+ `A 'bounded' constraint enforces those bounds by rejecting instead of clamping -- ` +
100
+ `set at least one bound with update_resource before declaring this constraint.`);
101
+ }
102
+ if (getConstraintsForResource(params.resourceId).some((c) => c.kind === "bounded")) {
103
+ throw new Error(`Resource '${params.resourceId}' already has a 'bounded' constraint.`);
104
+ }
105
+ return insertConstraint(params.gameId, "bounded", [params.resourceId], null, null);
106
+ }
107
+ /**
108
+ * Declare a 'monotonic' constraint: the resource's value may only move in
109
+ * the given direction. 'increasing' means it may never decrease;
110
+ * 'decreasing' means it may never increase. Holding steady is always
111
+ * allowed.
112
+ */
113
+ export function declareMonotonicConstraint(params) {
114
+ validateGameExists(params.gameId);
115
+ const resource = getResource(params.resourceId);
116
+ if (!resource) {
117
+ throw new Error(`Resource '${params.resourceId}' not found.`);
118
+ }
119
+ if (getConstraintsForResource(params.resourceId).some((c) => c.kind === "monotonic")) {
120
+ throw new Error(`Resource '${params.resourceId}' already has a 'monotonic' constraint.`);
121
+ }
122
+ return insertConstraint(params.gameId, "monotonic", [params.resourceId], params.direction, null);
123
+ }
124
+ /**
125
+ * Register a 'conserved' constraint: a set of resources that must always
126
+ * sum to a fixed total.
127
+ *
128
+ * Validation performed here, all rejecting rather than silently coercing:
129
+ * - at least two distinct resources
130
+ * - every resourceId must exist
131
+ * - `total` must be a finite number
132
+ * - no resource may already belong to another 'conserved' constraint --
133
+ * a resource can be a member of at most one conserved set at a time, so
134
+ * that transferResourceValue() (resource.ts) never has to guess which
135
+ * set a transfer touching that resource is supposed to preserve
136
+ * - the members' CURRENT values must already sum to `total`. This
137
+ * constraint records an existing invariant, it does not establish one --
138
+ * if the values don't already sum to `total`, that's almost always a
139
+ * caller mistake (wrong total, forgot a member), and silently rewriting
140
+ * resource values to make it true would hide that mistake.
141
+ *
142
+ * A resource may still separately hold a 'bounded' and/or 'monotonic'
143
+ * constraint alongside 'conserved' -- those are orthogonal and both remain
144
+ * enforced during transfers (see transferResourceValue()).
145
+ */
146
+ export function declareConservedConstraint(params) {
147
+ validateGameExists(params.gameId);
148
+ const uniqueIds = Array.from(new Set(params.resourceIds));
149
+ if (uniqueIds.length < 2) {
150
+ throw new Error("A 'conserved' constraint requires at least two distinct resources.");
151
+ }
152
+ if (!Number.isFinite(params.total)) {
153
+ throw new Error(`'total' must be a finite number for a 'conserved' constraint; got ${params.total}.`);
154
+ }
155
+ let sum = 0;
156
+ for (const resourceId of uniqueIds) {
157
+ const resource = getResource(resourceId);
158
+ if (!resource) {
159
+ throw new Error(`Resource '${resourceId}' not found.`);
160
+ }
161
+ if (getConstraintsForResource(resourceId).some((c) => c.kind === "conserved")) {
162
+ throw new Error(`Resource '${resourceId}' already belongs to a 'conserved' constraint. A resource may only be a ` +
163
+ `member of one 'conserved' set at a time -- remove the existing constraint first (remove_resource_constraint) ` +
164
+ `if you need to redefine the set it belongs to.`);
165
+ }
166
+ sum += resource.value;
167
+ }
168
+ if (Math.abs(sum - params.total) > CONSERVED_SUM_EPSILON) {
169
+ throw new Error(`Cannot declare a 'conserved' constraint with total ${params.total}: the current values of ` +
170
+ `[${uniqueIds.join(", ")}] sum to ${sum}, not ${params.total}. Adjust the resources' values (via ` +
171
+ `update_resource_value, before declaring the constraint) or the declared total so they already match -- ` +
172
+ `declaring this constraint does not rewrite resource values to make a mismatched total true.`);
173
+ }
174
+ return insertConstraint(params.gameId, "conserved", uniqueIds, null, params.total);
175
+ }
176
+ /** List constraints for a game, optionally filtered to ones governing a given resource. */
177
+ export function listConstraints(gameId, resourceId) {
178
+ const db = getDatabase();
179
+ const rows = db
180
+ .prepare(`SELECT * FROM resource_constraints WHERE game_id = ? ORDER BY created_at`)
181
+ .all(gameId);
182
+ const constraints = rows.map(rowToConstraint);
183
+ if (resourceId === undefined)
184
+ return constraints;
185
+ return constraints.filter((c) => c.resourceIds.includes(resourceId));
186
+ }
187
+ /** All constraints (of any kind) that govern the given resource. */
188
+ export function getConstraintsForResource(resourceId) {
189
+ const db = getDatabase();
190
+ const rows = db
191
+ .prepare(`SELECT rc.* FROM resource_constraints rc
192
+ JOIN resource_constraint_members rcm ON rcm.constraint_id = rc.id
193
+ WHERE rcm.resource_id = ?
194
+ ORDER BY rc.created_at`)
195
+ .all(resourceId);
196
+ return rows.map(rowToConstraint);
197
+ }
198
+ /** Remove a constraint by id. Returns true if a row was deleted. */
199
+ export function removeConstraint(id) {
200
+ const db = getDatabase();
201
+ const result = db.prepare(`DELETE FROM resource_constraints WHERE id = ?`).run(id);
202
+ return result.changes > 0;
203
+ }
204
+ /**
205
+ * Check whether an intended value change for a single resource would
206
+ * violate any 'bounded' or 'monotonic' constraint declared on it. Throws
207
+ * ConstraintViolationError on violation; returns void otherwise. Shared by
208
+ * checkResourceConstraints() (the single-resource write path) and
209
+ * transferResourceValue() (resource.ts; the two-resource conserved-transfer
210
+ * path) -- both paths must respect 'bounded'/'monotonic' the same way.
211
+ * Deliberately does not look at 'conserved' constraints; callers decide
212
+ * separately what to do about those (see checkResourceConstraints() below
213
+ * and transferResourceValue()).
214
+ */
215
+ export function checkBoundedAndMonotonicConstraints(resourceId, previousValue, intendedValue, bounds) {
216
+ const constraints = getConstraintsForResource(resourceId);
217
+ for (const constraint of constraints) {
218
+ if (constraint.kind === "monotonic") {
219
+ if (constraint.direction === "increasing" && intendedValue < previousValue) {
220
+ throw new ConstraintViolationError("monotonic", resourceId, `Resource '${resourceId}' is constrained to never decrease; rejected change from ${previousValue} to ${intendedValue}.`);
221
+ }
222
+ if (constraint.direction === "decreasing" && intendedValue > previousValue) {
223
+ throw new ConstraintViolationError("monotonic", resourceId, `Resource '${resourceId}' is constrained to never increase; rejected change from ${previousValue} to ${intendedValue}.`);
224
+ }
225
+ }
226
+ if (constraint.kind === "bounded") {
227
+ if (bounds.minValue !== null && intendedValue < bounds.minValue) {
228
+ throw new ConstraintViolationError("bounded", resourceId, `Resource '${resourceId}' is bounded-constrained (min ${bounds.minValue}); rejected value ${intendedValue} instead of clamping.`);
229
+ }
230
+ if (bounds.maxValue !== null && intendedValue > bounds.maxValue) {
231
+ throw new ConstraintViolationError("bounded", resourceId, `Resource '${resourceId}' is bounded-constrained (max ${bounds.maxValue}); rejected value ${intendedValue} instead of clamping.`);
232
+ }
233
+ }
234
+ // constraint.kind === "conserved": handled by callers, not here.
235
+ }
236
+ }
237
+ /**
238
+ * Check whether an intended value change for a single resource would
239
+ * violate any constraint declared on it. Throws ConstraintViolationError on
240
+ * violation; returns void otherwise. Called from updateResourceValue() in
241
+ * resource.ts before the row is written.
242
+ *
243
+ * 'conserved' is handled specially here: ANY direct single-resource write to
244
+ * a conserved member is rejected, unconditionally, regardless of whether the
245
+ * particular delta would happen to preserve the total. The server cannot
246
+ * know where update_resource_value's counterpart delta should come from --
247
+ * writing one member without atomically adjusting another would silently
248
+ * break the set's invariant, which is exactly the failure this constraint
249
+ * exists to prevent. Use transfer_resource_value (transferResourceValue() in
250
+ * resource.ts) instead, which moves value between two members of the same
251
+ * set atomically.
252
+ */
253
+ export function checkResourceConstraints(resourceId, previousValue, intendedValue, bounds) {
254
+ const constraints = getConstraintsForResource(resourceId);
255
+ checkBoundedAndMonotonicConstraints(resourceId, previousValue, intendedValue, bounds);
256
+ const conserved = constraints.find((c) => c.kind === "conserved");
257
+ if (conserved) {
258
+ throw new ConstraintViolationError("conserved", resourceId, `Resource '${resourceId}' is a member of a 'conserved' constraint (id '${conserved.id}', total ${conserved.total}) ` +
259
+ `and cannot be written directly via update_resource_value -- a single-resource write is ambiguous about where ` +
260
+ `the counterpart delta should come from, and could silently break the set's total. Use transfer_resource_value ` +
261
+ `to move value between two members of this set atomically instead.`);
262
+ }
263
+ }
264
+ /** All conserved constraints (of kind 'conserved') governing a resource,
265
+ * i.e. zero or one (declareConservedConstraint() rejects overlapping
266
+ * conserved membership, so a resource can belong to at most one). */
267
+ export function getConservedConstraintFor(resourceId) {
268
+ return getConstraintsForResource(resourceId).find((c) => c.kind === "conserved") ?? null;
269
+ }
@@ -0,0 +1,23 @@
1
+ import type { DiceRoll, CheckResult } from "../types/index.js";
2
+ export declare function roll(expression: string): DiceRoll;
3
+ export declare function check(params: {
4
+ gameId: string;
5
+ characterId: string;
6
+ skill?: string;
7
+ attribute?: string;
8
+ difficulty: number;
9
+ bonusModifier?: number;
10
+ }): CheckResult;
11
+ export declare function contest(params: {
12
+ gameId: string;
13
+ attackerId: string;
14
+ defenderId: string;
15
+ attackerSkill?: string;
16
+ defenderSkill?: string;
17
+ attackerAttribute?: string;
18
+ defenderAttribute?: string;
19
+ }): {
20
+ attackerResult: CheckResult;
21
+ defenderResult: CheckResult;
22
+ winner: "attacker" | "defender" | "tie";
23
+ };
@@ -0,0 +1,111 @@
1
+ import { getRules } from "./rules.js";
2
+ import { getCharacter } from "./character.js";
3
+ // Maximum limits to prevent DoS attacks
4
+ const MAX_DICE_COUNT = 100;
5
+ const MAX_DICE_SIDES = 1000;
6
+ // Parse and roll dice expressions like "2d6+3", "1d20-2", "3d8"
7
+ export function roll(expression) {
8
+ const regex = /^(\d+)?d(\d+)([+-]\d+)?$/i;
9
+ const match = expression.replace(/\s/g, "").match(regex);
10
+ if (!match) {
11
+ throw new Error(`Invalid dice expression: ${expression}. Expected format: NdX+M (e.g., 2d6+3)`);
12
+ }
13
+ const count = match[1] ? parseInt(match[1], 10) : 1;
14
+ const sides = parseInt(match[2], 10);
15
+ const modifier = match[3] ? parseInt(match[3], 10) : 0;
16
+ // Validate bounds to prevent DoS
17
+ if (count < 1 || count > MAX_DICE_COUNT) {
18
+ throw new Error(`Dice count must be between 1 and ${MAX_DICE_COUNT}, got: ${count}`);
19
+ }
20
+ if (sides < 1 || sides > MAX_DICE_SIDES) {
21
+ throw new Error(`Dice sides must be between 1 and ${MAX_DICE_SIDES}, got: ${sides}`);
22
+ }
23
+ const rolls = [];
24
+ for (let i = 0; i < count; i++) {
25
+ rolls.push(Math.floor(Math.random() * sides) + 1);
26
+ }
27
+ const total = rolls.reduce((sum, r) => sum + r, 0) + modifier;
28
+ return {
29
+ expression,
30
+ rolls,
31
+ modifier,
32
+ total,
33
+ };
34
+ }
35
+ // Perform a skill/ability check using game rules
36
+ export function check(params) {
37
+ const rules = getRules(params.gameId);
38
+ if (!rules) {
39
+ throw new Error(`No rules set for game ${params.gameId}`);
40
+ }
41
+ const character = getCharacter(params.characterId);
42
+ if (!character) {
43
+ throw new Error(`Character ${params.characterId} not found`);
44
+ }
45
+ // Calculate modifier from character stats
46
+ let modifier = params.bonusModifier || 0;
47
+ if (params.skill && character.skills[params.skill] !== undefined) {
48
+ modifier += character.skills[params.skill];
49
+ }
50
+ if (params.attribute && character.attributes[params.attribute] !== undefined) {
51
+ // Common formula: (attribute - 10) / 2, but we'll just add the raw value
52
+ // The DM agent should configure how modifiers work in the rules
53
+ modifier += Math.floor((character.attributes[params.attribute] - 10) / 2);
54
+ }
55
+ // Roll the base dice
56
+ const diceRoll = roll(rules.checkMechanics.baseDice);
57
+ const total = diceRoll.total + modifier;
58
+ // Determine success
59
+ const success = total >= params.difficulty;
60
+ const criticalSuccess = rules.checkMechanics.criticalSuccess !== undefined &&
61
+ diceRoll.rolls[0] >= rules.checkMechanics.criticalSuccess;
62
+ const criticalFailure = rules.checkMechanics.criticalFailure !== undefined &&
63
+ diceRoll.rolls[0] <= rules.checkMechanics.criticalFailure;
64
+ return {
65
+ roll: diceRoll,
66
+ modifier,
67
+ total,
68
+ difficulty: params.difficulty,
69
+ success: criticalFailure ? false : criticalSuccess ? true : success,
70
+ criticalSuccess,
71
+ criticalFailure,
72
+ margin: total - params.difficulty,
73
+ };
74
+ }
75
+ // Opposed check between two characters
76
+ export function contest(params) {
77
+ const rules = getRules(params.gameId);
78
+ if (!rules) {
79
+ throw new Error(`No rules set for game ${params.gameId}`);
80
+ }
81
+ // Both roll against difficulty 0, we compare totals
82
+ const attackerResult = check({
83
+ gameId: params.gameId,
84
+ characterId: params.attackerId,
85
+ skill: params.attackerSkill,
86
+ attribute: params.attackerAttribute,
87
+ difficulty: 0,
88
+ });
89
+ const defenderResult = check({
90
+ gameId: params.gameId,
91
+ characterId: params.defenderId,
92
+ skill: params.defenderSkill,
93
+ attribute: params.defenderAttribute,
94
+ difficulty: 0,
95
+ });
96
+ let winner;
97
+ if (attackerResult.total > defenderResult.total) {
98
+ winner = "attacker";
99
+ }
100
+ else if (defenderResult.total > attackerResult.total) {
101
+ winner = "defender";
102
+ }
103
+ else {
104
+ winner = "tie";
105
+ }
106
+ return {
107
+ attackerResult,
108
+ defenderResult,
109
+ winner,
110
+ };
111
+ }
@@ -0,0 +1,120 @@
1
+ export type BorderRadiusStyle = "sharp" | "rounded" | "soft";
2
+ export type CardStyle = "clean" | "grungy" | "tech" | "parchment" | "metallic" | "wooden";
3
+ export interface DisplayConfig {
4
+ bgColor: string;
5
+ bgSecondary: string;
6
+ bgElevated: string;
7
+ textColor: string;
8
+ textMuted: string;
9
+ accentColor: string;
10
+ accentHover: string;
11
+ borderColor: string;
12
+ successColor: string;
13
+ warningColor: string;
14
+ dangerColor: string;
15
+ codeBackground: string;
16
+ codeText: string;
17
+ borderRadius: BorderRadiusStyle;
18
+ cardStyle: CardStyle;
19
+ fontDisplay: string;
20
+ fontBody: string;
21
+ fontMono: string;
22
+ showHealthBars: boolean;
23
+ showConditionTags: boolean;
24
+ showImages: boolean;
25
+ appTitle: string;
26
+ }
27
+ export declare const themePresets: Record<string, Partial<DisplayConfig>>;
28
+ /**
29
+ * Get the current display configuration
30
+ */
31
+ export declare function getDisplayConfig(): DisplayConfig;
32
+ /**
33
+ * Set the display configuration (full or partial update)
34
+ */
35
+ export declare function setDisplayConfig(config: Partial<DisplayConfig>): DisplayConfig;
36
+ /**
37
+ * Reset display configuration to defaults
38
+ */
39
+ export declare function resetDisplayConfig(): DisplayConfig;
40
+ /**
41
+ * Apply a preset theme
42
+ */
43
+ export declare function applyThemePreset(presetName: string): DisplayConfig | null;
44
+ /**
45
+ * List available theme presets
46
+ */
47
+ export declare function listThemePresets(): Array<{
48
+ name: string;
49
+ preview: Partial<DisplayConfig>;
50
+ }>;
51
+ /**
52
+ * Set specific display options (convenience function)
53
+ */
54
+ export declare function setDisplayOptions(options: {
55
+ showHealthBars?: boolean;
56
+ showConditionTags?: boolean;
57
+ showImages?: boolean;
58
+ }): DisplayConfig;
59
+ /**
60
+ * Set theme colors (convenience function)
61
+ */
62
+ export declare function setThemeColors(colors: {
63
+ bgColor?: string;
64
+ bgSecondary?: string;
65
+ bgElevated?: string;
66
+ textColor?: string;
67
+ textMuted?: string;
68
+ accentColor?: string;
69
+ accentHover?: string;
70
+ borderColor?: string;
71
+ successColor?: string;
72
+ warningColor?: string;
73
+ dangerColor?: string;
74
+ codeBackground?: string;
75
+ codeText?: string;
76
+ }): DisplayConfig;
77
+ /**
78
+ * Set the app title
79
+ */
80
+ export declare function setAppTitle(title: string): DisplayConfig;
81
+ /**
82
+ * Set fonts (Google Fonts names)
83
+ */
84
+ export declare function setFonts(fonts: {
85
+ fontDisplay?: string;
86
+ fontBody?: string;
87
+ fontMono?: string;
88
+ }): DisplayConfig;
89
+ /**
90
+ * Set visual style options
91
+ */
92
+ export declare function setVisualStyle(style: {
93
+ borderRadius?: BorderRadiusStyle;
94
+ cardStyle?: CardStyle;
95
+ }): DisplayConfig;
96
+ /**
97
+ * Get display configuration for a specific game
98
+ * Falls back to global config if no game theme exists
99
+ */
100
+ export declare function getGameDisplayConfig(gameId: string): DisplayConfig;
101
+ /**
102
+ * Set display configuration for a specific game
103
+ */
104
+ export declare function setGameDisplayConfig(gameId: string, config: Partial<DisplayConfig>): DisplayConfig;
105
+ /**
106
+ * Apply a preset theme to a specific game
107
+ */
108
+ export declare function applyGameThemePreset(gameId: string, presetName: string): DisplayConfig | null;
109
+ /**
110
+ * Reset game theme (removes game-specific config, falls back to global)
111
+ */
112
+ export declare function resetGameTheme(gameId: string): void;
113
+ /**
114
+ * Check if a game has a custom theme
115
+ */
116
+ export declare function hasGameTheme(gameId: string): boolean;
117
+ /**
118
+ * Infer and apply theme based on game genre/setting
119
+ */
120
+ export declare function inferAndApplyTheme(gameId: string, genre: string, setting?: string): DisplayConfig;