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.
- package/LICENSE +22 -0
- package/README.md +79 -0
- package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
- package/dist/__tests__/engineVocabulary.test.js +147 -0
- package/dist/db/__tests__/connection.test.d.ts +1 -0
- package/dist/db/__tests__/connection.test.js +72 -0
- package/dist/db/__tests__/testDb.d.ts +33 -0
- package/dist/db/__tests__/testDb.js +41 -0
- package/dist/db/connection.d.ts +22 -0
- package/dist/db/connection.js +107 -0
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +725 -0
- package/dist/events/emitter.d.ts +22 -0
- package/dist/events/emitter.js +71 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/server.js +649 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -0
- package/dist/register/abilities.d.ts +2 -0
- package/dist/register/abilities.js +165 -0
- package/dist/register/audio.d.ts +2 -0
- package/dist/register/audio.js +326 -0
- package/dist/register/batch.d.ts +2 -0
- package/dist/register/batch.js +343 -0
- package/dist/register/character.d.ts +2 -0
- package/dist/register/character.js +324 -0
- package/dist/register/combat.d.ts +2 -0
- package/dist/register/combat.js +207 -0
- package/dist/register/core.d.ts +2 -0
- package/dist/register/core.js +1040 -0
- package/dist/register/display.d.ts +2 -0
- package/dist/register/display.js +263 -0
- package/dist/register/factions.d.ts +2 -0
- package/dist/register/factions.js +186 -0
- package/dist/register/images.d.ts +2 -0
- package/dist/register/images.js +400 -0
- package/dist/register/inventory.d.ts +2 -0
- package/dist/register/inventory.js +115 -0
- package/dist/register/mcp-prompts.d.ts +2 -0
- package/dist/register/mcp-prompts.js +684 -0
- package/dist/register/mcp-resources.d.ts +2 -0
- package/dist/register/mcp-resources.js +335 -0
- package/dist/register/narrative.d.ts +2 -0
- package/dist/register/narrative.js +242 -0
- package/dist/register/notes.d.ts +2 -0
- package/dist/register/notes.js +170 -0
- package/dist/register/pause.d.ts +2 -0
- package/dist/register/pause.js +580 -0
- package/dist/register/quests.d.ts +2 -0
- package/dist/register/quests.js +118 -0
- package/dist/register/relationships.d.ts +2 -0
- package/dist/register/relationships.js +147 -0
- package/dist/register/resources.d.ts +2 -0
- package/dist/register/resources.js +277 -0
- package/dist/register/secrets.d.ts +2 -0
- package/dist/register/secrets.js +192 -0
- package/dist/register/status.d.ts +2 -0
- package/dist/register/status.js +130 -0
- package/dist/register/tables.d.ts +2 -0
- package/dist/register/tables.js +146 -0
- package/dist/register/tags.d.ts +2 -0
- package/dist/register/tags.js +114 -0
- package/dist/register/time.d.ts +2 -0
- package/dist/register/time.js +281 -0
- package/dist/register/world.d.ts +2 -0
- package/dist/register/world.js +127 -0
- package/dist/schemas/index.d.ts +921 -0
- package/dist/schemas/index.js +121 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/tools/__tests__/audio.test.d.ts +1 -0
- package/dist/tools/__tests__/audio.test.js +59 -0
- package/dist/tools/__tests__/conserved.test.d.ts +1 -0
- package/dist/tools/__tests__/conserved.test.js +488 -0
- package/dist/tools/__tests__/constraint.test.d.ts +1 -0
- package/dist/tools/__tests__/constraint.test.js +212 -0
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
- package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
- package/dist/tools/__tests__/images.test.d.ts +1 -0
- package/dist/tools/__tests__/images.test.js +59 -0
- package/dist/tools/__tests__/relationship.test.d.ts +1 -0
- package/dist/tools/__tests__/relationship.test.js +132 -0
- package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
- package/dist/tools/__tests__/resource-constraints.test.js +131 -0
- package/dist/tools/__tests__/resource.test.d.ts +1 -0
- package/dist/tools/__tests__/resource.test.js +190 -0
- package/dist/tools/__tests__/time.test.d.ts +1 -0
- package/dist/tools/__tests__/time.test.js +404 -0
- package/dist/tools/__tests__/timers.test.d.ts +1 -0
- package/dist/tools/__tests__/timers.test.js +426 -0
- package/dist/tools/__tests__/world.test.d.ts +1 -0
- package/dist/tools/__tests__/world.test.js +70 -0
- package/dist/tools/ability.d.ts +48 -0
- package/dist/tools/ability.js +238 -0
- package/dist/tools/audio.d.ts +24 -0
- package/dist/tools/audio.js +365 -0
- package/dist/tools/character.d.ts +70 -0
- package/dist/tools/character.js +309 -0
- package/dist/tools/combat.d.ts +13 -0
- package/dist/tools/combat.js +195 -0
- package/dist/tools/constraint.d.ts +132 -0
- package/dist/tools/constraint.js +269 -0
- package/dist/tools/dice.d.ts +23 -0
- package/dist/tools/dice.js +111 -0
- package/dist/tools/display.d.ts +120 -0
- package/dist/tools/display.js +528 -0
- package/dist/tools/faction.d.ts +61 -0
- package/dist/tools/faction.js +269 -0
- package/dist/tools/game.d.ts +96 -0
- package/dist/tools/game.js +526 -0
- package/dist/tools/image-prompt.d.ts +49 -0
- package/dist/tools/image-prompt.js +479 -0
- package/dist/tools/images.d.ts +47 -0
- package/dist/tools/images.js +449 -0
- package/dist/tools/inventory.d.ts +20 -0
- package/dist/tools/inventory.js +145 -0
- package/dist/tools/narrative.d.ts +58 -0
- package/dist/tools/narrative.js +237 -0
- package/dist/tools/notes.d.ts +41 -0
- package/dist/tools/notes.js +220 -0
- package/dist/tools/pause.d.ts +110 -0
- package/dist/tools/pause.js +1254 -0
- package/dist/tools/quest.d.ts +34 -0
- package/dist/tools/quest.js +164 -0
- package/dist/tools/relationship.d.ts +74 -0
- package/dist/tools/relationship.js +324 -0
- package/dist/tools/resource.d.ts +93 -0
- package/dist/tools/resource.js +374 -0
- package/dist/tools/rules.d.ts +4 -0
- package/dist/tools/rules.js +30 -0
- package/dist/tools/secrets.d.ts +49 -0
- package/dist/tools/secrets.js +195 -0
- package/dist/tools/status.d.ts +36 -0
- package/dist/tools/status.js +218 -0
- package/dist/tools/tables.d.ts +33 -0
- package/dist/tools/tables.js +209 -0
- package/dist/tools/tags.d.ts +52 -0
- package/dist/tools/tags.js +176 -0
- package/dist/tools/time.d.ts +33 -0
- package/dist/tools/time.js +276 -0
- package/dist/tools/timers.d.ts +41 -0
- package/dist/tools/timers.js +215 -0
- package/dist/tools/world.d.ts +78 -0
- package/dist/tools/world.js +331 -0
- package/dist/types/index.d.ts +969 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/__tests__/json.test.d.ts +1 -0
- package/dist/utils/__tests__/json.test.js +55 -0
- package/dist/utils/__tests__/validation.test.d.ts +1 -0
- package/dist/utils/__tests__/validation.test.js +90 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/json.d.ts +9 -0
- package/dist/utils/json.js +23 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/output-schemas.d.ts +594 -0
- package/dist/utils/output-schemas.js +331 -0
- package/dist/utils/tool-annotations.d.ts +147 -0
- package/dist/utils/tool-annotations.js +98 -0
- package/dist/utils/validation.d.ts +34 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/verbosity.d.ts +57 -0
- package/dist/utils/verbosity.js +67 -0
- package/dist/utils/webui.d.ts +20 -0
- package/dist/utils/webui.js +35 -0
- 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;
|