@pompidup/kingdomino-engine 3.1.0 → 3.3.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/README.md +240 -110
- package/dist/index.d.ts +597 -161
- package/dist/index.js +2133 -747
- package/dist/index.js.map +1 -1
- package/dist/index.universal.js +3805 -0
- package/dist/wasm-bridge.js +2928 -0
- package/package.json +23 -10
|
@@ -0,0 +1,3805 @@
|
|
|
1
|
+
// src/core/domain/types/game.ts
|
|
2
|
+
var gameSteps = {
|
|
3
|
+
/** Waiting for players to be added */
|
|
4
|
+
addPlayers: "addPlayers",
|
|
5
|
+
/** Optional rules selection phase */
|
|
6
|
+
options: "options",
|
|
7
|
+
/** Ready to start the game */
|
|
8
|
+
start: "start",
|
|
9
|
+
/** Game has ended, results available */
|
|
10
|
+
result: "result"
|
|
11
|
+
};
|
|
12
|
+
var isGameWithNextAction = (game) => {
|
|
13
|
+
return game.nextAction.type === "action";
|
|
14
|
+
};
|
|
15
|
+
var isGameWithNextStep = (game) => {
|
|
16
|
+
return game.nextAction.type === "step";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/core/useCases/gameDebug.ts
|
|
20
|
+
var summarizeGameState = (game) => {
|
|
21
|
+
const nextAction = isGameWithNextAction(game) ? `action:${game.nextAction.nextAction}` : `step:${game.nextAction.step}`;
|
|
22
|
+
return {
|
|
23
|
+
gameId: game.id,
|
|
24
|
+
turn: game.turn,
|
|
25
|
+
nextAction,
|
|
26
|
+
playersCount: game.players.length,
|
|
27
|
+
dominoesRemaining: game.dominoes.length,
|
|
28
|
+
currentDominoesCount: game.currentDominoes.length,
|
|
29
|
+
lordsCount: game.lords.length
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
var defaultDebugLogger = {
|
|
33
|
+
log: (entry) => {
|
|
34
|
+
const phase = entry.phase.toUpperCase().padEnd(5);
|
|
35
|
+
const base = `[KDomino:Debug] ${entry.timestamp} ${phase} ${entry.method}`;
|
|
36
|
+
if (entry.phase === "error") {
|
|
37
|
+
console.error(`${base} | error: ${entry.error}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (entry.summary) {
|
|
41
|
+
const s = entry.summary;
|
|
42
|
+
console.log(
|
|
43
|
+
`${base} | turn=${s.turn} next=${s.nextAction} players=${s.playersCount} dominoes=${s.dominoesRemaining}`
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(base);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var isGameState = (value) => {
|
|
51
|
+
return typeof value === "object" && value !== null && "id" in value && "nextAction" in value && "players" in value && "dominoes" in value;
|
|
52
|
+
};
|
|
53
|
+
var hasGameInCommand = (cmd) => {
|
|
54
|
+
return typeof cmd === "object" && cmd !== null && "game" in cmd && isGameState(cmd.game);
|
|
55
|
+
};
|
|
56
|
+
var extractParams = (cmd) => {
|
|
57
|
+
if (typeof cmd !== "object" || cmd === null) return void 0;
|
|
58
|
+
const record = cmd;
|
|
59
|
+
const params = {};
|
|
60
|
+
if ("mode" in record) params.mode = record.mode;
|
|
61
|
+
if ("lordId" in record) params.lordId = record.lordId;
|
|
62
|
+
if ("dominoPick" in record) params.dominoPick = record.dominoPick;
|
|
63
|
+
if ("position" in record) params.position = record.position;
|
|
64
|
+
if ("rotation" in record) params.rotation = record.rotation;
|
|
65
|
+
if ("players" in record) params.players = record.players;
|
|
66
|
+
if ("extraRules" in record) params.extraRules = record.extraRules;
|
|
67
|
+
return Object.keys(params).length > 0 ? params : void 0;
|
|
68
|
+
};
|
|
69
|
+
var wrapMethod = (method, fn, logger, level) => {
|
|
70
|
+
return (cmd) => {
|
|
71
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
72
|
+
const beforeSummary = level !== "minimal" && hasGameInCommand(cmd) ? summarizeGameState(cmd.game) : null;
|
|
73
|
+
const beforeEntry = {
|
|
74
|
+
timestamp,
|
|
75
|
+
method,
|
|
76
|
+
phase: "before",
|
|
77
|
+
summary: beforeSummary,
|
|
78
|
+
params: level !== "minimal" ? extractParams(cmd) : void 0
|
|
79
|
+
};
|
|
80
|
+
logger.log(beforeEntry);
|
|
81
|
+
const start = performance.now();
|
|
82
|
+
let result;
|
|
83
|
+
try {
|
|
84
|
+
result = fn(cmd);
|
|
85
|
+
} catch (err3) {
|
|
86
|
+
const durationMs2 = Math.round((performance.now() - start) * 100) / 100;
|
|
87
|
+
const errorEntry = {
|
|
88
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
+
method,
|
|
90
|
+
phase: "error",
|
|
91
|
+
summary: beforeSummary,
|
|
92
|
+
error: err3 instanceof Error ? err3.message : String(err3),
|
|
93
|
+
durationMs: durationMs2
|
|
94
|
+
};
|
|
95
|
+
logger.log(errorEntry);
|
|
96
|
+
throw err3;
|
|
97
|
+
}
|
|
98
|
+
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
99
|
+
const afterSummary = level !== "minimal" && isGameState(result) ? summarizeGameState(result) : null;
|
|
100
|
+
const afterEntry = {
|
|
101
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
method,
|
|
103
|
+
phase: "after",
|
|
104
|
+
summary: afterSummary,
|
|
105
|
+
durationMs,
|
|
106
|
+
fullState: level === "verbose" && isGameState(result) ? result : void 0
|
|
107
|
+
};
|
|
108
|
+
logger.log(afterEntry);
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
var wrapWithDebug = (engine, options) => {
|
|
113
|
+
const logger = options?.logger ?? defaultDebugLogger;
|
|
114
|
+
const level = options?.level ?? "standard";
|
|
115
|
+
return {
|
|
116
|
+
getModes: wrapMethod("getModes", engine.getModes, logger, level),
|
|
117
|
+
getExtraRules: wrapMethod(
|
|
118
|
+
"getExtraRules",
|
|
119
|
+
engine.getExtraRules,
|
|
120
|
+
logger,
|
|
121
|
+
level
|
|
122
|
+
),
|
|
123
|
+
createGame: wrapMethod("createGame", engine.createGame, logger, level),
|
|
124
|
+
addPlayers: wrapMethod("addPlayers", engine.addPlayers, logger, level),
|
|
125
|
+
addExtraRules: wrapMethod(
|
|
126
|
+
"addExtraRules",
|
|
127
|
+
engine.addExtraRules,
|
|
128
|
+
logger,
|
|
129
|
+
level
|
|
130
|
+
),
|
|
131
|
+
startGame: wrapMethod("startGame", engine.startGame, logger, level),
|
|
132
|
+
chooseDomino: wrapMethod(
|
|
133
|
+
"chooseDomino",
|
|
134
|
+
engine.chooseDomino,
|
|
135
|
+
logger,
|
|
136
|
+
level
|
|
137
|
+
),
|
|
138
|
+
placeDomino: wrapMethod("placeDomino", engine.placeDomino, logger, level),
|
|
139
|
+
discardDomino: wrapMethod(
|
|
140
|
+
"discardDomino",
|
|
141
|
+
engine.discardDomino,
|
|
142
|
+
logger,
|
|
143
|
+
level
|
|
144
|
+
),
|
|
145
|
+
getResults: wrapMethod("getResults", engine.getResults, logger, level),
|
|
146
|
+
calculateScore: wrapMethod(
|
|
147
|
+
"calculateScore",
|
|
148
|
+
engine.calculateScore,
|
|
149
|
+
logger,
|
|
150
|
+
level
|
|
151
|
+
),
|
|
152
|
+
getValidPlacements: wrapMethod(
|
|
153
|
+
"getValidPlacements",
|
|
154
|
+
engine.getValidPlacements,
|
|
155
|
+
logger,
|
|
156
|
+
level
|
|
157
|
+
),
|
|
158
|
+
canPlaceDomino: wrapMethod(
|
|
159
|
+
"canPlaceDomino",
|
|
160
|
+
engine.canPlaceDomino,
|
|
161
|
+
logger,
|
|
162
|
+
level
|
|
163
|
+
),
|
|
164
|
+
serialize: wrapMethod("serialize", engine.serialize, logger, level),
|
|
165
|
+
deserialize: wrapMethod("deserialize", engine.deserialize, logger, level),
|
|
166
|
+
getDynastyResults: wrapMethod(
|
|
167
|
+
"getDynastyResults",
|
|
168
|
+
engine.getDynastyResults,
|
|
169
|
+
logger,
|
|
170
|
+
level
|
|
171
|
+
)
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/core/useCases/gameEvents.ts
|
|
176
|
+
var isGameEnded = (game) => isGameWithNextStep(game) && game.nextAction.step === "result";
|
|
177
|
+
var wrapWithEvents = (engine, callbacks) => ({
|
|
178
|
+
// Read-only methods — pass through
|
|
179
|
+
getModes: (cmd) => engine.getModes(cmd),
|
|
180
|
+
getExtraRules: (cmd) => engine.getExtraRules(cmd),
|
|
181
|
+
calculateScore: (cmd) => engine.calculateScore(cmd),
|
|
182
|
+
getValidPlacements: (cmd) => engine.getValidPlacements(cmd),
|
|
183
|
+
canPlaceDomino: (cmd) => engine.canPlaceDomino(cmd),
|
|
184
|
+
serialize: (cmd) => engine.serialize(cmd),
|
|
185
|
+
deserialize: (cmd) => engine.deserialize(cmd),
|
|
186
|
+
getResults: (cmd) => engine.getResults(cmd),
|
|
187
|
+
getDynastyResults: (cmd) => engine.getDynastyResults(cmd),
|
|
188
|
+
// State-changing methods — emit events after execution
|
|
189
|
+
createGame: (cmd) => {
|
|
190
|
+
const game = engine.createGame(cmd);
|
|
191
|
+
callbacks.onGameCreated?.({ game });
|
|
192
|
+
return game;
|
|
193
|
+
},
|
|
194
|
+
addPlayers: (cmd) => {
|
|
195
|
+
const game = engine.addPlayers(cmd);
|
|
196
|
+
callbacks.onPlayersAdded?.({ game });
|
|
197
|
+
return game;
|
|
198
|
+
},
|
|
199
|
+
addExtraRules: (cmd) => engine.addExtraRules(cmd),
|
|
200
|
+
startGame: (cmd) => {
|
|
201
|
+
const game = engine.startGame(cmd);
|
|
202
|
+
callbacks.onGameStarted?.({ game });
|
|
203
|
+
callbacks.onTurnStart?.({ game, turn: game.turn });
|
|
204
|
+
return game;
|
|
205
|
+
},
|
|
206
|
+
chooseDomino: (cmd) => {
|
|
207
|
+
const turnBefore = cmd.game.turn;
|
|
208
|
+
const game = engine.chooseDomino(cmd);
|
|
209
|
+
callbacks.onDominoPicked?.({
|
|
210
|
+
game,
|
|
211
|
+
lordId: cmd.lordId,
|
|
212
|
+
dominoNumber: cmd.dominoPick
|
|
213
|
+
});
|
|
214
|
+
if (game.turn !== turnBefore) {
|
|
215
|
+
callbacks.onTurnEnd?.({ game, turn: turnBefore });
|
|
216
|
+
callbacks.onTurnStart?.({ game, turn: game.turn });
|
|
217
|
+
}
|
|
218
|
+
return game;
|
|
219
|
+
},
|
|
220
|
+
placeDomino: (cmd) => {
|
|
221
|
+
const game = engine.placeDomino(cmd);
|
|
222
|
+
callbacks.onDominoPlaced?.({ game, lordId: cmd.lordId });
|
|
223
|
+
if (isGameEnded(game)) {
|
|
224
|
+
callbacks.onGameEnd?.({ game });
|
|
225
|
+
}
|
|
226
|
+
return game;
|
|
227
|
+
},
|
|
228
|
+
discardDomino: (cmd) => {
|
|
229
|
+
const game = engine.discardDomino(cmd);
|
|
230
|
+
callbacks.onDominoDiscarded?.({ game, lordId: cmd.lordId });
|
|
231
|
+
if (isGameEnded(game)) {
|
|
232
|
+
callbacks.onGameEnd?.({ game });
|
|
233
|
+
}
|
|
234
|
+
return game;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// src/adapterServerside/consoleLogger.ts
|
|
239
|
+
var consoleLogger = (isLoggingEnabled) => {
|
|
240
|
+
const formatMessage = (level, message) => {
|
|
241
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
242
|
+
return `${timestamp} [KDomino_Engine] ${level}: ${message}`;
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
info: (message) => {
|
|
246
|
+
if (isLoggingEnabled) {
|
|
247
|
+
console.log(formatMessage("info", message));
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
error: (message) => {
|
|
251
|
+
if (isLoggingEnabled) {
|
|
252
|
+
console.error(formatMessage("error", message));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
var silentLogger = {
|
|
258
|
+
info: () => {
|
|
259
|
+
},
|
|
260
|
+
error: () => {
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/datasources/dominoes.json
|
|
265
|
+
var dominoes_default = {
|
|
266
|
+
Classic: [
|
|
267
|
+
{
|
|
268
|
+
left: {
|
|
269
|
+
type: "wheat",
|
|
270
|
+
crowns: 0
|
|
271
|
+
},
|
|
272
|
+
right: {
|
|
273
|
+
type: "wheat",
|
|
274
|
+
crowns: 0
|
|
275
|
+
},
|
|
276
|
+
number: 1
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
left: {
|
|
280
|
+
type: "wheat",
|
|
281
|
+
crowns: 0
|
|
282
|
+
},
|
|
283
|
+
right: {
|
|
284
|
+
type: "wheat",
|
|
285
|
+
crowns: 0
|
|
286
|
+
},
|
|
287
|
+
number: 2
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
left: {
|
|
291
|
+
type: "forest",
|
|
292
|
+
crowns: 0
|
|
293
|
+
},
|
|
294
|
+
right: {
|
|
295
|
+
type: "forest",
|
|
296
|
+
crowns: 0
|
|
297
|
+
},
|
|
298
|
+
number: 3
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
left: {
|
|
302
|
+
type: "forest",
|
|
303
|
+
crowns: 0
|
|
304
|
+
},
|
|
305
|
+
right: {
|
|
306
|
+
type: "forest",
|
|
307
|
+
crowns: 0
|
|
308
|
+
},
|
|
309
|
+
number: 4
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
left: {
|
|
313
|
+
type: "forest",
|
|
314
|
+
crowns: 0
|
|
315
|
+
},
|
|
316
|
+
right: {
|
|
317
|
+
type: "forest",
|
|
318
|
+
crowns: 0
|
|
319
|
+
},
|
|
320
|
+
number: 5
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
left: {
|
|
324
|
+
type: "forest",
|
|
325
|
+
crowns: 0
|
|
326
|
+
},
|
|
327
|
+
right: {
|
|
328
|
+
type: "forest",
|
|
329
|
+
crowns: 0
|
|
330
|
+
},
|
|
331
|
+
number: 6
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
left: {
|
|
335
|
+
type: "sea",
|
|
336
|
+
crowns: 0
|
|
337
|
+
},
|
|
338
|
+
right: {
|
|
339
|
+
type: "sea",
|
|
340
|
+
crowns: 0
|
|
341
|
+
},
|
|
342
|
+
number: 7
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
left: {
|
|
346
|
+
type: "sea",
|
|
347
|
+
crowns: 0
|
|
348
|
+
},
|
|
349
|
+
right: {
|
|
350
|
+
type: "sea",
|
|
351
|
+
crowns: 0
|
|
352
|
+
},
|
|
353
|
+
number: 8
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
left: {
|
|
357
|
+
type: "sea",
|
|
358
|
+
crowns: 0
|
|
359
|
+
},
|
|
360
|
+
right: {
|
|
361
|
+
type: "sea",
|
|
362
|
+
crowns: 0
|
|
363
|
+
},
|
|
364
|
+
number: 9
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
left: {
|
|
368
|
+
type: "plain",
|
|
369
|
+
crowns: 0
|
|
370
|
+
},
|
|
371
|
+
right: {
|
|
372
|
+
type: "plain",
|
|
373
|
+
crowns: 0
|
|
374
|
+
},
|
|
375
|
+
number: 10
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
left: {
|
|
379
|
+
type: "plain",
|
|
380
|
+
crowns: 0
|
|
381
|
+
},
|
|
382
|
+
right: {
|
|
383
|
+
type: "plain",
|
|
384
|
+
crowns: 0
|
|
385
|
+
},
|
|
386
|
+
number: 11
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
left: {
|
|
390
|
+
type: "swamp",
|
|
391
|
+
crowns: 0
|
|
392
|
+
},
|
|
393
|
+
right: {
|
|
394
|
+
type: "swamp",
|
|
395
|
+
crowns: 0
|
|
396
|
+
},
|
|
397
|
+
number: 12
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
left: {
|
|
401
|
+
type: "wheat",
|
|
402
|
+
crowns: 0
|
|
403
|
+
},
|
|
404
|
+
right: {
|
|
405
|
+
type: "forest",
|
|
406
|
+
crowns: 0
|
|
407
|
+
},
|
|
408
|
+
number: 13
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
left: {
|
|
412
|
+
type: "wheat",
|
|
413
|
+
crowns: 0
|
|
414
|
+
},
|
|
415
|
+
right: {
|
|
416
|
+
type: "sea",
|
|
417
|
+
crowns: 0
|
|
418
|
+
},
|
|
419
|
+
number: 14
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
left: {
|
|
423
|
+
type: "wheat",
|
|
424
|
+
crowns: 0
|
|
425
|
+
},
|
|
426
|
+
right: {
|
|
427
|
+
type: "plain",
|
|
428
|
+
crowns: 0
|
|
429
|
+
},
|
|
430
|
+
number: 15
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
left: {
|
|
434
|
+
type: "wheat",
|
|
435
|
+
crowns: 0
|
|
436
|
+
},
|
|
437
|
+
right: {
|
|
438
|
+
type: "swamp",
|
|
439
|
+
crowns: 0
|
|
440
|
+
},
|
|
441
|
+
number: 16
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
left: {
|
|
445
|
+
type: "forest",
|
|
446
|
+
crowns: 0
|
|
447
|
+
},
|
|
448
|
+
right: {
|
|
449
|
+
type: "sea",
|
|
450
|
+
crowns: 0
|
|
451
|
+
},
|
|
452
|
+
number: 17
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
left: {
|
|
456
|
+
type: "forest",
|
|
457
|
+
crowns: 0
|
|
458
|
+
},
|
|
459
|
+
right: {
|
|
460
|
+
type: "plain",
|
|
461
|
+
crowns: 0
|
|
462
|
+
},
|
|
463
|
+
number: 18
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
left: {
|
|
467
|
+
type: "wheat",
|
|
468
|
+
crowns: 1
|
|
469
|
+
},
|
|
470
|
+
right: {
|
|
471
|
+
type: "forest",
|
|
472
|
+
crowns: 0
|
|
473
|
+
},
|
|
474
|
+
number: 19
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
left: {
|
|
478
|
+
type: "wheat",
|
|
479
|
+
crowns: 1
|
|
480
|
+
},
|
|
481
|
+
right: {
|
|
482
|
+
type: "sea",
|
|
483
|
+
crowns: 0
|
|
484
|
+
},
|
|
485
|
+
number: 20
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
left: {
|
|
489
|
+
type: "wheat",
|
|
490
|
+
crowns: 1
|
|
491
|
+
},
|
|
492
|
+
right: {
|
|
493
|
+
type: "plain",
|
|
494
|
+
crowns: 0
|
|
495
|
+
},
|
|
496
|
+
number: 21
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
left: {
|
|
500
|
+
type: "wheat",
|
|
501
|
+
crowns: 1
|
|
502
|
+
},
|
|
503
|
+
right: {
|
|
504
|
+
type: "swamp",
|
|
505
|
+
crowns: 0
|
|
506
|
+
},
|
|
507
|
+
number: 22
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
left: {
|
|
511
|
+
type: "wheat",
|
|
512
|
+
crowns: 1
|
|
513
|
+
},
|
|
514
|
+
right: {
|
|
515
|
+
type: "mine",
|
|
516
|
+
crowns: 0
|
|
517
|
+
},
|
|
518
|
+
number: 23
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
left: {
|
|
522
|
+
type: "forest",
|
|
523
|
+
crowns: 1
|
|
524
|
+
},
|
|
525
|
+
right: {
|
|
526
|
+
type: "wheat",
|
|
527
|
+
crowns: 0
|
|
528
|
+
},
|
|
529
|
+
number: 24
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
left: {
|
|
533
|
+
type: "forest",
|
|
534
|
+
crowns: 1
|
|
535
|
+
},
|
|
536
|
+
right: {
|
|
537
|
+
type: "wheat",
|
|
538
|
+
crowns: 0
|
|
539
|
+
},
|
|
540
|
+
number: 25
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
left: {
|
|
544
|
+
type: "forest",
|
|
545
|
+
crowns: 1
|
|
546
|
+
},
|
|
547
|
+
right: {
|
|
548
|
+
type: "wheat",
|
|
549
|
+
crowns: 0
|
|
550
|
+
},
|
|
551
|
+
number: 26
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
left: {
|
|
555
|
+
type: "forest",
|
|
556
|
+
crowns: 1
|
|
557
|
+
},
|
|
558
|
+
right: {
|
|
559
|
+
type: "wheat",
|
|
560
|
+
crowns: 0
|
|
561
|
+
},
|
|
562
|
+
number: 27
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
left: {
|
|
566
|
+
type: "forest",
|
|
567
|
+
crowns: 1
|
|
568
|
+
},
|
|
569
|
+
right: {
|
|
570
|
+
type: "sea",
|
|
571
|
+
crowns: 0
|
|
572
|
+
},
|
|
573
|
+
number: 28
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
left: {
|
|
577
|
+
type: "forest",
|
|
578
|
+
crowns: 1
|
|
579
|
+
},
|
|
580
|
+
right: {
|
|
581
|
+
type: "plain",
|
|
582
|
+
crowns: 0
|
|
583
|
+
},
|
|
584
|
+
number: 29
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
left: {
|
|
588
|
+
type: "sea",
|
|
589
|
+
crowns: 1
|
|
590
|
+
},
|
|
591
|
+
right: {
|
|
592
|
+
type: "wheat",
|
|
593
|
+
crowns: 0
|
|
594
|
+
},
|
|
595
|
+
number: 30
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
left: {
|
|
599
|
+
type: "sea",
|
|
600
|
+
crowns: 1
|
|
601
|
+
},
|
|
602
|
+
right: {
|
|
603
|
+
type: "wheat",
|
|
604
|
+
crowns: 0
|
|
605
|
+
},
|
|
606
|
+
number: 31
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
left: {
|
|
610
|
+
type: "sea",
|
|
611
|
+
crowns: 1
|
|
612
|
+
},
|
|
613
|
+
right: {
|
|
614
|
+
type: "forest",
|
|
615
|
+
crowns: 0
|
|
616
|
+
},
|
|
617
|
+
number: 32
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
left: {
|
|
621
|
+
type: "sea",
|
|
622
|
+
crowns: 1
|
|
623
|
+
},
|
|
624
|
+
right: {
|
|
625
|
+
type: "forest",
|
|
626
|
+
crowns: 0
|
|
627
|
+
},
|
|
628
|
+
number: 33
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
left: {
|
|
632
|
+
type: "sea",
|
|
633
|
+
crowns: 1
|
|
634
|
+
},
|
|
635
|
+
right: {
|
|
636
|
+
type: "forest",
|
|
637
|
+
crowns: 0
|
|
638
|
+
},
|
|
639
|
+
number: 34
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
left: {
|
|
643
|
+
type: "sea",
|
|
644
|
+
crowns: 1
|
|
645
|
+
},
|
|
646
|
+
right: {
|
|
647
|
+
type: "forest",
|
|
648
|
+
crowns: 0
|
|
649
|
+
},
|
|
650
|
+
number: 35
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
left: {
|
|
654
|
+
type: "wheat",
|
|
655
|
+
crowns: 0
|
|
656
|
+
},
|
|
657
|
+
right: {
|
|
658
|
+
type: "plain",
|
|
659
|
+
crowns: 1
|
|
660
|
+
},
|
|
661
|
+
number: 36
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
left: {
|
|
665
|
+
type: "sea",
|
|
666
|
+
crowns: 0
|
|
667
|
+
},
|
|
668
|
+
right: {
|
|
669
|
+
type: "plain",
|
|
670
|
+
crowns: 1
|
|
671
|
+
},
|
|
672
|
+
number: 37
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
left: {
|
|
676
|
+
type: "wheat",
|
|
677
|
+
crowns: 0
|
|
678
|
+
},
|
|
679
|
+
right: {
|
|
680
|
+
type: "swamp",
|
|
681
|
+
crowns: 1
|
|
682
|
+
},
|
|
683
|
+
number: 38
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
left: {
|
|
687
|
+
type: "plain",
|
|
688
|
+
crowns: 0
|
|
689
|
+
},
|
|
690
|
+
right: {
|
|
691
|
+
type: "swamp",
|
|
692
|
+
crowns: 0
|
|
693
|
+
},
|
|
694
|
+
number: 39
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
left: {
|
|
698
|
+
type: "mine",
|
|
699
|
+
crowns: 1
|
|
700
|
+
},
|
|
701
|
+
right: {
|
|
702
|
+
type: "wheat",
|
|
703
|
+
crowns: 0
|
|
704
|
+
},
|
|
705
|
+
number: 40
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
left: {
|
|
709
|
+
type: "wheat",
|
|
710
|
+
crowns: 0
|
|
711
|
+
},
|
|
712
|
+
right: {
|
|
713
|
+
type: "plain",
|
|
714
|
+
crowns: 2
|
|
715
|
+
},
|
|
716
|
+
number: 41
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
left: {
|
|
720
|
+
type: "sea",
|
|
721
|
+
crowns: 0
|
|
722
|
+
},
|
|
723
|
+
right: {
|
|
724
|
+
type: "plain",
|
|
725
|
+
crowns: 2
|
|
726
|
+
},
|
|
727
|
+
number: 42
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
left: {
|
|
731
|
+
type: "wheat",
|
|
732
|
+
crowns: 0
|
|
733
|
+
},
|
|
734
|
+
right: {
|
|
735
|
+
type: "swamp",
|
|
736
|
+
crowns: 2
|
|
737
|
+
},
|
|
738
|
+
number: 43
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
left: {
|
|
742
|
+
type: "plain",
|
|
743
|
+
crowns: 0
|
|
744
|
+
},
|
|
745
|
+
right: {
|
|
746
|
+
type: "swamp",
|
|
747
|
+
crowns: 2
|
|
748
|
+
},
|
|
749
|
+
number: 44
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
left: {
|
|
753
|
+
type: "mine",
|
|
754
|
+
crowns: 2
|
|
755
|
+
},
|
|
756
|
+
right: {
|
|
757
|
+
type: "wheat",
|
|
758
|
+
crowns: 0
|
|
759
|
+
},
|
|
760
|
+
number: 45
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
left: {
|
|
764
|
+
type: "swamp",
|
|
765
|
+
crowns: 0
|
|
766
|
+
},
|
|
767
|
+
right: {
|
|
768
|
+
type: "mine",
|
|
769
|
+
crowns: 2
|
|
770
|
+
},
|
|
771
|
+
number: 46
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
left: {
|
|
775
|
+
type: "swamp",
|
|
776
|
+
crowns: 0
|
|
777
|
+
},
|
|
778
|
+
right: {
|
|
779
|
+
type: "mine",
|
|
780
|
+
crowns: 2
|
|
781
|
+
},
|
|
782
|
+
number: 47
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
left: {
|
|
786
|
+
type: "wheat",
|
|
787
|
+
crowns: 0
|
|
788
|
+
},
|
|
789
|
+
right: {
|
|
790
|
+
type: "mine",
|
|
791
|
+
crowns: 3
|
|
792
|
+
},
|
|
793
|
+
number: 48
|
|
794
|
+
}
|
|
795
|
+
]
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// src/adapterServerside/jsonDominoes.ts
|
|
799
|
+
var jsonDominoes = () => {
|
|
800
|
+
const dominoes = {};
|
|
801
|
+
for (const [key, value] of Object.entries(dominoes_default)) {
|
|
802
|
+
const currentDominoes = [];
|
|
803
|
+
value.forEach((domino) => {
|
|
804
|
+
currentDominoes.push({
|
|
805
|
+
left: {
|
|
806
|
+
type: domino.left.type,
|
|
807
|
+
crowns: domino.left.crowns
|
|
808
|
+
},
|
|
809
|
+
right: {
|
|
810
|
+
type: domino.right.type,
|
|
811
|
+
crowns: domino.right.crowns
|
|
812
|
+
},
|
|
813
|
+
number: domino.number
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
dominoes[key] = currentDominoes;
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
getForMode: (mode) => {
|
|
820
|
+
return dominoes[mode.name] || [];
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
var jsonDominoes_default = jsonDominoes;
|
|
825
|
+
|
|
826
|
+
// src/datasources/modes.json
|
|
827
|
+
var modes_default = [
|
|
828
|
+
{
|
|
829
|
+
name: "Classic",
|
|
830
|
+
description: "Original KingDomino rules"
|
|
831
|
+
}
|
|
832
|
+
];
|
|
833
|
+
|
|
834
|
+
// src/adapterServerside/jsonModes.ts
|
|
835
|
+
var jsonModes = () => {
|
|
836
|
+
const modes = [];
|
|
837
|
+
modes_default.forEach((mode) => {
|
|
838
|
+
modes.push({
|
|
839
|
+
name: mode.name,
|
|
840
|
+
description: mode.description
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
return {
|
|
844
|
+
getAvailables: () => {
|
|
845
|
+
return modes;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
var jsonModes_default = jsonModes;
|
|
850
|
+
|
|
851
|
+
// src/datasources/rules.json
|
|
852
|
+
var rules_default = {
|
|
853
|
+
commun: [
|
|
854
|
+
{
|
|
855
|
+
playersLimit: 1,
|
|
856
|
+
lordsPerPlayer: 1,
|
|
857
|
+
maxDominoes: 48,
|
|
858
|
+
dominoesPerTurn: 4,
|
|
859
|
+
maxTurns: 12,
|
|
860
|
+
maxKingdomSize: 5
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
playersLimit: 2,
|
|
864
|
+
lordsPerPlayer: 2,
|
|
865
|
+
maxDominoes: 24,
|
|
866
|
+
dominoesPerTurn: 4,
|
|
867
|
+
maxTurns: 6,
|
|
868
|
+
maxKingdomSize: 5
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
playersLimit: 3,
|
|
872
|
+
lordsPerPlayer: 1,
|
|
873
|
+
maxDominoes: 48,
|
|
874
|
+
dominoesPerTurn: 4,
|
|
875
|
+
maxTurns: 12,
|
|
876
|
+
maxKingdomSize: 5
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
playersLimit: 4,
|
|
880
|
+
lordsPerPlayer: 1,
|
|
881
|
+
maxDominoes: 48,
|
|
882
|
+
dominoesPerTurn: 4,
|
|
883
|
+
maxTurns: 12,
|
|
884
|
+
maxKingdomSize: 5
|
|
885
|
+
}
|
|
886
|
+
],
|
|
887
|
+
extra: [
|
|
888
|
+
{
|
|
889
|
+
name: "The middle Kingdom",
|
|
890
|
+
description: "Gain 10 additional points if your castle is in the middle of the kingdom.",
|
|
891
|
+
mode: [{ name: "Classic", description: "King Domino classic mode" }]
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: "Harmony",
|
|
895
|
+
description: "Gain 5 additional points if your kingdom is complete (no discarded dominoes).",
|
|
896
|
+
mode: [{ name: "Classic", description: "King Domino classic mode" }]
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "The Mighty Duel",
|
|
900
|
+
description: "Use all 48 dominoes and build a 7x7 kingdom. For 2 players only.",
|
|
901
|
+
mode: [{ name: "Classic", description: "King Domino classic mode" }],
|
|
902
|
+
playersLimit: 2
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
name: "Dynasty",
|
|
906
|
+
description: "Play 3 games in a row. The player with the highest total points wins.",
|
|
907
|
+
mode: [{ name: "Classic", description: "King Domino classic mode" }]
|
|
908
|
+
}
|
|
909
|
+
]
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/adapterServerside/jsonRules.ts
|
|
913
|
+
var jsonRules = () => {
|
|
914
|
+
const rules = {
|
|
915
|
+
basic: {},
|
|
916
|
+
extraRules: []
|
|
917
|
+
};
|
|
918
|
+
const commonRules = rules_default.commun;
|
|
919
|
+
const extraRules = rules_default.extra;
|
|
920
|
+
commonRules.forEach((rule) => {
|
|
921
|
+
rules.basic[rule.playersLimit] = {
|
|
922
|
+
lords: rule.lordsPerPlayer,
|
|
923
|
+
maxDominoes: rule.maxDominoes,
|
|
924
|
+
dominoesPerTurn: rule.dominoesPerTurn,
|
|
925
|
+
maxTurns: rule.maxTurns,
|
|
926
|
+
maxKingdomSize: rule.maxKingdomSize
|
|
927
|
+
};
|
|
928
|
+
});
|
|
929
|
+
extraRules.forEach((rule) => {
|
|
930
|
+
rules.extraRules.push({
|
|
931
|
+
name: rule.name,
|
|
932
|
+
description: rule.description,
|
|
933
|
+
mode: rule.mode,
|
|
934
|
+
..."playersLimit" in rule && { playersLimit: rule.playersLimit }
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
return {
|
|
938
|
+
getAll: () => {
|
|
939
|
+
return rules;
|
|
940
|
+
},
|
|
941
|
+
getAllExtra: () => {
|
|
942
|
+
return rules.extraRules;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
};
|
|
946
|
+
var jsonRules_default = jsonRules;
|
|
947
|
+
|
|
948
|
+
// src/adapterServerside/shuffle.ts
|
|
949
|
+
var shuffleMethod = (array) => {
|
|
950
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
951
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
952
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
953
|
+
}
|
|
954
|
+
return array;
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/adapterServerside/uuid.ts
|
|
958
|
+
var uuidMethod = () => {
|
|
959
|
+
return globalThis.crypto.randomUUID();
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// src/core/domain/errors/domainErrors.ts
|
|
963
|
+
var ErrorCode = {
|
|
964
|
+
// Game flow errors
|
|
965
|
+
INVALID_STEP: "INVALID_STEP",
|
|
966
|
+
STEP_EXECUTION_FAILED: "STEP_EXECUTION_FAILED",
|
|
967
|
+
ACTION_EXECUTION_FAILED: "ACTION_EXECUTION_FAILED",
|
|
968
|
+
// Entity not found errors
|
|
969
|
+
LORD_NOT_FOUND: "LORD_NOT_FOUND",
|
|
970
|
+
PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND",
|
|
971
|
+
DOMINO_NOT_FOUND: "DOMINO_NOT_FOUND",
|
|
972
|
+
MODE_NOT_FOUND: "MODE_NOT_FOUND",
|
|
973
|
+
// Validation errors
|
|
974
|
+
INVALID_PLAYER_COUNT: "INVALID_PLAYER_COUNT",
|
|
975
|
+
INVALID_PLAYER_NAME: "INVALID_PLAYER_NAME",
|
|
976
|
+
INVALID_PLACEMENT: "INVALID_PLACEMENT",
|
|
977
|
+
PLACEMENT_NOT_EMPTY: "PLACEMENT_NOT_EMPTY",
|
|
978
|
+
PLACEMENT_NOT_ADJACENT: "PLACEMENT_NOT_ADJACENT",
|
|
979
|
+
PLACEMENT_INVALID_TERRAIN: "PLACEMENT_INVALID_TERRAIN",
|
|
980
|
+
PLACEMENT_OUT_OF_BOUNDS: "PLACEMENT_OUT_OF_BOUNDS",
|
|
981
|
+
PLACEMENT_EXCEEDS_KINGDOM_SIZE: "PLACEMENT_EXCEEDS_KINGDOM_SIZE",
|
|
982
|
+
// Action errors
|
|
983
|
+
NOT_YOUR_TURN: "NOT_YOUR_TURN",
|
|
984
|
+
CANNOT_PICK: "CANNOT_PICK",
|
|
985
|
+
CANNOT_PLACE: "CANNOT_PLACE",
|
|
986
|
+
DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED",
|
|
987
|
+
// History errors
|
|
988
|
+
UNDO_UNAVAILABLE: "UNDO_UNAVAILABLE",
|
|
989
|
+
REDO_UNAVAILABLE: "REDO_UNAVAILABLE"
|
|
990
|
+
};
|
|
991
|
+
var createDomainError = (code, message, context) => ({
|
|
992
|
+
code,
|
|
993
|
+
message,
|
|
994
|
+
context
|
|
995
|
+
});
|
|
996
|
+
var DomainException = class extends Error {
|
|
997
|
+
code;
|
|
998
|
+
context;
|
|
999
|
+
constructor(error) {
|
|
1000
|
+
super(error.message);
|
|
1001
|
+
this.name = "DomainException";
|
|
1002
|
+
this.code = error.code;
|
|
1003
|
+
this.context = error.context;
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
var InvalidStepError = class extends DomainException {
|
|
1007
|
+
constructor(message, context) {
|
|
1008
|
+
super({
|
|
1009
|
+
code: ErrorCode.INVALID_STEP,
|
|
1010
|
+
message,
|
|
1011
|
+
context
|
|
1012
|
+
});
|
|
1013
|
+
this.name = "InvalidStepError";
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
var StepExecutionError = class extends DomainException {
|
|
1017
|
+
constructor(message, code = ErrorCode.STEP_EXECUTION_FAILED, context) {
|
|
1018
|
+
super({
|
|
1019
|
+
code,
|
|
1020
|
+
message,
|
|
1021
|
+
context
|
|
1022
|
+
});
|
|
1023
|
+
this.name = "StepExecutionError";
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
var ActionExecutionError = class extends DomainException {
|
|
1027
|
+
constructor(message, code = ErrorCode.ACTION_EXECUTION_FAILED, context) {
|
|
1028
|
+
super({
|
|
1029
|
+
code,
|
|
1030
|
+
message,
|
|
1031
|
+
context
|
|
1032
|
+
});
|
|
1033
|
+
this.name = "ActionExecutionError";
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
var NotFoundError = class extends DomainException {
|
|
1037
|
+
constructor(message, code = ErrorCode.LORD_NOT_FOUND, context) {
|
|
1038
|
+
super({
|
|
1039
|
+
code,
|
|
1040
|
+
message,
|
|
1041
|
+
context
|
|
1042
|
+
});
|
|
1043
|
+
this.name = "NotFoundError";
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
var ValidationError = class extends DomainException {
|
|
1047
|
+
constructor(message, code, context) {
|
|
1048
|
+
super({
|
|
1049
|
+
code,
|
|
1050
|
+
message,
|
|
1051
|
+
context
|
|
1052
|
+
});
|
|
1053
|
+
this.name = "ValidationError";
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
var UndoError = class extends DomainException {
|
|
1057
|
+
constructor(message = "No state to undo", context) {
|
|
1058
|
+
super({
|
|
1059
|
+
code: ErrorCode.UNDO_UNAVAILABLE,
|
|
1060
|
+
message,
|
|
1061
|
+
context
|
|
1062
|
+
});
|
|
1063
|
+
this.name = "UndoError";
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
var RedoError = class extends DomainException {
|
|
1067
|
+
constructor(message = "No state to redo", context) {
|
|
1068
|
+
super({
|
|
1069
|
+
code: ErrorCode.REDO_UNAVAILABLE,
|
|
1070
|
+
message,
|
|
1071
|
+
context
|
|
1072
|
+
});
|
|
1073
|
+
this.name = "RedoError";
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// src/core/i18n/translations.ts
|
|
1078
|
+
var TranslationKey = {
|
|
1079
|
+
// Error messages
|
|
1080
|
+
ERROR_INVALID_STEP: "error.invalidStep",
|
|
1081
|
+
ERROR_STEP_EXECUTION_FAILED: "error.stepExecutionFailed",
|
|
1082
|
+
ERROR_ACTION_EXECUTION_FAILED: "error.actionExecutionFailed",
|
|
1083
|
+
ERROR_LORD_NOT_FOUND: "error.lordNotFound",
|
|
1084
|
+
ERROR_PLAYER_NOT_FOUND: "error.playerNotFound",
|
|
1085
|
+
ERROR_DOMINO_NOT_FOUND: "error.dominoNotFound",
|
|
1086
|
+
ERROR_MODE_NOT_FOUND: "error.modeNotFound",
|
|
1087
|
+
ERROR_INVALID_PLACEMENT: "error.invalidPlacement",
|
|
1088
|
+
ERROR_PLACEMENT_NOT_EMPTY: "error.placementNotEmpty",
|
|
1089
|
+
ERROR_PLACEMENT_NOT_ADJACENT: "error.placementNotAdjacent",
|
|
1090
|
+
ERROR_PLACEMENT_INVALID_TERRAIN: "error.placementInvalidTerrain",
|
|
1091
|
+
ERROR_PLACEMENT_OUT_OF_BOUNDS: "error.placementOutOfBounds",
|
|
1092
|
+
ERROR_PLACEMENT_EXCEEDS_KINGDOM_SIZE: "error.placementExceedsKingdomSize",
|
|
1093
|
+
ERROR_NOT_YOUR_TURN: "error.notYourTurn",
|
|
1094
|
+
ERROR_CANNOT_PICK: "error.cannotPick",
|
|
1095
|
+
ERROR_CANNOT_PLACE: "error.cannotPlace",
|
|
1096
|
+
ERROR_DOMINO_ALREADY_PICKED: "error.dominoAlreadyPicked",
|
|
1097
|
+
ERROR_INVALID_PLAYER_COUNT: "error.invalidPlayerCount",
|
|
1098
|
+
ERROR_INVALID_PLAYER_NAME: "error.invalidPlayerName",
|
|
1099
|
+
ERROR_UNDO_UNAVAILABLE: "error.undoUnavailable",
|
|
1100
|
+
ERROR_REDO_UNAVAILABLE: "error.redoUnavailable",
|
|
1101
|
+
// Game mode names
|
|
1102
|
+
MODE_CLASSIC: "mode.classic",
|
|
1103
|
+
// Extra rule names
|
|
1104
|
+
RULE_MIDDLE_KINGDOM: "rule.middleKingdom",
|
|
1105
|
+
RULE_HARMONY: "rule.harmony",
|
|
1106
|
+
// Ground types
|
|
1107
|
+
GROUND_CASTLE: "ground.castle",
|
|
1108
|
+
GROUND_WHEAT: "ground.wheat",
|
|
1109
|
+
GROUND_FOREST: "ground.forest",
|
|
1110
|
+
GROUND_SEA: "ground.sea",
|
|
1111
|
+
GROUND_PLAIN: "ground.plain",
|
|
1112
|
+
GROUND_SWAMP: "ground.swamp",
|
|
1113
|
+
GROUND_MINE: "ground.mine",
|
|
1114
|
+
// Game phases
|
|
1115
|
+
PHASE_ADD_PLAYERS: "phase.addPlayers",
|
|
1116
|
+
PHASE_OPTIONS: "phase.options",
|
|
1117
|
+
PHASE_START: "phase.start",
|
|
1118
|
+
PHASE_RESULT: "phase.result",
|
|
1119
|
+
// Actions
|
|
1120
|
+
ACTION_PICK_DOMINO: "action.pickDomino",
|
|
1121
|
+
ACTION_PLACE_DOMINO: "action.placeDomino",
|
|
1122
|
+
ACTION_PASS: "action.pass"
|
|
1123
|
+
};
|
|
1124
|
+
var defaultTranslations = {
|
|
1125
|
+
// Error messages
|
|
1126
|
+
[TranslationKey.ERROR_INVALID_STEP]: "Invalid game step",
|
|
1127
|
+
[TranslationKey.ERROR_STEP_EXECUTION_FAILED]: "Step execution failed",
|
|
1128
|
+
[TranslationKey.ERROR_ACTION_EXECUTION_FAILED]: "Action execution failed",
|
|
1129
|
+
[TranslationKey.ERROR_LORD_NOT_FOUND]: "Lord not found",
|
|
1130
|
+
[TranslationKey.ERROR_PLAYER_NOT_FOUND]: "Player not found",
|
|
1131
|
+
[TranslationKey.ERROR_DOMINO_NOT_FOUND]: "Domino not found",
|
|
1132
|
+
[TranslationKey.ERROR_MODE_NOT_FOUND]: "Game mode not found",
|
|
1133
|
+
[TranslationKey.ERROR_INVALID_PLACEMENT]: "Invalid placement",
|
|
1134
|
+
[TranslationKey.ERROR_PLACEMENT_NOT_EMPTY]: "Position is not empty",
|
|
1135
|
+
[TranslationKey.ERROR_PLACEMENT_NOT_ADJACENT]: "Not adjacent to existing tiles",
|
|
1136
|
+
[TranslationKey.ERROR_PLACEMENT_INVALID_TERRAIN]: "Terrain type does not match",
|
|
1137
|
+
[TranslationKey.ERROR_PLACEMENT_OUT_OF_BOUNDS]: "Placement is out of bounds",
|
|
1138
|
+
[TranslationKey.ERROR_PLACEMENT_EXCEEDS_KINGDOM_SIZE]: "Placement exceeds kingdom size",
|
|
1139
|
+
[TranslationKey.ERROR_NOT_YOUR_TURN]: "It is not your turn",
|
|
1140
|
+
[TranslationKey.ERROR_CANNOT_PICK]: "Cannot pick a domino now",
|
|
1141
|
+
[TranslationKey.ERROR_CANNOT_PLACE]: "Cannot place a domino now",
|
|
1142
|
+
[TranslationKey.ERROR_DOMINO_ALREADY_PICKED]: "Domino has already been picked",
|
|
1143
|
+
[TranslationKey.ERROR_INVALID_PLAYER_COUNT]: "Invalid number of players (1-4 required)",
|
|
1144
|
+
[TranslationKey.ERROR_INVALID_PLAYER_NAME]: "Player name must be at least 3 characters",
|
|
1145
|
+
[TranslationKey.ERROR_UNDO_UNAVAILABLE]: "No state to undo",
|
|
1146
|
+
[TranslationKey.ERROR_REDO_UNAVAILABLE]: "No state to redo",
|
|
1147
|
+
// Game mode names
|
|
1148
|
+
[TranslationKey.MODE_CLASSIC]: "Classic",
|
|
1149
|
+
// Extra rule names
|
|
1150
|
+
[TranslationKey.RULE_MIDDLE_KINGDOM]: "The Middle Kingdom",
|
|
1151
|
+
[TranslationKey.RULE_HARMONY]: "Harmony",
|
|
1152
|
+
// Ground types
|
|
1153
|
+
[TranslationKey.GROUND_CASTLE]: "Castle",
|
|
1154
|
+
[TranslationKey.GROUND_WHEAT]: "Wheat Field",
|
|
1155
|
+
[TranslationKey.GROUND_FOREST]: "Forest",
|
|
1156
|
+
[TranslationKey.GROUND_SEA]: "Sea",
|
|
1157
|
+
[TranslationKey.GROUND_PLAIN]: "Plain",
|
|
1158
|
+
[TranslationKey.GROUND_SWAMP]: "Swamp",
|
|
1159
|
+
[TranslationKey.GROUND_MINE]: "Mine",
|
|
1160
|
+
// Game phases
|
|
1161
|
+
[TranslationKey.PHASE_ADD_PLAYERS]: "Add Players",
|
|
1162
|
+
[TranslationKey.PHASE_OPTIONS]: "Game Options",
|
|
1163
|
+
[TranslationKey.PHASE_START]: "Start Game",
|
|
1164
|
+
[TranslationKey.PHASE_RESULT]: "Game Results",
|
|
1165
|
+
// Actions
|
|
1166
|
+
[TranslationKey.ACTION_PICK_DOMINO]: "Pick Domino",
|
|
1167
|
+
[TranslationKey.ACTION_PLACE_DOMINO]: "Place Domino",
|
|
1168
|
+
[TranslationKey.ACTION_PASS]: "Pass"
|
|
1169
|
+
};
|
|
1170
|
+
var errorCodeToTranslationKey = {
|
|
1171
|
+
[ErrorCode.INVALID_STEP]: TranslationKey.ERROR_INVALID_STEP,
|
|
1172
|
+
[ErrorCode.STEP_EXECUTION_FAILED]: TranslationKey.ERROR_STEP_EXECUTION_FAILED,
|
|
1173
|
+
[ErrorCode.ACTION_EXECUTION_FAILED]: TranslationKey.ERROR_ACTION_EXECUTION_FAILED,
|
|
1174
|
+
[ErrorCode.LORD_NOT_FOUND]: TranslationKey.ERROR_LORD_NOT_FOUND,
|
|
1175
|
+
[ErrorCode.PLAYER_NOT_FOUND]: TranslationKey.ERROR_PLAYER_NOT_FOUND,
|
|
1176
|
+
[ErrorCode.DOMINO_NOT_FOUND]: TranslationKey.ERROR_DOMINO_NOT_FOUND,
|
|
1177
|
+
[ErrorCode.MODE_NOT_FOUND]: TranslationKey.ERROR_MODE_NOT_FOUND,
|
|
1178
|
+
[ErrorCode.INVALID_PLAYER_COUNT]: TranslationKey.ERROR_INVALID_PLAYER_COUNT,
|
|
1179
|
+
[ErrorCode.INVALID_PLAYER_NAME]: TranslationKey.ERROR_INVALID_PLAYER_NAME,
|
|
1180
|
+
[ErrorCode.INVALID_PLACEMENT]: TranslationKey.ERROR_INVALID_PLACEMENT,
|
|
1181
|
+
[ErrorCode.PLACEMENT_NOT_EMPTY]: TranslationKey.ERROR_PLACEMENT_NOT_EMPTY,
|
|
1182
|
+
[ErrorCode.PLACEMENT_NOT_ADJACENT]: TranslationKey.ERROR_PLACEMENT_NOT_ADJACENT,
|
|
1183
|
+
[ErrorCode.PLACEMENT_INVALID_TERRAIN]: TranslationKey.ERROR_PLACEMENT_INVALID_TERRAIN,
|
|
1184
|
+
[ErrorCode.PLACEMENT_OUT_OF_BOUNDS]: TranslationKey.ERROR_PLACEMENT_OUT_OF_BOUNDS,
|
|
1185
|
+
[ErrorCode.PLACEMENT_EXCEEDS_KINGDOM_SIZE]: TranslationKey.ERROR_PLACEMENT_EXCEEDS_KINGDOM_SIZE,
|
|
1186
|
+
[ErrorCode.NOT_YOUR_TURN]: TranslationKey.ERROR_NOT_YOUR_TURN,
|
|
1187
|
+
[ErrorCode.CANNOT_PICK]: TranslationKey.ERROR_CANNOT_PICK,
|
|
1188
|
+
[ErrorCode.CANNOT_PLACE]: TranslationKey.ERROR_CANNOT_PLACE,
|
|
1189
|
+
[ErrorCode.DOMINO_ALREADY_PICKED]: TranslationKey.ERROR_DOMINO_ALREADY_PICKED,
|
|
1190
|
+
[ErrorCode.UNDO_UNAVAILABLE]: TranslationKey.ERROR_UNDO_UNAVAILABLE,
|
|
1191
|
+
[ErrorCode.REDO_UNAVAILABLE]: TranslationKey.ERROR_REDO_UNAVAILABLE
|
|
1192
|
+
};
|
|
1193
|
+
var translateErrorCode = (translator, code, params) => {
|
|
1194
|
+
const key = errorCodeToTranslationKey[code];
|
|
1195
|
+
return translator.t(key, params);
|
|
1196
|
+
};
|
|
1197
|
+
var createTranslator = (translations = {}) => {
|
|
1198
|
+
const mergedTranslations = { ...defaultTranslations, ...translations };
|
|
1199
|
+
return {
|
|
1200
|
+
t(key, params) {
|
|
1201
|
+
let text = mergedTranslations[key] || key;
|
|
1202
|
+
if (params) {
|
|
1203
|
+
for (const [paramKey, value] of Object.entries(params)) {
|
|
1204
|
+
text = text.replace(
|
|
1205
|
+
new RegExp(`\\{\\{${paramKey}\\}\\}`, "g"),
|
|
1206
|
+
String(value)
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return text;
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
};
|
|
1214
|
+
var defaultTranslator = createTranslator();
|
|
1215
|
+
|
|
1216
|
+
// src/utils/result.ts
|
|
1217
|
+
var ok = (value) => ({ tag: "Ok", value });
|
|
1218
|
+
var err = (error) => ({ tag: "Err", error });
|
|
1219
|
+
var isOk = (result) => result.tag === "Ok";
|
|
1220
|
+
var isErr = (result) => result.tag === "Err";
|
|
1221
|
+
var unwrap = (result) => {
|
|
1222
|
+
if (result.tag === "Err") {
|
|
1223
|
+
throw new Error(result.error);
|
|
1224
|
+
}
|
|
1225
|
+
return result.value;
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// src/application/handlers/addExtraRulesHandler.ts
|
|
1229
|
+
var addExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
1230
|
+
const { game, extraRules } = command;
|
|
1231
|
+
logger.info(`Adding extra rules to game: ${game.id}`);
|
|
1232
|
+
logger.info(`Extra rules: ${extraRules}`);
|
|
1233
|
+
if (!isGameWithNextStep(game)) {
|
|
1234
|
+
logger.error(
|
|
1235
|
+
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1236
|
+
);
|
|
1237
|
+
throw new InvalidStepError(
|
|
1238
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1239
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
if (game.nextAction.step !== "options") {
|
|
1243
|
+
logger.error(
|
|
1244
|
+
`Required game with options step but got: ${game.nextAction.step}`
|
|
1245
|
+
);
|
|
1246
|
+
throw new InvalidStepError(
|
|
1247
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1248
|
+
{ expected: "options", actual: game.nextAction.step, gameId: game.id }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const result = useCase(game, extraRules);
|
|
1252
|
+
if (isErr(result)) {
|
|
1253
|
+
logger.error(`Error adding extra rules: ${result.error}`);
|
|
1254
|
+
const code = result.error;
|
|
1255
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1256
|
+
gameId: game.id,
|
|
1257
|
+
extraRules: command.extraRules
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
logger.info(`Extra rules added to game: ${game.id}`);
|
|
1261
|
+
return result.value;
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
// src/application/handlers/addPlayersHandler.ts
|
|
1265
|
+
var addPlayersHandler = (logger, translator, useCase) => (command) => {
|
|
1266
|
+
const { game, players } = command;
|
|
1267
|
+
logger.info(`Adding players to game: ${game.id}`);
|
|
1268
|
+
if (!isGameWithNextStep(game)) {
|
|
1269
|
+
logger.error(
|
|
1270
|
+
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1271
|
+
);
|
|
1272
|
+
throw new InvalidStepError(
|
|
1273
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1274
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (game.nextAction.step !== "addPlayers") {
|
|
1278
|
+
logger.error(
|
|
1279
|
+
`Required game with addPlayers step but got: ${game.nextAction.step}`
|
|
1280
|
+
);
|
|
1281
|
+
throw new InvalidStepError(
|
|
1282
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1283
|
+
{
|
|
1284
|
+
expected: "addPlayers",
|
|
1285
|
+
actual: game.nextAction.step,
|
|
1286
|
+
gameId: game.id
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
const result = useCase(game, players);
|
|
1291
|
+
if (isErr(result)) {
|
|
1292
|
+
logger.error(`Error adding players: ${result.error}`);
|
|
1293
|
+
const code = result.error;
|
|
1294
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1295
|
+
gameId: game.id,
|
|
1296
|
+
players: command.players
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
logger.info(`Players added to game: ${game.id}`);
|
|
1300
|
+
return result.value;
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// src/application/handlers/calculateScoreHandler.ts
|
|
1304
|
+
var calculateScoreHandler = (logger, translator, useCase) => (command) => {
|
|
1305
|
+
const { kingdom } = command;
|
|
1306
|
+
logger.info(`Calculating score for kingdom`);
|
|
1307
|
+
const result = useCase(kingdom);
|
|
1308
|
+
if (isErr(result)) {
|
|
1309
|
+
logger.error(`Error calculating score: ${result.error}`);
|
|
1310
|
+
throw new Error(result.error);
|
|
1311
|
+
}
|
|
1312
|
+
logger.info(`Score calculated: ${result.value}`);
|
|
1313
|
+
return result.value;
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
// src/application/handlers/canPlaceDominoHandler.ts
|
|
1317
|
+
var canPlaceDominoHandler = (logger, translator, useCase) => {
|
|
1318
|
+
return (command) => {
|
|
1319
|
+
logger.info(`Checking if domino ${command.domino.number} can be placed`);
|
|
1320
|
+
const canPlace = useCase(
|
|
1321
|
+
command.kingdom,
|
|
1322
|
+
command.domino,
|
|
1323
|
+
command.maxKingdomSize
|
|
1324
|
+
);
|
|
1325
|
+
logger.info(`Can place domino: ${canPlace}`);
|
|
1326
|
+
return canPlace;
|
|
1327
|
+
};
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
// src/application/handlers/chooseDominoHandler.ts
|
|
1331
|
+
var chooseDominoHandler = (logger, translator, useCase) => (command) => {
|
|
1332
|
+
const { game, lordId, dominoPick } = command;
|
|
1333
|
+
logger.info(
|
|
1334
|
+
`Choosing domino: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
1335
|
+
);
|
|
1336
|
+
if (!isGameWithNextAction(game)) {
|
|
1337
|
+
logger.error(
|
|
1338
|
+
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
1339
|
+
);
|
|
1340
|
+
throw new InvalidStepError(
|
|
1341
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1342
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
if (game.nextAction.nextAction !== "pickDomino") {
|
|
1346
|
+
logger.error(
|
|
1347
|
+
`Required game with pickDomino action but got: ${game.nextAction.nextAction}`
|
|
1348
|
+
);
|
|
1349
|
+
throw new InvalidStepError(
|
|
1350
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1351
|
+
{
|
|
1352
|
+
expected: "pickDomino",
|
|
1353
|
+
actual: game.nextAction.nextAction,
|
|
1354
|
+
gameId: game.id
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
const result = useCase(game, lordId, dominoPick);
|
|
1359
|
+
if (isErr(result)) {
|
|
1360
|
+
logger.error(`Error choosing domino: ${result.error}`);
|
|
1361
|
+
const code = result.error;
|
|
1362
|
+
throw new ActionExecutionError(
|
|
1363
|
+
translateErrorCode(translator, code),
|
|
1364
|
+
code,
|
|
1365
|
+
{
|
|
1366
|
+
gameId: game.id,
|
|
1367
|
+
lordId: command.lordId,
|
|
1368
|
+
dominoPick: command.dominoPick
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
logger.info(
|
|
1373
|
+
`Domino chosen: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
1374
|
+
);
|
|
1375
|
+
return result.value;
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// src/application/handlers/createGameHandler.ts
|
|
1379
|
+
var createGameHandler = (logger, translator, useCase) => (command) => {
|
|
1380
|
+
logger.info(`Creating game with mode: ${command.mode}`);
|
|
1381
|
+
const result = useCase(command.mode, command.seed);
|
|
1382
|
+
if (isErr(result)) {
|
|
1383
|
+
logger.error(`Error creating game: ${result.error}`);
|
|
1384
|
+
const code = result.error;
|
|
1385
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1386
|
+
mode: command.mode
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
logger.info(`Game created: ${result.value.id}`);
|
|
1390
|
+
return result.value;
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
// src/application/handlers/discardDominoHandler.ts
|
|
1394
|
+
var discardDominoHandler = (logger, translator, useCase) => (command) => {
|
|
1395
|
+
const { game, lordId } = command;
|
|
1396
|
+
logger.info(`Discarding domino for lord: ${lordId} in game: ${game.id}`);
|
|
1397
|
+
if (!isGameWithNextAction(game)) {
|
|
1398
|
+
logger.error(
|
|
1399
|
+
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
1400
|
+
);
|
|
1401
|
+
throw new InvalidStepError(
|
|
1402
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1403
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
if (game.nextAction.nextAction !== "placeDomino") {
|
|
1407
|
+
logger.error(
|
|
1408
|
+
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
1409
|
+
);
|
|
1410
|
+
throw new InvalidStepError(
|
|
1411
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1412
|
+
{
|
|
1413
|
+
expected: "placeDomino",
|
|
1414
|
+
actual: game.nextAction.nextAction,
|
|
1415
|
+
gameId: game.id
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
const result = useCase(game, lordId);
|
|
1420
|
+
if (isErr(result)) {
|
|
1421
|
+
logger.error(`Error discarding domino: ${result.error}`);
|
|
1422
|
+
const code = result.error;
|
|
1423
|
+
throw new ActionExecutionError(
|
|
1424
|
+
translateErrorCode(translator, code),
|
|
1425
|
+
code,
|
|
1426
|
+
{ gameId: game.id, lordId: command.lordId }
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (isGameWithNextAction(result.value)) {
|
|
1430
|
+
logger.info(`Domino discarded: ${lordId} in game: ${game.id}`);
|
|
1431
|
+
return result.value;
|
|
1432
|
+
}
|
|
1433
|
+
logger.info(`Domino discarded: ${lordId} in game: ${game.id}`);
|
|
1434
|
+
return result.value;
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// src/application/handlers/getDynastyResultHandler.ts
|
|
1438
|
+
var getDynastyResultHandler = (logger, translator, useCase) => {
|
|
1439
|
+
return (command) => {
|
|
1440
|
+
logger.info(
|
|
1441
|
+
`Calculating dynasty results for ${command.games.length} games`
|
|
1442
|
+
);
|
|
1443
|
+
const results = useCase(command.games);
|
|
1444
|
+
logger.info(`Dynasty results calculated`);
|
|
1445
|
+
return results;
|
|
1446
|
+
};
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
// src/application/handlers/getExtraRulesHandler.ts
|
|
1450
|
+
var getExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
1451
|
+
const { mode, players } = command;
|
|
1452
|
+
logger.info(
|
|
1453
|
+
`Getting extra rules for mode: ${mode} and players: ${players}`
|
|
1454
|
+
);
|
|
1455
|
+
const result = useCase(mode, players);
|
|
1456
|
+
if (isErr(result)) {
|
|
1457
|
+
logger.error(`Error getting extra rules: ${result.error}`);
|
|
1458
|
+
const code = result.error;
|
|
1459
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1460
|
+
mode: command.mode
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
logger.info(`Extra rules retrieved: ${result.value}`);
|
|
1464
|
+
return result.value;
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/application/handlers/getModesHandler.ts
|
|
1468
|
+
var getModesHandler = (logger, translator, useCase) => (_command) => {
|
|
1469
|
+
logger.info("Getting modes");
|
|
1470
|
+
const result = useCase();
|
|
1471
|
+
if (isErr(result)) {
|
|
1472
|
+
logger.error(`Error getting modes: ${result.error}`);
|
|
1473
|
+
const code = result.error;
|
|
1474
|
+
throw new NotFoundError(translateErrorCode(translator, code), code);
|
|
1475
|
+
}
|
|
1476
|
+
logger.info(`Modes retrieved: ${result.value}`);
|
|
1477
|
+
return result.value;
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/application/handlers/getResultHandler.ts
|
|
1481
|
+
var getResultHandler = (logger, translator, useCases) => (command) => {
|
|
1482
|
+
const { getResultUseCase: getResultUseCase2, calculateScoreUseCase: calculateScoreUseCase2 } = useCases;
|
|
1483
|
+
const { game } = command;
|
|
1484
|
+
logger.info(`Getting result for game: ${game.id}`);
|
|
1485
|
+
if (!isGameWithNextStep(game)) {
|
|
1486
|
+
logger.error(
|
|
1487
|
+
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1488
|
+
);
|
|
1489
|
+
throw new InvalidStepError(
|
|
1490
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1491
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
if (game.nextAction.step !== "result") {
|
|
1495
|
+
logger.error(
|
|
1496
|
+
`Required game with result step but got: ${game.nextAction.step}`
|
|
1497
|
+
);
|
|
1498
|
+
throw new InvalidStepError(
|
|
1499
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1500
|
+
{ expected: "result", actual: game.nextAction.step, gameId: game.id }
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
const { players } = game;
|
|
1504
|
+
const scoreResult = players.map((player) => {
|
|
1505
|
+
const { kingdom } = player;
|
|
1506
|
+
const result2 = calculateScoreUseCase2(kingdom);
|
|
1507
|
+
if (isErr(result2)) {
|
|
1508
|
+
logger.error(`Error when calculating score: ${result2.error}`);
|
|
1509
|
+
const code = result2.error;
|
|
1510
|
+
throw new StepExecutionError(
|
|
1511
|
+
translateErrorCode(translator, code),
|
|
1512
|
+
code,
|
|
1513
|
+
{
|
|
1514
|
+
gameId: game.id
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
return {
|
|
1519
|
+
playerId: player.id,
|
|
1520
|
+
playerName: player.name,
|
|
1521
|
+
details: {
|
|
1522
|
+
...result2.value
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
});
|
|
1526
|
+
const result = getResultUseCase2(game, scoreResult);
|
|
1527
|
+
if (isErr(result)) {
|
|
1528
|
+
logger.error(`Error getting result: ${result.error}`);
|
|
1529
|
+
const code = result.error;
|
|
1530
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1531
|
+
gameId: game.id
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
logger.info(`Result retrieved for game: ${game.id}`);
|
|
1535
|
+
return result.value;
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
// src/application/handlers/getValidPlacementsHandler.ts
|
|
1539
|
+
var getValidPlacementsHandler = (logger, translator, useCase) => {
|
|
1540
|
+
return (command) => {
|
|
1541
|
+
logger.info(
|
|
1542
|
+
`Getting valid placements for domino: ${command.domino.number}`
|
|
1543
|
+
);
|
|
1544
|
+
const placements = useCase(
|
|
1545
|
+
command.kingdom,
|
|
1546
|
+
command.domino,
|
|
1547
|
+
command.maxKingdomSize
|
|
1548
|
+
);
|
|
1549
|
+
logger.info(`Found ${placements.length} valid placements`);
|
|
1550
|
+
return placements;
|
|
1551
|
+
};
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
// src/application/handlers/placeDominoHandler.ts
|
|
1555
|
+
var placeDominoHandler = (logger, translator, useCase) => (command) => {
|
|
1556
|
+
const { game, lordId, position, rotation } = command;
|
|
1557
|
+
logger.info(
|
|
1558
|
+
`Placing domino: ${lordId} in position: ${JSON.stringify(
|
|
1559
|
+
position
|
|
1560
|
+
)} with rotation: ${rotation} in game: ${game.id}`
|
|
1561
|
+
);
|
|
1562
|
+
if (!isGameWithNextAction(game)) {
|
|
1563
|
+
logger.error(
|
|
1564
|
+
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
1565
|
+
);
|
|
1566
|
+
throw new InvalidStepError(
|
|
1567
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1568
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (game.nextAction.nextAction !== "placeDomino") {
|
|
1572
|
+
logger.error(
|
|
1573
|
+
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
1574
|
+
);
|
|
1575
|
+
throw new InvalidStepError(
|
|
1576
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1577
|
+
{
|
|
1578
|
+
expected: "placeDomino",
|
|
1579
|
+
actual: game.nextAction.nextAction,
|
|
1580
|
+
gameId: game.id
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
const result = useCase(game, lordId, position, rotation);
|
|
1585
|
+
if (isErr(result)) {
|
|
1586
|
+
logger.error(`Error placing domino: ${result.error}`);
|
|
1587
|
+
const code = result.error;
|
|
1588
|
+
throw new ActionExecutionError(
|
|
1589
|
+
translateErrorCode(translator, code),
|
|
1590
|
+
code,
|
|
1591
|
+
{
|
|
1592
|
+
gameId: game.id,
|
|
1593
|
+
lordId: command.lordId,
|
|
1594
|
+
position: command.position,
|
|
1595
|
+
rotation: command.rotation
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
if (isGameWithNextAction(result.value)) {
|
|
1600
|
+
logger.info(
|
|
1601
|
+
`Domino placed: ${lordId} in position: ${JSON.stringify(
|
|
1602
|
+
position
|
|
1603
|
+
)} with rotation: ${rotation} in game: ${game.id}`
|
|
1604
|
+
);
|
|
1605
|
+
return result.value;
|
|
1606
|
+
}
|
|
1607
|
+
logger.info(
|
|
1608
|
+
`Domino placed: ${lordId} in position: ${JSON.stringify(
|
|
1609
|
+
position
|
|
1610
|
+
)} with rotation: ${rotation} in game: ${game.id}`
|
|
1611
|
+
);
|
|
1612
|
+
return result.value;
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// src/core/useCases/serialization.ts
|
|
1616
|
+
var SERIALIZATION_VERSION = 1;
|
|
1617
|
+
var serializeGame = (game) => {
|
|
1618
|
+
const serialized = {
|
|
1619
|
+
version: SERIALIZATION_VERSION,
|
|
1620
|
+
data: game,
|
|
1621
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1622
|
+
};
|
|
1623
|
+
return JSON.stringify(serialized);
|
|
1624
|
+
};
|
|
1625
|
+
var deserializeGame = (json) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const parsed = JSON.parse(json);
|
|
1628
|
+
if (!parsed.version || !parsed.data) {
|
|
1629
|
+
return err("Invalid serialized game format");
|
|
1630
|
+
}
|
|
1631
|
+
if (parsed.version > SERIALIZATION_VERSION) {
|
|
1632
|
+
return err(
|
|
1633
|
+
`Unsupported serialization version: ${parsed.version}. Current version: ${SERIALIZATION_VERSION}`
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
const game = parsed.data;
|
|
1637
|
+
if (!game.id || !game.players || !game.lords || !game.nextAction) {
|
|
1638
|
+
return err("Invalid game state structure");
|
|
1639
|
+
}
|
|
1640
|
+
return ok(game);
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
return err(`Failed to parse game JSON: ${error.message}`);
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
var createSavePoint = (game) => {
|
|
1646
|
+
return {
|
|
1647
|
+
serialized: serializeGame(game),
|
|
1648
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1649
|
+
gameId: game.id,
|
|
1650
|
+
turn: game.turn
|
|
1651
|
+
};
|
|
1652
|
+
};
|
|
1653
|
+
var restoreFromSavePoint = (savePoint) => {
|
|
1654
|
+
return deserializeGame(savePoint.serialized);
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
// src/application/handlers/serializeGameHandler.ts
|
|
1658
|
+
var serializeGameHandler = (logger, _translator) => {
|
|
1659
|
+
return (command) => {
|
|
1660
|
+
logger.info(`Serializing game: ${command.game.id}`);
|
|
1661
|
+
const serialized = serializeGame(command.game);
|
|
1662
|
+
logger.info(`Game serialized successfully`);
|
|
1663
|
+
return serialized;
|
|
1664
|
+
};
|
|
1665
|
+
};
|
|
1666
|
+
var deserializeGameHandler = (logger, translator) => {
|
|
1667
|
+
return (command) => {
|
|
1668
|
+
logger.info(`Deserializing game from JSON`);
|
|
1669
|
+
const result = deserializeGame(command.json);
|
|
1670
|
+
if (isErr(result)) {
|
|
1671
|
+
logger.error(`Failed to deserialize game: ${result.error}`);
|
|
1672
|
+
const code = result.error;
|
|
1673
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code);
|
|
1674
|
+
}
|
|
1675
|
+
const game = unwrap(result);
|
|
1676
|
+
logger.info(`Game deserialized successfully: ${game.id}`);
|
|
1677
|
+
return game;
|
|
1678
|
+
};
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
// src/application/handlers/startGameHandler.ts
|
|
1682
|
+
var startGameHandler = (logger, translator, useCase) => (command) => {
|
|
1683
|
+
const { game } = command;
|
|
1684
|
+
logger.info(`Starting game: ${game.id}`);
|
|
1685
|
+
if (!isGameWithNextStep(game)) {
|
|
1686
|
+
logger.error(
|
|
1687
|
+
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1688
|
+
);
|
|
1689
|
+
throw new InvalidStepError(
|
|
1690
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1691
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
if (game.nextAction.step !== "start" && game.nextAction.step !== "options") {
|
|
1695
|
+
logger.error(
|
|
1696
|
+
`Required game with start step or options step but got: ${game.nextAction.step}`
|
|
1697
|
+
);
|
|
1698
|
+
throw new InvalidStepError(
|
|
1699
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1700
|
+
{
|
|
1701
|
+
expected: "start|options",
|
|
1702
|
+
actual: game.nextAction.step,
|
|
1703
|
+
gameId: game.id
|
|
1704
|
+
}
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const result = useCase(game);
|
|
1708
|
+
if (isErr(result)) {
|
|
1709
|
+
logger.error(`Error starting game: ${result.error}`);
|
|
1710
|
+
const code = result.error;
|
|
1711
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1712
|
+
gameId: game.id
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
logger.info(`Game started: ${game.id}`);
|
|
1716
|
+
return result.value;
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
// src/core/domain/types/kingdom.ts
|
|
1720
|
+
var GRIDSIZE = 9;
|
|
1721
|
+
var MAX_KINGDOM_SIZE = 5;
|
|
1722
|
+
|
|
1723
|
+
// src/core/domain/types/domino.ts
|
|
1724
|
+
var GROUND = [
|
|
1725
|
+
"castle",
|
|
1726
|
+
"wheat",
|
|
1727
|
+
"forest",
|
|
1728
|
+
"sea",
|
|
1729
|
+
"plain",
|
|
1730
|
+
"swamp",
|
|
1731
|
+
"mine"
|
|
1732
|
+
];
|
|
1733
|
+
|
|
1734
|
+
// src/core/domain/types/player.ts
|
|
1735
|
+
var playerActions = {
|
|
1736
|
+
/** Place a previously picked domino on the kingdom */
|
|
1737
|
+
placeDomino: "placeDomino",
|
|
1738
|
+
/** Pick a domino from the revealed set */
|
|
1739
|
+
pickDomino: "pickDomino",
|
|
1740
|
+
/** Pass/discard when no valid placement exists */
|
|
1741
|
+
pass: "pass"
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
// src/core/domain/entities/rule.ts
|
|
1745
|
+
var getAvailablesExtraRules = (extraRules, mode, numberOfPlayers) => {
|
|
1746
|
+
const availableExtraRules = extraRules.filter((rule) => {
|
|
1747
|
+
return rule.mode.filter((m) => m.name === mode).length > 0 && (!rule.playersLimit || rule.playersLimit === numberOfPlayers);
|
|
1748
|
+
});
|
|
1749
|
+
if (availableExtraRules.length === 0) {
|
|
1750
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1751
|
+
}
|
|
1752
|
+
return ok(availableExtraRules);
|
|
1753
|
+
};
|
|
1754
|
+
var toExtraRule = (rule, extraRules) => {
|
|
1755
|
+
const foundRule = extraRules.find((r) => r.name === rule);
|
|
1756
|
+
if (!foundRule) {
|
|
1757
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1758
|
+
}
|
|
1759
|
+
return ok(foundRule);
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
// src/utils/seededShuffle.ts
|
|
1763
|
+
var hashSeed = (str) => {
|
|
1764
|
+
let hash = 2166136261;
|
|
1765
|
+
for (let i = 0; i < str.length; i++) {
|
|
1766
|
+
hash ^= str.charCodeAt(i);
|
|
1767
|
+
hash = Math.imul(hash, 16777619);
|
|
1768
|
+
}
|
|
1769
|
+
return hash >>> 0;
|
|
1770
|
+
};
|
|
1771
|
+
var mulberry32 = (seed) => {
|
|
1772
|
+
let state = seed;
|
|
1773
|
+
return () => {
|
|
1774
|
+
state |= 0;
|
|
1775
|
+
state = state + 1831565813 | 0;
|
|
1776
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
1777
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1778
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1779
|
+
};
|
|
1780
|
+
};
|
|
1781
|
+
var createSeededShuffle = (seed, salt) => {
|
|
1782
|
+
const numericSeed = hashSeed(`${seed}|${salt}`);
|
|
1783
|
+
const random = mulberry32(numericSeed);
|
|
1784
|
+
return (array) => {
|
|
1785
|
+
const result = [...array];
|
|
1786
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
1787
|
+
const j = Math.floor(random() * (i + 1));
|
|
1788
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
1789
|
+
}
|
|
1790
|
+
return result;
|
|
1791
|
+
};
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
// src/core/useCases/addExtraRules.ts
|
|
1795
|
+
var addExtraRulesUseCase = (deps) => (game, rules) => {
|
|
1796
|
+
const { ruleRepository, dominoesRepository, shuffleMethod: shuffleMethod2 } = deps;
|
|
1797
|
+
const availableExtraRules = ruleRepository.getAllExtra();
|
|
1798
|
+
const newExtraRules = [];
|
|
1799
|
+
for (const rule of rules) {
|
|
1800
|
+
const result = toExtraRule(rule, availableExtraRules);
|
|
1801
|
+
if (isErr(result)) {
|
|
1802
|
+
return result;
|
|
1803
|
+
}
|
|
1804
|
+
newExtraRules.push(result.value);
|
|
1805
|
+
}
|
|
1806
|
+
let updatedBasic = game.rules.basic;
|
|
1807
|
+
let updatedDominoes = game.dominoes;
|
|
1808
|
+
const hasMightyDuel = newExtraRules.some((r) => r.name === "The Mighty Duel");
|
|
1809
|
+
if (hasMightyDuel) {
|
|
1810
|
+
updatedBasic = {
|
|
1811
|
+
...updatedBasic,
|
|
1812
|
+
maxDominoes: 48,
|
|
1813
|
+
maxTurns: 12,
|
|
1814
|
+
maxKingdomSize: 7
|
|
1815
|
+
};
|
|
1816
|
+
const allDominoes = dominoesRepository.getForMode(game.mode);
|
|
1817
|
+
if (allDominoes) {
|
|
1818
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "mighty-duel") : shuffleMethod2;
|
|
1819
|
+
updatedDominoes = shuffle(allDominoes);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const updatedRules = {
|
|
1823
|
+
basic: updatedBasic,
|
|
1824
|
+
extra: newExtraRules
|
|
1825
|
+
};
|
|
1826
|
+
const next = {
|
|
1827
|
+
type: "step",
|
|
1828
|
+
step: gameSteps.start
|
|
1829
|
+
};
|
|
1830
|
+
const updatedGame = {
|
|
1831
|
+
...game,
|
|
1832
|
+
dominoes: updatedDominoes,
|
|
1833
|
+
rules: updatedRules,
|
|
1834
|
+
nextAction: next
|
|
1835
|
+
};
|
|
1836
|
+
return ok(updatedGame);
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
// src/core/domain/entities/domino.ts
|
|
1840
|
+
var isGround = (ground) => {
|
|
1841
|
+
return GROUND.includes(ground);
|
|
1842
|
+
};
|
|
1843
|
+
var createTile = (ground, crowns = 0) => {
|
|
1844
|
+
if (isGround(ground)) {
|
|
1845
|
+
return { type: ground, crowns };
|
|
1846
|
+
}
|
|
1847
|
+
return { type: ground, crowns: 0 };
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
// src/core/domain/entities/kingdom.ts
|
|
1851
|
+
var createEmptyKingdom = () => {
|
|
1852
|
+
const emptyTile = createTile("empty");
|
|
1853
|
+
return Array.from(
|
|
1854
|
+
{ length: GRIDSIZE },
|
|
1855
|
+
() => Array.from({ length: GRIDSIZE }, () => emptyTile)
|
|
1856
|
+
);
|
|
1857
|
+
};
|
|
1858
|
+
var placeCastle = (kingdom) => {
|
|
1859
|
+
const castle = createTile("castle");
|
|
1860
|
+
const newKingdom = deepCopyKingdom(kingdom);
|
|
1861
|
+
newKingdom[4][4] = castle;
|
|
1862
|
+
return newKingdom;
|
|
1863
|
+
};
|
|
1864
|
+
var placeTile = (kingdom, position, tile) => {
|
|
1865
|
+
const newKingdom = deepCopyKingdom(kingdom);
|
|
1866
|
+
newKingdom[position.y][position.x] = tile;
|
|
1867
|
+
return newKingdom;
|
|
1868
|
+
};
|
|
1869
|
+
var getTile = (kingdom, position) => {
|
|
1870
|
+
const tile = kingdom[position.y]?.[position.x];
|
|
1871
|
+
if (!tile) {
|
|
1872
|
+
return err(ErrorCode.PLACEMENT_OUT_OF_BOUNDS);
|
|
1873
|
+
}
|
|
1874
|
+
return ok(tile);
|
|
1875
|
+
};
|
|
1876
|
+
var calculateDominoPosition = (position, rotation, domino) => {
|
|
1877
|
+
let firstTile;
|
|
1878
|
+
let secondTile;
|
|
1879
|
+
switch (rotation) {
|
|
1880
|
+
case 0:
|
|
1881
|
+
firstTile = {
|
|
1882
|
+
tile: domino.left,
|
|
1883
|
+
position: {
|
|
1884
|
+
x: position.x,
|
|
1885
|
+
y: position.y
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
secondTile = {
|
|
1889
|
+
tile: domino.right,
|
|
1890
|
+
position: {
|
|
1891
|
+
x: position.x + 1,
|
|
1892
|
+
y: position.y
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
break;
|
|
1896
|
+
case 90:
|
|
1897
|
+
firstTile = {
|
|
1898
|
+
tile: domino.left,
|
|
1899
|
+
position: {
|
|
1900
|
+
x: position.x,
|
|
1901
|
+
y: position.y
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
secondTile = {
|
|
1905
|
+
tile: domino.right,
|
|
1906
|
+
position: {
|
|
1907
|
+
x: position.x,
|
|
1908
|
+
y: position.y + 1
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
break;
|
|
1912
|
+
case 180:
|
|
1913
|
+
firstTile = {
|
|
1914
|
+
tile: domino.right,
|
|
1915
|
+
position: {
|
|
1916
|
+
x: position.x,
|
|
1917
|
+
y: position.y
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
secondTile = {
|
|
1921
|
+
tile: domino.left,
|
|
1922
|
+
position: {
|
|
1923
|
+
x: position.x + 1,
|
|
1924
|
+
y: position.y
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
break;
|
|
1928
|
+
case 270:
|
|
1929
|
+
firstTile = {
|
|
1930
|
+
tile: domino.right,
|
|
1931
|
+
position: {
|
|
1932
|
+
x: position.x,
|
|
1933
|
+
y: position.y
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
secondTile = {
|
|
1937
|
+
tile: domino.left,
|
|
1938
|
+
position: {
|
|
1939
|
+
x: position.x,
|
|
1940
|
+
y: position.y + 1
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
return [firstTile, secondTile];
|
|
1946
|
+
};
|
|
1947
|
+
var placeDomino = (kingdom, position, rotation, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
1948
|
+
const [firstTile, secondTile] = calculateDominoPosition(
|
|
1949
|
+
position,
|
|
1950
|
+
rotation,
|
|
1951
|
+
domino
|
|
1952
|
+
);
|
|
1953
|
+
const isFreePlaceResult = isFreePlace(
|
|
1954
|
+
kingdom,
|
|
1955
|
+
firstTile.position,
|
|
1956
|
+
secondTile.position
|
|
1957
|
+
);
|
|
1958
|
+
if (isErr(isFreePlaceResult)) {
|
|
1959
|
+
return isFreePlaceResult;
|
|
1960
|
+
}
|
|
1961
|
+
if (exceedsMaxSize(kingdom, firstTile.position, secondTile.position, maxKingdomSize)) {
|
|
1962
|
+
return err(ErrorCode.PLACEMENT_EXCEEDS_KINGDOM_SIZE);
|
|
1963
|
+
}
|
|
1964
|
+
const adjacentTiles = isAdjacent(
|
|
1965
|
+
kingdom,
|
|
1966
|
+
firstTile.position,
|
|
1967
|
+
secondTile.position
|
|
1968
|
+
);
|
|
1969
|
+
if (adjacentTiles.length === 0) {
|
|
1970
|
+
return err(ErrorCode.PLACEMENT_NOT_ADJACENT);
|
|
1971
|
+
}
|
|
1972
|
+
const hasValidAdjacentTiles = hasValidAdjacent(adjacentTiles, [
|
|
1973
|
+
firstTile.tile,
|
|
1974
|
+
secondTile.tile
|
|
1975
|
+
]);
|
|
1976
|
+
if (!hasValidAdjacentTiles) {
|
|
1977
|
+
return err(ErrorCode.PLACEMENT_INVALID_TERRAIN);
|
|
1978
|
+
}
|
|
1979
|
+
return ok(placeTiles(kingdom, [firstTile, secondTile]));
|
|
1980
|
+
};
|
|
1981
|
+
var placeTiles = (kingdom, tiles) => {
|
|
1982
|
+
let newKingdom = deepCopyKingdom(kingdom);
|
|
1983
|
+
for (const { tile, position } of tiles) {
|
|
1984
|
+
newKingdom = placeTile(newKingdom, position, tile);
|
|
1985
|
+
}
|
|
1986
|
+
return newKingdom;
|
|
1987
|
+
};
|
|
1988
|
+
var isFreePlace = (kingdom, firstPosition, secondPosition) => {
|
|
1989
|
+
const firstTile = getTile(kingdom, firstPosition);
|
|
1990
|
+
const secondTile = getTile(kingdom, secondPosition);
|
|
1991
|
+
if (isErr(firstTile)) {
|
|
1992
|
+
return firstTile;
|
|
1993
|
+
}
|
|
1994
|
+
if (isErr(secondTile)) {
|
|
1995
|
+
return secondTile;
|
|
1996
|
+
}
|
|
1997
|
+
if (firstTile.value.type !== "empty" || secondTile.value.type !== "empty") {
|
|
1998
|
+
return err(ErrorCode.PLACEMENT_NOT_EMPTY);
|
|
1999
|
+
}
|
|
2000
|
+
return ok(true);
|
|
2001
|
+
};
|
|
2002
|
+
var isAdjacent = (kingdom, firstPosition, secondPosition) => {
|
|
2003
|
+
const offsets = [
|
|
2004
|
+
{ x: -1, y: 0 },
|
|
2005
|
+
{ x: 1, y: 0 },
|
|
2006
|
+
{ x: 0, y: -1 },
|
|
2007
|
+
{ x: 0, y: 1 }
|
|
2008
|
+
];
|
|
2009
|
+
const isOwnPosition = (x, y) => x === firstPosition.x && y === firstPosition.y || x === secondPosition.x && y === secondPosition.y;
|
|
2010
|
+
const adjacentTiles = [];
|
|
2011
|
+
for (const pos of [firstPosition, secondPosition]) {
|
|
2012
|
+
for (const offset of offsets) {
|
|
2013
|
+
const nx = pos.x + offset.x;
|
|
2014
|
+
const ny = pos.y + offset.y;
|
|
2015
|
+
if (isOwnPosition(nx, ny)) continue;
|
|
2016
|
+
const tile = getTile(kingdom, { x: nx, y: ny });
|
|
2017
|
+
if (isOk(tile) && tile.value.type !== "empty") {
|
|
2018
|
+
adjacentTiles.push(tile.value);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return adjacentTiles;
|
|
2023
|
+
};
|
|
2024
|
+
var hasValidAdjacent = (adjacentTiles, tiles) => {
|
|
2025
|
+
return adjacentTiles.some((adjacentTile) => {
|
|
2026
|
+
return tiles.some((tile) => {
|
|
2027
|
+
return tile.type === adjacentTile.type || adjacentTile.type === "castle";
|
|
2028
|
+
});
|
|
2029
|
+
});
|
|
2030
|
+
};
|
|
2031
|
+
var checkCastleIsInMiddle = (kingdom) => {
|
|
2032
|
+
let castleX;
|
|
2033
|
+
let castleY;
|
|
2034
|
+
for (let y = 0; y < kingdom.length; y++) {
|
|
2035
|
+
const x = kingdom[y].findIndex((tile) => tile.type === "castle");
|
|
2036
|
+
if (x !== -1) {
|
|
2037
|
+
castleX = x;
|
|
2038
|
+
castleY = y;
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (castleX === void 0 || castleY === void 0) {
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
const bbox = getBoundingBox(kingdom);
|
|
2046
|
+
if (!bbox) return false;
|
|
2047
|
+
const centerX = (bbox.minX + bbox.maxX) / 2;
|
|
2048
|
+
const centerY = (bbox.minY + bbox.maxY) / 2;
|
|
2049
|
+
return castleX === centerX && castleY === centerY;
|
|
2050
|
+
};
|
|
2051
|
+
var getBoundingBox = (kingdom) => {
|
|
2052
|
+
let minX = Infinity;
|
|
2053
|
+
let maxX = -Infinity;
|
|
2054
|
+
let minY = Infinity;
|
|
2055
|
+
let maxY = -Infinity;
|
|
2056
|
+
for (let y = 0; y < kingdom.length; y++) {
|
|
2057
|
+
for (let x = 0; x < kingdom[y].length; x++) {
|
|
2058
|
+
if (kingdom[y][x].type !== "empty") {
|
|
2059
|
+
if (x < minX) minX = x;
|
|
2060
|
+
if (x > maxX) maxX = x;
|
|
2061
|
+
if (y < minY) minY = y;
|
|
2062
|
+
if (y > maxY) maxY = y;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
if (minX === Infinity) return null;
|
|
2067
|
+
return { minX, maxX, minY, maxY };
|
|
2068
|
+
};
|
|
2069
|
+
var exceedsMaxSize = (kingdom, firstPosition, secondPosition, maxSize = MAX_KINGDOM_SIZE) => {
|
|
2070
|
+
const bbox = getBoundingBox(kingdom);
|
|
2071
|
+
let minX = bbox?.minX ?? Infinity;
|
|
2072
|
+
let maxX = bbox?.maxX ?? -Infinity;
|
|
2073
|
+
let minY = bbox?.minY ?? Infinity;
|
|
2074
|
+
let maxY = bbox?.maxY ?? -Infinity;
|
|
2075
|
+
for (const pos of [firstPosition, secondPosition]) {
|
|
2076
|
+
if (pos.x < minX) minX = pos.x;
|
|
2077
|
+
if (pos.x > maxX) maxX = pos.x;
|
|
2078
|
+
if (pos.y < minY) minY = pos.y;
|
|
2079
|
+
if (pos.y > maxY) maxY = pos.y;
|
|
2080
|
+
}
|
|
2081
|
+
return maxX - minX + 1 > maxSize || maxY - minY + 1 > maxSize;
|
|
2082
|
+
};
|
|
2083
|
+
var countDominoes = (kingdom) => {
|
|
2084
|
+
const notEmpties = kingdom.flat().filter((tile) => tile.type !== "empty");
|
|
2085
|
+
return (notEmpties.length - 1) / 2;
|
|
2086
|
+
};
|
|
2087
|
+
var deepCopyKingdom = (kingdom) => {
|
|
2088
|
+
return kingdom.map((line) => line.map((tile) => ({ ...tile })));
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
// src/core/domain/entities/player.ts
|
|
2092
|
+
var MINPLAYERSNAMELENGTH = 3;
|
|
2093
|
+
var validatePlayer = (name) => {
|
|
2094
|
+
if (!name || name.trim().length < MINPLAYERSNAMELENGTH) {
|
|
2095
|
+
return err(ErrorCode.INVALID_PLAYER_NAME);
|
|
2096
|
+
}
|
|
2097
|
+
return ok(name);
|
|
2098
|
+
};
|
|
2099
|
+
var createPlayer = (playerName, id, kingdom, bot) => {
|
|
2100
|
+
const name = validatePlayer(playerName);
|
|
2101
|
+
if (isErr(name)) {
|
|
2102
|
+
return name;
|
|
2103
|
+
}
|
|
2104
|
+
return ok({
|
|
2105
|
+
name: name.value,
|
|
2106
|
+
id,
|
|
2107
|
+
kingdom,
|
|
2108
|
+
...bot && { bot }
|
|
2109
|
+
});
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
// src/core/useCases/addPlayers.ts
|
|
2113
|
+
var normalizePlayerInput = (input) => {
|
|
2114
|
+
if (typeof input === "string") {
|
|
2115
|
+
return { name: input };
|
|
2116
|
+
}
|
|
2117
|
+
return { name: input.name, bot: input.bot };
|
|
2118
|
+
};
|
|
2119
|
+
var addPlayersUseCase = (deps) => (game, players) => {
|
|
2120
|
+
const { uuidMethod: uuidMethod2, shuffleMethod: shuffleMethod2, ruleRepository } = deps;
|
|
2121
|
+
if (!players || players.length < 1 || players.length > 4) {
|
|
2122
|
+
return err(ErrorCode.INVALID_PLAYER_COUNT);
|
|
2123
|
+
}
|
|
2124
|
+
const newPlayers = [];
|
|
2125
|
+
for (const player of players) {
|
|
2126
|
+
const { name, bot } = normalizePlayerInput(player);
|
|
2127
|
+
let kingdom = createEmptyKingdom();
|
|
2128
|
+
kingdom = placeCastle(kingdom);
|
|
2129
|
+
let id = uuidMethod2();
|
|
2130
|
+
const newPlayer = createPlayer(name, id, kingdom, bot);
|
|
2131
|
+
if (isErr(newPlayer)) {
|
|
2132
|
+
return newPlayer;
|
|
2133
|
+
}
|
|
2134
|
+
newPlayers.push(newPlayer.value);
|
|
2135
|
+
}
|
|
2136
|
+
const rules = ruleRepository.getAll();
|
|
2137
|
+
const basicRules = rules.basic[newPlayers.length];
|
|
2138
|
+
if (!basicRules) {
|
|
2139
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
2140
|
+
}
|
|
2141
|
+
const dominoes = game.dominoes;
|
|
2142
|
+
const { maxDominoes } = basicRules;
|
|
2143
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "dominoes") : shuffleMethod2;
|
|
2144
|
+
const shuffledDominoes = shuffle(dominoes);
|
|
2145
|
+
const splicedDominoes = shuffledDominoes.slice(0, maxDominoes);
|
|
2146
|
+
const next = {
|
|
2147
|
+
type: "step",
|
|
2148
|
+
step: "options"
|
|
2149
|
+
};
|
|
2150
|
+
const updatedGame = {
|
|
2151
|
+
...game,
|
|
2152
|
+
players: newPlayers,
|
|
2153
|
+
dominoes: splicedDominoes,
|
|
2154
|
+
rules: {
|
|
2155
|
+
...game.rules,
|
|
2156
|
+
basic: basicRules
|
|
2157
|
+
},
|
|
2158
|
+
nextAction: next
|
|
2159
|
+
};
|
|
2160
|
+
return ok(updatedGame);
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
// src/core/useCases/calculateScore.ts
|
|
2164
|
+
var calculateScoreUseCase = (kingdom) => {
|
|
2165
|
+
const rows = kingdom.length;
|
|
2166
|
+
const cols = kingdom[0].length;
|
|
2167
|
+
const visited = kingdom.map((row) => row.map(() => false));
|
|
2168
|
+
const properties = [];
|
|
2169
|
+
const directions = [
|
|
2170
|
+
[-1, 0],
|
|
2171
|
+
[1, 0],
|
|
2172
|
+
[0, -1],
|
|
2173
|
+
[0, 1]
|
|
2174
|
+
];
|
|
2175
|
+
for (let y = 0; y < rows; y++) {
|
|
2176
|
+
for (let x = 0; x < cols; x++) {
|
|
2177
|
+
const tile = kingdom[y][x];
|
|
2178
|
+
if (visited[y][x] || tile.type === "empty" || tile.type === "castle") {
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
const type = tile.type;
|
|
2182
|
+
let size = 0;
|
|
2183
|
+
let crowns = 0;
|
|
2184
|
+
const queue = [[x, y]];
|
|
2185
|
+
visited[y][x] = true;
|
|
2186
|
+
while (queue.length > 0) {
|
|
2187
|
+
const [cx, cy] = queue.pop();
|
|
2188
|
+
const current = kingdom[cy][cx];
|
|
2189
|
+
size++;
|
|
2190
|
+
crowns += current.crowns;
|
|
2191
|
+
for (const [dx, dy] of directions) {
|
|
2192
|
+
const nx = cx + dx;
|
|
2193
|
+
const ny = cy + dy;
|
|
2194
|
+
if (nx >= 0 && ny >= 0 && nx < cols && ny < rows && !visited[ny][nx] && kingdom[ny][nx].type === type) {
|
|
2195
|
+
visited[ny][nx] = true;
|
|
2196
|
+
queue.push([nx, ny]);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
properties.push({ size, crowns });
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (properties.length === 0) {
|
|
2204
|
+
return ok({
|
|
2205
|
+
points: 0,
|
|
2206
|
+
maxPropertiesSize: 0,
|
|
2207
|
+
totalCrowns: 0
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
let points = 0;
|
|
2211
|
+
let maxPropertiesSize = 0;
|
|
2212
|
+
let totalCrowns = 0;
|
|
2213
|
+
for (const property of properties) {
|
|
2214
|
+
points += property.size * property.crowns;
|
|
2215
|
+
if (property.size > maxPropertiesSize) {
|
|
2216
|
+
maxPropertiesSize = property.size;
|
|
2217
|
+
}
|
|
2218
|
+
totalCrowns += property.crowns;
|
|
2219
|
+
}
|
|
2220
|
+
return ok({
|
|
2221
|
+
points,
|
|
2222
|
+
maxPropertiesSize,
|
|
2223
|
+
totalCrowns
|
|
2224
|
+
});
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
// src/core/domain/entities/lord.ts
|
|
2228
|
+
var createLord = (id, playerId) => {
|
|
2229
|
+
return {
|
|
2230
|
+
id,
|
|
2231
|
+
playerId,
|
|
2232
|
+
turnEnded: false,
|
|
2233
|
+
hasPick: false,
|
|
2234
|
+
hasPlace: true
|
|
2235
|
+
};
|
|
2236
|
+
};
|
|
2237
|
+
var hasPick = (lord) => lord.hasPick;
|
|
2238
|
+
var hasPlace = (lord) => lord.hasPlace;
|
|
2239
|
+
var hasTurnEnded = (lord) => lord.turnEnded;
|
|
2240
|
+
var canPass = (lord) => {
|
|
2241
|
+
return !hasPick(lord) && !hasPlace(lord) && !hasTurnEnded(lord);
|
|
2242
|
+
};
|
|
2243
|
+
var canPick = (lord) => {
|
|
2244
|
+
return !hasPick(lord) && hasPlace(lord) && !hasTurnEnded(lord);
|
|
2245
|
+
};
|
|
2246
|
+
var canPlaceAndDominoPickedIsDefined = (lord) => {
|
|
2247
|
+
return !hasPick(lord) && !hasPlace(lord) && !hasTurnEnded(lord) && lord.dominoPicked !== void 0;
|
|
2248
|
+
};
|
|
2249
|
+
var updateLordOrder = (lords) => {
|
|
2250
|
+
return [...lords].sort((a, b) => {
|
|
2251
|
+
return (a.dominoPicked?.number ?? 0) - (b.dominoPicked?.number ?? 0);
|
|
2252
|
+
});
|
|
2253
|
+
};
|
|
2254
|
+
var resetLordsActions = (lords) => {
|
|
2255
|
+
return lords.map((lord) => ({
|
|
2256
|
+
...lord,
|
|
2257
|
+
turnEnded: false,
|
|
2258
|
+
hasPick: false,
|
|
2259
|
+
hasPlace: false
|
|
2260
|
+
}));
|
|
2261
|
+
};
|
|
2262
|
+
var nextLordWithAction = (lords) => {
|
|
2263
|
+
const nextLord = findNextLord(lords);
|
|
2264
|
+
return {
|
|
2265
|
+
type: "action",
|
|
2266
|
+
nextLord: nextLord.id,
|
|
2267
|
+
nextAction: hasPlace(nextLord) ? playerActions.pickDomino : playerActions.placeDomino
|
|
2268
|
+
};
|
|
2269
|
+
};
|
|
2270
|
+
var findNextLord = (lords) => {
|
|
2271
|
+
const lord = lords.find((lord2) => !hasTurnEnded(lord2));
|
|
2272
|
+
if (!lord) {
|
|
2273
|
+
return lords[0];
|
|
2274
|
+
}
|
|
2275
|
+
return lord;
|
|
2276
|
+
};
|
|
2277
|
+
var allLordsHavePlayed = (lords) => {
|
|
2278
|
+
return lords.every((lord) => lord.turnEnded);
|
|
2279
|
+
};
|
|
2280
|
+
|
|
2281
|
+
// src/core/useCases/chooseDomino.ts
|
|
2282
|
+
var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
2283
|
+
const currentLord = game.lords.find(
|
|
2284
|
+
(lord) => lord.id === game.nextAction.nextLord
|
|
2285
|
+
);
|
|
2286
|
+
if (!currentLord) {
|
|
2287
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
2288
|
+
}
|
|
2289
|
+
if (currentLord.id !== lordId) {
|
|
2290
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
2291
|
+
}
|
|
2292
|
+
if (!canPick(currentLord)) {
|
|
2293
|
+
return err(ErrorCode.CANNOT_PICK);
|
|
2294
|
+
}
|
|
2295
|
+
const selectedDomino = game.currentDominoes.find(
|
|
2296
|
+
(domino) => domino.domino.number === dominoPick
|
|
2297
|
+
);
|
|
2298
|
+
if (!selectedDomino) {
|
|
2299
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
2300
|
+
}
|
|
2301
|
+
if (selectedDomino.picked) {
|
|
2302
|
+
return err(ErrorCode.DOMINO_ALREADY_PICKED);
|
|
2303
|
+
}
|
|
2304
|
+
const updatedCurrentDominoes = game.currentDominoes.map(
|
|
2305
|
+
(domino) => domino.domino.number === dominoPick ? { ...domino, picked: true, lordId: currentLord.id } : domino
|
|
2306
|
+
);
|
|
2307
|
+
const updatedLords = game.lords.map(
|
|
2308
|
+
(lord) => lord.id === currentLord.id ? { ...lord, hasPick: true, turnEnded: true, dominoPicked: selectedDomino.domino } : lord
|
|
2309
|
+
);
|
|
2310
|
+
const turnEnded = allLordsHavePlayed(updatedLords);
|
|
2311
|
+
if (turnEnded) {
|
|
2312
|
+
const lords = resetLordsActions(updateLordOrder(updatedLords));
|
|
2313
|
+
const updatedTurn = game.turn + 1;
|
|
2314
|
+
const next2 = nextLordWithAction(lords);
|
|
2315
|
+
const dominoesCopy = [...game.dominoes];
|
|
2316
|
+
const dominoesDrawn = dominoesCopy.splice(
|
|
2317
|
+
0,
|
|
2318
|
+
game.rules.basic.dominoesPerTurn
|
|
2319
|
+
);
|
|
2320
|
+
dominoesDrawn.sort((a, b) => a.number - b.number);
|
|
2321
|
+
const updatedCurrentDominoes2 = dominoesDrawn.map((domino, index) => {
|
|
2322
|
+
return {
|
|
2323
|
+
domino,
|
|
2324
|
+
picked: false,
|
|
2325
|
+
lordId: null,
|
|
2326
|
+
position: index + 1
|
|
2327
|
+
};
|
|
2328
|
+
});
|
|
2329
|
+
return ok({
|
|
2330
|
+
...game,
|
|
2331
|
+
currentDominoes: updatedCurrentDominoes2,
|
|
2332
|
+
dominoes: dominoesCopy,
|
|
2333
|
+
lords,
|
|
2334
|
+
turn: updatedTurn,
|
|
2335
|
+
nextAction: next2
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
const next = nextLordWithAction(updatedLords);
|
|
2339
|
+
const updatedState = {
|
|
2340
|
+
...game,
|
|
2341
|
+
currentDominoes: updatedCurrentDominoes,
|
|
2342
|
+
lords: updatedLords,
|
|
2343
|
+
nextAction: next
|
|
2344
|
+
};
|
|
2345
|
+
return ok(updatedState);
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
// src/core/domain/entities/game.ts
|
|
2349
|
+
var create = (payload) => {
|
|
2350
|
+
const { id, mode, dominoes, seed } = payload;
|
|
2351
|
+
return {
|
|
2352
|
+
id,
|
|
2353
|
+
dominoes,
|
|
2354
|
+
currentDominoes: [],
|
|
2355
|
+
players: [],
|
|
2356
|
+
lords: [],
|
|
2357
|
+
turn: 0,
|
|
2358
|
+
nextAction: {
|
|
2359
|
+
type: "step",
|
|
2360
|
+
step: gameSteps.addPlayers
|
|
2361
|
+
},
|
|
2362
|
+
rules: {
|
|
2363
|
+
basic: {
|
|
2364
|
+
lords: 0,
|
|
2365
|
+
maxDominoes: 0,
|
|
2366
|
+
dominoesPerTurn: 0,
|
|
2367
|
+
maxTurns: 0,
|
|
2368
|
+
maxKingdomSize: 5
|
|
2369
|
+
},
|
|
2370
|
+
extra: []
|
|
2371
|
+
},
|
|
2372
|
+
mode,
|
|
2373
|
+
...seed !== void 0 && { seed }
|
|
2374
|
+
};
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
// src/core/domain/entities/mode.ts
|
|
2378
|
+
var findMode = (mode, availableMode) => {
|
|
2379
|
+
return availableMode.find((m) => m.name === mode);
|
|
2380
|
+
};
|
|
2381
|
+
var createMode = (mode, availableMode) => {
|
|
2382
|
+
const foundMode = findMode(mode, availableMode);
|
|
2383
|
+
if (foundMode) {
|
|
2384
|
+
return ok(foundMode);
|
|
2385
|
+
}
|
|
2386
|
+
return err(ErrorCode.MODE_NOT_FOUND);
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
// src/core/useCases/createGame.ts
|
|
2390
|
+
var createGameUseCase = (deps) => (mode, seed) => {
|
|
2391
|
+
const { modeRepository, dominoesRepository, uuidMethod: uuidMethod2 } = deps;
|
|
2392
|
+
const id = uuidMethod2();
|
|
2393
|
+
const gameSeed = seed ?? uuidMethod2();
|
|
2394
|
+
const availableMode = modeRepository.getAvailables();
|
|
2395
|
+
const newMode = createMode(mode, availableMode);
|
|
2396
|
+
if (isErr(newMode)) {
|
|
2397
|
+
return newMode;
|
|
2398
|
+
}
|
|
2399
|
+
const dominoes = dominoesRepository.getForMode(newMode.value);
|
|
2400
|
+
if (!dominoes || dominoes.length === 0) {
|
|
2401
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
2402
|
+
}
|
|
2403
|
+
const payload = {
|
|
2404
|
+
id,
|
|
2405
|
+
mode: newMode.value,
|
|
2406
|
+
dominoes,
|
|
2407
|
+
seed: gameSeed
|
|
2408
|
+
};
|
|
2409
|
+
const newGame = create(payload);
|
|
2410
|
+
return ok(newGame);
|
|
2411
|
+
};
|
|
2412
|
+
|
|
2413
|
+
// src/core/useCases/getValidPlacements.ts
|
|
2414
|
+
var getValidPlacementsUseCase = (kingdom, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
2415
|
+
const validPlacements = [];
|
|
2416
|
+
const rotations = [0, 90, 180, 270];
|
|
2417
|
+
for (let y = 0; y < GRIDSIZE; y++) {
|
|
2418
|
+
for (let x = 0; x < GRIDSIZE; x++) {
|
|
2419
|
+
const position = { x, y };
|
|
2420
|
+
for (const rotation of rotations) {
|
|
2421
|
+
const result = placeDomino(kingdom, position, rotation, domino, maxKingdomSize);
|
|
2422
|
+
if (isOk(result)) {
|
|
2423
|
+
validPlacements.push({ position, rotation });
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return validPlacements;
|
|
2429
|
+
};
|
|
2430
|
+
var canPlaceDominoUseCase = (kingdom, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
2431
|
+
const rotations = [0, 90, 180, 270];
|
|
2432
|
+
for (let y = 0; y < GRIDSIZE; y++) {
|
|
2433
|
+
for (let x = 0; x < GRIDSIZE; x++) {
|
|
2434
|
+
const position = { x, y };
|
|
2435
|
+
for (const rotation of rotations) {
|
|
2436
|
+
const result = placeDomino(kingdom, position, rotation, domino, maxKingdomSize);
|
|
2437
|
+
if (isOk(result)) {
|
|
2438
|
+
return true;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return false;
|
|
2444
|
+
};
|
|
2445
|
+
|
|
2446
|
+
// src/core/useCases/discardDomino.ts
|
|
2447
|
+
var discardDominoUseCase = (game, lordId) => {
|
|
2448
|
+
const nextAction = game.nextAction;
|
|
2449
|
+
const currentLord = game.lords.find(
|
|
2450
|
+
(lord) => lord.id === nextAction.nextLord
|
|
2451
|
+
);
|
|
2452
|
+
if (!currentLord) {
|
|
2453
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
2454
|
+
}
|
|
2455
|
+
if (currentLord.id !== lordId) {
|
|
2456
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
2457
|
+
}
|
|
2458
|
+
if (!canPass(currentLord)) {
|
|
2459
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
2460
|
+
}
|
|
2461
|
+
if (currentLord.dominoPicked) {
|
|
2462
|
+
const currentPlayer = game.players.find(
|
|
2463
|
+
(player) => player.id === currentLord.playerId
|
|
2464
|
+
);
|
|
2465
|
+
if (currentPlayer && canPlaceDominoUseCase(currentPlayer.kingdom, currentLord.dominoPicked, game.rules.basic.maxKingdomSize)) {
|
|
2466
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
const maxTurns = game.rules.basic.maxTurns;
|
|
2470
|
+
const currentTurn = game.turn;
|
|
2471
|
+
const isLastTurn = currentTurn === maxTurns;
|
|
2472
|
+
const updatedLord = game.lords.map((lord) => {
|
|
2473
|
+
if (lord.id === currentLord.id) {
|
|
2474
|
+
if (isLastTurn) {
|
|
2475
|
+
return {
|
|
2476
|
+
...lord,
|
|
2477
|
+
turnEnded: true,
|
|
2478
|
+
hasPlace: true
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
...lord,
|
|
2483
|
+
hasPlace: true
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
return lord;
|
|
2487
|
+
});
|
|
2488
|
+
let updatedGame;
|
|
2489
|
+
const resultStep = {
|
|
2490
|
+
type: "step",
|
|
2491
|
+
step: gameSteps.result
|
|
2492
|
+
};
|
|
2493
|
+
if (isLastTurn && allLordsHavePlayed(updatedLord)) {
|
|
2494
|
+
updatedGame = {
|
|
2495
|
+
...game,
|
|
2496
|
+
lords: updatedLord,
|
|
2497
|
+
nextAction: resultStep
|
|
2498
|
+
};
|
|
2499
|
+
} else {
|
|
2500
|
+
updatedGame = {
|
|
2501
|
+
...game,
|
|
2502
|
+
lords: updatedLord,
|
|
2503
|
+
nextAction: nextLordWithAction(updatedLord)
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
return ok(updatedGame);
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
// src/core/useCases/getDynastyResult.ts
|
|
2510
|
+
var getDynastyResultUseCase = (games) => {
|
|
2511
|
+
const playerMap = /* @__PURE__ */ new Map();
|
|
2512
|
+
for (const game of games) {
|
|
2513
|
+
for (const result of game.result) {
|
|
2514
|
+
const existing = playerMap.get(result.playerId);
|
|
2515
|
+
if (existing) {
|
|
2516
|
+
existing.points.push(result.details.points);
|
|
2517
|
+
} else {
|
|
2518
|
+
playerMap.set(result.playerId, {
|
|
2519
|
+
name: result.playerName,
|
|
2520
|
+
points: [result.details.points]
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
const results = [];
|
|
2526
|
+
for (const [playerId, data] of playerMap) {
|
|
2527
|
+
results.push({
|
|
2528
|
+
playerId,
|
|
2529
|
+
playerName: data.name,
|
|
2530
|
+
totalPoints: data.points.reduce((sum, p) => sum + p, 0),
|
|
2531
|
+
gamesPoints: data.points
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
results.sort((a, b) => b.totalPoints - a.totalPoints);
|
|
2535
|
+
const finalResults = [];
|
|
2536
|
+
for (let i = 0; i < results.length; i++) {
|
|
2537
|
+
const current = results[i];
|
|
2538
|
+
const previous = i > 0 ? finalResults[i - 1] : null;
|
|
2539
|
+
const position = previous && current.totalPoints === previous.totalPoints ? previous.position : i + 1;
|
|
2540
|
+
finalResults.push({ ...current, position });
|
|
2541
|
+
}
|
|
2542
|
+
return finalResults;
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
// src/core/useCases/getExtraRules.ts
|
|
2546
|
+
var getExtraRulesUseCase = (deps) => (mode, players) => {
|
|
2547
|
+
const { ruleRepository } = deps;
|
|
2548
|
+
const allExtraRules = ruleRepository.getAllExtra();
|
|
2549
|
+
if (!allExtraRules) {
|
|
2550
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
2551
|
+
}
|
|
2552
|
+
return getAvailablesExtraRules(allExtraRules, mode, players);
|
|
2553
|
+
};
|
|
2554
|
+
|
|
2555
|
+
// src/core/useCases/getModes.ts
|
|
2556
|
+
var getModesUseCase = (deps) => () => {
|
|
2557
|
+
const { modeRepository } = deps;
|
|
2558
|
+
return ok(modeRepository.getAvailables());
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
// src/core/useCases/getResult.ts
|
|
2562
|
+
var getResultUseCase = (game, scoreResult) => {
|
|
2563
|
+
const { rules } = game;
|
|
2564
|
+
const { basic, extra } = rules;
|
|
2565
|
+
const finalScoreResult = scoreResult.map((score) => {
|
|
2566
|
+
let finalScore = score.details.points;
|
|
2567
|
+
const player = game.players.find((p) => p.id === score.playerId);
|
|
2568
|
+
if (!player) {
|
|
2569
|
+
return score;
|
|
2570
|
+
}
|
|
2571
|
+
const { kingdom } = player;
|
|
2572
|
+
if (extra.length > 0) {
|
|
2573
|
+
extra.forEach((extraRule) => {
|
|
2574
|
+
if (extraRule.name === "The middle Kingdom") {
|
|
2575
|
+
const castleIsInMiddle = checkCastleIsInMiddle(kingdom);
|
|
2576
|
+
if (castleIsInMiddle) {
|
|
2577
|
+
finalScore += 10;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
if (extraRule.name === "Harmony") {
|
|
2581
|
+
const totalDominoes = countDominoes(kingdom);
|
|
2582
|
+
const dominoLimit = basic.maxDominoes;
|
|
2583
|
+
const totalPlayers = game.players.length;
|
|
2584
|
+
const dominoesPerPlayer = dominoLimit / totalPlayers;
|
|
2585
|
+
if (totalDominoes === dominoesPerPlayer) {
|
|
2586
|
+
finalScore += 5;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
return {
|
|
2592
|
+
...score,
|
|
2593
|
+
details: {
|
|
2594
|
+
...score.details,
|
|
2595
|
+
points: finalScore
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
});
|
|
2599
|
+
finalScoreResult.sort((a, b) => {
|
|
2600
|
+
if (a.details.points !== b.details.points) {
|
|
2601
|
+
return b.details.points - a.details.points;
|
|
2602
|
+
}
|
|
2603
|
+
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
2604
|
+
});
|
|
2605
|
+
const result = [];
|
|
2606
|
+
let isTie = false;
|
|
2607
|
+
result.push({
|
|
2608
|
+
playerId: finalScoreResult[0].playerId,
|
|
2609
|
+
playerName: finalScoreResult[0].playerName,
|
|
2610
|
+
details: finalScoreResult[0].details,
|
|
2611
|
+
position: 1
|
|
2612
|
+
});
|
|
2613
|
+
for (let i = 1; i < finalScoreResult.length; i++) {
|
|
2614
|
+
const currentScore = finalScoreResult[i];
|
|
2615
|
+
const previousScore = finalScoreResult[i - 1];
|
|
2616
|
+
if (previousScore && currentScore && currentScore.details.points === previousScore.details.points && currentScore.details.maxPropertiesSize === previousScore.details.maxPropertiesSize) {
|
|
2617
|
+
isTie = true;
|
|
2618
|
+
}
|
|
2619
|
+
if (isTie) {
|
|
2620
|
+
result.push({
|
|
2621
|
+
...currentScore,
|
|
2622
|
+
position: result[i - 1].position
|
|
2623
|
+
});
|
|
2624
|
+
} else {
|
|
2625
|
+
result.push({
|
|
2626
|
+
...currentScore,
|
|
2627
|
+
position: i + 1
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
isTie = false;
|
|
2631
|
+
}
|
|
2632
|
+
return ok({
|
|
2633
|
+
...game,
|
|
2634
|
+
result
|
|
2635
|
+
});
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
// src/core/useCases/placeDomino.ts
|
|
2639
|
+
var placeDominoUseCase = (game, lordId, position, rotation) => {
|
|
2640
|
+
const nextAction = game.nextAction;
|
|
2641
|
+
const currentLord = game.lords.find(
|
|
2642
|
+
(lord) => lord.id === nextAction.nextLord
|
|
2643
|
+
);
|
|
2644
|
+
if (!currentLord) {
|
|
2645
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
2646
|
+
}
|
|
2647
|
+
if (currentLord.id !== lordId) {
|
|
2648
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
2649
|
+
}
|
|
2650
|
+
if (!canPlaceAndDominoPickedIsDefined(currentLord)) {
|
|
2651
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
2652
|
+
}
|
|
2653
|
+
const domino = currentLord.dominoPicked;
|
|
2654
|
+
const currentPlayer = game.players.find(
|
|
2655
|
+
(player) => player.id === currentLord.playerId
|
|
2656
|
+
);
|
|
2657
|
+
if (!currentPlayer) {
|
|
2658
|
+
return err(ErrorCode.PLAYER_NOT_FOUND);
|
|
2659
|
+
}
|
|
2660
|
+
const updatedKingdom = placeDomino(
|
|
2661
|
+
currentPlayer.kingdom,
|
|
2662
|
+
position,
|
|
2663
|
+
rotation,
|
|
2664
|
+
domino,
|
|
2665
|
+
game.rules.basic.maxKingdomSize
|
|
2666
|
+
);
|
|
2667
|
+
if (isErr(updatedKingdom)) {
|
|
2668
|
+
return updatedKingdom;
|
|
2669
|
+
}
|
|
2670
|
+
const updatedPlayers = game.players.map((player) => {
|
|
2671
|
+
if (player.id === currentPlayer.id) {
|
|
2672
|
+
player.kingdom = updatedKingdom.value;
|
|
2673
|
+
}
|
|
2674
|
+
return player;
|
|
2675
|
+
});
|
|
2676
|
+
const maxTurns = game.rules.basic.maxTurns;
|
|
2677
|
+
const currentTurn = game.turn;
|
|
2678
|
+
const isLastTurn = currentTurn === maxTurns;
|
|
2679
|
+
const updatedLords = game.lords.map((lord) => {
|
|
2680
|
+
if (lord.id === currentLord.id) {
|
|
2681
|
+
if (isLastTurn) {
|
|
2682
|
+
return {
|
|
2683
|
+
...lord,
|
|
2684
|
+
turnEnded: true,
|
|
2685
|
+
hasPlace: true
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2689
|
+
...lord,
|
|
2690
|
+
hasPlace: true
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
return lord;
|
|
2694
|
+
});
|
|
2695
|
+
let updatedGame;
|
|
2696
|
+
const resultStep = {
|
|
2697
|
+
type: "step",
|
|
2698
|
+
step: gameSteps.result
|
|
2699
|
+
};
|
|
2700
|
+
if (isLastTurn && allLordsHavePlayed(updatedLords)) {
|
|
2701
|
+
updatedGame = {
|
|
2702
|
+
...game,
|
|
2703
|
+
lords: updatedLords,
|
|
2704
|
+
nextAction: resultStep,
|
|
2705
|
+
players: updatedPlayers
|
|
2706
|
+
};
|
|
2707
|
+
} else {
|
|
2708
|
+
updatedGame = {
|
|
2709
|
+
...game,
|
|
2710
|
+
lords: updatedLords,
|
|
2711
|
+
nextAction: nextLordWithAction(updatedLords),
|
|
2712
|
+
players: updatedPlayers
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
return ok(updatedGame);
|
|
2716
|
+
};
|
|
2717
|
+
|
|
2718
|
+
// src/core/useCases/startGame.ts
|
|
2719
|
+
var startGameUseCase = (deps) => (game) => {
|
|
2720
|
+
const { uuidMethod: uuidMethod2, shuffleMethod: shuffleMethod2 } = deps;
|
|
2721
|
+
const {
|
|
2722
|
+
rules: {
|
|
2723
|
+
basic: { lords: maxLords, dominoesPerTurn }
|
|
2724
|
+
},
|
|
2725
|
+
players,
|
|
2726
|
+
dominoes
|
|
2727
|
+
} = game;
|
|
2728
|
+
const numberOfPlayers = players.length;
|
|
2729
|
+
const newLords = [];
|
|
2730
|
+
for (let i = 0; i < numberOfPlayers; i++) {
|
|
2731
|
+
for (let j = 0; j < maxLords; j++) {
|
|
2732
|
+
const lord = createLord(uuidMethod2(), players[i].id);
|
|
2733
|
+
newLords.push(lord);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "lords") : shuffleMethod2;
|
|
2737
|
+
const shuffledLords = shuffle(newLords);
|
|
2738
|
+
const dominoesCopy = [...dominoes];
|
|
2739
|
+
const dominoesDrawn = dominoesCopy.splice(0, dominoesPerTurn);
|
|
2740
|
+
dominoesDrawn.sort((a, b) => a.number - b.number);
|
|
2741
|
+
const nextAction = {
|
|
2742
|
+
type: "action",
|
|
2743
|
+
nextLord: shuffledLords[0].id,
|
|
2744
|
+
nextAction: playerActions.pickDomino
|
|
2745
|
+
};
|
|
2746
|
+
const newState = {
|
|
2747
|
+
...game,
|
|
2748
|
+
lords: shuffledLords,
|
|
2749
|
+
nextAction,
|
|
2750
|
+
dominoes: dominoesCopy,
|
|
2751
|
+
currentDominoes: dominoesDrawn.map((domino, index) => {
|
|
2752
|
+
return {
|
|
2753
|
+
domino,
|
|
2754
|
+
picked: false,
|
|
2755
|
+
lordId: null,
|
|
2756
|
+
position: index + 1
|
|
2757
|
+
};
|
|
2758
|
+
})
|
|
2759
|
+
};
|
|
2760
|
+
return ok(newState);
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
// src/config.ts
|
|
2764
|
+
var configureEngine = (config) => {
|
|
2765
|
+
const modeRepository = jsonModes_default();
|
|
2766
|
+
const dominoesRepository = jsonDominoes_default();
|
|
2767
|
+
const ruleRepository = jsonRules_default();
|
|
2768
|
+
const uuid = config.uuidMethod || uuidMethod;
|
|
2769
|
+
const shuffle = config.shuffleMethod || shuffleMethod;
|
|
2770
|
+
const translator = config.translator || defaultTranslator;
|
|
2771
|
+
const logger = config.logger ? config.logger : config.logging ? consoleLogger(true) : silentLogger;
|
|
2772
|
+
return {
|
|
2773
|
+
createGameHandler: createGameHandler(
|
|
2774
|
+
logger,
|
|
2775
|
+
translator,
|
|
2776
|
+
createGameUseCase({
|
|
2777
|
+
modeRepository,
|
|
2778
|
+
dominoesRepository,
|
|
2779
|
+
uuidMethod: uuid
|
|
2780
|
+
})
|
|
2781
|
+
),
|
|
2782
|
+
getModesHandler: getModesHandler(
|
|
2783
|
+
logger,
|
|
2784
|
+
translator,
|
|
2785
|
+
getModesUseCase({ modeRepository })
|
|
2786
|
+
),
|
|
2787
|
+
getExtraRulesHandler: getExtraRulesHandler(
|
|
2788
|
+
logger,
|
|
2789
|
+
translator,
|
|
2790
|
+
getExtraRulesUseCase({ ruleRepository })
|
|
2791
|
+
),
|
|
2792
|
+
addPlayersHandler: addPlayersHandler(
|
|
2793
|
+
logger,
|
|
2794
|
+
translator,
|
|
2795
|
+
addPlayersUseCase({
|
|
2796
|
+
uuidMethod: uuid,
|
|
2797
|
+
shuffleMethod: shuffle,
|
|
2798
|
+
ruleRepository
|
|
2799
|
+
})
|
|
2800
|
+
),
|
|
2801
|
+
addExtraRulesHandler: addExtraRulesHandler(
|
|
2802
|
+
logger,
|
|
2803
|
+
translator,
|
|
2804
|
+
addExtraRulesUseCase({
|
|
2805
|
+
ruleRepository,
|
|
2806
|
+
dominoesRepository,
|
|
2807
|
+
shuffleMethod: shuffle
|
|
2808
|
+
})
|
|
2809
|
+
),
|
|
2810
|
+
startGameHandler: startGameHandler(
|
|
2811
|
+
logger,
|
|
2812
|
+
translator,
|
|
2813
|
+
startGameUseCase({ uuidMethod: uuid, shuffleMethod: shuffle })
|
|
2814
|
+
),
|
|
2815
|
+
chooseDominoHandler: chooseDominoHandler(
|
|
2816
|
+
logger,
|
|
2817
|
+
translator,
|
|
2818
|
+
chooseDominoUseCase
|
|
2819
|
+
),
|
|
2820
|
+
placeDominoHandler: placeDominoHandler(
|
|
2821
|
+
logger,
|
|
2822
|
+
translator,
|
|
2823
|
+
placeDominoUseCase
|
|
2824
|
+
),
|
|
2825
|
+
discardDominoHandler: discardDominoHandler(
|
|
2826
|
+
logger,
|
|
2827
|
+
translator,
|
|
2828
|
+
discardDominoUseCase
|
|
2829
|
+
),
|
|
2830
|
+
getResultHandler: getResultHandler(logger, translator, {
|
|
2831
|
+
getResultUseCase,
|
|
2832
|
+
calculateScoreUseCase
|
|
2833
|
+
}),
|
|
2834
|
+
calculateScoreHandler: calculateScoreHandler(
|
|
2835
|
+
logger,
|
|
2836
|
+
translator,
|
|
2837
|
+
calculateScoreUseCase
|
|
2838
|
+
),
|
|
2839
|
+
getValidPlacementsHandler: getValidPlacementsHandler(
|
|
2840
|
+
logger,
|
|
2841
|
+
translator,
|
|
2842
|
+
getValidPlacementsUseCase
|
|
2843
|
+
),
|
|
2844
|
+
canPlaceDominoHandler: canPlaceDominoHandler(
|
|
2845
|
+
logger,
|
|
2846
|
+
translator,
|
|
2847
|
+
canPlaceDominoUseCase
|
|
2848
|
+
),
|
|
2849
|
+
serializeGameHandler: serializeGameHandler(logger, translator),
|
|
2850
|
+
deserializeGameHandler: deserializeGameHandler(logger, translator),
|
|
2851
|
+
getDynastyResultHandler: getDynastyResultHandler(
|
|
2852
|
+
logger,
|
|
2853
|
+
translator,
|
|
2854
|
+
getDynastyResultUseCase
|
|
2855
|
+
)
|
|
2856
|
+
};
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
// src/core/domain/entities/validateGameState.ts
|
|
2860
|
+
var issue = (path, code, message, severity = "error") => ({ path, code, message, severity });
|
|
2861
|
+
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
2862
|
+
var isNonNegativeInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
2863
|
+
var isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
2864
|
+
var validGrounds = /* @__PURE__ */ new Set([...GROUND, "empty"]);
|
|
2865
|
+
var validPlayerActions = new Set(Object.values(playerActions));
|
|
2866
|
+
var validGameSteps = new Set(Object.values(gameSteps));
|
|
2867
|
+
var validateGameStructure = (game) => {
|
|
2868
|
+
const issues = [];
|
|
2869
|
+
if (!isNonEmptyString(game.id)) {
|
|
2870
|
+
issues.push(issue("id", "INVALID_ID", "Game id must be a non-empty string"));
|
|
2871
|
+
}
|
|
2872
|
+
if (!isNonNegativeInteger(game.turn)) {
|
|
2873
|
+
issues.push(issue("turn", "INVALID_TURN", "Turn must be a non-negative integer"));
|
|
2874
|
+
}
|
|
2875
|
+
if (!Array.isArray(game.dominoes)) {
|
|
2876
|
+
issues.push(issue("dominoes", "INVALID_DOMINOES", "Dominoes must be an array"));
|
|
2877
|
+
}
|
|
2878
|
+
if (!Array.isArray(game.currentDominoes)) {
|
|
2879
|
+
issues.push(issue("currentDominoes", "INVALID_CURRENT_DOMINOES", "CurrentDominoes must be an array"));
|
|
2880
|
+
}
|
|
2881
|
+
if (!Array.isArray(game.players)) {
|
|
2882
|
+
issues.push(issue("players", "INVALID_PLAYERS", "Players must be an array"));
|
|
2883
|
+
}
|
|
2884
|
+
if (!Array.isArray(game.lords)) {
|
|
2885
|
+
issues.push(issue("lords", "INVALID_LORDS", "Lords must be an array"));
|
|
2886
|
+
}
|
|
2887
|
+
if (!game.rules || typeof game.rules !== "object") {
|
|
2888
|
+
issues.push(issue("rules", "INVALID_RULES", "Rules must be an object"));
|
|
2889
|
+
} else {
|
|
2890
|
+
if (!game.rules.basic || typeof game.rules.basic !== "object") {
|
|
2891
|
+
issues.push(issue("rules.basic", "INVALID_BASIC_RULES", "Rules.basic must be an object"));
|
|
2892
|
+
}
|
|
2893
|
+
if (!Array.isArray(game.rules.extra)) {
|
|
2894
|
+
issues.push(issue("rules.extra", "INVALID_EXTRA_RULES", "Rules.extra must be an array"));
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (!game.mode || typeof game.mode !== "object") {
|
|
2898
|
+
issues.push(issue("mode", "INVALID_MODE", "Mode must be an object"));
|
|
2899
|
+
} else {
|
|
2900
|
+
if (!isNonEmptyString(game.mode.name)) {
|
|
2901
|
+
issues.push(issue("mode.name", "INVALID_MODE_NAME", "Mode name must be a non-empty string"));
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
if (!game.nextAction || typeof game.nextAction !== "object") {
|
|
2905
|
+
issues.push(issue("nextAction", "INVALID_NEXT_ACTION", "NextAction must be an object"));
|
|
2906
|
+
} else if (game.nextAction.type !== "action" && game.nextAction.type !== "step") {
|
|
2907
|
+
issues.push(issue("nextAction.type", "INVALID_NEXT_ACTION_TYPE", "NextAction.type must be 'action' or 'step'"));
|
|
2908
|
+
}
|
|
2909
|
+
return issues;
|
|
2910
|
+
};
|
|
2911
|
+
var validateTile = (tile, path) => {
|
|
2912
|
+
const issues = [];
|
|
2913
|
+
if (!tile || typeof tile !== "object") {
|
|
2914
|
+
issues.push(issue(path, "INVALID_TILE", `Tile at ${path} must be an object`));
|
|
2915
|
+
return issues;
|
|
2916
|
+
}
|
|
2917
|
+
const t = tile;
|
|
2918
|
+
if (typeof t.type !== "string" || !validGrounds.has(t.type)) {
|
|
2919
|
+
issues.push(issue(`${path}.type`, "INVALID_TILE_TYPE", `Invalid terrain type at ${path}`));
|
|
2920
|
+
}
|
|
2921
|
+
if (typeof t.crowns !== "number" || t.crowns < 0) {
|
|
2922
|
+
issues.push(issue(`${path}.crowns`, "INVALID_TILE_CROWNS", `Invalid crowns at ${path}`));
|
|
2923
|
+
}
|
|
2924
|
+
return issues;
|
|
2925
|
+
};
|
|
2926
|
+
var validatePlayers = (game) => {
|
|
2927
|
+
const issues = [];
|
|
2928
|
+
if (!Array.isArray(game.players)) return issues;
|
|
2929
|
+
const playerIds = /* @__PURE__ */ new Set();
|
|
2930
|
+
for (let i = 0; i < game.players.length; i++) {
|
|
2931
|
+
const player = game.players[i];
|
|
2932
|
+
const path = `players[${i}]`;
|
|
2933
|
+
if (!isNonEmptyString(player.id)) {
|
|
2934
|
+
issues.push(issue(`${path}.id`, "INVALID_PLAYER_ID", `Player id must be a non-empty string`));
|
|
2935
|
+
} else if (playerIds.has(player.id)) {
|
|
2936
|
+
issues.push(issue(`${path}.id`, "DUPLICATE_PLAYER_ID", `Duplicate player id: ${player.id}`));
|
|
2937
|
+
} else {
|
|
2938
|
+
playerIds.add(player.id);
|
|
2939
|
+
}
|
|
2940
|
+
if (typeof player.name !== "string" || player.name.length < 3) {
|
|
2941
|
+
issues.push(issue(`${path}.name`, "INVALID_PLAYER_NAME", `Player name must be at least 3 characters`));
|
|
2942
|
+
}
|
|
2943
|
+
if (!Array.isArray(player.kingdom)) {
|
|
2944
|
+
issues.push(issue(`${path}.kingdom`, "INVALID_KINGDOM", `Kingdom must be a 2D array`));
|
|
2945
|
+
continue;
|
|
2946
|
+
}
|
|
2947
|
+
if (player.kingdom.length !== GRIDSIZE) {
|
|
2948
|
+
issues.push(issue(`${path}.kingdom`, "INVALID_KINGDOM_GRID", `Kingdom must have ${GRIDSIZE} rows, got ${player.kingdom.length}`));
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
let castleCount = 0;
|
|
2952
|
+
for (let y = 0; y < player.kingdom.length; y++) {
|
|
2953
|
+
const row = player.kingdom[y];
|
|
2954
|
+
if (!Array.isArray(row) || row.length !== GRIDSIZE) {
|
|
2955
|
+
issues.push(issue(`${path}.kingdom[${y}]`, "INVALID_KINGDOM_ROW", `Kingdom row ${y} must have ${GRIDSIZE} columns`));
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
for (let x = 0; x < row.length; x++) {
|
|
2959
|
+
const cell = row[x];
|
|
2960
|
+
issues.push(...validateTile(cell, `${path}.kingdom[${y}][${x}]`));
|
|
2961
|
+
if (cell && typeof cell === "object" && cell.type === "castle") {
|
|
2962
|
+
castleCount++;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (castleCount === 0) {
|
|
2967
|
+
issues.push(issue(`${path}.kingdom`, "MISSING_CASTLE", `Kingdom must have exactly one castle tile`, "warning"));
|
|
2968
|
+
} else if (castleCount > 1) {
|
|
2969
|
+
issues.push(issue(`${path}.kingdom`, "MULTIPLE_CASTLES", `Kingdom has ${castleCount} castle tiles, expected 1`));
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
return issues;
|
|
2973
|
+
};
|
|
2974
|
+
var validateDominoes = (game) => {
|
|
2975
|
+
const issues = [];
|
|
2976
|
+
const allNumbers = /* @__PURE__ */ new Set();
|
|
2977
|
+
if (Array.isArray(game.dominoes)) {
|
|
2978
|
+
for (let i = 0; i < game.dominoes.length; i++) {
|
|
2979
|
+
const domino = game.dominoes[i];
|
|
2980
|
+
const path = `dominoes[${i}]`;
|
|
2981
|
+
if (!isPositiveInteger(domino.number)) {
|
|
2982
|
+
issues.push(issue(`${path}.number`, "INVALID_DOMINO_NUMBER", `Domino number must be a positive integer`));
|
|
2983
|
+
} else if (allNumbers.has(domino.number)) {
|
|
2984
|
+
issues.push(issue(`${path}.number`, "DUPLICATE_DOMINO_NUMBER", `Duplicate domino number: ${domino.number}`));
|
|
2985
|
+
} else {
|
|
2986
|
+
allNumbers.add(domino.number);
|
|
2987
|
+
}
|
|
2988
|
+
issues.push(...validateTile(domino.left, `${path}.left`));
|
|
2989
|
+
issues.push(...validateTile(domino.right, `${path}.right`));
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
if (Array.isArray(game.currentDominoes)) {
|
|
2993
|
+
for (let i = 0; i < game.currentDominoes.length; i++) {
|
|
2994
|
+
const revealed = game.currentDominoes[i];
|
|
2995
|
+
const path = `currentDominoes[${i}]`;
|
|
2996
|
+
if (!revealed.domino || typeof revealed.domino !== "object") {
|
|
2997
|
+
issues.push(issue(`${path}.domino`, "INVALID_REVEALED_DOMINO", `Revealed domino must be an object`));
|
|
2998
|
+
continue;
|
|
2999
|
+
}
|
|
3000
|
+
const domino = revealed.domino;
|
|
3001
|
+
if (!isPositiveInteger(domino.number)) {
|
|
3002
|
+
issues.push(issue(`${path}.domino.number`, "INVALID_DOMINO_NUMBER", `Domino number must be a positive integer`));
|
|
3003
|
+
} else if (allNumbers.has(domino.number)) {
|
|
3004
|
+
issues.push(issue(`${path}.domino.number`, "DUPLICATE_DOMINO_NUMBER", `Duplicate domino number: ${domino.number}`));
|
|
3005
|
+
} else {
|
|
3006
|
+
allNumbers.add(domino.number);
|
|
3007
|
+
}
|
|
3008
|
+
issues.push(...validateTile(domino.left, `${path}.domino.left`));
|
|
3009
|
+
issues.push(...validateTile(domino.right, `${path}.domino.right`));
|
|
3010
|
+
if (revealed.picked && revealed.lordId === null) {
|
|
3011
|
+
issues.push(issue(`${path}`, "PICKED_WITHOUT_LORD", `Domino is marked as picked but has no lordId`));
|
|
3012
|
+
}
|
|
3013
|
+
if (!revealed.picked && revealed.lordId !== null) {
|
|
3014
|
+
issues.push(issue(`${path}`, "UNPICKED_WITH_LORD", `Domino is not picked but has a lordId`));
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (Array.isArray(game.lords)) {
|
|
3019
|
+
const lordPickedNumbers = /* @__PURE__ */ new Set();
|
|
3020
|
+
for (let i = 0; i < game.lords.length; i++) {
|
|
3021
|
+
const lord = game.lords[i];
|
|
3022
|
+
if (lord.dominoPicked) {
|
|
3023
|
+
const path = `lords[${i}].dominoPicked`;
|
|
3024
|
+
const domino = lord.dominoPicked;
|
|
3025
|
+
if (!isPositiveInteger(domino.number)) {
|
|
3026
|
+
issues.push(issue(`${path}.number`, "INVALID_DOMINO_NUMBER", `Domino number must be a positive integer`));
|
|
3027
|
+
} else if (lordPickedNumbers.has(domino.number)) {
|
|
3028
|
+
issues.push(issue(`${path}.number`, "DUPLICATE_DOMINO_NUMBER", `Duplicate domino number among lords: ${domino.number}`));
|
|
3029
|
+
} else {
|
|
3030
|
+
lordPickedNumbers.add(domino.number);
|
|
3031
|
+
}
|
|
3032
|
+
issues.push(...validateTile(domino.left, `${path}.left`));
|
|
3033
|
+
issues.push(...validateTile(domino.right, `${path}.right`));
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
return issues;
|
|
3038
|
+
};
|
|
3039
|
+
var validateLords = (game) => {
|
|
3040
|
+
const issues = [];
|
|
3041
|
+
if (!Array.isArray(game.lords)) return issues;
|
|
3042
|
+
const playerIds = new Set(
|
|
3043
|
+
Array.isArray(game.players) ? game.players.map((p) => p.id) : []
|
|
3044
|
+
);
|
|
3045
|
+
const lordIds = /* @__PURE__ */ new Set();
|
|
3046
|
+
for (let i = 0; i < game.lords.length; i++) {
|
|
3047
|
+
const lord = game.lords[i];
|
|
3048
|
+
const path = `lords[${i}]`;
|
|
3049
|
+
if (!isNonEmptyString(lord.id)) {
|
|
3050
|
+
issues.push(issue(`${path}.id`, "INVALID_LORD_ID", `Lord id must be a non-empty string`));
|
|
3051
|
+
} else if (lordIds.has(lord.id)) {
|
|
3052
|
+
issues.push(issue(`${path}.id`, "DUPLICATE_LORD_ID", `Duplicate lord id: ${lord.id}`));
|
|
3053
|
+
} else {
|
|
3054
|
+
lordIds.add(lord.id);
|
|
3055
|
+
}
|
|
3056
|
+
if (!isNonEmptyString(lord.playerId)) {
|
|
3057
|
+
issues.push(issue(`${path}.playerId`, "INVALID_LORD_PLAYER_ID", `Lord playerId must be a non-empty string`));
|
|
3058
|
+
} else if (playerIds.size > 0 && !playerIds.has(lord.playerId)) {
|
|
3059
|
+
issues.push(issue(`${path}.playerId`, "LORD_PLAYER_NOT_FOUND", `Lord references non-existent player: ${lord.playerId}`));
|
|
3060
|
+
}
|
|
3061
|
+
if (lord.turnEnded && (!lord.hasPick || !lord.hasPlace)) {
|
|
3062
|
+
issues.push(issue(path, "INCONSISTENT_LORD_FLAGS", `Lord has turnEnded=true but hasPick=${lord.hasPick}, hasPlace=${lord.hasPlace}`, "warning"));
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
return issues;
|
|
3066
|
+
};
|
|
3067
|
+
var validateGameFlow = (game) => {
|
|
3068
|
+
const issues = [];
|
|
3069
|
+
if (!game.nextAction || typeof game.nextAction !== "object") return issues;
|
|
3070
|
+
const nextAction = game.nextAction;
|
|
3071
|
+
if (nextAction.type === "action") {
|
|
3072
|
+
const action = nextAction;
|
|
3073
|
+
if (!isNonEmptyString(action.nextLord)) {
|
|
3074
|
+
issues.push(issue("nextAction.nextLord", "INVALID_NEXT_LORD", "nextLord must be a non-empty string"));
|
|
3075
|
+
} else if (Array.isArray(game.lords) && !game.lords.some((l) => l.id === action.nextLord)) {
|
|
3076
|
+
issues.push(issue("nextAction.nextLord", "NEXT_LORD_NOT_FOUND", `nextLord references non-existent lord: ${action.nextLord}`));
|
|
3077
|
+
}
|
|
3078
|
+
if (typeof action.nextAction !== "string" || !validPlayerActions.has(action.nextAction)) {
|
|
3079
|
+
issues.push(issue("nextAction.nextAction", "INVALID_PLAYER_ACTION", `nextAction must be one of: ${[...validPlayerActions].join(", ")}`));
|
|
3080
|
+
}
|
|
3081
|
+
} else if (nextAction.type === "step") {
|
|
3082
|
+
const step = nextAction;
|
|
3083
|
+
if (typeof step.step !== "string" || !validGameSteps.has(step.step)) {
|
|
3084
|
+
issues.push(issue("nextAction.step", "INVALID_GAME_STEP", `step must be one of: ${[...validGameSteps].join(", ")}`));
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
return issues;
|
|
3088
|
+
};
|
|
3089
|
+
var isSetupPhase = (game) => {
|
|
3090
|
+
if (game.nextAction?.type !== "step") return false;
|
|
3091
|
+
const step = game.nextAction.step;
|
|
3092
|
+
return step === "addPlayers" || step === "options" || step === "start";
|
|
3093
|
+
};
|
|
3094
|
+
var validateRules = (game) => {
|
|
3095
|
+
const issues = [];
|
|
3096
|
+
if (!game.rules || typeof game.rules !== "object") return issues;
|
|
3097
|
+
const allowZero = isSetupPhase(game);
|
|
3098
|
+
const basic = game.rules.basic;
|
|
3099
|
+
if (basic && typeof basic === "object") {
|
|
3100
|
+
const fields = ["lords", "maxDominoes", "dominoesPerTurn", "maxTurns", "maxKingdomSize"];
|
|
3101
|
+
for (const field of fields) {
|
|
3102
|
+
const check = allowZero ? isNonNegativeInteger : isPositiveInteger;
|
|
3103
|
+
if (!check(basic[field])) {
|
|
3104
|
+
issues.push(issue(`rules.basic.${field}`, "INVALID_BASIC_RULE_VALUE", `rules.basic.${field} must be a positive integer`));
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
if (Array.isArray(game.rules.extra)) {
|
|
3109
|
+
for (let i = 0; i < game.rules.extra.length; i++) {
|
|
3110
|
+
const rule = game.rules.extra[i];
|
|
3111
|
+
if (!isNonEmptyString(rule.name)) {
|
|
3112
|
+
issues.push(issue(`rules.extra[${i}].name`, "INVALID_EXTRA_RULE_NAME", `Extra rule name must be a non-empty string`));
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
return issues;
|
|
3117
|
+
};
|
|
3118
|
+
var validateGameState = (game) => {
|
|
3119
|
+
const issues = [];
|
|
3120
|
+
issues.push(...validateGameStructure(game));
|
|
3121
|
+
issues.push(...validatePlayers(game));
|
|
3122
|
+
issues.push(...validateDominoes(game));
|
|
3123
|
+
issues.push(...validateLords(game));
|
|
3124
|
+
issues.push(...validateGameFlow(game));
|
|
3125
|
+
issues.push(...validateRules(game));
|
|
3126
|
+
return issues;
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
// src/core/useCases/botRegistry.ts
|
|
3130
|
+
var defaultStrategies = {
|
|
3131
|
+
random: randomStrategy,
|
|
3132
|
+
greedy: greedyStrategy,
|
|
3133
|
+
advanced: advancedStrategy,
|
|
3134
|
+
expert: expertStrategy
|
|
3135
|
+
};
|
|
3136
|
+
var getStrategy = (name, custom) => {
|
|
3137
|
+
return custom?.[name] ?? defaultStrategies[name];
|
|
3138
|
+
};
|
|
3139
|
+
var getStrategyNames = () => Object.keys(defaultStrategies);
|
|
3140
|
+
|
|
3141
|
+
// src/core/useCases/bot.ts
|
|
3142
|
+
var scoreKingdom = (kingdom) => {
|
|
3143
|
+
const result = calculateScoreUseCase(kingdom);
|
|
3144
|
+
return isOk(result) ? result.value.points : 0;
|
|
3145
|
+
};
|
|
3146
|
+
var simulatePlacement = (kingdom, domino, placement, maxKingdomSize) => {
|
|
3147
|
+
const result = placeDomino(
|
|
3148
|
+
kingdom,
|
|
3149
|
+
placement.position,
|
|
3150
|
+
placement.rotation,
|
|
3151
|
+
domino,
|
|
3152
|
+
maxKingdomSize
|
|
3153
|
+
);
|
|
3154
|
+
return isOk(result) ? result.value : null;
|
|
3155
|
+
};
|
|
3156
|
+
var bestPlacementScore = (kingdom, domino, placements, maxKingdomSize) => {
|
|
3157
|
+
let best = null;
|
|
3158
|
+
for (const placement of placements) {
|
|
3159
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxKingdomSize);
|
|
3160
|
+
if (!newKingdom) continue;
|
|
3161
|
+
const score = scoreKingdom(newKingdom);
|
|
3162
|
+
if (!best || score > best.score) {
|
|
3163
|
+
best = { placement, score, kingdom: newKingdom };
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return best;
|
|
3167
|
+
};
|
|
3168
|
+
var getMaxKingdomSize = (game) => game.rules.basic.maxKingdomSize;
|
|
3169
|
+
var getLordAndPlayer = (game, lordId) => {
|
|
3170
|
+
const lord = game.lords.find((l) => l.id === lordId);
|
|
3171
|
+
const player = lord ? game.players.find((p) => p.id === lord.playerId) : void 0;
|
|
3172
|
+
return { lord, player };
|
|
3173
|
+
};
|
|
3174
|
+
var getNextBatchDominoes = (game) => {
|
|
3175
|
+
const perTurn = game.rules.basic.dominoesPerTurn;
|
|
3176
|
+
return [...game.dominoes].slice(0, perTurn).sort((a, b) => a.number - b.number);
|
|
3177
|
+
};
|
|
3178
|
+
var randomStrategy = {
|
|
3179
|
+
chooseDomino: (context) => {
|
|
3180
|
+
const { availableDominoes } = context;
|
|
3181
|
+
const idx = Math.floor(Math.random() * availableDominoes.length);
|
|
3182
|
+
return availableDominoes[idx].domino.number;
|
|
3183
|
+
},
|
|
3184
|
+
choosePlacement: (context) => {
|
|
3185
|
+
const { validPlacements } = context;
|
|
3186
|
+
if (validPlacements.length === 0) return null;
|
|
3187
|
+
const idx = Math.floor(Math.random() * validPlacements.length);
|
|
3188
|
+
return validPlacements[idx];
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
var greedyStrategy = {
|
|
3192
|
+
chooseDomino: (context) => {
|
|
3193
|
+
const { game, lordId, availableDominoes } = context;
|
|
3194
|
+
const { player } = getLordAndPlayer(game, lordId);
|
|
3195
|
+
if (!player) return availableDominoes[0].domino.number;
|
|
3196
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3197
|
+
let bestScore = -1;
|
|
3198
|
+
let bestPick = availableDominoes[0].domino.number;
|
|
3199
|
+
for (const rd of availableDominoes) {
|
|
3200
|
+
const placements = getValidPlacementsUseCase(
|
|
3201
|
+
player.kingdom,
|
|
3202
|
+
rd.domino,
|
|
3203
|
+
maxSize
|
|
3204
|
+
);
|
|
3205
|
+
const result = bestPlacementScore(
|
|
3206
|
+
player.kingdom,
|
|
3207
|
+
rd.domino,
|
|
3208
|
+
placements,
|
|
3209
|
+
maxSize
|
|
3210
|
+
);
|
|
3211
|
+
const score = result ? result.score : 0;
|
|
3212
|
+
if (score > bestScore) {
|
|
3213
|
+
bestScore = score;
|
|
3214
|
+
bestPick = rd.domino.number;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return bestPick;
|
|
3218
|
+
},
|
|
3219
|
+
choosePlacement: (context) => {
|
|
3220
|
+
const { game, domino, kingdom, validPlacements } = context;
|
|
3221
|
+
if (validPlacements.length === 0) return null;
|
|
3222
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3223
|
+
const result = bestPlacementScore(kingdom, domino, validPlacements, maxSize);
|
|
3224
|
+
return result ? result.placement : validPlacements[0];
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
var evaluateWithLookahead = (kingdom, maxSize, nextDominoes) => {
|
|
3228
|
+
if (nextDominoes.length === 0) return 0;
|
|
3229
|
+
let bestNextScore = 0;
|
|
3230
|
+
for (const nextDomino of nextDominoes) {
|
|
3231
|
+
const nextPlacements = getValidPlacementsUseCase(kingdom, nextDomino, maxSize);
|
|
3232
|
+
const result = bestPlacementScore(kingdom, nextDomino, nextPlacements, maxSize);
|
|
3233
|
+
if (result && result.score > bestNextScore) {
|
|
3234
|
+
bestNextScore = result.score;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
return bestNextScore;
|
|
3238
|
+
};
|
|
3239
|
+
var advancedStrategy = {
|
|
3240
|
+
chooseDomino: (context) => {
|
|
3241
|
+
const { game, lordId, availableDominoes } = context;
|
|
3242
|
+
const { player } = getLordAndPlayer(game, lordId);
|
|
3243
|
+
if (!player) return availableDominoes[0].domino.number;
|
|
3244
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3245
|
+
const nextBatch = getNextBatchDominoes(game);
|
|
3246
|
+
let bestCombinedScore = -1;
|
|
3247
|
+
let bestPick = availableDominoes[0].domino.number;
|
|
3248
|
+
for (const rd of availableDominoes) {
|
|
3249
|
+
const placements = getValidPlacementsUseCase(
|
|
3250
|
+
player.kingdom,
|
|
3251
|
+
rd.domino,
|
|
3252
|
+
maxSize
|
|
3253
|
+
);
|
|
3254
|
+
const result = bestPlacementScore(
|
|
3255
|
+
player.kingdom,
|
|
3256
|
+
rd.domino,
|
|
3257
|
+
placements,
|
|
3258
|
+
maxSize
|
|
3259
|
+
);
|
|
3260
|
+
const immediateScore = result ? result.score : scoreKingdom(player.kingdom);
|
|
3261
|
+
const kingdomAfter = result ? result.kingdom : player.kingdom;
|
|
3262
|
+
const futureScore = evaluateWithLookahead(kingdomAfter, maxSize, nextBatch);
|
|
3263
|
+
const combined = immediateScore + futureScore * 0.8;
|
|
3264
|
+
if (combined > bestCombinedScore) {
|
|
3265
|
+
bestCombinedScore = combined;
|
|
3266
|
+
bestPick = rd.domino.number;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
return bestPick;
|
|
3270
|
+
},
|
|
3271
|
+
choosePlacement: (context) => {
|
|
3272
|
+
const { game, domino, kingdom, validPlacements } = context;
|
|
3273
|
+
if (validPlacements.length === 0) return null;
|
|
3274
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3275
|
+
const nextBatch = getNextBatchDominoes(game);
|
|
3276
|
+
let bestCombinedScore = -1;
|
|
3277
|
+
let bestPlacement = validPlacements[0];
|
|
3278
|
+
for (const placement of validPlacements) {
|
|
3279
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxSize);
|
|
3280
|
+
if (!newKingdom) continue;
|
|
3281
|
+
const immediateScore = scoreKingdom(newKingdom);
|
|
3282
|
+
const futureScore = evaluateWithLookahead(newKingdom, maxSize, nextBatch);
|
|
3283
|
+
const combined = immediateScore + futureScore * 0.8;
|
|
3284
|
+
if (combined > bestCombinedScore) {
|
|
3285
|
+
bestCombinedScore = combined;
|
|
3286
|
+
bestPlacement = placement;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return bestPlacement;
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
var EXPERT_BEAM_WIDTH = 2;
|
|
3293
|
+
var EXPERT_MAX_DEPTH = 2;
|
|
3294
|
+
var topPlacements = (kingdom, domino, placements, maxSize, limit) => {
|
|
3295
|
+
if (placements.length <= limit) return placements;
|
|
3296
|
+
const scored = placements.map((p) => {
|
|
3297
|
+
const k = simulatePlacement(kingdom, domino, p, maxSize);
|
|
3298
|
+
return { placement: p, score: k ? scoreKingdom(k) : 0 };
|
|
3299
|
+
}).sort((a, b) => b.score - a.score);
|
|
3300
|
+
return scored.slice(0, limit).map((s) => s.placement);
|
|
3301
|
+
};
|
|
3302
|
+
var expertEvaluate = (kingdom, remainingDominoes, dominoesPerTurn, maxSize, depth) => {
|
|
3303
|
+
if (depth <= 0 || remainingDominoes.length === 0) {
|
|
3304
|
+
return scoreKingdom(kingdom);
|
|
3305
|
+
}
|
|
3306
|
+
const batch = [...remainingDominoes].slice(0, dominoesPerTurn).sort((a, b) => a.number - b.number);
|
|
3307
|
+
const afterBatch = remainingDominoes.slice(dominoesPerTurn);
|
|
3308
|
+
let bestScore = scoreKingdom(kingdom);
|
|
3309
|
+
for (const domino of batch) {
|
|
3310
|
+
const allPlacements = getValidPlacementsUseCase(kingdom, domino, maxSize);
|
|
3311
|
+
if (allPlacements.length === 0) {
|
|
3312
|
+
const score = expertEvaluate(
|
|
3313
|
+
kingdom,
|
|
3314
|
+
afterBatch,
|
|
3315
|
+
dominoesPerTurn,
|
|
3316
|
+
maxSize,
|
|
3317
|
+
depth - 1
|
|
3318
|
+
);
|
|
3319
|
+
if (score > bestScore) bestScore = score;
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
const candidates = topPlacements(
|
|
3323
|
+
kingdom,
|
|
3324
|
+
domino,
|
|
3325
|
+
allPlacements,
|
|
3326
|
+
maxSize,
|
|
3327
|
+
EXPERT_BEAM_WIDTH
|
|
3328
|
+
);
|
|
3329
|
+
for (const placement of candidates) {
|
|
3330
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxSize);
|
|
3331
|
+
if (!newKingdom) continue;
|
|
3332
|
+
const score = expertEvaluate(
|
|
3333
|
+
newKingdom,
|
|
3334
|
+
afterBatch,
|
|
3335
|
+
dominoesPerTurn,
|
|
3336
|
+
maxSize,
|
|
3337
|
+
depth - 1
|
|
3338
|
+
);
|
|
3339
|
+
if (score > bestScore) bestScore = score;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return bestScore;
|
|
3343
|
+
};
|
|
3344
|
+
var expertStrategy = {
|
|
3345
|
+
chooseDomino: (context) => {
|
|
3346
|
+
const { game, lordId, availableDominoes } = context;
|
|
3347
|
+
const { player } = getLordAndPlayer(game, lordId);
|
|
3348
|
+
if (!player) return availableDominoes[0].domino.number;
|
|
3349
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3350
|
+
const perTurn = game.rules.basic.dominoesPerTurn;
|
|
3351
|
+
const turnsLeft = Math.min(
|
|
3352
|
+
game.rules.basic.maxTurns - game.turn,
|
|
3353
|
+
EXPERT_MAX_DEPTH
|
|
3354
|
+
);
|
|
3355
|
+
let bestScore = -1;
|
|
3356
|
+
let bestPick = availableDominoes[0].domino.number;
|
|
3357
|
+
for (const rd of availableDominoes) {
|
|
3358
|
+
const allPlacements = getValidPlacementsUseCase(
|
|
3359
|
+
player.kingdom,
|
|
3360
|
+
rd.domino,
|
|
3361
|
+
maxSize
|
|
3362
|
+
);
|
|
3363
|
+
if (allPlacements.length === 0) {
|
|
3364
|
+
const score = expertEvaluate(
|
|
3365
|
+
player.kingdom,
|
|
3366
|
+
game.dominoes,
|
|
3367
|
+
perTurn,
|
|
3368
|
+
maxSize,
|
|
3369
|
+
turnsLeft
|
|
3370
|
+
);
|
|
3371
|
+
if (score > bestScore) {
|
|
3372
|
+
bestScore = score;
|
|
3373
|
+
bestPick = rd.domino.number;
|
|
3374
|
+
}
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
const candidates = topPlacements(
|
|
3378
|
+
player.kingdom,
|
|
3379
|
+
rd.domino,
|
|
3380
|
+
allPlacements,
|
|
3381
|
+
maxSize,
|
|
3382
|
+
EXPERT_BEAM_WIDTH
|
|
3383
|
+
);
|
|
3384
|
+
for (const placement of candidates) {
|
|
3385
|
+
const newKingdom = simulatePlacement(
|
|
3386
|
+
player.kingdom,
|
|
3387
|
+
rd.domino,
|
|
3388
|
+
placement,
|
|
3389
|
+
maxSize
|
|
3390
|
+
);
|
|
3391
|
+
if (!newKingdom) continue;
|
|
3392
|
+
const score = expertEvaluate(
|
|
3393
|
+
newKingdom,
|
|
3394
|
+
game.dominoes,
|
|
3395
|
+
perTurn,
|
|
3396
|
+
maxSize,
|
|
3397
|
+
turnsLeft
|
|
3398
|
+
);
|
|
3399
|
+
if (score > bestScore) {
|
|
3400
|
+
bestScore = score;
|
|
3401
|
+
bestPick = rd.domino.number;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
return bestPick;
|
|
3406
|
+
},
|
|
3407
|
+
choosePlacement: (context) => {
|
|
3408
|
+
const { game, domino, kingdom, validPlacements } = context;
|
|
3409
|
+
if (validPlacements.length === 0) return null;
|
|
3410
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3411
|
+
const perTurn = game.rules.basic.dominoesPerTurn;
|
|
3412
|
+
const turnsLeft = Math.min(
|
|
3413
|
+
game.rules.basic.maxTurns - game.turn,
|
|
3414
|
+
EXPERT_MAX_DEPTH
|
|
3415
|
+
);
|
|
3416
|
+
let bestScore = -1;
|
|
3417
|
+
let bestPlacement = validPlacements[0];
|
|
3418
|
+
for (const placement of validPlacements) {
|
|
3419
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxSize);
|
|
3420
|
+
if (!newKingdom) continue;
|
|
3421
|
+
const score = expertEvaluate(
|
|
3422
|
+
newKingdom,
|
|
3423
|
+
game.dominoes,
|
|
3424
|
+
perTurn,
|
|
3425
|
+
maxSize,
|
|
3426
|
+
turnsLeft
|
|
3427
|
+
);
|
|
3428
|
+
if (score > bestScore) {
|
|
3429
|
+
bestScore = score;
|
|
3430
|
+
bestPlacement = placement;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
return bestPlacement;
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
var playBotTurn = (engine, game, strategy) => {
|
|
3437
|
+
const lordId = game.nextAction.nextLord;
|
|
3438
|
+
const action = game.nextAction.nextAction;
|
|
3439
|
+
if (action === "pickDomino") {
|
|
3440
|
+
const availableDominoes = game.currentDominoes.filter((d) => !d.picked);
|
|
3441
|
+
const dominoPick = strategy.chooseDomino({
|
|
3442
|
+
game,
|
|
3443
|
+
lordId,
|
|
3444
|
+
availableDominoes
|
|
3445
|
+
});
|
|
3446
|
+
return engine.chooseDomino({ game, lordId, dominoPick });
|
|
3447
|
+
}
|
|
3448
|
+
if (action === "placeDomino") {
|
|
3449
|
+
const { lord, player } = getLordAndPlayer(game, lordId);
|
|
3450
|
+
if (!lord?.dominoPicked || !player) {
|
|
3451
|
+
return engine.discardDomino({ game, lordId });
|
|
3452
|
+
}
|
|
3453
|
+
const domino = lord.dominoPicked;
|
|
3454
|
+
const kingdom = player.kingdom;
|
|
3455
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3456
|
+
const validPlacements = getValidPlacementsUseCase(kingdom, domino, maxSize);
|
|
3457
|
+
const placement = strategy.choosePlacement({
|
|
3458
|
+
game,
|
|
3459
|
+
lordId,
|
|
3460
|
+
domino,
|
|
3461
|
+
kingdom,
|
|
3462
|
+
validPlacements
|
|
3463
|
+
});
|
|
3464
|
+
if (!placement) {
|
|
3465
|
+
return engine.discardDomino({ game, lordId });
|
|
3466
|
+
}
|
|
3467
|
+
return engine.placeDomino({
|
|
3468
|
+
game,
|
|
3469
|
+
lordId,
|
|
3470
|
+
position: placement.position,
|
|
3471
|
+
rotation: placement.rotation
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
return engine.discardDomino({ game, lordId });
|
|
3475
|
+
};
|
|
3476
|
+
var isBotTurn = (game) => {
|
|
3477
|
+
const lordId = game.nextAction.nextLord;
|
|
3478
|
+
const lord = game.lords.find((l) => l.id === lordId);
|
|
3479
|
+
const player = lord ? game.players.find((p) => p.id === lord.playerId) : void 0;
|
|
3480
|
+
return !!player?.bot;
|
|
3481
|
+
};
|
|
3482
|
+
var playBotTurns = (engine, game, customStrategies) => {
|
|
3483
|
+
let current = game;
|
|
3484
|
+
while (isGameWithNextAction(current) && isBotTurn(current)) {
|
|
3485
|
+
const lordId = current.nextAction.nextLord;
|
|
3486
|
+
const lord = current.lords.find((l) => l.id === lordId);
|
|
3487
|
+
const player = lord ? current.players.find((p) => p.id === lord.playerId) : void 0;
|
|
3488
|
+
const strategyName = player?.bot?.strategyName ?? "random";
|
|
3489
|
+
const strategy = getStrategy(strategyName, customStrategies) ?? randomStrategy;
|
|
3490
|
+
current = playBotTurn(engine, current, strategy);
|
|
3491
|
+
}
|
|
3492
|
+
return current;
|
|
3493
|
+
};
|
|
3494
|
+
|
|
3495
|
+
// src/core/useCases/gameActionLog.ts
|
|
3496
|
+
var createActionLog = (gameId) => ({
|
|
3497
|
+
gameId,
|
|
3498
|
+
entries: []
|
|
3499
|
+
});
|
|
3500
|
+
var appendAction = (log, entry) => ({
|
|
3501
|
+
...log,
|
|
3502
|
+
entries: [
|
|
3503
|
+
...log.entries,
|
|
3504
|
+
{
|
|
3505
|
+
...entry,
|
|
3506
|
+
id: log.entries.length,
|
|
3507
|
+
timestamp: Date.now()
|
|
3508
|
+
}
|
|
3509
|
+
]
|
|
3510
|
+
});
|
|
3511
|
+
var getActions = (log) => log.entries;
|
|
3512
|
+
var getActionsByType = (log, type) => log.entries.filter((e) => e.action === type);
|
|
3513
|
+
var getActionsByTurn = (log, turn) => log.entries.filter((e) => e.turn === turn);
|
|
3514
|
+
var wrapWithActionLog = (engine) => {
|
|
3515
|
+
let log = createActionLog("");
|
|
3516
|
+
const record = (action, payload, turn) => {
|
|
3517
|
+
log = appendAction(log, { action, payload, turn });
|
|
3518
|
+
};
|
|
3519
|
+
const wrappedEngine = {
|
|
3520
|
+
// Read-only methods — pass through
|
|
3521
|
+
getModes: (cmd) => engine.getModes(cmd),
|
|
3522
|
+
getExtraRules: (cmd) => engine.getExtraRules(cmd),
|
|
3523
|
+
calculateScore: (cmd) => engine.calculateScore(cmd),
|
|
3524
|
+
getValidPlacements: (cmd) => engine.getValidPlacements(cmd),
|
|
3525
|
+
canPlaceDomino: (cmd) => engine.canPlaceDomino(cmd),
|
|
3526
|
+
serialize: (cmd) => engine.serialize(cmd),
|
|
3527
|
+
deserialize: (cmd) => engine.deserialize(cmd),
|
|
3528
|
+
getResults: (cmd) => engine.getResults(cmd),
|
|
3529
|
+
getDynastyResults: (cmd) => engine.getDynastyResults(cmd),
|
|
3530
|
+
// State-changing methods — record after execution
|
|
3531
|
+
createGame: (cmd) => {
|
|
3532
|
+
const game = engine.createGame(cmd);
|
|
3533
|
+
log = createActionLog(game.id);
|
|
3534
|
+
record("createGame", { mode: cmd.mode, seed: cmd.seed }, game.turn);
|
|
3535
|
+
return game;
|
|
3536
|
+
},
|
|
3537
|
+
addPlayers: (cmd) => {
|
|
3538
|
+
const game = engine.addPlayers(cmd);
|
|
3539
|
+
record("addPlayers", { players: cmd.players }, game.turn);
|
|
3540
|
+
return game;
|
|
3541
|
+
},
|
|
3542
|
+
addExtraRules: (cmd) => {
|
|
3543
|
+
const game = engine.addExtraRules(cmd);
|
|
3544
|
+
record("addExtraRules", { extraRules: cmd.extraRules }, game.turn);
|
|
3545
|
+
return game;
|
|
3546
|
+
},
|
|
3547
|
+
startGame: (cmd) => {
|
|
3548
|
+
const game = engine.startGame(cmd);
|
|
3549
|
+
record("startGame", {}, game.turn);
|
|
3550
|
+
return game;
|
|
3551
|
+
},
|
|
3552
|
+
chooseDomino: (cmd) => {
|
|
3553
|
+
const game = engine.chooseDomino(cmd);
|
|
3554
|
+
record(
|
|
3555
|
+
"chooseDomino",
|
|
3556
|
+
{ lordId: cmd.lordId, dominoPick: cmd.dominoPick },
|
|
3557
|
+
game.turn
|
|
3558
|
+
);
|
|
3559
|
+
return game;
|
|
3560
|
+
},
|
|
3561
|
+
placeDomino: (cmd) => {
|
|
3562
|
+
const game = engine.placeDomino(cmd);
|
|
3563
|
+
record(
|
|
3564
|
+
"placeDomino",
|
|
3565
|
+
{
|
|
3566
|
+
lordId: cmd.lordId,
|
|
3567
|
+
position: cmd.position,
|
|
3568
|
+
rotation: cmd.rotation
|
|
3569
|
+
},
|
|
3570
|
+
game.turn
|
|
3571
|
+
);
|
|
3572
|
+
return game;
|
|
3573
|
+
},
|
|
3574
|
+
discardDomino: (cmd) => {
|
|
3575
|
+
const game = engine.discardDomino(cmd);
|
|
3576
|
+
record("discardDomino", { lordId: cmd.lordId }, game.turn);
|
|
3577
|
+
return game;
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
return {
|
|
3581
|
+
engine: wrappedEngine,
|
|
3582
|
+
getLog: () => log
|
|
3583
|
+
};
|
|
3584
|
+
};
|
|
3585
|
+
var replayActions = (engine, log) => {
|
|
3586
|
+
if (log.entries.length === 0) {
|
|
3587
|
+
throw new Error("Cannot replay an empty action log");
|
|
3588
|
+
}
|
|
3589
|
+
const firstEntry = log.entries[0];
|
|
3590
|
+
if (firstEntry?.action !== "createGame") {
|
|
3591
|
+
throw new Error("Action log must start with a createGame action");
|
|
3592
|
+
}
|
|
3593
|
+
const firstPayload = firstEntry.payload;
|
|
3594
|
+
let game = engine.createGame({
|
|
3595
|
+
mode: firstPayload.mode,
|
|
3596
|
+
seed: firstPayload.seed
|
|
3597
|
+
});
|
|
3598
|
+
for (let i = 1; i < log.entries.length; i++) {
|
|
3599
|
+
const entry = log.entries[i];
|
|
3600
|
+
if (!entry) continue;
|
|
3601
|
+
switch (entry.action) {
|
|
3602
|
+
case "createGame": {
|
|
3603
|
+
const payload = entry.payload;
|
|
3604
|
+
game = engine.createGame({ mode: payload.mode, seed: payload.seed });
|
|
3605
|
+
break;
|
|
3606
|
+
}
|
|
3607
|
+
case "addPlayers": {
|
|
3608
|
+
const payload = entry.payload;
|
|
3609
|
+
game = engine.addPlayers({ game, players: payload.players });
|
|
3610
|
+
break;
|
|
3611
|
+
}
|
|
3612
|
+
case "addExtraRules": {
|
|
3613
|
+
const payload = entry.payload;
|
|
3614
|
+
game = engine.addExtraRules({
|
|
3615
|
+
game,
|
|
3616
|
+
extraRules: payload.extraRules
|
|
3617
|
+
});
|
|
3618
|
+
break;
|
|
3619
|
+
}
|
|
3620
|
+
case "startGame": {
|
|
3621
|
+
game = engine.startGame({ game });
|
|
3622
|
+
break;
|
|
3623
|
+
}
|
|
3624
|
+
case "chooseDomino": {
|
|
3625
|
+
const payload = entry.payload;
|
|
3626
|
+
game = engine.chooseDomino({
|
|
3627
|
+
game,
|
|
3628
|
+
lordId: payload.lordId,
|
|
3629
|
+
dominoPick: payload.dominoPick
|
|
3630
|
+
});
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
case "placeDomino": {
|
|
3634
|
+
const payload = entry.payload;
|
|
3635
|
+
game = engine.placeDomino({
|
|
3636
|
+
game,
|
|
3637
|
+
lordId: payload.lordId,
|
|
3638
|
+
position: payload.position,
|
|
3639
|
+
rotation: payload.rotation
|
|
3640
|
+
});
|
|
3641
|
+
break;
|
|
3642
|
+
}
|
|
3643
|
+
case "discardDomino": {
|
|
3644
|
+
const payload = entry.payload;
|
|
3645
|
+
game = engine.discardDomino({
|
|
3646
|
+
game,
|
|
3647
|
+
lordId: payload.lordId
|
|
3648
|
+
});
|
|
3649
|
+
break;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return game;
|
|
3654
|
+
};
|
|
3655
|
+
|
|
3656
|
+
// src/core/useCases/gameHistory.ts
|
|
3657
|
+
var createGameHistory = (initialState) => ({
|
|
3658
|
+
current: initialState,
|
|
3659
|
+
past: [],
|
|
3660
|
+
future: []
|
|
3661
|
+
});
|
|
3662
|
+
var pushState = (history, newState) => ({
|
|
3663
|
+
current: newState,
|
|
3664
|
+
past: [...history.past, history.current],
|
|
3665
|
+
future: []
|
|
3666
|
+
});
|
|
3667
|
+
var undo = (history) => {
|
|
3668
|
+
if (history.past.length === 0) {
|
|
3669
|
+
throw new UndoError();
|
|
3670
|
+
}
|
|
3671
|
+
const previous = history.past[history.past.length - 1];
|
|
3672
|
+
return {
|
|
3673
|
+
current: previous,
|
|
3674
|
+
past: history.past.slice(0, -1),
|
|
3675
|
+
future: [...history.future, history.current]
|
|
3676
|
+
};
|
|
3677
|
+
};
|
|
3678
|
+
var redo = (history) => {
|
|
3679
|
+
if (history.future.length === 0) {
|
|
3680
|
+
throw new RedoError();
|
|
3681
|
+
}
|
|
3682
|
+
const next = history.future[history.future.length - 1];
|
|
3683
|
+
return {
|
|
3684
|
+
current: next,
|
|
3685
|
+
past: [...history.past, history.current],
|
|
3686
|
+
future: history.future.slice(0, -1)
|
|
3687
|
+
};
|
|
3688
|
+
};
|
|
3689
|
+
var canUndo = (history) => history.past.length > 0;
|
|
3690
|
+
var canRedo = (history) => history.future.length > 0;
|
|
3691
|
+
var clearHistory = (history) => ({
|
|
3692
|
+
current: history.current,
|
|
3693
|
+
past: [],
|
|
3694
|
+
future: []
|
|
3695
|
+
});
|
|
3696
|
+
var getHistorySize = (history) => ({
|
|
3697
|
+
past: history.past.length,
|
|
3698
|
+
future: history.future.length
|
|
3699
|
+
});
|
|
3700
|
+
|
|
3701
|
+
// src/index.ts
|
|
3702
|
+
var createGameEngine = (config) => {
|
|
3703
|
+
const {
|
|
3704
|
+
createGameHandler: createGameHandler2,
|
|
3705
|
+
getModesHandler: getModesHandler2,
|
|
3706
|
+
getExtraRulesHandler: getExtraRulesHandler2,
|
|
3707
|
+
addPlayersHandler: addPlayersHandler2,
|
|
3708
|
+
addExtraRulesHandler: addExtraRulesHandler2,
|
|
3709
|
+
startGameHandler: startGameHandler2,
|
|
3710
|
+
chooseDominoHandler: chooseDominoHandler2,
|
|
3711
|
+
placeDominoHandler: placeDominoHandler2,
|
|
3712
|
+
discardDominoHandler: discardDominoHandler2,
|
|
3713
|
+
getResultHandler: getResultHandler2,
|
|
3714
|
+
calculateScoreHandler: calculateScoreHandler2,
|
|
3715
|
+
getValidPlacementsHandler: getValidPlacementsHandler2,
|
|
3716
|
+
canPlaceDominoHandler: canPlaceDominoHandler2,
|
|
3717
|
+
serializeGameHandler: serializeGameHandler2,
|
|
3718
|
+
deserializeGameHandler: deserializeGameHandler2,
|
|
3719
|
+
getDynastyResultHandler: getDynastyResultHandler2
|
|
3720
|
+
} = configureEngine(config);
|
|
3721
|
+
let engine = {
|
|
3722
|
+
getModes: (command) => getModesHandler2(command),
|
|
3723
|
+
getExtraRules: (command) => getExtraRulesHandler2(command),
|
|
3724
|
+
createGame: (command) => createGameHandler2(command),
|
|
3725
|
+
addPlayers: (command) => addPlayersHandler2(command),
|
|
3726
|
+
addExtraRules: (command) => addExtraRulesHandler2(command),
|
|
3727
|
+
startGame: (command) => startGameHandler2(command),
|
|
3728
|
+
chooseDomino: (command) => chooseDominoHandler2(command),
|
|
3729
|
+
placeDomino: (command) => placeDominoHandler2(command),
|
|
3730
|
+
discardDomino: (command) => discardDominoHandler2(command),
|
|
3731
|
+
getResults: (command) => getResultHandler2(command),
|
|
3732
|
+
calculateScore: (command) => calculateScoreHandler2(command),
|
|
3733
|
+
getValidPlacements: (command) => getValidPlacementsHandler2(command),
|
|
3734
|
+
canPlaceDomino: (command) => canPlaceDominoHandler2(command),
|
|
3735
|
+
serialize: (command) => serializeGameHandler2(command),
|
|
3736
|
+
deserialize: (command) => deserializeGameHandler2(command),
|
|
3737
|
+
getDynastyResults: (command) => getDynastyResultHandler2(command)
|
|
3738
|
+
};
|
|
3739
|
+
if (config.events) {
|
|
3740
|
+
engine = wrapWithEvents(engine, config.events);
|
|
3741
|
+
}
|
|
3742
|
+
if (config.debug) {
|
|
3743
|
+
const debugOptions = typeof config.debug === "object" ? config.debug : void 0;
|
|
3744
|
+
engine = wrapWithDebug(engine, debugOptions);
|
|
3745
|
+
}
|
|
3746
|
+
return engine;
|
|
3747
|
+
};
|
|
3748
|
+
export {
|
|
3749
|
+
ActionExecutionError,
|
|
3750
|
+
DomainException,
|
|
3751
|
+
ErrorCode,
|
|
3752
|
+
GRIDSIZE,
|
|
3753
|
+
GROUND,
|
|
3754
|
+
InvalidStepError,
|
|
3755
|
+
MAX_KINGDOM_SIZE,
|
|
3756
|
+
NotFoundError,
|
|
3757
|
+
RedoError,
|
|
3758
|
+
StepExecutionError,
|
|
3759
|
+
TranslationKey,
|
|
3760
|
+
UndoError,
|
|
3761
|
+
ValidationError,
|
|
3762
|
+
advancedStrategy,
|
|
3763
|
+
appendAction,
|
|
3764
|
+
canRedo,
|
|
3765
|
+
canUndo,
|
|
3766
|
+
clearHistory,
|
|
3767
|
+
createActionLog,
|
|
3768
|
+
createDomainError,
|
|
3769
|
+
createGameEngine,
|
|
3770
|
+
createGameHistory,
|
|
3771
|
+
createSavePoint,
|
|
3772
|
+
createTranslator,
|
|
3773
|
+
defaultTranslations,
|
|
3774
|
+
defaultTranslator,
|
|
3775
|
+
deserializeGame,
|
|
3776
|
+
errorCodeToTranslationKey,
|
|
3777
|
+
expertStrategy,
|
|
3778
|
+
gameSteps,
|
|
3779
|
+
getActions,
|
|
3780
|
+
getActionsByTurn,
|
|
3781
|
+
getActionsByType,
|
|
3782
|
+
getHistorySize,
|
|
3783
|
+
getStrategy,
|
|
3784
|
+
getStrategyNames,
|
|
3785
|
+
greedyStrategy,
|
|
3786
|
+
isBotTurn,
|
|
3787
|
+
isGameWithNextAction,
|
|
3788
|
+
isGameWithNextStep,
|
|
3789
|
+
playBotTurn,
|
|
3790
|
+
playBotTurns,
|
|
3791
|
+
playerActions,
|
|
3792
|
+
pushState,
|
|
3793
|
+
randomStrategy,
|
|
3794
|
+
redo,
|
|
3795
|
+
replayActions,
|
|
3796
|
+
restoreFromSavePoint,
|
|
3797
|
+
serializeGame,
|
|
3798
|
+
summarizeGameState,
|
|
3799
|
+
translateErrorCode,
|
|
3800
|
+
undo,
|
|
3801
|
+
validateGameState,
|
|
3802
|
+
wrapWithActionLog,
|
|
3803
|
+
wrapWithDebug,
|
|
3804
|
+
wrapWithEvents
|
|
3805
|
+
};
|