@dreamboard-games/sdk 0.3.0-alpha.1 → 0.4.0-alpha.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/REFERENCE.md +19935 -0
- package/dist/{chunk-3RQEICD3.js → chunk-2LDZ5C3T.js} +2 -2
- package/dist/chunk-3OZMHZK3.js +40 -0
- package/dist/chunk-3OZMHZK3.js.map +1 -0
- package/dist/{chunk-TXXLS3OI.js → chunk-4G7552LO.js} +3 -3
- package/dist/{chunk-P7VMTJ5D.js → chunk-MNKDSGIA.js} +2 -2
- package/dist/{chunk-P7VMTJ5D.js.map → chunk-MNKDSGIA.js.map} +1 -1
- package/dist/{chunk-W6SGFWXH.js → chunk-NBAEEHAU.js} +101 -60
- package/dist/chunk-NBAEEHAU.js.map +1 -0
- package/dist/{chunk-ADYH6PVT.js → chunk-P7F4L2FF.js} +49 -25
- package/dist/chunk-P7F4L2FF.js.map +1 -0
- package/dist/{chunk-QZ6X4B5P.js → chunk-Q5O7GPVY.js} +1 -1
- package/dist/{chunk-QZ6X4B5P.js.map → chunk-Q5O7GPVY.js.map} +1 -1
- package/dist/chunk-U5KBV2BA.js +2654 -0
- package/dist/chunk-U5KBV2BA.js.map +1 -0
- package/dist/{chunk-Y2AFYBMB.js → chunk-Y6Y2YABD.js} +9 -5
- package/dist/chunk-Y6Y2YABD.js.map +1 -0
- package/dist/codegen.d.ts +44 -51
- package/dist/codegen.js +1 -1
- package/dist/{index.d-BGqVifr_.d.ts → index.d-BL3bT5lt.d.ts} +82 -3
- package/dist/index.js +1 -1
- package/dist/package-set.d.ts +2 -2
- package/dist/package-set.js +1 -1
- package/dist/reducer/advanced.d.ts +78 -0
- package/dist/reducer/advanced.js +139 -0
- package/dist/reducer/advanced.js.map +1 -0
- package/dist/reducer-contract.d.ts +1 -1
- package/dist/reducer-contract.js +2 -2
- package/dist/reducer.d.ts +363 -3263
- package/dist/reducer.js +2778 -4081
- package/dist/reducer.js.map +1 -1
- package/dist/runtime/primitives.d.ts +3 -3
- package/dist/runtime/primitives.js +3 -3
- package/dist/runtime/runtime-api.d.ts +1 -1
- package/dist/runtime/workspace-contract.d.ts +18 -4
- package/dist/runtime/workspace-contract.js +4 -4
- package/dist/{runtime-api-tGL3ArsB.d.ts → runtime-api-BXd70c2e.d.ts} +6 -0
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +85 -20
- package/dist/runtime.js.map +1 -1
- package/dist/stale-contract-artifact-error-BelRiIDR.d.ts +66 -0
- package/dist/testing.d.ts +59 -5
- package/dist/testing.js +170 -6
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +112 -119
- package/dist/ui/components.js +1 -1
- package/dist/ui/plugin-styles.css +2 -250
- package/dist/{ui-contract-SctM4ibF.d.ts → ui-contract-BUC6iS3s.d.ts} +1 -1
- package/dist/ui.js +2 -2
- package/dist/views-B0hlW6IT.d.ts +3153 -0
- package/package.json +18 -10
- package/dist/chunk-ADYH6PVT.js.map +0 -1
- package/dist/chunk-W6SGFWXH.js.map +0 -1
- package/dist/chunk-Y2AFYBMB.js.map +0 -1
- /package/dist/{chunk-3RQEICD3.js.map → chunk-2LDZ5C3T.js.map} +0 -0
- /package/dist/{chunk-TXXLS3OI.js.map → chunk-4G7552LO.js.map} +0 -0
|
@@ -0,0 +1,2654 @@
|
|
|
1
|
+
// src/reducer/model/manifest.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function toNonEmptyStringTuple(values) {
|
|
4
|
+
const [first, ...rest] = Array.from(values);
|
|
5
|
+
if (typeof first === "undefined") {
|
|
6
|
+
throw new Error("Expected a non-empty literal tuple");
|
|
7
|
+
}
|
|
8
|
+
return [first, ...rest];
|
|
9
|
+
}
|
|
10
|
+
var manifestScopedSchemas = /* @__PURE__ */ new WeakSet();
|
|
11
|
+
function markManifestScopedSchema(schema) {
|
|
12
|
+
manifestScopedSchemas.add(schema);
|
|
13
|
+
return schema;
|
|
14
|
+
}
|
|
15
|
+
function isManifestScopedSchema(schema) {
|
|
16
|
+
return typeof schema === "object" && schema !== null && manifestScopedSchemas.has(schema);
|
|
17
|
+
}
|
|
18
|
+
function createManifestStringLiteralSchema(values) {
|
|
19
|
+
const schema = values.length === 0 ? z.string() : z.enum(toNonEmptyStringTuple(values));
|
|
20
|
+
return markManifestScopedSchema(schema);
|
|
21
|
+
}
|
|
22
|
+
function assumeManifestSchema(schema) {
|
|
23
|
+
return schema;
|
|
24
|
+
}
|
|
25
|
+
function cloneManifestDefault(value) {
|
|
26
|
+
if (typeof globalThis.structuredClone === "function") {
|
|
27
|
+
return globalThis.structuredClone(value);
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(JSON.stringify(value));
|
|
30
|
+
}
|
|
31
|
+
function resolveManifestPlayerIds(manifestPlayerIds, playerIds) {
|
|
32
|
+
if (!playerIds || playerIds.length === 0) {
|
|
33
|
+
return manifestPlayerIds;
|
|
34
|
+
}
|
|
35
|
+
const knownPlayerIds = new Set(manifestPlayerIds);
|
|
36
|
+
return playerIds.filter(
|
|
37
|
+
(playerId) => knownPlayerIds.has(playerId)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function createManifestRuntimeSchema({
|
|
41
|
+
phaseNameSchema,
|
|
42
|
+
playerIdSchema,
|
|
43
|
+
setupProfileIdSchema
|
|
44
|
+
}) {
|
|
45
|
+
return z.object({
|
|
46
|
+
prompts: z.array(
|
|
47
|
+
z.object({
|
|
48
|
+
id: z.string(),
|
|
49
|
+
promptId: z.string(),
|
|
50
|
+
to: playerIdSchema,
|
|
51
|
+
title: z.string().optional(),
|
|
52
|
+
payload: z.unknown().optional(),
|
|
53
|
+
options: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
|
54
|
+
resume: z.object({ id: z.string(), data: z.unknown() })
|
|
55
|
+
})
|
|
56
|
+
).default([]),
|
|
57
|
+
rng: z.object({
|
|
58
|
+
seed: z.number().nullable().optional(),
|
|
59
|
+
cursor: z.number().int().default(0),
|
|
60
|
+
trace: z.array(z.string()).default([])
|
|
61
|
+
}).default({
|
|
62
|
+
seed: null,
|
|
63
|
+
cursor: 0,
|
|
64
|
+
trace: []
|
|
65
|
+
}),
|
|
66
|
+
setup: z.object({
|
|
67
|
+
profileId: setupProfileIdSchema,
|
|
68
|
+
optionValues: z.record(z.string(), z.string().nullable()).default({})
|
|
69
|
+
}).nullable().default(null),
|
|
70
|
+
simultaneous: z.object({
|
|
71
|
+
current: z.object({
|
|
72
|
+
phaseName: phaseNameSchema,
|
|
73
|
+
actors: z.array(playerIdSchema),
|
|
74
|
+
submissions: z.record(
|
|
75
|
+
z.string(),
|
|
76
|
+
z.object({
|
|
77
|
+
interactionId: z.string(),
|
|
78
|
+
params: z.unknown()
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
}).nullable()
|
|
82
|
+
}).default({ current: null }),
|
|
83
|
+
lastTransition: z.object({
|
|
84
|
+
from: phaseNameSchema,
|
|
85
|
+
to: phaseNameSchema
|
|
86
|
+
}).nullable().default(null),
|
|
87
|
+
nextInstanceId: z.number().int().default(1)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function createManifestGameStateSchema({
|
|
91
|
+
tableSchema,
|
|
92
|
+
playerIdSchema,
|
|
93
|
+
setupProfileIdSchema,
|
|
94
|
+
phaseNameSchema,
|
|
95
|
+
publicSchema,
|
|
96
|
+
privateSchema,
|
|
97
|
+
hiddenSchema,
|
|
98
|
+
phasesSchema
|
|
99
|
+
}) {
|
|
100
|
+
return z.object({
|
|
101
|
+
table: tableSchema,
|
|
102
|
+
public: publicSchema,
|
|
103
|
+
private: z.record(z.string(), privateSchema),
|
|
104
|
+
hidden: hiddenSchema,
|
|
105
|
+
flow: z.object({
|
|
106
|
+
currentPhase: phaseNameSchema,
|
|
107
|
+
turn: z.number().int(),
|
|
108
|
+
round: z.number().int(),
|
|
109
|
+
activePlayers: z.array(playerIdSchema)
|
|
110
|
+
}),
|
|
111
|
+
phase: phasesSchema,
|
|
112
|
+
runtime: createManifestRuntimeSchema({
|
|
113
|
+
phaseNameSchema,
|
|
114
|
+
playerIdSchema,
|
|
115
|
+
setupProfileIdSchema
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/reducer/model/error-codes.ts
|
|
121
|
+
var FrameworkErrorCodes = {
|
|
122
|
+
NOT_YOUR_TURN: "NOT_YOUR_TURN",
|
|
123
|
+
WRONG_PHASE: "WRONG_PHASE",
|
|
124
|
+
WRONG_STEP: "WRONG_STEP",
|
|
125
|
+
INVALID_PARAMS: "INVALID_PARAMS",
|
|
126
|
+
UNKNOWN_INTERACTION: "UNKNOWN_INTERACTION",
|
|
127
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/reducer/per-player.ts
|
|
131
|
+
import { z as z2 } from "zod";
|
|
132
|
+
function asPlayerId(raw) {
|
|
133
|
+
return raw;
|
|
134
|
+
}
|
|
135
|
+
function isPlayerId(value) {
|
|
136
|
+
return typeof value === "string" && value.length > 0;
|
|
137
|
+
}
|
|
138
|
+
function perPlayer(ids, init) {
|
|
139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const [index, id] of ids.entries()) {
|
|
142
|
+
if (seen.has(id)) {
|
|
143
|
+
throw new Error(`perPlayer: duplicate player id '${id}'`);
|
|
144
|
+
}
|
|
145
|
+
seen.add(id);
|
|
146
|
+
entries.push([id, init(id, index)]);
|
|
147
|
+
}
|
|
148
|
+
return { __perPlayer: true, entries };
|
|
149
|
+
}
|
|
150
|
+
function perPlayerKeys(value) {
|
|
151
|
+
return value.entries.map((entry) => entry[0]);
|
|
152
|
+
}
|
|
153
|
+
function perPlayerValues(value) {
|
|
154
|
+
return value.entries.map((entry) => entry[1]);
|
|
155
|
+
}
|
|
156
|
+
function perPlayerEntries(value) {
|
|
157
|
+
return value.entries;
|
|
158
|
+
}
|
|
159
|
+
function perPlayerSize(value) {
|
|
160
|
+
return value.entries.length;
|
|
161
|
+
}
|
|
162
|
+
function perPlayerHas(value, id) {
|
|
163
|
+
for (const [candidate] of value.entries) {
|
|
164
|
+
if (candidate === id) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
function perPlayerGet(value, id) {
|
|
171
|
+
for (const [candidate, v] of value.entries) {
|
|
172
|
+
if (candidate === id) {
|
|
173
|
+
return v;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return void 0;
|
|
177
|
+
}
|
|
178
|
+
function perPlayerRequire(value, id) {
|
|
179
|
+
const found = perPlayerGet(value, id);
|
|
180
|
+
if (found === void 0 && !perPlayerHas(value, id)) {
|
|
181
|
+
throw new Error(`perPlayerRequire: missing entry for player id '${id}'`);
|
|
182
|
+
}
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
function perPlayerSet(value, id, next) {
|
|
186
|
+
const entries = value.entries.slice();
|
|
187
|
+
const index = entries.findIndex((entry) => entry[0] === id);
|
|
188
|
+
if (index >= 0) {
|
|
189
|
+
entries[index] = [id, next];
|
|
190
|
+
} else {
|
|
191
|
+
entries.push([id, next]);
|
|
192
|
+
}
|
|
193
|
+
return { __perPlayer: true, entries };
|
|
194
|
+
}
|
|
195
|
+
function perPlayerMap(value, f) {
|
|
196
|
+
return {
|
|
197
|
+
__perPlayer: true,
|
|
198
|
+
entries: value.entries.map(
|
|
199
|
+
([id, v], index) => [id, f(v, id, index)]
|
|
200
|
+
)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function isPerPlayer(value) {
|
|
204
|
+
if (typeof value !== "object" || value === null) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const candidate = value;
|
|
208
|
+
if (candidate.__perPlayer !== true) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(candidate.entries)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return candidate.entries.every(
|
|
215
|
+
(entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function perPlayerSchema(valueSchema, options = {}) {
|
|
219
|
+
const keySchema = options.playerIdSchema ?? z2.string().min(1);
|
|
220
|
+
const base = z2.object({
|
|
221
|
+
__perPlayer: z2.literal(true),
|
|
222
|
+
entries: z2.array(z2.tuple([keySchema, valueSchema]))
|
|
223
|
+
}).transform(
|
|
224
|
+
(value) => ({
|
|
225
|
+
__perPlayer: true,
|
|
226
|
+
entries: value.entries
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
if (!options.players) {
|
|
230
|
+
return base;
|
|
231
|
+
}
|
|
232
|
+
const expected = options.players;
|
|
233
|
+
return base.superRefine((value, ctx) => {
|
|
234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const [id] of value.entries) {
|
|
236
|
+
if (seen.has(id)) {
|
|
237
|
+
ctx.addIssue({
|
|
238
|
+
code: "custom",
|
|
239
|
+
message: `Duplicate player id '${id}'`
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
seen.add(id);
|
|
243
|
+
}
|
|
244
|
+
for (const expectedId of expected) {
|
|
245
|
+
if (!seen.has(expectedId)) {
|
|
246
|
+
ctx.addIssue({
|
|
247
|
+
code: "custom",
|
|
248
|
+
message: `Missing entry for player id '${expectedId}'`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const id of seen) {
|
|
253
|
+
if (!expected.some((expectedId) => expectedId === id)) {
|
|
254
|
+
ctx.addIssue({
|
|
255
|
+
code: "custom",
|
|
256
|
+
message: `Unexpected player id '${id}' (allowed: ${expected.join(", ")})`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function sharedBoardRef(baseId) {
|
|
263
|
+
return { baseId };
|
|
264
|
+
}
|
|
265
|
+
function perPlayerBoardRef(baseId, seat) {
|
|
266
|
+
return { baseId, seat };
|
|
267
|
+
}
|
|
268
|
+
function boardRef(baseId, seat) {
|
|
269
|
+
if (seat === void 0) {
|
|
270
|
+
return { baseId };
|
|
271
|
+
}
|
|
272
|
+
return { baseId, seat };
|
|
273
|
+
}
|
|
274
|
+
function boardRefKey(ref) {
|
|
275
|
+
return ref.seat === void 0 ? ref.baseId : `${ref.baseId}:${ref.seat}`;
|
|
276
|
+
}
|
|
277
|
+
function parseBoardRefKey(key) {
|
|
278
|
+
if (!key.length) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const colon = key.indexOf(":");
|
|
282
|
+
if (colon < 0) {
|
|
283
|
+
return { baseId: key };
|
|
284
|
+
}
|
|
285
|
+
const baseId = key.slice(0, colon);
|
|
286
|
+
const seat = key.slice(colon + 1);
|
|
287
|
+
if (!baseId.length || !seat.length) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return { baseId, seat };
|
|
291
|
+
}
|
|
292
|
+
function boardRefSchema(options = {}) {
|
|
293
|
+
const baseSchema = options.baseIdSchema ?? z2.string().min(1);
|
|
294
|
+
const playerSchema = options.playerIdSchema ?? z2.string().min(1);
|
|
295
|
+
const shared = z2.strictObject({ baseId: baseSchema });
|
|
296
|
+
const perPlayer2 = z2.strictObject({ baseId: baseSchema, seat: playerSchema });
|
|
297
|
+
return z2.union([perPlayer2, shared]);
|
|
298
|
+
}
|
|
299
|
+
function isSharedBoardRef(ref) {
|
|
300
|
+
return ref.seat === void 0;
|
|
301
|
+
}
|
|
302
|
+
function isPerPlayerBoardRef(ref) {
|
|
303
|
+
return ref.seat !== void 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/reducer/inputs/targetRule.ts
|
|
307
|
+
var DEFAULT_MISSING_CANDIDATE_ISSUE = {
|
|
308
|
+
errorCode: "TARGET_NOT_ELIGIBLE",
|
|
309
|
+
message: "Target is not eligible."
|
|
310
|
+
};
|
|
311
|
+
function createTargetRule(candidates, predicates, options = {}) {
|
|
312
|
+
const missingCandidateIssue = options.missingCandidateIssue ?? DEFAULT_MISSING_CANDIDATE_ISSUE;
|
|
313
|
+
const equals = options.equals ?? Object.is;
|
|
314
|
+
const validate = (ctx, target) => {
|
|
315
|
+
if (!candidates(ctx).some((candidate) => equals(candidate, target))) {
|
|
316
|
+
return missingCandidateIssue;
|
|
317
|
+
}
|
|
318
|
+
for (const predicate of predicates) {
|
|
319
|
+
if (!predicate.test({ ...ctx, targetId: target, target })) {
|
|
320
|
+
return {
|
|
321
|
+
errorCode: predicate.errorCode,
|
|
322
|
+
message: predicate.message
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
};
|
|
328
|
+
const rule = {
|
|
329
|
+
eligible: (ctx) => candidates(ctx).filter((targetId) => validate(ctx, targetId) == null),
|
|
330
|
+
validate,
|
|
331
|
+
isEligible: (ctx, target) => validate(ctx, target) == null,
|
|
332
|
+
bind: (ctx) => ({
|
|
333
|
+
eligible: () => rule.eligible(ctx),
|
|
334
|
+
validate: (id) => rule.validate(ctx, id),
|
|
335
|
+
isEligible: (id) => rule.isEligible(ctx, id)
|
|
336
|
+
})
|
|
337
|
+
};
|
|
338
|
+
return rule;
|
|
339
|
+
}
|
|
340
|
+
function createTargetRuleBuilder(buildRule, predicates = []) {
|
|
341
|
+
return {
|
|
342
|
+
where: (predicate) => createTargetRuleBuilder(buildRule, [...predicates, predicate]),
|
|
343
|
+
build: () => buildRule(predicates)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/reducer/inputs/cardTarget.ts
|
|
348
|
+
function cardIdsForZone(state, playerId, q, zoneId) {
|
|
349
|
+
const table = state.table;
|
|
350
|
+
if (zoneId in (table.hands ?? {}) || zoneId in (table.zones?.perPlayer ?? {})) {
|
|
351
|
+
return q.zone.playerCards(
|
|
352
|
+
playerId,
|
|
353
|
+
zoneId
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (zoneId in (table.decks ?? {}) || zoneId in (table.zones?.shared ?? {})) {
|
|
357
|
+
return q.zone.sharedCards(zoneId);
|
|
358
|
+
}
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
function createCardTargetBuilder(zoneIds) {
|
|
362
|
+
return createTargetRuleBuilder(
|
|
363
|
+
(predicates) => ({
|
|
364
|
+
...createTargetRule(
|
|
365
|
+
({ state, playerId, q }) => zoneIds.flatMap(
|
|
366
|
+
(zoneId) => cardIdsForZone(state, playerId, q, zoneId)
|
|
367
|
+
),
|
|
368
|
+
predicates,
|
|
369
|
+
{
|
|
370
|
+
missingCandidateIssue: {
|
|
371
|
+
errorCode: "CARD_TARGET_NOT_ELIGIBLE",
|
|
372
|
+
message: "Card target is not eligible."
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
),
|
|
376
|
+
zoneIds,
|
|
377
|
+
zoneId: zoneIds[0],
|
|
378
|
+
targetKind: "card"
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
var cardTarget = {
|
|
383
|
+
zones(zoneIds) {
|
|
384
|
+
return createCardTargetBuilder(zoneIds);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/reducer/inputs/cardInput.ts
|
|
389
|
+
import { z as z3 } from "zod";
|
|
390
|
+
function cardInput(options) {
|
|
391
|
+
const target = options.target;
|
|
392
|
+
const dependsOn = options.dependsOn?.map((dependency) => dependency.key);
|
|
393
|
+
return {
|
|
394
|
+
kind: "card",
|
|
395
|
+
schema: z3.string(),
|
|
396
|
+
eligibleTargets: ((state, playerId, q, values) => target.eligible({
|
|
397
|
+
state,
|
|
398
|
+
playerId,
|
|
399
|
+
q,
|
|
400
|
+
values
|
|
401
|
+
})),
|
|
402
|
+
validateTarget: ((state, playerId, q, targetId, values) => target.validate(
|
|
403
|
+
{
|
|
404
|
+
state,
|
|
405
|
+
playerId,
|
|
406
|
+
q,
|
|
407
|
+
values
|
|
408
|
+
},
|
|
409
|
+
targetId
|
|
410
|
+
)),
|
|
411
|
+
...dependsOn ? { dependsOn } : {},
|
|
412
|
+
domain: (state, playerId, q, _derived, values) => ({
|
|
413
|
+
type: "cardTarget",
|
|
414
|
+
projection: "resolved",
|
|
415
|
+
targetKind: target.targetKind,
|
|
416
|
+
zoneIds: target.zoneIds,
|
|
417
|
+
eligibleTargets: target.eligible({
|
|
418
|
+
state,
|
|
419
|
+
playerId,
|
|
420
|
+
q,
|
|
421
|
+
values
|
|
422
|
+
}).map(String)
|
|
423
|
+
}),
|
|
424
|
+
meta: {
|
|
425
|
+
zoneId: target.zoneId,
|
|
426
|
+
zoneIds: target.zoneIds,
|
|
427
|
+
targetKind: target.targetKind
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/reducer/table/internal.ts
|
|
433
|
+
function ppRead(value, playerId) {
|
|
434
|
+
if (value === void 0) return void 0;
|
|
435
|
+
return perPlayerGet(value, playerId);
|
|
436
|
+
}
|
|
437
|
+
function ppWrite(value, playerId, next) {
|
|
438
|
+
const base = value ?? { __perPlayer: true, entries: [] };
|
|
439
|
+
return perPlayerSet(base, playerId, next);
|
|
440
|
+
}
|
|
441
|
+
function ensureArray(value) {
|
|
442
|
+
return Array.isArray(value) ? [...value] : [];
|
|
443
|
+
}
|
|
444
|
+
function locationPosition(location) {
|
|
445
|
+
return "position" in location && typeof location.position === "number" ? location.position : Number.MAX_SAFE_INTEGER;
|
|
446
|
+
}
|
|
447
|
+
function orderedComponentIdsForLocation(table, predicate) {
|
|
448
|
+
return Object.entries(table.componentLocations).filter(([, location]) => predicate(location)).sort(
|
|
449
|
+
(left, right) => locationPosition(left[1]) - locationPosition(right[1])
|
|
450
|
+
).map(([componentId]) => componentId);
|
|
451
|
+
}
|
|
452
|
+
function hasOwnKey(record, key) {
|
|
453
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
454
|
+
}
|
|
455
|
+
function isSharedZoneId(table, zoneId) {
|
|
456
|
+
return hasOwnKey(table.decks, zoneId) || hasOwnKey(table.zones.shared, zoneId);
|
|
457
|
+
}
|
|
458
|
+
function isPlayerZoneId(table, zoneId) {
|
|
459
|
+
return hasOwnKey(table.hands, zoneId) || hasOwnKey(table.zones.perPlayer, zoneId) || hasOwnKey(table.handVisibility, zoneId);
|
|
460
|
+
}
|
|
461
|
+
function zoneScopeForId(table, zoneId) {
|
|
462
|
+
if (isPlayerZoneId(table, zoneId)) {
|
|
463
|
+
return "perPlayer";
|
|
464
|
+
}
|
|
465
|
+
if (isSharedZoneId(table, zoneId)) {
|
|
466
|
+
return "shared";
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function assertZoneScope(table, zoneId, expectedScope, operation, argumentName) {
|
|
471
|
+
const actualScope = zoneScopeForId(table, zoneId);
|
|
472
|
+
if (actualScope === expectedScope) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (actualScope === null) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Unknown zone '${zoneId}' passed as ${argumentName} to ${operation}.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Zone '${zoneId}' has scope '${actualScope}', but ${operation} requires ${argumentName} to be a ${expectedScope === "shared" ? "shared" : "perPlayer"} zone.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
function syncSharedZoneWithDeck(table, zoneId, nextCards) {
|
|
485
|
+
table.decks[zoneId] = [...nextCards];
|
|
486
|
+
table.zones.shared[zoneId] = [
|
|
487
|
+
...nextCards
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
function syncPlayerZoneWithHand(table, zoneId, playerId, nextCards) {
|
|
491
|
+
table.hands[zoneId] = ppWrite(table.hands[zoneId], playerId, [
|
|
492
|
+
...nextCards
|
|
493
|
+
]);
|
|
494
|
+
table.zones.perPlayer[zoneId] = ppWrite(
|
|
495
|
+
table.zones.perPlayer[zoneId],
|
|
496
|
+
playerId,
|
|
497
|
+
[...nextCards]
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/reducer/table/board-queries.ts
|
|
502
|
+
function getBoard(table, boardId) {
|
|
503
|
+
return table.boards.byId[boardId];
|
|
504
|
+
}
|
|
505
|
+
function getHexBoard(table, boardId) {
|
|
506
|
+
return getBoard(table, boardId);
|
|
507
|
+
}
|
|
508
|
+
function getTiledBoard(table, boardId) {
|
|
509
|
+
return getBoard(table, boardId);
|
|
510
|
+
}
|
|
511
|
+
function getSquareBoard(table, boardId) {
|
|
512
|
+
return getBoard(table, boardId);
|
|
513
|
+
}
|
|
514
|
+
function getSpace(table, boardId, spaceId) {
|
|
515
|
+
return getBoard(table, boardId).spaces[spaceId];
|
|
516
|
+
}
|
|
517
|
+
function getHexSpace(table, boardId, spaceId) {
|
|
518
|
+
return getHexBoard(table, boardId).spaces[spaceId];
|
|
519
|
+
}
|
|
520
|
+
function getSquareSpace(table, boardId, spaceId) {
|
|
521
|
+
return getSquareBoard(table, boardId).spaces[spaceId];
|
|
522
|
+
}
|
|
523
|
+
function getContainer(table, boardId, containerId) {
|
|
524
|
+
return getBoard(table, boardId).containers[containerId];
|
|
525
|
+
}
|
|
526
|
+
function getEdge(table, boardId, edgeId) {
|
|
527
|
+
const edge = getTiledBoard(table, boardId).edges.find(
|
|
528
|
+
(candidate) => candidate.id === edgeId
|
|
529
|
+
);
|
|
530
|
+
if (!edge) {
|
|
531
|
+
throw new Error(`Unknown edge '${edgeId}' on board '${boardId}'.`);
|
|
532
|
+
}
|
|
533
|
+
return edge;
|
|
534
|
+
}
|
|
535
|
+
function getVertex(table, boardId, vertexId) {
|
|
536
|
+
const vertex = getTiledBoard(table, boardId).vertices.find(
|
|
537
|
+
(candidate) => candidate.id === vertexId
|
|
538
|
+
);
|
|
539
|
+
if (!vertex) {
|
|
540
|
+
throw new Error(`Unknown vertex '${vertexId}' on board '${boardId}'.`);
|
|
541
|
+
}
|
|
542
|
+
return vertex;
|
|
543
|
+
}
|
|
544
|
+
function getHexSpaceAt(table, boardId, q, r) {
|
|
545
|
+
return Object.values(getHexBoard(table, boardId).spaces).find(
|
|
546
|
+
(space) => space.q === q && space.r === r
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
function getSquareSpaceAt(table, boardId, row, col) {
|
|
550
|
+
return Object.values(getSquareBoard(table, boardId).spaces).find(
|
|
551
|
+
(space) => space.row === row && space.col === col
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
function getSpaceEdges(table, boardId, spaceId) {
|
|
555
|
+
return getTiledBoard(table, boardId).edges.filter((edge) => edge.spaceIds.includes(spaceId)).map((edge) => edge.id);
|
|
556
|
+
}
|
|
557
|
+
function getSpaceVertices(table, boardId, spaceId) {
|
|
558
|
+
return getTiledBoard(table, boardId).vertices.filter((vertex) => vertex.spaceIds.includes(spaceId)).map((vertex) => vertex.id);
|
|
559
|
+
}
|
|
560
|
+
function getIncidentEdges(table, boardId, vertexId) {
|
|
561
|
+
const tiledBoard = getTiledBoard(table, boardId);
|
|
562
|
+
const vertex = tiledBoard.vertices.find(
|
|
563
|
+
(candidate) => candidate.id === vertexId
|
|
564
|
+
);
|
|
565
|
+
if (!vertex) {
|
|
566
|
+
throw new Error(`Unknown vertex '${vertexId}' on board '${boardId}'.`);
|
|
567
|
+
}
|
|
568
|
+
const vertexSpaceIds = new Set(vertex.spaceIds);
|
|
569
|
+
return tiledBoard.edges.filter(
|
|
570
|
+
(edge) => edge.spaceIds.every((spaceId) => vertexSpaceIds.has(spaceId))
|
|
571
|
+
).map((edge) => edge.id);
|
|
572
|
+
}
|
|
573
|
+
function getIncidentVertices(table, boardId, edgeId) {
|
|
574
|
+
const tiledBoard = getTiledBoard(table, boardId);
|
|
575
|
+
const edge = tiledBoard.edges.find((candidate) => candidate.id === edgeId);
|
|
576
|
+
if (!edge) {
|
|
577
|
+
throw new Error(`Unknown edge '${edgeId}' on board '${boardId}'.`);
|
|
578
|
+
}
|
|
579
|
+
const edgeSpaceIds = new Set(edge.spaceIds);
|
|
580
|
+
return tiledBoard.vertices.filter(
|
|
581
|
+
(vertex) => Array.from(edgeSpaceIds).every(
|
|
582
|
+
(spaceId) => vertex.spaceIds.includes(spaceId)
|
|
583
|
+
)
|
|
584
|
+
).map((vertex) => vertex.id);
|
|
585
|
+
}
|
|
586
|
+
function getRelatedSpaces(table, boardId, spaceId, relationTypeId) {
|
|
587
|
+
const board = getBoard(table, boardId);
|
|
588
|
+
const related = /* @__PURE__ */ new Set();
|
|
589
|
+
for (const relation of board.relations) {
|
|
590
|
+
if (relation.typeId !== relationTypeId) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (relation.fromSpaceId === spaceId) {
|
|
594
|
+
related.add(relation.toSpaceId);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (!relation.directed && relation.toSpaceId === spaceId) {
|
|
598
|
+
related.add(relation.fromSpaceId);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return [...related];
|
|
602
|
+
}
|
|
603
|
+
function getAdjacentSpaces(table, boardId, spaceId) {
|
|
604
|
+
return getRelatedSpaces(
|
|
605
|
+
table,
|
|
606
|
+
boardId,
|
|
607
|
+
spaceId,
|
|
608
|
+
"adjacent"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
function getSpaceDistance(table, boardId, fromSpaceId, toSpaceId) {
|
|
612
|
+
if (fromSpaceId === toSpaceId) {
|
|
613
|
+
return 0;
|
|
614
|
+
}
|
|
615
|
+
const visited = /* @__PURE__ */ new Set([fromSpaceId]);
|
|
616
|
+
let frontier = [fromSpaceId];
|
|
617
|
+
let distance = 0;
|
|
618
|
+
while (frontier.length > 0) {
|
|
619
|
+
distance += 1;
|
|
620
|
+
const nextFrontier = [];
|
|
621
|
+
for (const currentSpaceId of frontier) {
|
|
622
|
+
for (const neighborId of getAdjacentSpaces(
|
|
623
|
+
table,
|
|
624
|
+
boardId,
|
|
625
|
+
currentSpaceId
|
|
626
|
+
)) {
|
|
627
|
+
if (neighborId === toSpaceId) {
|
|
628
|
+
return distance;
|
|
629
|
+
}
|
|
630
|
+
if (!visited.has(neighborId)) {
|
|
631
|
+
visited.add(neighborId);
|
|
632
|
+
nextFrontier.push(neighborId);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
frontier = nextFrontier;
|
|
637
|
+
}
|
|
638
|
+
return Number.POSITIVE_INFINITY;
|
|
639
|
+
}
|
|
640
|
+
function getSquareNeighbors(table, boardId, spaceId, options = {}) {
|
|
641
|
+
const board = getSquareBoard(table, boardId);
|
|
642
|
+
const space = getSquareSpace(table, boardId, spaceId);
|
|
643
|
+
const offsets = options.mode === "diagonal" ? [
|
|
644
|
+
[-1, -1],
|
|
645
|
+
[-1, 1],
|
|
646
|
+
[1, -1],
|
|
647
|
+
[1, 1]
|
|
648
|
+
] : options.mode === "all" ? [
|
|
649
|
+
[-1, 0],
|
|
650
|
+
[0, 1],
|
|
651
|
+
[1, 0],
|
|
652
|
+
[0, -1],
|
|
653
|
+
[-1, -1],
|
|
654
|
+
[-1, 1],
|
|
655
|
+
[1, -1],
|
|
656
|
+
[1, 1]
|
|
657
|
+
] : [
|
|
658
|
+
[-1, 0],
|
|
659
|
+
[0, 1],
|
|
660
|
+
[1, 0],
|
|
661
|
+
[0, -1]
|
|
662
|
+
];
|
|
663
|
+
return offsets.map(
|
|
664
|
+
([rowOffset, colOffset]) => Object.values(board.spaces).find(
|
|
665
|
+
(candidate) => candidate.row === space.row + rowOffset && candidate.col === space.col + colOffset
|
|
666
|
+
)
|
|
667
|
+
).filter((candidate) => candidate !== void 0).map((candidate) => candidate.id);
|
|
668
|
+
}
|
|
669
|
+
function getSquareDistance(table, boardId, fromSpaceId, toSpaceId, options = {}) {
|
|
670
|
+
const from = getSquareSpace(table, boardId, fromSpaceId);
|
|
671
|
+
const to = getSquareSpace(table, boardId, toSpaceId);
|
|
672
|
+
const rowDistance = Math.abs(from.row - to.row);
|
|
673
|
+
const colDistance = Math.abs(from.col - to.col);
|
|
674
|
+
return options.metric === "chebyshev" ? Math.max(rowDistance, colDistance) : rowDistance + colDistance;
|
|
675
|
+
}
|
|
676
|
+
function getBoardsByTypeId(table, typeId) {
|
|
677
|
+
return Object.entries(table.boards.byId).filter(([, board]) => board.typeId === typeId).map(([boardId]) => boardId);
|
|
678
|
+
}
|
|
679
|
+
function getSpacesByTypeId(table, boardId, typeId) {
|
|
680
|
+
return Object.entries(getBoard(table, boardId).spaces).filter(([, space]) => space.typeId === typeId).map(([spaceId]) => spaceId);
|
|
681
|
+
}
|
|
682
|
+
function getEdgesByTypeId(table, boardId, typeId) {
|
|
683
|
+
return getTiledBoard(table, boardId).edges.filter((edge) => edge.typeId === typeId).map((edge) => edge.id);
|
|
684
|
+
}
|
|
685
|
+
function getVerticesByTypeId(table, boardId, typeId) {
|
|
686
|
+
return getTiledBoard(table, boardId).vertices.filter((vertex) => vertex.typeId === typeId).map((vertex) => vertex.id);
|
|
687
|
+
}
|
|
688
|
+
function getComponentsOnSpace(table, boardId, spaceId) {
|
|
689
|
+
const zoneId = getSpace(table, boardId, spaceId).zoneId;
|
|
690
|
+
return orderedComponentIdsForLocation(
|
|
691
|
+
table,
|
|
692
|
+
(location) => location.type === "OnSpace" && location.boardId === boardId && location.spaceId === spaceId || location.type === "InZone" && typeof zoneId === "string" && zoneId.length > 0 && location.zoneId === zoneId
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function getComponentsInContainer(table, boardId, containerId) {
|
|
696
|
+
const zoneId = getContainer(table, boardId, containerId).zoneId;
|
|
697
|
+
return orderedComponentIdsForLocation(
|
|
698
|
+
table,
|
|
699
|
+
(location) => location.type === "InContainer" && location.boardId === boardId && location.containerId === containerId || location.type === "InZone" && typeof zoneId === "string" && zoneId.length > 0 && location.zoneId === zoneId
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
function getComponentsOnEdge(table, boardId, edgeId) {
|
|
703
|
+
return orderedComponentIdsForLocation(
|
|
704
|
+
table,
|
|
705
|
+
(location) => location.type === "OnEdge" && location.boardId === boardId && location.edgeId === edgeId
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
function getComponentsOnVertex(table, boardId, vertexId) {
|
|
709
|
+
return orderedComponentIdsForLocation(
|
|
710
|
+
table,
|
|
711
|
+
(location) => location.type === "OnVertex" && location.boardId === boardId && location.vertexId === vertexId
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/reducer/table/card-validation.ts
|
|
716
|
+
function allowedCardSetIdsForZone(table, zoneId) {
|
|
717
|
+
return table.zones.cardSetIdsByZoneId?.[zoneId] ?? [];
|
|
718
|
+
}
|
|
719
|
+
function assertCardAllowedInZone(table, zoneId, componentId) {
|
|
720
|
+
const card = table.cards[componentId];
|
|
721
|
+
if (!card) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const allowedCardSetIds = allowedCardSetIdsForZone(table, zoneId);
|
|
725
|
+
if (allowedCardSetIds.length > 0 && !allowedCardSetIds.includes(card.cardSetId)) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Card '${componentId}' from card set '${card.cardSetId}' cannot enter zone '${zoneId}'.`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function assertCardAllowedInContainer(table, boardId, containerId, componentId) {
|
|
732
|
+
const card = table.cards[componentId];
|
|
733
|
+
if (!card) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const allowedCardSetIds = getContainer(table, boardId, containerId).allowedCardSetIds ?? [];
|
|
737
|
+
if (allowedCardSetIds.length > 0 && !allowedCardSetIds.includes(card.cardSetId)) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
`Card '${componentId}' from card set '${card.cardSetId}' cannot enter container '${containerId}' on board '${boardId}'.`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/reducer/table/phase-state.ts
|
|
745
|
+
function setActivePlayers(state, activePlayers) {
|
|
746
|
+
return {
|
|
747
|
+
...state,
|
|
748
|
+
flow: {
|
|
749
|
+
...state.flow,
|
|
750
|
+
activePlayers
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function setPhaseState(state, phaseState) {
|
|
755
|
+
return {
|
|
756
|
+
...state,
|
|
757
|
+
phase: phaseState
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/reducer/table/clone.ts
|
|
762
|
+
var cloneRuntimeTableCallCount = 0;
|
|
763
|
+
function cloneRuntimeBoardState(board) {
|
|
764
|
+
if (board.layout !== "generic") {
|
|
765
|
+
return {
|
|
766
|
+
...board,
|
|
767
|
+
fields: { ...board.fields },
|
|
768
|
+
spaces: Object.fromEntries(
|
|
769
|
+
Object.entries(board.spaces).map(([spaceId, space]) => [
|
|
770
|
+
spaceId,
|
|
771
|
+
{
|
|
772
|
+
...space,
|
|
773
|
+
fields: { ...space.fields }
|
|
774
|
+
}
|
|
775
|
+
])
|
|
776
|
+
),
|
|
777
|
+
relations: board.relations.map((relation) => ({
|
|
778
|
+
...relation,
|
|
779
|
+
fields: { ...relation.fields }
|
|
780
|
+
})),
|
|
781
|
+
containers: Object.fromEntries(
|
|
782
|
+
Object.entries(board.containers).map(([containerId, container]) => [
|
|
783
|
+
containerId,
|
|
784
|
+
{
|
|
785
|
+
...container,
|
|
786
|
+
fields: { ...container.fields }
|
|
787
|
+
}
|
|
788
|
+
])
|
|
789
|
+
),
|
|
790
|
+
edges: board.edges.map((edge) => ({
|
|
791
|
+
...edge,
|
|
792
|
+
spaceIds: [...edge.spaceIds],
|
|
793
|
+
fields: { ...edge.fields }
|
|
794
|
+
})),
|
|
795
|
+
vertices: board.vertices.map((vertex) => ({
|
|
796
|
+
...vertex,
|
|
797
|
+
spaceIds: [...vertex.spaceIds],
|
|
798
|
+
fields: { ...vertex.fields }
|
|
799
|
+
}))
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
...board,
|
|
804
|
+
fields: { ...board.fields },
|
|
805
|
+
spaces: Object.fromEntries(
|
|
806
|
+
Object.entries(board.spaces).map(([spaceId, space]) => [
|
|
807
|
+
spaceId,
|
|
808
|
+
{
|
|
809
|
+
...space,
|
|
810
|
+
fields: { ...space.fields }
|
|
811
|
+
}
|
|
812
|
+
])
|
|
813
|
+
),
|
|
814
|
+
relations: board.relations.map((relation) => ({
|
|
815
|
+
...relation,
|
|
816
|
+
fields: { ...relation.fields }
|
|
817
|
+
})),
|
|
818
|
+
containers: Object.fromEntries(
|
|
819
|
+
Object.entries(board.containers).map(([containerId, container]) => [
|
|
820
|
+
containerId,
|
|
821
|
+
{
|
|
822
|
+
...container,
|
|
823
|
+
fields: { ...container.fields }
|
|
824
|
+
}
|
|
825
|
+
])
|
|
826
|
+
)
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function cloneRuntimeTable(table) {
|
|
830
|
+
cloneRuntimeTableCallCount += 1;
|
|
831
|
+
return {
|
|
832
|
+
...table,
|
|
833
|
+
zones: {
|
|
834
|
+
shared: Object.fromEntries(
|
|
835
|
+
Object.entries(table.zones.shared).map(([zoneId, componentIds]) => [
|
|
836
|
+
zoneId,
|
|
837
|
+
[...componentIds]
|
|
838
|
+
])
|
|
839
|
+
),
|
|
840
|
+
perPlayer: Object.fromEntries(
|
|
841
|
+
Object.entries(table.zones.perPlayer).map(([zoneId, players]) => [
|
|
842
|
+
zoneId,
|
|
843
|
+
perPlayerMap(players, (componentIds) => [
|
|
844
|
+
...componentIds
|
|
845
|
+
])
|
|
846
|
+
])
|
|
847
|
+
),
|
|
848
|
+
visibility: { ...table.zones.visibility },
|
|
849
|
+
cardSetIdsByZoneId: table.zones.cardSetIdsByZoneId ? Object.fromEntries(
|
|
850
|
+
Object.entries(table.zones.cardSetIdsByZoneId).map(
|
|
851
|
+
([zoneId, cardSetIds]) => [zoneId, [...cardSetIds]]
|
|
852
|
+
)
|
|
853
|
+
) : table.zones.cardSetIdsByZoneId
|
|
854
|
+
},
|
|
855
|
+
decks: Object.fromEntries(
|
|
856
|
+
Object.entries(table.decks).map(([deckId, cards]) => [
|
|
857
|
+
deckId,
|
|
858
|
+
[...cards]
|
|
859
|
+
])
|
|
860
|
+
),
|
|
861
|
+
hands: Object.fromEntries(
|
|
862
|
+
Object.entries(table.hands).map(([handId, players]) => [
|
|
863
|
+
handId,
|
|
864
|
+
perPlayerMap(players, (cards) => [...cards])
|
|
865
|
+
])
|
|
866
|
+
),
|
|
867
|
+
handVisibility: { ...table.handVisibility },
|
|
868
|
+
pieces: Object.fromEntries(
|
|
869
|
+
Object.entries(table.pieces).map(([pieceId, piece]) => [
|
|
870
|
+
pieceId,
|
|
871
|
+
{ ...piece }
|
|
872
|
+
])
|
|
873
|
+
),
|
|
874
|
+
componentLocations: { ...table.componentLocations },
|
|
875
|
+
ownerOfCard: { ...table.ownerOfCard },
|
|
876
|
+
visibility: { ...table.visibility },
|
|
877
|
+
resources: perPlayerMap(
|
|
878
|
+
table.resources,
|
|
879
|
+
(resources) => ({ ...resources })
|
|
880
|
+
),
|
|
881
|
+
boards: {
|
|
882
|
+
...table.boards,
|
|
883
|
+
byId: Object.fromEntries(
|
|
884
|
+
Object.entries(table.boards.byId).map(([boardId, board]) => [
|
|
885
|
+
boardId,
|
|
886
|
+
cloneRuntimeBoardState(board)
|
|
887
|
+
])
|
|
888
|
+
),
|
|
889
|
+
hex: Object.fromEntries(
|
|
890
|
+
Object.entries(table.boards.hex ?? {}).map(([boardId, board]) => [
|
|
891
|
+
boardId,
|
|
892
|
+
cloneRuntimeBoardState(board)
|
|
893
|
+
])
|
|
894
|
+
),
|
|
895
|
+
square: Object.fromEntries(
|
|
896
|
+
Object.entries(table.boards.square ?? {}).map(([boardId, board]) => [
|
|
897
|
+
boardId,
|
|
898
|
+
cloneRuntimeBoardState(board)
|
|
899
|
+
])
|
|
900
|
+
)
|
|
901
|
+
},
|
|
902
|
+
dice: Object.fromEntries(
|
|
903
|
+
Object.entries(table.dice).map(([dieId, die]) => [dieId, { ...die }])
|
|
904
|
+
)
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/reducer/table/component-locations.ts
|
|
909
|
+
function getComponentLocation(table, componentId) {
|
|
910
|
+
return table.componentLocations[componentId];
|
|
911
|
+
}
|
|
912
|
+
function getComponentDeckLocation(table, componentId) {
|
|
913
|
+
const location = getComponentLocation(table, componentId);
|
|
914
|
+
if (location?.type !== "InDeck") {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
componentId,
|
|
919
|
+
deckId: location.deckId,
|
|
920
|
+
cards: table.decks[location.deckId],
|
|
921
|
+
location
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function getComponentHandLocation(table, componentId) {
|
|
925
|
+
const location = getComponentLocation(table, componentId);
|
|
926
|
+
if (location?.type !== "InHand") {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
componentId,
|
|
931
|
+
handId: location.handId,
|
|
932
|
+
playerId: location.playerId,
|
|
933
|
+
cards: ppRead(
|
|
934
|
+
table.hands[location.handId],
|
|
935
|
+
location.playerId
|
|
936
|
+
),
|
|
937
|
+
location
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function getComponentZoneLocation(table, componentId) {
|
|
941
|
+
const location = getComponentLocation(table, componentId);
|
|
942
|
+
if (location?.type !== "InZone") {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
componentId,
|
|
947
|
+
zoneId: location.zoneId,
|
|
948
|
+
location
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function getComponentSpaceLocation(table, componentId) {
|
|
952
|
+
const location = getComponentLocation(table, componentId);
|
|
953
|
+
if (location?.type !== "OnSpace") {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
const boardId = location.boardId;
|
|
957
|
+
const spaceId = location.spaceId;
|
|
958
|
+
return {
|
|
959
|
+
componentId,
|
|
960
|
+
boardId,
|
|
961
|
+
board: getBoard(table, boardId),
|
|
962
|
+
spaceId,
|
|
963
|
+
space: getSpace(table, boardId, spaceId),
|
|
964
|
+
location
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function getComponentContainerLocation(table, componentId) {
|
|
968
|
+
const location = getComponentLocation(table, componentId);
|
|
969
|
+
if (location?.type !== "InContainer") {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
const boardId = location.boardId;
|
|
973
|
+
const containerId = location.containerId;
|
|
974
|
+
return {
|
|
975
|
+
componentId,
|
|
976
|
+
boardId,
|
|
977
|
+
board: getBoard(table, boardId),
|
|
978
|
+
containerId,
|
|
979
|
+
container: getContainer(table, boardId, containerId),
|
|
980
|
+
location
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function getComponentEdgeLocation(table, componentId) {
|
|
984
|
+
const location = getComponentLocation(table, componentId);
|
|
985
|
+
if (location?.type !== "OnEdge") {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const boardId = location.boardId;
|
|
989
|
+
const edgeId = location.edgeId;
|
|
990
|
+
return {
|
|
991
|
+
componentId,
|
|
992
|
+
boardId,
|
|
993
|
+
board: getTiledBoard(table, boardId),
|
|
994
|
+
edgeId,
|
|
995
|
+
edge: getEdge(table, boardId, edgeId),
|
|
996
|
+
location
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function getComponentVertexLocation(table, componentId) {
|
|
1000
|
+
const location = getComponentLocation(table, componentId);
|
|
1001
|
+
if (location?.type !== "OnVertex") {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
const boardId = location.boardId;
|
|
1005
|
+
const vertexId = location.vertexId;
|
|
1006
|
+
return {
|
|
1007
|
+
componentId,
|
|
1008
|
+
boardId,
|
|
1009
|
+
board: getTiledBoard(table, boardId),
|
|
1010
|
+
vertexId,
|
|
1011
|
+
vertex: getVertex(table, boardId, vertexId),
|
|
1012
|
+
location
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function getComponentSlotLocation(table, componentId) {
|
|
1016
|
+
const location = getComponentLocation(table, componentId);
|
|
1017
|
+
if (location?.type !== "InSlot") {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
componentId,
|
|
1022
|
+
host: location.host,
|
|
1023
|
+
slotId: location.slotId,
|
|
1024
|
+
location
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/reducer/table/zone-queries.ts
|
|
1029
|
+
function matchesSlotHost(location, host, slotId) {
|
|
1030
|
+
return location.type === "InSlot" && location.host.kind === host.kind && location.host.id === host.id && (slotId === void 0 || location.slotId === slotId);
|
|
1031
|
+
}
|
|
1032
|
+
function componentPlayerId(table, componentId) {
|
|
1033
|
+
const piece = table.pieces[componentId];
|
|
1034
|
+
if (piece) {
|
|
1035
|
+
return piece.ownerId ?? null;
|
|
1036
|
+
}
|
|
1037
|
+
const die = table.dice[componentId];
|
|
1038
|
+
if (die) {
|
|
1039
|
+
return die.ownerId ?? null;
|
|
1040
|
+
}
|
|
1041
|
+
const owner = table.ownerOfCard[componentId];
|
|
1042
|
+
if (owner !== void 0) {
|
|
1043
|
+
return owner ?? null;
|
|
1044
|
+
}
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
function componentData(table, componentId) {
|
|
1048
|
+
const piece = table.pieces[componentId];
|
|
1049
|
+
if (piece) {
|
|
1050
|
+
return piece.properties;
|
|
1051
|
+
}
|
|
1052
|
+
const die = table.dice[componentId];
|
|
1053
|
+
if (die) {
|
|
1054
|
+
return die.properties;
|
|
1055
|
+
}
|
|
1056
|
+
const card = table.cards[componentId];
|
|
1057
|
+
if (card) {
|
|
1058
|
+
return card.properties;
|
|
1059
|
+
}
|
|
1060
|
+
return void 0;
|
|
1061
|
+
}
|
|
1062
|
+
function getSharedZoneCards(table, zoneId) {
|
|
1063
|
+
assertZoneScope(
|
|
1064
|
+
table,
|
|
1065
|
+
zoneId,
|
|
1066
|
+
"shared",
|
|
1067
|
+
"getSharedZoneCards",
|
|
1068
|
+
"zoneId"
|
|
1069
|
+
);
|
|
1070
|
+
return [
|
|
1071
|
+
...ensureArray(table.zones.shared[zoneId] ?? table.decks[zoneId])
|
|
1072
|
+
];
|
|
1073
|
+
}
|
|
1074
|
+
function getPlayerZoneCards(table, playerId, zoneId) {
|
|
1075
|
+
assertZoneScope(
|
|
1076
|
+
table,
|
|
1077
|
+
zoneId,
|
|
1078
|
+
"perPlayer",
|
|
1079
|
+
"getPlayerZoneCards",
|
|
1080
|
+
"zoneId"
|
|
1081
|
+
);
|
|
1082
|
+
const cards = ppRead(table.zones.perPlayer[zoneId], playerId) ?? ppRead(table.hands[zoneId], playerId);
|
|
1083
|
+
return [
|
|
1084
|
+
...ensureArray(cards)
|
|
1085
|
+
];
|
|
1086
|
+
}
|
|
1087
|
+
function collectZoneIds(...sources) {
|
|
1088
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1089
|
+
for (const source of sources) {
|
|
1090
|
+
if (!source) continue;
|
|
1091
|
+
for (const key of Object.keys(source)) {
|
|
1092
|
+
seen.add(key);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return [...seen];
|
|
1096
|
+
}
|
|
1097
|
+
function getAllSharedZoneCards(table) {
|
|
1098
|
+
const zoneIds = collectZoneIds(
|
|
1099
|
+
table.zones.shared,
|
|
1100
|
+
table.decks
|
|
1101
|
+
);
|
|
1102
|
+
const result = {};
|
|
1103
|
+
for (const zoneId of zoneIds) {
|
|
1104
|
+
result[zoneId] = getSharedZoneCards(
|
|
1105
|
+
table,
|
|
1106
|
+
zoneId
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
function getAllPlayerZoneCards(table, zoneId) {
|
|
1112
|
+
const result = {};
|
|
1113
|
+
for (const playerId of table.playerOrder) {
|
|
1114
|
+
result[playerId] = getPlayerZoneCards(
|
|
1115
|
+
table,
|
|
1116
|
+
playerId,
|
|
1117
|
+
zoneId
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
return result;
|
|
1121
|
+
}
|
|
1122
|
+
function getCard(table, cardId) {
|
|
1123
|
+
const card = table.cards[cardId];
|
|
1124
|
+
return {
|
|
1125
|
+
id: card.id,
|
|
1126
|
+
cardType: card.cardType,
|
|
1127
|
+
name: card.name,
|
|
1128
|
+
text: card.text,
|
|
1129
|
+
properties: card.properties
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function getCardsById(table, cardIds) {
|
|
1133
|
+
return Object.fromEntries(
|
|
1134
|
+
cardIds.map((cardId) => [
|
|
1135
|
+
cardId,
|
|
1136
|
+
table.cards[cardId] ? getCard(table, cardId) : void 0
|
|
1137
|
+
])
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
function getSharedZoneCardCollection(table, zoneId) {
|
|
1141
|
+
const cardIds = getSharedZoneCards(table, zoneId);
|
|
1142
|
+
return {
|
|
1143
|
+
cardIds,
|
|
1144
|
+
cardsById: getCardsById(table, cardIds)
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function getPlayerZoneCardCollection(table, playerId, zoneId) {
|
|
1148
|
+
const cardIds = getPlayerZoneCards(table, playerId, zoneId);
|
|
1149
|
+
return {
|
|
1150
|
+
cardIds,
|
|
1151
|
+
cardsById: getCardsById(table, cardIds)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function getSlotOccupants(table, host, slotId) {
|
|
1155
|
+
return orderedComponentIdsForLocation(
|
|
1156
|
+
table,
|
|
1157
|
+
(location) => matchesSlotHost(location, host, slotId)
|
|
1158
|
+
).map((componentId) => ({
|
|
1159
|
+
pieceId: componentId,
|
|
1160
|
+
playerId: componentPlayerId(
|
|
1161
|
+
table,
|
|
1162
|
+
componentId
|
|
1163
|
+
),
|
|
1164
|
+
slotId,
|
|
1165
|
+
data: componentData(table, componentId)
|
|
1166
|
+
}));
|
|
1167
|
+
}
|
|
1168
|
+
function getSlotOccupantsByHost(table, host) {
|
|
1169
|
+
const occupantsBySlot = {};
|
|
1170
|
+
orderedComponentIdsForLocation(
|
|
1171
|
+
table,
|
|
1172
|
+
(location) => matchesSlotHost(location, host)
|
|
1173
|
+
).forEach((componentId) => {
|
|
1174
|
+
const location = table.componentLocations[componentId];
|
|
1175
|
+
if (!location || !matchesSlotHost(location, host)) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const slotOccupant = {
|
|
1179
|
+
pieceId: componentId,
|
|
1180
|
+
playerId: componentPlayerId(
|
|
1181
|
+
table,
|
|
1182
|
+
componentId
|
|
1183
|
+
),
|
|
1184
|
+
slotId: location.slotId,
|
|
1185
|
+
data: componentData(table, componentId)
|
|
1186
|
+
};
|
|
1187
|
+
(occupantsBySlot[location.slotId] ??= []).push(slotOccupant);
|
|
1188
|
+
});
|
|
1189
|
+
return occupantsBySlot;
|
|
1190
|
+
}
|
|
1191
|
+
function getCardOwner(table, cardId) {
|
|
1192
|
+
return table.ownerOfCard[cardId];
|
|
1193
|
+
}
|
|
1194
|
+
function getCardVisibility(table, cardId) {
|
|
1195
|
+
return table.visibility[cardId];
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/reducer/table/resource-ops.ts
|
|
1199
|
+
function getPlayerOrder(table) {
|
|
1200
|
+
return table.playerOrder;
|
|
1201
|
+
}
|
|
1202
|
+
function getPlayerResources(table, playerId) {
|
|
1203
|
+
return perPlayerGet(
|
|
1204
|
+
table.resources,
|
|
1205
|
+
playerId
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
function getPlayerResourceAmount(table, playerId, resourceId) {
|
|
1209
|
+
const playerResources = perPlayerGet(
|
|
1210
|
+
table.resources,
|
|
1211
|
+
playerId
|
|
1212
|
+
);
|
|
1213
|
+
if (!playerResources) return 0;
|
|
1214
|
+
const value = playerResources[resourceId];
|
|
1215
|
+
return typeof value === "number" ? value : 0;
|
|
1216
|
+
}
|
|
1217
|
+
function getPlayerResourceTotal(table, playerId) {
|
|
1218
|
+
const playerResources = perPlayerGet(
|
|
1219
|
+
table.resources,
|
|
1220
|
+
playerId
|
|
1221
|
+
);
|
|
1222
|
+
if (!playerResources) return 0;
|
|
1223
|
+
let total = 0;
|
|
1224
|
+
for (const key of Object.keys(playerResources)) {
|
|
1225
|
+
const value = playerResources[key];
|
|
1226
|
+
if (typeof value === "number") total += value;
|
|
1227
|
+
}
|
|
1228
|
+
return total;
|
|
1229
|
+
}
|
|
1230
|
+
function getNextPlayerInOrder(table, playerId) {
|
|
1231
|
+
const order = table.playerOrder;
|
|
1232
|
+
if (order.length === 0) return null;
|
|
1233
|
+
const idx = order.indexOf(playerId);
|
|
1234
|
+
if (idx < 0) return null;
|
|
1235
|
+
return order[(idx + 1) % order.length] ?? null;
|
|
1236
|
+
}
|
|
1237
|
+
function forEachResourceEntry(amounts, visit) {
|
|
1238
|
+
for (const resourceId of Object.keys(amounts)) {
|
|
1239
|
+
const amount = amounts[resourceId];
|
|
1240
|
+
if (typeof amount !== "number" || amount === 0) continue;
|
|
1241
|
+
visit(resourceId, amount);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function canAffordResources(table, playerId, amounts) {
|
|
1245
|
+
for (const resourceId of Object.keys(amounts)) {
|
|
1246
|
+
const required = amounts[resourceId];
|
|
1247
|
+
if (typeof required !== "number" || required <= 0) continue;
|
|
1248
|
+
if (getPlayerResourceAmount(table, playerId, resourceId) < required) {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1254
|
+
function getMissingResources(table, playerId, amounts) {
|
|
1255
|
+
const missing = {};
|
|
1256
|
+
for (const resourceId of Object.keys(amounts)) {
|
|
1257
|
+
const required = amounts[resourceId];
|
|
1258
|
+
if (typeof required !== "number" || required <= 0) continue;
|
|
1259
|
+
const have = getPlayerResourceAmount(table, playerId, resourceId);
|
|
1260
|
+
if (have < required) missing[resourceId] = required - have;
|
|
1261
|
+
}
|
|
1262
|
+
return missing;
|
|
1263
|
+
}
|
|
1264
|
+
function writePlayerResources(table, playerId, nextForPlayer) {
|
|
1265
|
+
table.resources = perPlayerSet(
|
|
1266
|
+
table.resources,
|
|
1267
|
+
playerId,
|
|
1268
|
+
nextForPlayer
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
function addPlayerResources(table, playerId, amounts) {
|
|
1272
|
+
const nextTable = { ...table };
|
|
1273
|
+
addPlayerResourcesInPlace(nextTable, playerId, amounts);
|
|
1274
|
+
return nextTable;
|
|
1275
|
+
}
|
|
1276
|
+
function addPlayerResourcesInPlace(table, playerId, amounts) {
|
|
1277
|
+
const prev = perPlayerGet(
|
|
1278
|
+
table.resources,
|
|
1279
|
+
playerId
|
|
1280
|
+
) ?? {};
|
|
1281
|
+
const next = { ...prev };
|
|
1282
|
+
forEachResourceEntry(amounts, (resourceId, amount) => {
|
|
1283
|
+
if (amount < 0) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`addPlayerResources: negative amount for resource '${resourceId}'. Use spendPlayerResources or transferPlayerResources instead.`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
next[resourceId] = (next[resourceId] ?? 0) + amount;
|
|
1289
|
+
});
|
|
1290
|
+
writePlayerResources(table, playerId, next);
|
|
1291
|
+
}
|
|
1292
|
+
function spendPlayerResources(table, playerId, amounts) {
|
|
1293
|
+
const nextTable = { ...table };
|
|
1294
|
+
spendPlayerResourcesInPlace(nextTable, playerId, amounts);
|
|
1295
|
+
return nextTable;
|
|
1296
|
+
}
|
|
1297
|
+
function spendPlayerResourcesInPlace(table, playerId, amounts) {
|
|
1298
|
+
if (!canAffordResources(table, playerId, amounts)) {
|
|
1299
|
+
const missing = getMissingResources(table, playerId, amounts);
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`spendPlayerResources: player '${playerId}' cannot afford ${JSON.stringify(
|
|
1302
|
+
missing
|
|
1303
|
+
)}. Check canAfford in your validate step first.`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
const prev = perPlayerGet(
|
|
1307
|
+
table.resources,
|
|
1308
|
+
playerId
|
|
1309
|
+
) ?? {};
|
|
1310
|
+
const next = { ...prev };
|
|
1311
|
+
forEachResourceEntry(amounts, (resourceId, amount) => {
|
|
1312
|
+
if (amount < 0) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`spendPlayerResources: negative amount for resource '${resourceId}'. Pass positive amounts \u2014 the op deducts them from the player.`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
next[resourceId] = Math.max(0, (next[resourceId] ?? 0) - amount);
|
|
1318
|
+
});
|
|
1319
|
+
writePlayerResources(table, playerId, next);
|
|
1320
|
+
}
|
|
1321
|
+
function transferPlayerResources(table, fromPlayerId, toPlayerId, amounts) {
|
|
1322
|
+
const nextTable = { ...table };
|
|
1323
|
+
transferPlayerResourcesInPlace(nextTable, fromPlayerId, toPlayerId, amounts);
|
|
1324
|
+
return nextTable;
|
|
1325
|
+
}
|
|
1326
|
+
function transferPlayerResourcesInPlace(table, fromPlayerId, toPlayerId, amounts) {
|
|
1327
|
+
spendPlayerResourcesInPlace(table, fromPlayerId, amounts);
|
|
1328
|
+
addPlayerResourcesInPlace(table, toPlayerId, amounts);
|
|
1329
|
+
}
|
|
1330
|
+
function setPlayerResource(table, playerId, resourceId, amount) {
|
|
1331
|
+
const nextTable = { ...table };
|
|
1332
|
+
setPlayerResourceInPlace(nextTable, playerId, resourceId, amount);
|
|
1333
|
+
return nextTable;
|
|
1334
|
+
}
|
|
1335
|
+
function setPlayerResourceInPlace(table, playerId, resourceId, amount) {
|
|
1336
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`setPlayerResource: amount must be a non-negative finite number, got ${amount}.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const prev = perPlayerGet(
|
|
1342
|
+
table.resources,
|
|
1343
|
+
playerId
|
|
1344
|
+
) ?? {};
|
|
1345
|
+
writePlayerResources(table, playerId, {
|
|
1346
|
+
...prev,
|
|
1347
|
+
[resourceId]: amount
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/reducer/table/component-mutations.ts
|
|
1352
|
+
function reindexSpaceOccupants(table, boardId, spaceId) {
|
|
1353
|
+
getComponentsOnSpace(table, boardId, spaceId).forEach(
|
|
1354
|
+
(componentId, index) => {
|
|
1355
|
+
const location = table.componentLocations[componentId];
|
|
1356
|
+
if (location?.type === "OnSpace") {
|
|
1357
|
+
table.componentLocations[componentId] = {
|
|
1358
|
+
...location,
|
|
1359
|
+
position: index
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
function reindexContainerOccupants(table, boardId, containerId) {
|
|
1366
|
+
getComponentsInContainer(table, boardId, containerId).forEach(
|
|
1367
|
+
(componentId, index) => {
|
|
1368
|
+
const location = table.componentLocations[componentId];
|
|
1369
|
+
if (location?.type === "InContainer") {
|
|
1370
|
+
table.componentLocations[componentId] = {
|
|
1371
|
+
...location,
|
|
1372
|
+
position: index
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
function reindexEdgeOccupants(table, boardId, edgeId) {
|
|
1379
|
+
orderedComponentIdsForLocation(
|
|
1380
|
+
table,
|
|
1381
|
+
(location) => location.type === "OnEdge" && location.boardId === boardId && location.edgeId === edgeId
|
|
1382
|
+
).forEach((componentId, index) => {
|
|
1383
|
+
const location = table.componentLocations[componentId];
|
|
1384
|
+
if (location?.type === "OnEdge") {
|
|
1385
|
+
table.componentLocations[componentId] = {
|
|
1386
|
+
...location,
|
|
1387
|
+
position: index
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function reindexVertexOccupants(table, boardId, vertexId) {
|
|
1393
|
+
orderedComponentIdsForLocation(
|
|
1394
|
+
table,
|
|
1395
|
+
(location) => location.type === "OnVertex" && location.boardId === boardId && location.vertexId === vertexId
|
|
1396
|
+
).forEach((componentId, index) => {
|
|
1397
|
+
const location = table.componentLocations[componentId];
|
|
1398
|
+
if (location?.type === "OnVertex") {
|
|
1399
|
+
table.componentLocations[componentId] = {
|
|
1400
|
+
...location,
|
|
1401
|
+
position: index
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
function reindexSlotOccupants(table, host, slotId) {
|
|
1407
|
+
orderedComponentIdsForLocation(
|
|
1408
|
+
table,
|
|
1409
|
+
(location) => location.type === "InSlot" && location.host.kind === host.kind && location.host.id === host.id && location.slotId === slotId
|
|
1410
|
+
).forEach((componentId, index) => {
|
|
1411
|
+
const location = table.componentLocations[componentId];
|
|
1412
|
+
if (location?.type === "InSlot") {
|
|
1413
|
+
table.componentLocations[componentId] = {
|
|
1414
|
+
...location,
|
|
1415
|
+
position: index
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
function removeComponentFromCurrentLocation(table, componentId) {
|
|
1421
|
+
const currentLocation = table.componentLocations[componentId];
|
|
1422
|
+
if (!currentLocation) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (currentLocation.type === "InDeck") {
|
|
1426
|
+
const nextCards = ensureArray(table.decks[currentLocation.deckId]).filter(
|
|
1427
|
+
(candidate) => candidate !== componentId
|
|
1428
|
+
);
|
|
1429
|
+
syncSharedZoneWithDeck(
|
|
1430
|
+
table,
|
|
1431
|
+
currentLocation.deckId,
|
|
1432
|
+
nextCards
|
|
1433
|
+
);
|
|
1434
|
+
nextCards.forEach((cardId, index) => {
|
|
1435
|
+
const location = table.componentLocations[cardId];
|
|
1436
|
+
if (location?.type === "InDeck") {
|
|
1437
|
+
table.componentLocations[cardId] = {
|
|
1438
|
+
...location,
|
|
1439
|
+
position: index
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (currentLocation.type === "InHand") {
|
|
1446
|
+
const nextCards = ensureArray(
|
|
1447
|
+
ppRead(table.hands[currentLocation.handId], currentLocation.playerId)
|
|
1448
|
+
).filter((candidate) => candidate !== componentId);
|
|
1449
|
+
syncPlayerZoneWithHand(
|
|
1450
|
+
table,
|
|
1451
|
+
currentLocation.handId,
|
|
1452
|
+
currentLocation.playerId,
|
|
1453
|
+
nextCards
|
|
1454
|
+
);
|
|
1455
|
+
nextCards.forEach((cardId, index) => {
|
|
1456
|
+
const location = table.componentLocations[cardId];
|
|
1457
|
+
if (location?.type === "InHand") {
|
|
1458
|
+
table.componentLocations[cardId] = {
|
|
1459
|
+
...location,
|
|
1460
|
+
position: index
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (currentLocation.type === "OnSpace") {
|
|
1467
|
+
delete table.componentLocations[componentId];
|
|
1468
|
+
reindexSpaceOccupants(
|
|
1469
|
+
table,
|
|
1470
|
+
currentLocation.boardId,
|
|
1471
|
+
currentLocation.spaceId
|
|
1472
|
+
);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (currentLocation.type === "InZone") {
|
|
1476
|
+
if (currentLocation.zoneId in table.zones.shared) {
|
|
1477
|
+
const nextComponents = ensureArray(
|
|
1478
|
+
table.zones.shared[currentLocation.zoneId]
|
|
1479
|
+
).filter((candidate) => candidate !== componentId);
|
|
1480
|
+
syncSharedZoneWithDeck(
|
|
1481
|
+
table,
|
|
1482
|
+
currentLocation.zoneId,
|
|
1483
|
+
nextComponents
|
|
1484
|
+
);
|
|
1485
|
+
nextComponents.forEach((currentComponentId, index) => {
|
|
1486
|
+
const location = table.componentLocations[currentComponentId];
|
|
1487
|
+
if (location?.type === "InZone") {
|
|
1488
|
+
table.componentLocations[currentComponentId] = {
|
|
1489
|
+
...location,
|
|
1490
|
+
position: index
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
delete table.componentLocations[componentId];
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (currentLocation.type === "InContainer") {
|
|
1499
|
+
delete table.componentLocations[componentId];
|
|
1500
|
+
reindexContainerOccupants(
|
|
1501
|
+
table,
|
|
1502
|
+
currentLocation.boardId,
|
|
1503
|
+
currentLocation.containerId
|
|
1504
|
+
);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (currentLocation.type === "OnEdge") {
|
|
1508
|
+
delete table.componentLocations[componentId];
|
|
1509
|
+
reindexEdgeOccupants(
|
|
1510
|
+
table,
|
|
1511
|
+
currentLocation.boardId,
|
|
1512
|
+
currentLocation.edgeId
|
|
1513
|
+
);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (currentLocation.type === "OnVertex") {
|
|
1517
|
+
delete table.componentLocations[componentId];
|
|
1518
|
+
reindexVertexOccupants(
|
|
1519
|
+
table,
|
|
1520
|
+
currentLocation.boardId,
|
|
1521
|
+
currentLocation.vertexId
|
|
1522
|
+
);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (currentLocation.type === "InSlot") {
|
|
1526
|
+
delete table.componentLocations[componentId];
|
|
1527
|
+
reindexSlotOccupants(table, currentLocation.host, currentLocation.slotId);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
delete table.componentLocations[componentId];
|
|
1531
|
+
}
|
|
1532
|
+
function moveComponentToSpace(table, componentId, boardId, spaceId) {
|
|
1533
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1534
|
+
moveComponentToSpaceInPlace(nextTable, componentId, boardId, spaceId);
|
|
1535
|
+
return nextTable;
|
|
1536
|
+
}
|
|
1537
|
+
function moveComponentToSpaceInPlace(table, componentId, boardId, spaceId) {
|
|
1538
|
+
const position = getComponentsOnSpace(table, boardId, spaceId).length;
|
|
1539
|
+
removeComponentFromCurrentLocation(table, componentId);
|
|
1540
|
+
table.componentLocations[componentId] = {
|
|
1541
|
+
type: "OnSpace",
|
|
1542
|
+
boardId,
|
|
1543
|
+
spaceId,
|
|
1544
|
+
position
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function moveComponentToContainer(table, componentId, boardId, containerId) {
|
|
1548
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1549
|
+
moveComponentToContainerInPlace(nextTable, componentId, boardId, containerId);
|
|
1550
|
+
return nextTable;
|
|
1551
|
+
}
|
|
1552
|
+
function moveComponentToContainerInPlace(table, componentId, boardId, containerId) {
|
|
1553
|
+
assertCardAllowedInContainer(table, boardId, containerId, componentId);
|
|
1554
|
+
const position = getComponentsInContainer(table, boardId, containerId).length;
|
|
1555
|
+
removeComponentFromCurrentLocation(table, componentId);
|
|
1556
|
+
table.componentLocations[componentId] = {
|
|
1557
|
+
type: "InContainer",
|
|
1558
|
+
boardId,
|
|
1559
|
+
containerId,
|
|
1560
|
+
position
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
function moveComponentToDetached(table, componentId) {
|
|
1564
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1565
|
+
moveComponentToDetachedInPlace(nextTable, componentId);
|
|
1566
|
+
return nextTable;
|
|
1567
|
+
}
|
|
1568
|
+
function moveComponentToDetachedInPlace(table, componentId) {
|
|
1569
|
+
removeComponentFromCurrentLocation(table, componentId);
|
|
1570
|
+
table.componentLocations[componentId] = { type: "Detached" };
|
|
1571
|
+
}
|
|
1572
|
+
function moveComponentToEdge(table, componentId, boardId, edgeId) {
|
|
1573
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1574
|
+
moveComponentToEdgeInPlace(nextTable, componentId, boardId, edgeId);
|
|
1575
|
+
return nextTable;
|
|
1576
|
+
}
|
|
1577
|
+
function moveComponentToEdgeInPlace(table, componentId, boardId, edgeId) {
|
|
1578
|
+
getEdge(table, boardId, edgeId);
|
|
1579
|
+
const position = getComponentsOnEdge(table, boardId, edgeId).length;
|
|
1580
|
+
removeComponentFromCurrentLocation(table, componentId);
|
|
1581
|
+
table.componentLocations[componentId] = {
|
|
1582
|
+
type: "OnEdge",
|
|
1583
|
+
boardId,
|
|
1584
|
+
edgeId,
|
|
1585
|
+
position
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function moveComponentToVertex(table, componentId, boardId, vertexId) {
|
|
1589
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1590
|
+
moveComponentToVertexInPlace(nextTable, componentId, boardId, vertexId);
|
|
1591
|
+
return nextTable;
|
|
1592
|
+
}
|
|
1593
|
+
function moveComponentToVertexInPlace(table, componentId, boardId, vertexId) {
|
|
1594
|
+
getVertex(table, boardId, vertexId);
|
|
1595
|
+
const position = getComponentsOnVertex(table, boardId, vertexId).length;
|
|
1596
|
+
removeComponentFromCurrentLocation(table, componentId);
|
|
1597
|
+
table.componentLocations[componentId] = {
|
|
1598
|
+
type: "OnVertex",
|
|
1599
|
+
boardId,
|
|
1600
|
+
vertexId,
|
|
1601
|
+
position
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/reducer/table/card-mutations.ts
|
|
1606
|
+
function shufflePlayerZoneCards(table, zoneId, playerId, nextCards) {
|
|
1607
|
+
if (nextCards === void 0) {
|
|
1608
|
+
const fromZone = ppRead(
|
|
1609
|
+
table.zones.perPlayer[zoneId] ?? table.hands[zoneId],
|
|
1610
|
+
playerId
|
|
1611
|
+
);
|
|
1612
|
+
return [...ensureArray(fromZone)];
|
|
1613
|
+
}
|
|
1614
|
+
table.hands[zoneId] = ppWrite(table.hands[zoneId], playerId, [
|
|
1615
|
+
...nextCards
|
|
1616
|
+
]);
|
|
1617
|
+
table.zones.perPlayer[zoneId] = ppWrite(
|
|
1618
|
+
table.zones.perPlayer[zoneId],
|
|
1619
|
+
playerId,
|
|
1620
|
+
[...nextCards]
|
|
1621
|
+
);
|
|
1622
|
+
return [...nextCards];
|
|
1623
|
+
}
|
|
1624
|
+
function appendToDeck(table, deckId, cardId, playedBy = null, position = "bottom") {
|
|
1625
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1626
|
+
appendToDeckInPlace(nextTable, deckId, cardId, playedBy, position);
|
|
1627
|
+
return nextTable;
|
|
1628
|
+
}
|
|
1629
|
+
function appendToDeckInPlace(table, deckId, cardId, playedBy = null, position = "bottom") {
|
|
1630
|
+
assertZoneScope(
|
|
1631
|
+
table,
|
|
1632
|
+
deckId,
|
|
1633
|
+
"shared",
|
|
1634
|
+
"addCardToSharedZone",
|
|
1635
|
+
"zoneId"
|
|
1636
|
+
);
|
|
1637
|
+
const deckCards = [...ensureArray(table.decks[deckId])];
|
|
1638
|
+
assertCardAllowedInZone(table, deckId, cardId);
|
|
1639
|
+
if (position === "top") {
|
|
1640
|
+
deckCards.unshift(cardId);
|
|
1641
|
+
} else {
|
|
1642
|
+
deckCards.push(cardId);
|
|
1643
|
+
}
|
|
1644
|
+
syncSharedZoneWithDeck(table, deckId, deckCards);
|
|
1645
|
+
for (const [index, currentCardId] of deckCards.entries()) {
|
|
1646
|
+
if (currentCardId === cardId) {
|
|
1647
|
+
table.componentLocations[currentCardId] = {
|
|
1648
|
+
type: "InDeck",
|
|
1649
|
+
deckId,
|
|
1650
|
+
playedBy,
|
|
1651
|
+
position: index
|
|
1652
|
+
};
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
const existing = table.componentLocations[currentCardId];
|
|
1656
|
+
table.componentLocations[currentCardId] = {
|
|
1657
|
+
type: "InDeck",
|
|
1658
|
+
deckId,
|
|
1659
|
+
playedBy: existing?.type === "InDeck" ? existing.playedBy : null,
|
|
1660
|
+
position: index
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
table.ownerOfCard[cardId] = playedBy;
|
|
1664
|
+
table.visibility[cardId] = {
|
|
1665
|
+
faceUp: true
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function removeFromDeck(table, deckId, cardId) {
|
|
1669
|
+
const nextTable = cloneRuntimeTable(table);
|
|
1670
|
+
removeFromDeckInPlace(nextTable, deckId, cardId);
|
|
1671
|
+
return nextTable;
|
|
1672
|
+
}
|
|
1673
|
+
function removeFromDeckInPlace(table, deckId, cardId) {
|
|
1674
|
+
assertZoneScope(
|
|
1675
|
+
table,
|
|
1676
|
+
deckId,
|
|
1677
|
+
"shared",
|
|
1678
|
+
"removeCardFromSharedZone",
|
|
1679
|
+
"zoneId"
|
|
1680
|
+
);
|
|
1681
|
+
const remaining = ensureArray(table.decks[deckId]).filter(
|
|
1682
|
+
(candidate) => candidate !== cardId
|
|
1683
|
+
);
|
|
1684
|
+
syncSharedZoneWithDeck(table, deckId, remaining);
|
|
1685
|
+
for (const [index, currentCardId] of remaining.entries()) {
|
|
1686
|
+
const currentLocation = table.componentLocations[currentCardId];
|
|
1687
|
+
if (currentLocation?.type === "InDeck") {
|
|
1688
|
+
table.componentLocations[currentCardId] = {
|
|
1689
|
+
...currentLocation,
|
|
1690
|
+
position: index
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function computeVisibilityForPlayerZone(table, zoneId, playerId) {
|
|
1696
|
+
const mode = table.handVisibility[zoneId];
|
|
1697
|
+
if (mode === "all" || mode === "public") {
|
|
1698
|
+
return { faceUp: true };
|
|
1699
|
+
}
|
|
1700
|
+
return { faceUp: false, visibleTo: [playerId] };
|
|
1701
|
+
}
|
|
1702
|
+
function moveFromHandToDeck(options) {
|
|
1703
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
1704
|
+
moveFromHandToDeckInPlace({
|
|
1705
|
+
...options,
|
|
1706
|
+
table: nextTable
|
|
1707
|
+
});
|
|
1708
|
+
return nextTable;
|
|
1709
|
+
}
|
|
1710
|
+
function moveFromHandToDeckInPlace(options) {
|
|
1711
|
+
assertZoneScope(
|
|
1712
|
+
options.table,
|
|
1713
|
+
options.handId,
|
|
1714
|
+
"perPlayer",
|
|
1715
|
+
"moveCardFromPlayerZoneToSharedZone",
|
|
1716
|
+
"fromZoneId"
|
|
1717
|
+
);
|
|
1718
|
+
assertZoneScope(
|
|
1719
|
+
options.table,
|
|
1720
|
+
options.deckId,
|
|
1721
|
+
"shared",
|
|
1722
|
+
"moveCardFromPlayerZoneToSharedZone",
|
|
1723
|
+
"toZoneId"
|
|
1724
|
+
);
|
|
1725
|
+
const currentHand = ensureArray(
|
|
1726
|
+
ppRead(options.table.hands[options.handId], options.playerId)
|
|
1727
|
+
).filter((candidate) => candidate !== options.cardId);
|
|
1728
|
+
syncPlayerZoneWithHand(
|
|
1729
|
+
options.table,
|
|
1730
|
+
options.handId,
|
|
1731
|
+
options.playerId,
|
|
1732
|
+
currentHand
|
|
1733
|
+
);
|
|
1734
|
+
for (const [index, currentCardId] of currentHand.entries()) {
|
|
1735
|
+
options.table.componentLocations[currentCardId] = {
|
|
1736
|
+
type: "InHand",
|
|
1737
|
+
handId: options.handId,
|
|
1738
|
+
playerId: options.playerId,
|
|
1739
|
+
position: index
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
options.table.ownerOfCard[options.cardId] = options.playedBy ?? options.playerId;
|
|
1743
|
+
options.table.visibility[options.cardId] = {
|
|
1744
|
+
faceUp: true
|
|
1745
|
+
};
|
|
1746
|
+
appendToDeckInPlace(
|
|
1747
|
+
options.table,
|
|
1748
|
+
options.deckId,
|
|
1749
|
+
options.cardId,
|
|
1750
|
+
options.playedBy ?? options.playerId,
|
|
1751
|
+
options.position ?? "bottom"
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
function moveCardFromPlayerZoneToSharedZone(options) {
|
|
1755
|
+
return moveFromHandToDeck({
|
|
1756
|
+
table: options.table,
|
|
1757
|
+
playerId: options.playerId,
|
|
1758
|
+
handId: options.fromZoneId,
|
|
1759
|
+
cardId: options.cardId,
|
|
1760
|
+
deckId: options.toZoneId,
|
|
1761
|
+
playedBy: options.playedBy,
|
|
1762
|
+
position: options.position
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
function moveCardFromPlayerZoneToSharedZoneInPlace(options) {
|
|
1766
|
+
moveFromHandToDeckInPlace({
|
|
1767
|
+
table: options.table,
|
|
1768
|
+
playerId: options.playerId,
|
|
1769
|
+
handId: options.fromZoneId,
|
|
1770
|
+
cardId: options.cardId,
|
|
1771
|
+
deckId: options.toZoneId,
|
|
1772
|
+
playedBy: options.playedBy,
|
|
1773
|
+
position: options.position
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function dealCardsBetweenPlayerZones(options) {
|
|
1777
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
1778
|
+
dealCardsBetweenPlayerZonesInPlace({
|
|
1779
|
+
...options,
|
|
1780
|
+
table: nextTable
|
|
1781
|
+
});
|
|
1782
|
+
return nextTable;
|
|
1783
|
+
}
|
|
1784
|
+
function dealCardsBetweenPlayerZonesInPlace(options) {
|
|
1785
|
+
let nextTable = options.table;
|
|
1786
|
+
for (let i = 0; i < options.count; i += 1) {
|
|
1787
|
+
const sourceCards = ensureArray(
|
|
1788
|
+
ppRead(
|
|
1789
|
+
nextTable.zones.perPlayer[options.fromZoneId] ?? nextTable.hands[options.fromZoneId],
|
|
1790
|
+
options.playerId
|
|
1791
|
+
)
|
|
1792
|
+
);
|
|
1793
|
+
const topCardId = sourceCards[0];
|
|
1794
|
+
if (topCardId === void 0) {
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
moveCardBetweenPlayerZonesInPlace({
|
|
1798
|
+
table: nextTable,
|
|
1799
|
+
playerId: options.playerId,
|
|
1800
|
+
fromZoneId: options.fromZoneId,
|
|
1801
|
+
toZoneId: options.toZoneId,
|
|
1802
|
+
cardId: topCardId
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function moveCardFromSharedZoneToPlayerZone(options) {
|
|
1807
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
1808
|
+
moveCardFromSharedZoneToPlayerZoneInPlace({
|
|
1809
|
+
...options,
|
|
1810
|
+
table: nextTable
|
|
1811
|
+
});
|
|
1812
|
+
return nextTable;
|
|
1813
|
+
}
|
|
1814
|
+
function moveCardFromSharedZoneToPlayerZoneInPlace(options) {
|
|
1815
|
+
assertZoneScope(
|
|
1816
|
+
options.table,
|
|
1817
|
+
options.fromZoneId,
|
|
1818
|
+
"shared",
|
|
1819
|
+
"moveCardFromSharedZoneToPlayerZone",
|
|
1820
|
+
"fromZoneId"
|
|
1821
|
+
);
|
|
1822
|
+
assertZoneScope(
|
|
1823
|
+
options.table,
|
|
1824
|
+
options.toZoneId,
|
|
1825
|
+
"perPlayer",
|
|
1826
|
+
"moveCardFromSharedZoneToPlayerZone",
|
|
1827
|
+
"toZoneId"
|
|
1828
|
+
);
|
|
1829
|
+
const sourceCards = ensureArray(
|
|
1830
|
+
options.table.zones.shared[options.fromZoneId] ?? options.table.decks[options.fromZoneId]
|
|
1831
|
+
);
|
|
1832
|
+
if (!sourceCards.includes(options.cardId)) {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
`Card '${String(options.cardId)}' is not in shared zone '${String(
|
|
1835
|
+
options.fromZoneId
|
|
1836
|
+
)}'.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
assertCardAllowedInZone(
|
|
1840
|
+
options.table,
|
|
1841
|
+
options.toZoneId,
|
|
1842
|
+
options.cardId
|
|
1843
|
+
);
|
|
1844
|
+
const remainingSource = sourceCards.filter(
|
|
1845
|
+
(candidate) => candidate !== options.cardId
|
|
1846
|
+
);
|
|
1847
|
+
syncSharedZoneWithDeck(options.table, options.fromZoneId, remainingSource);
|
|
1848
|
+
for (const [index, currentCardId] of remainingSource.entries()) {
|
|
1849
|
+
const existing = options.table.componentLocations[currentCardId];
|
|
1850
|
+
if (existing?.type === "InDeck") {
|
|
1851
|
+
options.table.componentLocations[currentCardId] = {
|
|
1852
|
+
...existing,
|
|
1853
|
+
position: index
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const destinationCards = ensureArray(
|
|
1858
|
+
ppRead(
|
|
1859
|
+
options.table.zones.perPlayer[options.toZoneId] ?? options.table.hands[options.toZoneId],
|
|
1860
|
+
options.playerId
|
|
1861
|
+
)
|
|
1862
|
+
);
|
|
1863
|
+
const nextDestination = options.position === "top" ? [options.cardId, ...destinationCards] : [...destinationCards, options.cardId];
|
|
1864
|
+
syncPlayerZoneWithHand(
|
|
1865
|
+
options.table,
|
|
1866
|
+
options.toZoneId,
|
|
1867
|
+
options.playerId,
|
|
1868
|
+
nextDestination
|
|
1869
|
+
);
|
|
1870
|
+
for (const [index, currentCardId] of nextDestination.entries()) {
|
|
1871
|
+
options.table.componentLocations[currentCardId] = {
|
|
1872
|
+
type: "InHand",
|
|
1873
|
+
handId: options.toZoneId,
|
|
1874
|
+
playerId: options.playerId,
|
|
1875
|
+
position: index
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
options.table.ownerOfCard[options.cardId] = options.playerId;
|
|
1879
|
+
options.table.visibility[options.cardId] = computeVisibilityForPlayerZone(
|
|
1880
|
+
options.table,
|
|
1881
|
+
options.toZoneId,
|
|
1882
|
+
options.playerId
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
function moveCardBetweenPlayerZones(options) {
|
|
1886
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
1887
|
+
moveCardBetweenPlayerZonesInPlace({
|
|
1888
|
+
...options,
|
|
1889
|
+
table: nextTable
|
|
1890
|
+
});
|
|
1891
|
+
return nextTable;
|
|
1892
|
+
}
|
|
1893
|
+
function moveCardBetweenPlayerZonesInPlace(options) {
|
|
1894
|
+
assertZoneScope(
|
|
1895
|
+
options.table,
|
|
1896
|
+
options.fromZoneId,
|
|
1897
|
+
"perPlayer",
|
|
1898
|
+
"moveCardBetweenPlayerZones",
|
|
1899
|
+
"fromZoneId"
|
|
1900
|
+
);
|
|
1901
|
+
assertZoneScope(
|
|
1902
|
+
options.table,
|
|
1903
|
+
options.toZoneId,
|
|
1904
|
+
"perPlayer",
|
|
1905
|
+
"moveCardBetweenPlayerZones",
|
|
1906
|
+
"toZoneId"
|
|
1907
|
+
);
|
|
1908
|
+
const sourceCards = ensureArray(
|
|
1909
|
+
ppRead(
|
|
1910
|
+
options.table.zones.perPlayer[options.fromZoneId] ?? options.table.hands[options.fromZoneId],
|
|
1911
|
+
options.playerId
|
|
1912
|
+
)
|
|
1913
|
+
);
|
|
1914
|
+
if (!sourceCards.includes(options.cardId)) {
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
`Card '${String(options.cardId)}' is not in zone '${String(
|
|
1917
|
+
options.fromZoneId
|
|
1918
|
+
)}' for player '${String(options.playerId)}'.`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
assertCardAllowedInZone(
|
|
1922
|
+
options.table,
|
|
1923
|
+
options.toZoneId,
|
|
1924
|
+
options.cardId
|
|
1925
|
+
);
|
|
1926
|
+
const remainingSource = sourceCards.filter(
|
|
1927
|
+
(candidate) => candidate !== options.cardId
|
|
1928
|
+
);
|
|
1929
|
+
syncPlayerZoneWithHand(
|
|
1930
|
+
options.table,
|
|
1931
|
+
options.fromZoneId,
|
|
1932
|
+
options.playerId,
|
|
1933
|
+
remainingSource
|
|
1934
|
+
);
|
|
1935
|
+
for (const [index, currentCardId] of remainingSource.entries()) {
|
|
1936
|
+
const existing = options.table.componentLocations[currentCardId];
|
|
1937
|
+
if (existing?.type === "InHand") {
|
|
1938
|
+
options.table.componentLocations[currentCardId] = {
|
|
1939
|
+
...existing,
|
|
1940
|
+
position: index
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const destinationCards = ensureArray(
|
|
1945
|
+
ppRead(
|
|
1946
|
+
options.table.zones.perPlayer[options.toZoneId] ?? options.table.hands[options.toZoneId],
|
|
1947
|
+
options.playerId
|
|
1948
|
+
)
|
|
1949
|
+
);
|
|
1950
|
+
const nextDestination = options.position === "top" ? [options.cardId, ...destinationCards] : [...destinationCards, options.cardId];
|
|
1951
|
+
syncPlayerZoneWithHand(
|
|
1952
|
+
options.table,
|
|
1953
|
+
options.toZoneId,
|
|
1954
|
+
options.playerId,
|
|
1955
|
+
nextDestination
|
|
1956
|
+
);
|
|
1957
|
+
for (const [index, currentCardId] of nextDestination.entries()) {
|
|
1958
|
+
options.table.componentLocations[currentCardId] = {
|
|
1959
|
+
type: "InHand",
|
|
1960
|
+
handId: options.toZoneId,
|
|
1961
|
+
playerId: options.playerId,
|
|
1962
|
+
position: index
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
options.table.visibility[options.cardId] = computeVisibilityForPlayerZone(
|
|
1966
|
+
options.table,
|
|
1967
|
+
options.toZoneId,
|
|
1968
|
+
options.playerId
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
function moveCardBetweenSharedZones(options) {
|
|
1972
|
+
const nextTable = cloneRuntimeTable(options.table);
|
|
1973
|
+
moveCardBetweenSharedZonesInPlace({
|
|
1974
|
+
...options,
|
|
1975
|
+
table: nextTable
|
|
1976
|
+
});
|
|
1977
|
+
return nextTable;
|
|
1978
|
+
}
|
|
1979
|
+
function moveCardBetweenSharedZonesInPlace(options) {
|
|
1980
|
+
removeFromDeckInPlace(options.table, options.fromZoneId, options.cardId);
|
|
1981
|
+
appendToDeckInPlace(
|
|
1982
|
+
options.table,
|
|
1983
|
+
options.toZoneId,
|
|
1984
|
+
options.cardId,
|
|
1985
|
+
options.playedBy ?? null,
|
|
1986
|
+
options.position ?? "bottom"
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
function removeCardFromSharedZone(table, deckId, cardId) {
|
|
1990
|
+
return removeFromDeck(table, deckId, cardId);
|
|
1991
|
+
}
|
|
1992
|
+
function removeCardFromSharedZoneInPlace(table, deckId, cardId) {
|
|
1993
|
+
removeFromDeckInPlace(table, deckId, cardId);
|
|
1994
|
+
}
|
|
1995
|
+
function addCardToSharedZone(table, deckId, cardId, playedBy = null, position = "bottom") {
|
|
1996
|
+
return appendToDeck(table, deckId, cardId, playedBy, position);
|
|
1997
|
+
}
|
|
1998
|
+
function addCardToSharedZoneInPlace(table, deckId, cardId, playedBy = null, position = "bottom") {
|
|
1999
|
+
appendToDeckInPlace(table, deckId, cardId, playedBy, position);
|
|
2000
|
+
}
|
|
2001
|
+
function dealCardsFromDeckToHand(table, fromZoneId, playerId, toZoneId, count) {
|
|
2002
|
+
const nextTable = cloneRuntimeTable(table);
|
|
2003
|
+
dealCardsFromDeckToHandInPlace(
|
|
2004
|
+
nextTable,
|
|
2005
|
+
fromZoneId,
|
|
2006
|
+
playerId,
|
|
2007
|
+
toZoneId,
|
|
2008
|
+
count
|
|
2009
|
+
);
|
|
2010
|
+
return nextTable;
|
|
2011
|
+
}
|
|
2012
|
+
function dealCardsFromDeckToHandInPlace(table, fromZoneId, playerId, toZoneId, count) {
|
|
2013
|
+
const publicHands = new Set(
|
|
2014
|
+
Object.entries(table.handVisibility).filter(([, mode]) => mode === "all" || mode === "public").map(([handId]) => handId)
|
|
2015
|
+
);
|
|
2016
|
+
for (let index = 0; index < count; index += 1) {
|
|
2017
|
+
const nextCard = ensureArray(table.decks[fromZoneId])[0];
|
|
2018
|
+
if (!nextCard) {
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
table.decks[fromZoneId] = ensureArray(table.decks[fromZoneId]).slice(
|
|
2022
|
+
1
|
|
2023
|
+
);
|
|
2024
|
+
table.zones.shared[fromZoneId] = [
|
|
2025
|
+
...ensureArray(table.decks[fromZoneId])
|
|
2026
|
+
];
|
|
2027
|
+
const prevHand = ppRead(table.hands[toZoneId], playerId);
|
|
2028
|
+
const nextHand = [...ensureArray(prevHand), nextCard];
|
|
2029
|
+
assertCardAllowedInZone(table, toZoneId, nextCard);
|
|
2030
|
+
table.hands[toZoneId] = ppWrite(
|
|
2031
|
+
table.hands[toZoneId],
|
|
2032
|
+
playerId,
|
|
2033
|
+
nextHand
|
|
2034
|
+
);
|
|
2035
|
+
table.zones.perPlayer[toZoneId] = ppWrite(
|
|
2036
|
+
table.zones.perPlayer[toZoneId],
|
|
2037
|
+
playerId,
|
|
2038
|
+
[...nextHand]
|
|
2039
|
+
);
|
|
2040
|
+
table.componentLocations[nextCard] = {
|
|
2041
|
+
type: "InHand",
|
|
2042
|
+
handId: toZoneId,
|
|
2043
|
+
playerId,
|
|
2044
|
+
position: nextHand.length - 1
|
|
2045
|
+
};
|
|
2046
|
+
table.ownerOfCard[nextCard] = playerId;
|
|
2047
|
+
table.visibility[nextCard] = publicHands.has(toZoneId) ? {
|
|
2048
|
+
faceUp: true
|
|
2049
|
+
} : {
|
|
2050
|
+
faceUp: false,
|
|
2051
|
+
visibleTo: [playerId]
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/reducer/rng.ts
|
|
2057
|
+
function nextRandomInt(bound, rng) {
|
|
2058
|
+
if (bound <= 0) {
|
|
2059
|
+
throw new Error("Random bound must be positive.");
|
|
2060
|
+
}
|
|
2061
|
+
const seed = rng.seed ?? 1;
|
|
2062
|
+
const raw = hashSeedCursor(seed, rng.cursor);
|
|
2063
|
+
const value = raw % bound;
|
|
2064
|
+
return [
|
|
2065
|
+
value,
|
|
2066
|
+
{
|
|
2067
|
+
...rng,
|
|
2068
|
+
cursor: rng.cursor + 1,
|
|
2069
|
+
trace: [
|
|
2070
|
+
...rng.trace,
|
|
2071
|
+
`cursor=${rng.cursor};bound=${bound};value=${value}`
|
|
2072
|
+
]
|
|
2073
|
+
}
|
|
2074
|
+
];
|
|
2075
|
+
}
|
|
2076
|
+
function hashSeedCursor(seed, cursor) {
|
|
2077
|
+
const seedLo = seed | 0;
|
|
2078
|
+
const seedHi = Math.floor(seed / 4294967296) | 0;
|
|
2079
|
+
let x = Math.imul(seedLo, 2654435761);
|
|
2080
|
+
x = x + Math.imul(seedHi, 2246822519) | 0;
|
|
2081
|
+
x = x + Math.imul(cursor | 0, 3266489917) | 0;
|
|
2082
|
+
x = Math.imul(x ^ x >>> 16, 2246822507);
|
|
2083
|
+
x = Math.imul(x ^ x >>> 13, 3266489909);
|
|
2084
|
+
x = x ^ x >>> 16;
|
|
2085
|
+
return x >>> 0;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/reducer/setup-bootstrap.ts
|
|
2089
|
+
function isCardComponent(table, componentId) {
|
|
2090
|
+
return componentId in table.cards;
|
|
2091
|
+
}
|
|
2092
|
+
function resolveRuntimeBoardId(table, boardId, playerId) {
|
|
2093
|
+
if (playerId) {
|
|
2094
|
+
const perPlayerBoardId = `${boardId}:${playerId}`;
|
|
2095
|
+
if (table.boards.byId[perPlayerBoardId]) {
|
|
2096
|
+
return perPlayerBoardId;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (table.boards.byId[boardId]) {
|
|
2100
|
+
return boardId;
|
|
2101
|
+
}
|
|
2102
|
+
throw new Error(`Unknown board '${boardId}'.`);
|
|
2103
|
+
}
|
|
2104
|
+
function resolveContainerRef(table, ref) {
|
|
2105
|
+
if (ref.type === "sharedZone") {
|
|
2106
|
+
return {
|
|
2107
|
+
kind: "sharedZone",
|
|
2108
|
+
zoneId: ref.zoneId
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
if (ref.type === "playerZone") {
|
|
2112
|
+
return {
|
|
2113
|
+
kind: "playerZone",
|
|
2114
|
+
zoneId: ref.zoneId,
|
|
2115
|
+
playerId: ref.playerId
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (ref.type === "sharedBoardContainer") {
|
|
2119
|
+
const board2 = table.boards.byId[resolveRuntimeBoardId(table, ref.boardId)];
|
|
2120
|
+
const container = board2?.containers[ref.containerId];
|
|
2121
|
+
if (!container) {
|
|
2122
|
+
throw new Error(
|
|
2123
|
+
`Unknown board container '${ref.containerId}' on board '${ref.boardId}'.`
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
return {
|
|
2127
|
+
kind: "container",
|
|
2128
|
+
boardId: board2.id,
|
|
2129
|
+
containerId: ref.containerId,
|
|
2130
|
+
zoneId: container.zoneId
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
if (ref.type === "playerBoardContainer") {
|
|
2134
|
+
const board2 = table.boards.byId[resolveRuntimeBoardId(table, ref.boardId, ref.playerId)];
|
|
2135
|
+
const container = board2?.containers[ref.containerId];
|
|
2136
|
+
if (!container) {
|
|
2137
|
+
throw new Error(
|
|
2138
|
+
`Unknown board container '${ref.containerId}' on board '${ref.boardId}' for '${ref.playerId}'.`
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
kind: "container",
|
|
2143
|
+
boardId: board2.id,
|
|
2144
|
+
containerId: ref.containerId,
|
|
2145
|
+
zoneId: container.zoneId
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
if (ref.type === "sharedBoardSpace") {
|
|
2149
|
+
const board2 = table.boards.byId[resolveRuntimeBoardId(table, ref.boardId)];
|
|
2150
|
+
if (!board2) {
|
|
2151
|
+
throw new Error(`Unknown board '${ref.boardId}'.`);
|
|
2152
|
+
}
|
|
2153
|
+
const space2 = board2?.spaces[ref.spaceId];
|
|
2154
|
+
if (!space2) {
|
|
2155
|
+
throw new Error(
|
|
2156
|
+
`Unknown board space '${ref.spaceId}' on board '${ref.boardId}'.`
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
return {
|
|
2160
|
+
kind: "space",
|
|
2161
|
+
boardId: board2.id,
|
|
2162
|
+
spaceId: ref.spaceId
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
const board = table.boards.byId[resolveRuntimeBoardId(table, ref.boardId, ref.playerId)];
|
|
2166
|
+
if (!board) {
|
|
2167
|
+
throw new Error(
|
|
2168
|
+
`Unknown board '${ref.boardId}' for player '${ref.playerId}'.`
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
const space = board?.spaces[ref.spaceId];
|
|
2172
|
+
if (!space) {
|
|
2173
|
+
throw new Error(
|
|
2174
|
+
`Unknown board space '${ref.spaceId}' on board '${ref.boardId}' for '${ref.playerId}'.`
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
return {
|
|
2178
|
+
kind: "space",
|
|
2179
|
+
boardId: board.id,
|
|
2180
|
+
spaceId: ref.spaceId
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
function readContainerComponents(table, ref) {
|
|
2184
|
+
if (ref.kind === "sharedZone") {
|
|
2185
|
+
return [
|
|
2186
|
+
...ensureArray(table.zones.shared[ref.zoneId] ?? table.decks[ref.zoneId])
|
|
2187
|
+
];
|
|
2188
|
+
}
|
|
2189
|
+
if (ref.kind === "playerZone") {
|
|
2190
|
+
const zoneValue = table.zones.perPlayer[ref.zoneId];
|
|
2191
|
+
const handValue = table.hands[ref.zoneId];
|
|
2192
|
+
const seat = ref.playerId;
|
|
2193
|
+
return [
|
|
2194
|
+
...ensureArray(
|
|
2195
|
+
(zoneValue !== void 0 ? perPlayerGet(zoneValue, seat) : void 0) ?? (handValue !== void 0 ? perPlayerGet(handValue, seat) : void 0)
|
|
2196
|
+
)
|
|
2197
|
+
];
|
|
2198
|
+
}
|
|
2199
|
+
if (ref.kind === "space") {
|
|
2200
|
+
return getComponentsOnSpace(table, ref.boardId, ref.spaceId);
|
|
2201
|
+
}
|
|
2202
|
+
return getComponentsInContainer(table, ref.boardId, ref.containerId);
|
|
2203
|
+
}
|
|
2204
|
+
function sharedZoneLocationFor(table, componentId, zoneId, position) {
|
|
2205
|
+
if (isCardComponent(table, componentId)) {
|
|
2206
|
+
return {
|
|
2207
|
+
type: "InDeck",
|
|
2208
|
+
deckId: zoneId,
|
|
2209
|
+
playedBy: null,
|
|
2210
|
+
position
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
return {
|
|
2214
|
+
type: "InZone",
|
|
2215
|
+
zoneId,
|
|
2216
|
+
playedBy: null,
|
|
2217
|
+
position
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function updateCardVisibilityForZone(table, componentId, ref) {
|
|
2221
|
+
if (!isCardComponent(table, componentId)) {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
if (ref.kind === "playerZone") {
|
|
2225
|
+
const visibility = table.zones.visibility[ref.zoneId];
|
|
2226
|
+
table.ownerOfCard[componentId] = ref.playerId;
|
|
2227
|
+
table.visibility[componentId] = visibility === "all" || visibility === "public" ? { faceUp: true } : { faceUp: false, visibleTo: [ref.playerId] };
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
if (ref.kind === "sharedZone") {
|
|
2231
|
+
const visibility = table.zones.visibility[ref.zoneId];
|
|
2232
|
+
if (visibility === "hidden" || visibility === "ownerOnly") {
|
|
2233
|
+
table.visibility[componentId] = { faceUp: false };
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
table.visibility[componentId] = { faceUp: true };
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function writeContainerComponents(table, ref, componentIds) {
|
|
2240
|
+
if (ref.kind === "sharedZone") {
|
|
2241
|
+
componentIds.forEach((componentId) => {
|
|
2242
|
+
assertCardAllowedInZone(table, ref.zoneId, componentId);
|
|
2243
|
+
});
|
|
2244
|
+
table.decks[ref.zoneId] = [...componentIds];
|
|
2245
|
+
table.zones.shared[ref.zoneId] = [...componentIds];
|
|
2246
|
+
componentIds.forEach((componentId, index) => {
|
|
2247
|
+
table.componentLocations[componentId] = sharedZoneLocationFor(
|
|
2248
|
+
table,
|
|
2249
|
+
componentId,
|
|
2250
|
+
ref.zoneId,
|
|
2251
|
+
index
|
|
2252
|
+
);
|
|
2253
|
+
updateCardVisibilityForZone(table, componentId, ref);
|
|
2254
|
+
});
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
if (ref.kind === "playerZone") {
|
|
2258
|
+
componentIds.forEach((componentId) => {
|
|
2259
|
+
assertCardAllowedInZone(table, ref.zoneId, componentId);
|
|
2260
|
+
});
|
|
2261
|
+
const seat = ref.playerId;
|
|
2262
|
+
const prevHand = table.hands[ref.zoneId] ?? {
|
|
2263
|
+
__perPlayer: true,
|
|
2264
|
+
entries: []
|
|
2265
|
+
};
|
|
2266
|
+
table.hands[ref.zoneId] = perPlayerSet(prevHand, seat, [...componentIds]);
|
|
2267
|
+
const prevZone = table.zones.perPlayer[ref.zoneId] ?? {
|
|
2268
|
+
__perPlayer: true,
|
|
2269
|
+
entries: []
|
|
2270
|
+
};
|
|
2271
|
+
table.zones.perPlayer[ref.zoneId] = perPlayerSet(prevZone, seat, [
|
|
2272
|
+
...componentIds
|
|
2273
|
+
]);
|
|
2274
|
+
componentIds.forEach((componentId, index) => {
|
|
2275
|
+
table.componentLocations[componentId] = {
|
|
2276
|
+
type: "InHand",
|
|
2277
|
+
handId: ref.zoneId,
|
|
2278
|
+
playerId: ref.playerId,
|
|
2279
|
+
position: index
|
|
2280
|
+
};
|
|
2281
|
+
updateCardVisibilityForZone(table, componentId, ref);
|
|
2282
|
+
});
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (ref.kind === "space") {
|
|
2286
|
+
componentIds.forEach((componentId, index) => {
|
|
2287
|
+
table.componentLocations[componentId] = {
|
|
2288
|
+
type: "OnSpace",
|
|
2289
|
+
boardId: ref.boardId,
|
|
2290
|
+
spaceId: ref.spaceId,
|
|
2291
|
+
position: index
|
|
2292
|
+
};
|
|
2293
|
+
});
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
componentIds.forEach((componentId) => {
|
|
2297
|
+
assertCardAllowedInContainer(
|
|
2298
|
+
table,
|
|
2299
|
+
ref.boardId,
|
|
2300
|
+
ref.containerId,
|
|
2301
|
+
componentId
|
|
2302
|
+
);
|
|
2303
|
+
});
|
|
2304
|
+
componentIds.forEach((componentId, index) => {
|
|
2305
|
+
table.componentLocations[componentId] = {
|
|
2306
|
+
type: "InContainer",
|
|
2307
|
+
boardId: ref.boardId,
|
|
2308
|
+
containerId: ref.containerId,
|
|
2309
|
+
position: index
|
|
2310
|
+
};
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
function selectMovedComponents(current, step) {
|
|
2314
|
+
if (step.componentIds && typeof step.count === "number") {
|
|
2315
|
+
throw new Error(
|
|
2316
|
+
"Setup bootstrap move step cannot specify both count and componentIds."
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
if (step.componentIds) {
|
|
2320
|
+
const missing = step.componentIds.filter(
|
|
2321
|
+
(componentId) => !current.includes(componentId)
|
|
2322
|
+
);
|
|
2323
|
+
if (missing.length > 0) {
|
|
2324
|
+
throw new Error(
|
|
2325
|
+
`Setup bootstrap move step references missing components: ${missing.join(", ")}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
return [...step.componentIds];
|
|
2329
|
+
}
|
|
2330
|
+
if (typeof step.count === "number") {
|
|
2331
|
+
return current.slice(0, step.count);
|
|
2332
|
+
}
|
|
2333
|
+
return [...current];
|
|
2334
|
+
}
|
|
2335
|
+
function resolveDealTargetRef(table, ref, playerId) {
|
|
2336
|
+
if (ref.type === "playerZone") {
|
|
2337
|
+
return {
|
|
2338
|
+
kind: "playerZone",
|
|
2339
|
+
zoneId: ref.zoneId,
|
|
2340
|
+
playerId
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
const board = table.boards.byId[resolveRuntimeBoardId(table, ref.boardId, playerId)];
|
|
2344
|
+
const container = board?.containers[ref.containerId];
|
|
2345
|
+
if (!container) {
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
`Unknown board container '${ref.containerId}' on board '${ref.boardId}' for '${playerId}'.`
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
return {
|
|
2351
|
+
kind: "container",
|
|
2352
|
+
boardId: board.id,
|
|
2353
|
+
containerId: ref.containerId,
|
|
2354
|
+
zoneId: container.zoneId
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function applySetupBootstrap(state, steps) {
|
|
2358
|
+
if (steps.length === 0) {
|
|
2359
|
+
return state;
|
|
2360
|
+
}
|
|
2361
|
+
const nextTable = cloneRuntimeTable(state.table);
|
|
2362
|
+
let nextRng = state.runtime.rng;
|
|
2363
|
+
for (const step of steps) {
|
|
2364
|
+
if (step.type === "shuffle") {
|
|
2365
|
+
const container = resolveContainerRef(nextTable, step.container);
|
|
2366
|
+
const shuffled = readContainerComponents(nextTable, container);
|
|
2367
|
+
for (let lastIndex = shuffled.length - 1; lastIndex > 0; lastIndex -= 1) {
|
|
2368
|
+
const [swapIndex, updatedRng] = nextRandomInt(lastIndex + 1, nextRng);
|
|
2369
|
+
nextRng = updatedRng;
|
|
2370
|
+
const current = shuffled[lastIndex];
|
|
2371
|
+
const swap = shuffled[swapIndex];
|
|
2372
|
+
if (current === void 0 || swap === void 0) {
|
|
2373
|
+
throw new Error(
|
|
2374
|
+
`setup shuffle: invalid index (lastIndex=${lastIndex}, swapIndex=${swapIndex}, length=${shuffled.length})`
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
shuffled[lastIndex] = swap;
|
|
2378
|
+
shuffled[swapIndex] = current;
|
|
2379
|
+
}
|
|
2380
|
+
writeContainerComponents(nextTable, container, shuffled);
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (step.type === "move") {
|
|
2384
|
+
const from2 = resolveContainerRef(nextTable, step.from);
|
|
2385
|
+
const to = resolveContainerRef(nextTable, step.to);
|
|
2386
|
+
const currentFrom = readContainerComponents(nextTable, from2);
|
|
2387
|
+
const moved = selectMovedComponents(currentFrom, step);
|
|
2388
|
+
const remaining = currentFrom.filter(
|
|
2389
|
+
(componentId) => !moved.includes(componentId)
|
|
2390
|
+
);
|
|
2391
|
+
const currentTo = readContainerComponents(nextTable, to);
|
|
2392
|
+
writeContainerComponents(nextTable, from2, remaining);
|
|
2393
|
+
writeContainerComponents(nextTable, to, [...currentTo, ...moved]);
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
const from = resolveContainerRef(nextTable, step.from);
|
|
2397
|
+
if (from.kind === "playerZone") {
|
|
2398
|
+
throw new Error(
|
|
2399
|
+
"Setup bootstrap deal steps require a shared source container."
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
const playerIds = step.playerIds ? [...step.playerIds] : [...nextTable.playerOrder];
|
|
2403
|
+
let sourceComponents = readContainerComponents(nextTable, from);
|
|
2404
|
+
for (const playerId of playerIds) {
|
|
2405
|
+
if (sourceComponents.length === 0) {
|
|
2406
|
+
break;
|
|
2407
|
+
}
|
|
2408
|
+
const dealt = sourceComponents.slice(0, step.count);
|
|
2409
|
+
if (dealt.length === 0) {
|
|
2410
|
+
break;
|
|
2411
|
+
}
|
|
2412
|
+
sourceComponents = sourceComponents.slice(dealt.length);
|
|
2413
|
+
const target = resolveDealTargetRef(nextTable, step.to, playerId);
|
|
2414
|
+
const currentTo = readContainerComponents(nextTable, target);
|
|
2415
|
+
writeContainerComponents(nextTable, target, [...currentTo, ...dealt]);
|
|
2416
|
+
}
|
|
2417
|
+
writeContainerComponents(nextTable, from, sourceComponents);
|
|
2418
|
+
}
|
|
2419
|
+
return {
|
|
2420
|
+
...state,
|
|
2421
|
+
table: nextTable,
|
|
2422
|
+
runtime: {
|
|
2423
|
+
...state.runtime,
|
|
2424
|
+
rng: nextRng
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/reducer/definition-index.ts
|
|
2430
|
+
function phaseEntriesOf(definition) {
|
|
2431
|
+
return Object.entries(definition.phases);
|
|
2432
|
+
}
|
|
2433
|
+
function interactionEntriesOf(phase) {
|
|
2434
|
+
return Object.entries(
|
|
2435
|
+
phase.interactions ?? {}
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
function simultaneousSubmitEntriesOf(phase) {
|
|
2439
|
+
const submit = phase.submit;
|
|
2440
|
+
if (!submit) return [];
|
|
2441
|
+
return [
|
|
2442
|
+
["submit", submit]
|
|
2443
|
+
];
|
|
2444
|
+
}
|
|
2445
|
+
function cardActionEntriesOf(phase) {
|
|
2446
|
+
return Object.entries(
|
|
2447
|
+
phase.cardActions ?? {}
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
function stageEntriesOf(phase) {
|
|
2451
|
+
return Object.entries(
|
|
2452
|
+
phase.stages ?? {}
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
function zoneEntriesOf(phase) {
|
|
2456
|
+
return Array.from(
|
|
2457
|
+
phase.zones ?? []
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
function effectEntriesOf(phase) {
|
|
2461
|
+
return Object.entries(
|
|
2462
|
+
phase.effects ?? {}
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
function collectReducerDefinitionIndex(definition) {
|
|
2466
|
+
const phaseEntries = phaseEntriesOf(definition);
|
|
2467
|
+
const phasesByName = /* @__PURE__ */ new Map();
|
|
2468
|
+
for (const [phaseName, phase] of phaseEntries) {
|
|
2469
|
+
const interactionEntries = [
|
|
2470
|
+
...interactionEntriesOf(phase),
|
|
2471
|
+
...simultaneousSubmitEntriesOf(phase)
|
|
2472
|
+
];
|
|
2473
|
+
const cardActionEntries = cardActionEntriesOf(phase).map(([cardActionId, cardAction]) => [
|
|
2474
|
+
cardActionId,
|
|
2475
|
+
{
|
|
2476
|
+
...cardAction,
|
|
2477
|
+
__steps: cardAction.__steps,
|
|
2478
|
+
inputs: {
|
|
2479
|
+
cardId: cardInput({
|
|
2480
|
+
target: cardTarget.zones([cardAction.playFrom]).where({
|
|
2481
|
+
id: "card-type",
|
|
2482
|
+
errorCode: "WRONG_CARD_TYPE",
|
|
2483
|
+
message: `Card must be ${cardAction.cardType}.`,
|
|
2484
|
+
test: ({ q, targetId }) => q.card.get(targetId).cardType === cardAction.cardType
|
|
2485
|
+
}).build()
|
|
2486
|
+
}),
|
|
2487
|
+
...cardAction.inputs ?? {}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
]);
|
|
2491
|
+
const effects = effectEntriesOf(
|
|
2492
|
+
phase
|
|
2493
|
+
).map(([effectKey, effectValue]) => [
|
|
2494
|
+
effectKey,
|
|
2495
|
+
{
|
|
2496
|
+
id: effectValue.id ?? effectKey,
|
|
2497
|
+
type: effectValue.type ?? "rollDie",
|
|
2498
|
+
continuation: effectValue.__continuation
|
|
2499
|
+
}
|
|
2500
|
+
]);
|
|
2501
|
+
phasesByName.set(phaseName, {
|
|
2502
|
+
phaseName,
|
|
2503
|
+
phase,
|
|
2504
|
+
interactions: [...interactionEntries, ...cardActionEntries],
|
|
2505
|
+
stages: stageEntriesOf(phase),
|
|
2506
|
+
zones: zoneEntriesOf(phase),
|
|
2507
|
+
cardActions: cardActionEntriesOf(phase),
|
|
2508
|
+
effects
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
return {
|
|
2512
|
+
phaseEntries,
|
|
2513
|
+
phasesByName
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
export {
|
|
2518
|
+
markManifestScopedSchema,
|
|
2519
|
+
isManifestScopedSchema,
|
|
2520
|
+
createManifestStringLiteralSchema,
|
|
2521
|
+
assumeManifestSchema,
|
|
2522
|
+
cloneManifestDefault,
|
|
2523
|
+
resolveManifestPlayerIds,
|
|
2524
|
+
createManifestRuntimeSchema,
|
|
2525
|
+
createManifestGameStateSchema,
|
|
2526
|
+
FrameworkErrorCodes,
|
|
2527
|
+
asPlayerId,
|
|
2528
|
+
isPlayerId,
|
|
2529
|
+
perPlayer,
|
|
2530
|
+
perPlayerKeys,
|
|
2531
|
+
perPlayerValues,
|
|
2532
|
+
perPlayerEntries,
|
|
2533
|
+
perPlayerSize,
|
|
2534
|
+
perPlayerHas,
|
|
2535
|
+
perPlayerGet,
|
|
2536
|
+
perPlayerRequire,
|
|
2537
|
+
perPlayerSet,
|
|
2538
|
+
perPlayerMap,
|
|
2539
|
+
isPerPlayer,
|
|
2540
|
+
perPlayerSchema,
|
|
2541
|
+
sharedBoardRef,
|
|
2542
|
+
perPlayerBoardRef,
|
|
2543
|
+
boardRef,
|
|
2544
|
+
boardRefKey,
|
|
2545
|
+
parseBoardRefKey,
|
|
2546
|
+
boardRefSchema,
|
|
2547
|
+
isSharedBoardRef,
|
|
2548
|
+
isPerPlayerBoardRef,
|
|
2549
|
+
createTargetRule,
|
|
2550
|
+
createTargetRuleBuilder,
|
|
2551
|
+
cardTarget,
|
|
2552
|
+
cardInput,
|
|
2553
|
+
ensureArray,
|
|
2554
|
+
cloneRuntimeTable,
|
|
2555
|
+
getBoard,
|
|
2556
|
+
getHexBoard,
|
|
2557
|
+
getTiledBoard,
|
|
2558
|
+
getSquareBoard,
|
|
2559
|
+
getSpace,
|
|
2560
|
+
getHexSpace,
|
|
2561
|
+
getSquareSpace,
|
|
2562
|
+
getContainer,
|
|
2563
|
+
getEdge,
|
|
2564
|
+
getVertex,
|
|
2565
|
+
getHexSpaceAt,
|
|
2566
|
+
getSquareSpaceAt,
|
|
2567
|
+
getSpaceEdges,
|
|
2568
|
+
getSpaceVertices,
|
|
2569
|
+
getIncidentEdges,
|
|
2570
|
+
getIncidentVertices,
|
|
2571
|
+
getRelatedSpaces,
|
|
2572
|
+
getAdjacentSpaces,
|
|
2573
|
+
getSpaceDistance,
|
|
2574
|
+
getSquareNeighbors,
|
|
2575
|
+
getSquareDistance,
|
|
2576
|
+
getBoardsByTypeId,
|
|
2577
|
+
getSpacesByTypeId,
|
|
2578
|
+
getEdgesByTypeId,
|
|
2579
|
+
getVerticesByTypeId,
|
|
2580
|
+
getComponentsOnSpace,
|
|
2581
|
+
getComponentsInContainer,
|
|
2582
|
+
getComponentsOnEdge,
|
|
2583
|
+
getComponentsOnVertex,
|
|
2584
|
+
assertCardAllowedInContainer,
|
|
2585
|
+
getComponentLocation,
|
|
2586
|
+
getComponentDeckLocation,
|
|
2587
|
+
getComponentHandLocation,
|
|
2588
|
+
getComponentZoneLocation,
|
|
2589
|
+
getComponentSpaceLocation,
|
|
2590
|
+
getComponentContainerLocation,
|
|
2591
|
+
getComponentEdgeLocation,
|
|
2592
|
+
getComponentVertexLocation,
|
|
2593
|
+
getComponentSlotLocation,
|
|
2594
|
+
getSharedZoneCards,
|
|
2595
|
+
getPlayerZoneCards,
|
|
2596
|
+
getAllSharedZoneCards,
|
|
2597
|
+
getAllPlayerZoneCards,
|
|
2598
|
+
getCard,
|
|
2599
|
+
getCardsById,
|
|
2600
|
+
getSharedZoneCardCollection,
|
|
2601
|
+
getPlayerZoneCardCollection,
|
|
2602
|
+
getSlotOccupants,
|
|
2603
|
+
getSlotOccupantsByHost,
|
|
2604
|
+
getCardOwner,
|
|
2605
|
+
getCardVisibility,
|
|
2606
|
+
getPlayerOrder,
|
|
2607
|
+
getPlayerResources,
|
|
2608
|
+
getPlayerResourceAmount,
|
|
2609
|
+
getPlayerResourceTotal,
|
|
2610
|
+
getNextPlayerInOrder,
|
|
2611
|
+
canAffordResources,
|
|
2612
|
+
getMissingResources,
|
|
2613
|
+
addPlayerResources,
|
|
2614
|
+
addPlayerResourcesInPlace,
|
|
2615
|
+
spendPlayerResources,
|
|
2616
|
+
spendPlayerResourcesInPlace,
|
|
2617
|
+
transferPlayerResources,
|
|
2618
|
+
transferPlayerResourcesInPlace,
|
|
2619
|
+
setPlayerResource,
|
|
2620
|
+
setPlayerResourceInPlace,
|
|
2621
|
+
moveComponentToSpace,
|
|
2622
|
+
moveComponentToSpaceInPlace,
|
|
2623
|
+
moveComponentToContainer,
|
|
2624
|
+
moveComponentToContainerInPlace,
|
|
2625
|
+
moveComponentToDetached,
|
|
2626
|
+
moveComponentToDetachedInPlace,
|
|
2627
|
+
moveComponentToEdge,
|
|
2628
|
+
moveComponentToEdgeInPlace,
|
|
2629
|
+
moveComponentToVertex,
|
|
2630
|
+
moveComponentToVertexInPlace,
|
|
2631
|
+
shufflePlayerZoneCards,
|
|
2632
|
+
moveCardFromPlayerZoneToSharedZone,
|
|
2633
|
+
moveCardFromPlayerZoneToSharedZoneInPlace,
|
|
2634
|
+
dealCardsBetweenPlayerZones,
|
|
2635
|
+
dealCardsBetweenPlayerZonesInPlace,
|
|
2636
|
+
moveCardFromSharedZoneToPlayerZone,
|
|
2637
|
+
moveCardFromSharedZoneToPlayerZoneInPlace,
|
|
2638
|
+
moveCardBetweenPlayerZones,
|
|
2639
|
+
moveCardBetweenPlayerZonesInPlace,
|
|
2640
|
+
moveCardBetweenSharedZones,
|
|
2641
|
+
moveCardBetweenSharedZonesInPlace,
|
|
2642
|
+
removeCardFromSharedZone,
|
|
2643
|
+
removeCardFromSharedZoneInPlace,
|
|
2644
|
+
addCardToSharedZone,
|
|
2645
|
+
addCardToSharedZoneInPlace,
|
|
2646
|
+
dealCardsFromDeckToHand,
|
|
2647
|
+
dealCardsFromDeckToHandInPlace,
|
|
2648
|
+
setActivePlayers,
|
|
2649
|
+
setPhaseState,
|
|
2650
|
+
nextRandomInt,
|
|
2651
|
+
collectReducerDefinitionIndex,
|
|
2652
|
+
applySetupBootstrap
|
|
2653
|
+
};
|
|
2654
|
+
//# sourceMappingURL=chunk-U5KBV2BA.js.map
|