@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
package/dist/index.js
CHANGED
|
@@ -1,3 +1,266 @@
|
|
|
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
|
+
|
|
1
264
|
// src/datasources/dominoes.json
|
|
2
265
|
var dominoes_default = {
|
|
3
266
|
Classic: [
|
|
@@ -588,26 +851,37 @@ var jsonModes_default = jsonModes;
|
|
|
588
851
|
// src/datasources/rules.json
|
|
589
852
|
var rules_default = {
|
|
590
853
|
commun: [
|
|
854
|
+
{
|
|
855
|
+
playersLimit: 1,
|
|
856
|
+
lordsPerPlayer: 1,
|
|
857
|
+
maxDominoes: 48,
|
|
858
|
+
dominoesPerTurn: 4,
|
|
859
|
+
maxTurns: 12,
|
|
860
|
+
maxKingdomSize: 5
|
|
861
|
+
},
|
|
591
862
|
{
|
|
592
863
|
playersLimit: 2,
|
|
593
864
|
lordsPerPlayer: 2,
|
|
594
865
|
maxDominoes: 24,
|
|
595
866
|
dominoesPerTurn: 4,
|
|
596
|
-
maxTurns: 6
|
|
867
|
+
maxTurns: 6,
|
|
868
|
+
maxKingdomSize: 5
|
|
597
869
|
},
|
|
598
870
|
{
|
|
599
871
|
playersLimit: 3,
|
|
600
872
|
lordsPerPlayer: 1,
|
|
601
|
-
maxDominoes:
|
|
602
|
-
dominoesPerTurn:
|
|
603
|
-
maxTurns: 12
|
|
873
|
+
maxDominoes: 48,
|
|
874
|
+
dominoesPerTurn: 4,
|
|
875
|
+
maxTurns: 12,
|
|
876
|
+
maxKingdomSize: 5
|
|
604
877
|
},
|
|
605
878
|
{
|
|
606
879
|
playersLimit: 4,
|
|
607
880
|
lordsPerPlayer: 1,
|
|
608
881
|
maxDominoes: 48,
|
|
609
882
|
dominoesPerTurn: 4,
|
|
610
|
-
maxTurns: 12
|
|
883
|
+
maxTurns: 12,
|
|
884
|
+
maxKingdomSize: 5
|
|
611
885
|
}
|
|
612
886
|
],
|
|
613
887
|
extra: [
|
|
@@ -620,6 +894,17 @@ var rules_default = {
|
|
|
620
894
|
name: "Harmony",
|
|
621
895
|
description: "Gain 5 additional points if your kingdom is complete (no discarded dominoes).",
|
|
622
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" }]
|
|
623
908
|
}
|
|
624
909
|
]
|
|
625
910
|
};
|
|
@@ -637,14 +922,16 @@ var jsonRules = () => {
|
|
|
637
922
|
lords: rule.lordsPerPlayer,
|
|
638
923
|
maxDominoes: rule.maxDominoes,
|
|
639
924
|
dominoesPerTurn: rule.dominoesPerTurn,
|
|
640
|
-
maxTurns: rule.maxTurns
|
|
925
|
+
maxTurns: rule.maxTurns,
|
|
926
|
+
maxKingdomSize: rule.maxKingdomSize
|
|
641
927
|
};
|
|
642
928
|
});
|
|
643
929
|
extraRules.forEach((rule) => {
|
|
644
930
|
rules.extraRules.push({
|
|
645
931
|
name: rule.name,
|
|
646
932
|
description: rule.description,
|
|
647
|
-
mode: rule.mode
|
|
933
|
+
mode: rule.mode,
|
|
934
|
+
..."playersLimit" in rule && { playersLimit: rule.playersLimit }
|
|
648
935
|
});
|
|
649
936
|
});
|
|
650
937
|
return {
|
|
@@ -668,35 +955,8 @@ var shuffleMethod = (array) => {
|
|
|
668
955
|
};
|
|
669
956
|
|
|
670
957
|
// src/adapterServerside/uuid.ts
|
|
671
|
-
import { randomUUID } from "crypto";
|
|
672
958
|
var uuidMethod = () => {
|
|
673
|
-
return randomUUID();
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
// src/adapterServerside/consoleLogger.ts
|
|
677
|
-
var consoleLogger = (isLoggingEnabled) => {
|
|
678
|
-
const formatMessage = (level, message) => {
|
|
679
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
680
|
-
return `${timestamp} [KDomino_Engine] ${level}: ${message}`;
|
|
681
|
-
};
|
|
682
|
-
return {
|
|
683
|
-
info: (message) => {
|
|
684
|
-
if (isLoggingEnabled) {
|
|
685
|
-
console.log(formatMessage("info", message));
|
|
686
|
-
}
|
|
687
|
-
},
|
|
688
|
-
error: (message) => {
|
|
689
|
-
if (isLoggingEnabled) {
|
|
690
|
-
console.error(formatMessage("error", message));
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
};
|
|
695
|
-
var silentLogger = {
|
|
696
|
-
info: () => {
|
|
697
|
-
},
|
|
698
|
-
error: () => {
|
|
699
|
-
}
|
|
959
|
+
return globalThis.crypto.randomUUID();
|
|
700
960
|
};
|
|
701
961
|
|
|
702
962
|
// src/core/domain/errors/domainErrors.ts
|
|
@@ -718,11 +978,15 @@ var ErrorCode = {
|
|
|
718
978
|
PLACEMENT_NOT_ADJACENT: "PLACEMENT_NOT_ADJACENT",
|
|
719
979
|
PLACEMENT_INVALID_TERRAIN: "PLACEMENT_INVALID_TERRAIN",
|
|
720
980
|
PLACEMENT_OUT_OF_BOUNDS: "PLACEMENT_OUT_OF_BOUNDS",
|
|
981
|
+
PLACEMENT_EXCEEDS_KINGDOM_SIZE: "PLACEMENT_EXCEEDS_KINGDOM_SIZE",
|
|
721
982
|
// Action errors
|
|
722
983
|
NOT_YOUR_TURN: "NOT_YOUR_TURN",
|
|
723
984
|
CANNOT_PICK: "CANNOT_PICK",
|
|
724
985
|
CANNOT_PLACE: "CANNOT_PLACE",
|
|
725
|
-
DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED"
|
|
986
|
+
DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED",
|
|
987
|
+
// History errors
|
|
988
|
+
UNDO_UNAVAILABLE: "UNDO_UNAVAILABLE",
|
|
989
|
+
REDO_UNAVAILABLE: "REDO_UNAVAILABLE"
|
|
726
990
|
};
|
|
727
991
|
var createDomainError = (code, message, context) => ({
|
|
728
992
|
code,
|
|
@@ -750,9 +1014,9 @@ var InvalidStepError = class extends DomainException {
|
|
|
750
1014
|
}
|
|
751
1015
|
};
|
|
752
1016
|
var StepExecutionError = class extends DomainException {
|
|
753
|
-
constructor(message, context) {
|
|
1017
|
+
constructor(message, code = ErrorCode.STEP_EXECUTION_FAILED, context) {
|
|
754
1018
|
super({
|
|
755
|
-
code
|
|
1019
|
+
code,
|
|
756
1020
|
message,
|
|
757
1021
|
context
|
|
758
1022
|
});
|
|
@@ -789,133 +1053,345 @@ var ValidationError = class extends DomainException {
|
|
|
789
1053
|
this.name = "ValidationError";
|
|
790
1054
|
}
|
|
791
1055
|
};
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
start: "start",
|
|
801
|
-
/** Game has ended, results available */
|
|
802
|
-
result: "result"
|
|
803
|
-
};
|
|
804
|
-
var isGameWithNextAction = (game) => {
|
|
805
|
-
return game.nextAction.type === "action";
|
|
806
|
-
};
|
|
807
|
-
var isGameWithNextStep = (game) => {
|
|
808
|
-
return game.nextAction.type === "step";
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
// src/utils/result.ts
|
|
812
|
-
var ok = (value) => ({ tag: "Ok", value });
|
|
813
|
-
var err = (error) => ({ tag: "Err", error });
|
|
814
|
-
var isOk = (result) => result.tag === "Ok";
|
|
815
|
-
var isErr = (result) => result.tag === "Err";
|
|
816
|
-
var unwrap = (result) => {
|
|
817
|
-
if (result.tag === "Err") {
|
|
818
|
-
throw result.error;
|
|
819
|
-
}
|
|
820
|
-
return result.value;
|
|
821
|
-
};
|
|
822
|
-
|
|
823
|
-
// src/application/handlers/addExtraRulesHandler.ts
|
|
824
|
-
var addExtraRulesHandler = (logger, useCase) => (command) => {
|
|
825
|
-
const { game, extraRules } = command;
|
|
826
|
-
logger.info(`Adding extra rules to game: ${game.id}`);
|
|
827
|
-
logger.info(`Extra rules: ${extraRules}`);
|
|
828
|
-
if (!isGameWithNextStep(game)) {
|
|
829
|
-
logger.error(
|
|
830
|
-
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
831
|
-
);
|
|
832
|
-
throw new InvalidStepError("Required game with nextAction type: 'step'");
|
|
833
|
-
}
|
|
834
|
-
if (game.nextAction.step !== "options") {
|
|
835
|
-
logger.error(
|
|
836
|
-
`Required game with options step but got: ${game.nextAction.step}`
|
|
837
|
-
);
|
|
838
|
-
throw new InvalidStepError("Required game with options step");
|
|
839
|
-
}
|
|
840
|
-
const result = useCase(game, extraRules);
|
|
841
|
-
if (isErr(result)) {
|
|
842
|
-
logger.error(`Error adding extra rules: ${result.error}`);
|
|
843
|
-
throw new StepExecutionError(result.error);
|
|
844
|
-
}
|
|
845
|
-
logger.info(`Extra rules added to game: ${game.id}`);
|
|
846
|
-
return result.value;
|
|
847
|
-
};
|
|
848
|
-
|
|
849
|
-
// src/application/handlers/addPlayersHandler.ts
|
|
850
|
-
var addPlayersHandler = (logger, useCase) => (command) => {
|
|
851
|
-
const { game, players } = command;
|
|
852
|
-
logger.info(`Adding players to game: ${game.id}`);
|
|
853
|
-
if (!isGameWithNextStep(game)) {
|
|
854
|
-
logger.error(
|
|
855
|
-
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
856
|
-
);
|
|
857
|
-
throw new InvalidStepError("Required game with nextAction type: 'step'");
|
|
858
|
-
}
|
|
859
|
-
if (game.nextAction.step !== "addPlayers") {
|
|
860
|
-
logger.error(
|
|
861
|
-
`Required game with addPlayers step but got: ${game.nextAction.step}`
|
|
862
|
-
);
|
|
863
|
-
throw new InvalidStepError("Required game with addPlayers step");
|
|
864
|
-
}
|
|
865
|
-
const result = useCase(game, players);
|
|
866
|
-
if (isErr(result)) {
|
|
867
|
-
logger.error(`Error adding players: ${result.error}`);
|
|
868
|
-
throw new StepExecutionError(result.error);
|
|
869
|
-
}
|
|
870
|
-
logger.info(`Players added to game: ${game.id}`);
|
|
871
|
-
return result.value;
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
// src/application/handlers/chooseDominoHandler.ts
|
|
875
|
-
var chooseDominoHandler = (logger, useCase) => (command) => {
|
|
876
|
-
const { game, lordId, dominoPick } = command;
|
|
877
|
-
logger.info(
|
|
878
|
-
`Choosing domino: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
879
|
-
);
|
|
880
|
-
if (!isGameWithNextAction(game)) {
|
|
881
|
-
logger.error(
|
|
882
|
-
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
883
|
-
);
|
|
884
|
-
throw new InvalidStepError(
|
|
885
|
-
"Required game with nextAction type: 'action'"
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
if (game.nextAction.nextAction !== "pickDomino") {
|
|
889
|
-
logger.error(
|
|
890
|
-
`Required game with pickDomino action but got: ${game.nextAction.nextAction}`
|
|
891
|
-
);
|
|
892
|
-
throw new InvalidStepError("Required game with pickDomino action");
|
|
893
|
-
}
|
|
894
|
-
const result = useCase(game, lordId, dominoPick);
|
|
895
|
-
if (isErr(result)) {
|
|
896
|
-
logger.error(`Error choosing domino: ${result.error}`);
|
|
897
|
-
throw new ActionExecutionError(result.error);
|
|
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";
|
|
898
1064
|
}
|
|
899
|
-
logger.info(
|
|
900
|
-
`Domino chosen: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
901
|
-
);
|
|
902
|
-
return result.value;
|
|
903
1065
|
};
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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";
|
|
912
1074
|
}
|
|
913
|
-
logger.info(`Game created: ${result.value.id}`);
|
|
914
|
-
return result.value;
|
|
915
1075
|
};
|
|
916
1076
|
|
|
917
|
-
// src/
|
|
918
|
-
var
|
|
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) => {
|
|
919
1395
|
const { game, lordId } = command;
|
|
920
1396
|
logger.info(`Discarding domino for lord: ${lordId} in game: ${game.id}`);
|
|
921
1397
|
if (!isGameWithNextAction(game)) {
|
|
@@ -923,19 +1399,32 @@ var discardDominoHandler = (logger, useCase) => (command) => {
|
|
|
923
1399
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
924
1400
|
);
|
|
925
1401
|
throw new InvalidStepError(
|
|
926
|
-
|
|
1402
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1403
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
927
1404
|
);
|
|
928
1405
|
}
|
|
929
1406
|
if (game.nextAction.nextAction !== "placeDomino") {
|
|
930
1407
|
logger.error(
|
|
931
1408
|
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
932
1409
|
);
|
|
933
|
-
throw new InvalidStepError(
|
|
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
|
+
);
|
|
934
1418
|
}
|
|
935
1419
|
const result = useCase(game, lordId);
|
|
936
1420
|
if (isErr(result)) {
|
|
937
1421
|
logger.error(`Error discarding domino: ${result.error}`);
|
|
938
|
-
|
|
1422
|
+
const code = result.error;
|
|
1423
|
+
throw new ActionExecutionError(
|
|
1424
|
+
translateErrorCode(translator, code),
|
|
1425
|
+
code,
|
|
1426
|
+
{ gameId: game.id, lordId: command.lordId }
|
|
1427
|
+
);
|
|
939
1428
|
}
|
|
940
1429
|
if (isGameWithNextAction(result.value)) {
|
|
941
1430
|
logger.info(`Domino discarded: ${lordId} in game: ${game.id}`);
|
|
@@ -945,8 +1434,20 @@ var discardDominoHandler = (logger, useCase) => (command) => {
|
|
|
945
1434
|
return result.value;
|
|
946
1435
|
};
|
|
947
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
|
+
|
|
948
1449
|
// src/application/handlers/getExtraRulesHandler.ts
|
|
949
|
-
var getExtraRulesHandler = (logger, useCase) => (command) => {
|
|
1450
|
+
var getExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
950
1451
|
const { mode, players } = command;
|
|
951
1452
|
logger.info(
|
|
952
1453
|
`Getting extra rules for mode: ${mode} and players: ${players}`
|
|
@@ -954,26 +1455,30 @@ var getExtraRulesHandler = (logger, useCase) => (command) => {
|
|
|
954
1455
|
const result = useCase(mode, players);
|
|
955
1456
|
if (isErr(result)) {
|
|
956
1457
|
logger.error(`Error getting extra rules: ${result.error}`);
|
|
957
|
-
|
|
1458
|
+
const code = result.error;
|
|
1459
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1460
|
+
mode: command.mode
|
|
1461
|
+
});
|
|
958
1462
|
}
|
|
959
1463
|
logger.info(`Extra rules retrieved: ${result.value}`);
|
|
960
1464
|
return result.value;
|
|
961
1465
|
};
|
|
962
1466
|
|
|
963
1467
|
// src/application/handlers/getModesHandler.ts
|
|
964
|
-
var getModesHandler = (logger, useCase) => (_command) => {
|
|
1468
|
+
var getModesHandler = (logger, translator, useCase) => (_command) => {
|
|
965
1469
|
logger.info("Getting modes");
|
|
966
1470
|
const result = useCase();
|
|
967
1471
|
if (isErr(result)) {
|
|
968
1472
|
logger.error(`Error getting modes: ${result.error}`);
|
|
969
|
-
|
|
1473
|
+
const code = result.error;
|
|
1474
|
+
throw new NotFoundError(translateErrorCode(translator, code), code);
|
|
970
1475
|
}
|
|
971
1476
|
logger.info(`Modes retrieved: ${result.value}`);
|
|
972
1477
|
return result.value;
|
|
973
1478
|
};
|
|
974
1479
|
|
|
975
1480
|
// src/application/handlers/getResultHandler.ts
|
|
976
|
-
var getResultHandler = (logger, useCases) => (command) => {
|
|
1481
|
+
var getResultHandler = (logger, translator, useCases) => (command) => {
|
|
977
1482
|
const { getResultUseCase: getResultUseCase2, calculateScoreUseCase: calculateScoreUseCase2 } = useCases;
|
|
978
1483
|
const { game } = command;
|
|
979
1484
|
logger.info(`Getting result for game: ${game.id}`);
|
|
@@ -981,22 +1486,34 @@ var getResultHandler = (logger, useCases) => (command) => {
|
|
|
981
1486
|
logger.error(
|
|
982
1487
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
983
1488
|
);
|
|
984
|
-
throw new InvalidStepError(
|
|
1489
|
+
throw new InvalidStepError(
|
|
1490
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1491
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1492
|
+
);
|
|
985
1493
|
}
|
|
986
1494
|
if (game.nextAction.step !== "result") {
|
|
987
1495
|
logger.error(
|
|
988
1496
|
`Required game with result step but got: ${game.nextAction.step}`
|
|
989
1497
|
);
|
|
990
|
-
throw new InvalidStepError(
|
|
1498
|
+
throw new InvalidStepError(
|
|
1499
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1500
|
+
{ expected: "result", actual: game.nextAction.step, gameId: game.id }
|
|
1501
|
+
);
|
|
991
1502
|
}
|
|
992
|
-
let scoreResult;
|
|
993
1503
|
const { players } = game;
|
|
994
|
-
scoreResult = players.map((player) => {
|
|
1504
|
+
const scoreResult = players.map((player) => {
|
|
995
1505
|
const { kingdom } = player;
|
|
996
1506
|
const result2 = calculateScoreUseCase2(kingdom);
|
|
997
1507
|
if (isErr(result2)) {
|
|
998
1508
|
logger.error(`Error when calculating score: ${result2.error}`);
|
|
999
|
-
|
|
1509
|
+
const code = result2.error;
|
|
1510
|
+
throw new StepExecutionError(
|
|
1511
|
+
translateErrorCode(translator, code),
|
|
1512
|
+
code,
|
|
1513
|
+
{
|
|
1514
|
+
gameId: game.id
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1000
1517
|
}
|
|
1001
1518
|
return {
|
|
1002
1519
|
playerId: player.id,
|
|
@@ -1009,14 +1526,33 @@ var getResultHandler = (logger, useCases) => (command) => {
|
|
|
1009
1526
|
const result = getResultUseCase2(game, scoreResult);
|
|
1010
1527
|
if (isErr(result)) {
|
|
1011
1528
|
logger.error(`Error getting result: ${result.error}`);
|
|
1012
|
-
|
|
1529
|
+
const code = result.error;
|
|
1530
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1531
|
+
gameId: game.id
|
|
1532
|
+
});
|
|
1013
1533
|
}
|
|
1014
1534
|
logger.info(`Result retrieved for game: ${game.id}`);
|
|
1015
1535
|
return result.value;
|
|
1016
1536
|
};
|
|
1017
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
|
+
|
|
1018
1554
|
// src/application/handlers/placeDominoHandler.ts
|
|
1019
|
-
var placeDominoHandler = (logger, useCase) => (command) => {
|
|
1555
|
+
var placeDominoHandler = (logger, translator, useCase) => (command) => {
|
|
1020
1556
|
const { game, lordId, position, rotation } = command;
|
|
1021
1557
|
logger.info(
|
|
1022
1558
|
`Placing domino: ${lordId} in position: ${JSON.stringify(
|
|
@@ -1028,19 +1564,37 @@ var placeDominoHandler = (logger, useCase) => (command) => {
|
|
|
1028
1564
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
1029
1565
|
);
|
|
1030
1566
|
throw new InvalidStepError(
|
|
1031
|
-
|
|
1567
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1568
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
1032
1569
|
);
|
|
1033
1570
|
}
|
|
1034
1571
|
if (game.nextAction.nextAction !== "placeDomino") {
|
|
1035
1572
|
logger.error(
|
|
1036
1573
|
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
1037
1574
|
);
|
|
1038
|
-
throw new InvalidStepError(
|
|
1039
|
-
|
|
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
|
+
}
|
|
1040
1584
|
const result = useCase(game, lordId, position, rotation);
|
|
1041
1585
|
if (isErr(result)) {
|
|
1042
1586
|
logger.error(`Error placing domino: ${result.error}`);
|
|
1043
|
-
|
|
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
|
+
);
|
|
1044
1598
|
}
|
|
1045
1599
|
if (isGameWithNextAction(result.value)) {
|
|
1046
1600
|
logger.info(
|
|
@@ -1058,48 +1612,113 @@ var placeDominoHandler = (logger, useCase) => (command) => {
|
|
|
1058
1612
|
return result.value;
|
|
1059
1613
|
};
|
|
1060
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
|
+
|
|
1061
1681
|
// src/application/handlers/startGameHandler.ts
|
|
1062
|
-
var startGameHandler = (logger, useCase) => (command) => {
|
|
1682
|
+
var startGameHandler = (logger, translator, useCase) => (command) => {
|
|
1063
1683
|
const { game } = command;
|
|
1064
1684
|
logger.info(`Starting game: ${game.id}`);
|
|
1065
1685
|
if (!isGameWithNextStep(game)) {
|
|
1066
1686
|
logger.error(
|
|
1067
1687
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1068
1688
|
);
|
|
1069
|
-
throw new InvalidStepError(
|
|
1689
|
+
throw new InvalidStepError(
|
|
1690
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1691
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1692
|
+
);
|
|
1070
1693
|
}
|
|
1071
1694
|
if (game.nextAction.step !== "start" && game.nextAction.step !== "options") {
|
|
1072
1695
|
logger.error(
|
|
1073
1696
|
`Required game with start step or options step but got: ${game.nextAction.step}`
|
|
1074
1697
|
);
|
|
1075
1698
|
throw new InvalidStepError(
|
|
1076
|
-
|
|
1699
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1700
|
+
{
|
|
1701
|
+
expected: "start|options",
|
|
1702
|
+
actual: game.nextAction.step,
|
|
1703
|
+
gameId: game.id
|
|
1704
|
+
}
|
|
1077
1705
|
);
|
|
1078
1706
|
}
|
|
1079
1707
|
const result = useCase(game);
|
|
1080
1708
|
if (isErr(result)) {
|
|
1081
1709
|
logger.error(`Error starting game: ${result.error}`);
|
|
1082
|
-
|
|
1710
|
+
const code = result.error;
|
|
1711
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1712
|
+
gameId: game.id
|
|
1713
|
+
});
|
|
1083
1714
|
}
|
|
1084
1715
|
logger.info(`Game started: ${game.id}`);
|
|
1085
1716
|
return result.value;
|
|
1086
1717
|
};
|
|
1087
1718
|
|
|
1088
|
-
// src/application/handlers/calculateScoreHandler.ts
|
|
1089
|
-
var calculateScoreHandler = (logger, useCase) => (command) => {
|
|
1090
|
-
const { kingdom } = command;
|
|
1091
|
-
logger.info(`Calculating score for kingdom`);
|
|
1092
|
-
const result = useCase(kingdom);
|
|
1093
|
-
if (isErr(result)) {
|
|
1094
|
-
logger.error(`Error calculating score: ${result.error}`);
|
|
1095
|
-
throw new Error(result.error);
|
|
1096
|
-
}
|
|
1097
|
-
logger.info(`Score calculated: ${result.value}`);
|
|
1098
|
-
return result.value;
|
|
1099
|
-
};
|
|
1100
|
-
|
|
1101
1719
|
// src/core/domain/types/kingdom.ts
|
|
1102
1720
|
var GRIDSIZE = 9;
|
|
1721
|
+
var MAX_KINGDOM_SIZE = 5;
|
|
1103
1722
|
|
|
1104
1723
|
// src/core/domain/types/domino.ts
|
|
1105
1724
|
var GROUND = [
|
|
@@ -1122,6 +1741,101 @@ var playerActions = {
|
|
|
1122
1741
|
pass: "pass"
|
|
1123
1742
|
};
|
|
1124
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
|
+
|
|
1125
1839
|
// src/core/domain/entities/domino.ts
|
|
1126
1840
|
var isGround = (ground) => {
|
|
1127
1841
|
return GROUND.includes(ground);
|
|
@@ -1155,7 +1869,7 @@ var placeTile = (kingdom, position, tile) => {
|
|
|
1155
1869
|
var getTile = (kingdom, position) => {
|
|
1156
1870
|
const tile = kingdom[position.y]?.[position.x];
|
|
1157
1871
|
if (!tile) {
|
|
1158
|
-
return err(
|
|
1872
|
+
return err(ErrorCode.PLACEMENT_OUT_OF_BOUNDS);
|
|
1159
1873
|
}
|
|
1160
1874
|
return ok(tile);
|
|
1161
1875
|
};
|
|
@@ -1230,7 +1944,7 @@ var calculateDominoPosition = (position, rotation, domino) => {
|
|
|
1230
1944
|
}
|
|
1231
1945
|
return [firstTile, secondTile];
|
|
1232
1946
|
};
|
|
1233
|
-
var placeDomino = (kingdom, position, rotation, domino) => {
|
|
1947
|
+
var placeDomino = (kingdom, position, rotation, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
1234
1948
|
const [firstTile, secondTile] = calculateDominoPosition(
|
|
1235
1949
|
position,
|
|
1236
1950
|
rotation,
|
|
@@ -1244,20 +1958,23 @@ var placeDomino = (kingdom, position, rotation, domino) => {
|
|
|
1244
1958
|
if (isErr(isFreePlaceResult)) {
|
|
1245
1959
|
return isFreePlaceResult;
|
|
1246
1960
|
}
|
|
1961
|
+
if (exceedsMaxSize(kingdom, firstTile.position, secondTile.position, maxKingdomSize)) {
|
|
1962
|
+
return err(ErrorCode.PLACEMENT_EXCEEDS_KINGDOM_SIZE);
|
|
1963
|
+
}
|
|
1247
1964
|
const adjacentTiles = isAdjacent(
|
|
1248
1965
|
kingdom,
|
|
1249
1966
|
firstTile.position,
|
|
1250
1967
|
secondTile.position
|
|
1251
1968
|
);
|
|
1252
1969
|
if (adjacentTiles.length === 0) {
|
|
1253
|
-
return err(
|
|
1970
|
+
return err(ErrorCode.PLACEMENT_NOT_ADJACENT);
|
|
1254
1971
|
}
|
|
1255
1972
|
const hasValidAdjacentTiles = hasValidAdjacent(adjacentTiles, [
|
|
1256
1973
|
firstTile.tile,
|
|
1257
1974
|
secondTile.tile
|
|
1258
1975
|
]);
|
|
1259
1976
|
if (!hasValidAdjacentTiles) {
|
|
1260
|
-
return err(
|
|
1977
|
+
return err(ErrorCode.PLACEMENT_INVALID_TERRAIN);
|
|
1261
1978
|
}
|
|
1262
1979
|
return ok(placeTiles(kingdom, [firstTile, secondTile]));
|
|
1263
1980
|
};
|
|
@@ -1278,46 +1995,30 @@ var isFreePlace = (kingdom, firstPosition, secondPosition) => {
|
|
|
1278
1995
|
return secondTile;
|
|
1279
1996
|
}
|
|
1280
1997
|
if (firstTile.value.type !== "empty" || secondTile.value.type !== "empty") {
|
|
1281
|
-
return err(
|
|
1998
|
+
return err(ErrorCode.PLACEMENT_NOT_EMPTY);
|
|
1282
1999
|
}
|
|
1283
2000
|
return ok(true);
|
|
1284
2001
|
};
|
|
1285
2002
|
var isAdjacent = (kingdom, firstPosition, secondPosition) => {
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const adjacentTilesKeys = [
|
|
1292
|
-
{ x: xFirstPosition - 1, y: yFirstPosition },
|
|
1293
|
-
{ x: xFirstPosition, y: yFirstPosition - 1 },
|
|
1294
|
-
{ x: xFirstPosition + 1, y: yFirstPosition },
|
|
1295
|
-
{ x: xFirstPosition, y: yFirstPosition + 1 },
|
|
1296
|
-
{ x: xSecondPosition - 1, y: ySecondPosition },
|
|
1297
|
-
{ x: xSecondPosition, y: ySecondPosition - 1 },
|
|
1298
|
-
{ x: xSecondPosition + 1, y: ySecondPosition },
|
|
1299
|
-
{ x: xSecondPosition, y: ySecondPosition + 1 }
|
|
2003
|
+
const offsets = [
|
|
2004
|
+
{ x: -1, y: 0 },
|
|
2005
|
+
{ x: 1, y: 0 },
|
|
2006
|
+
{ x: 0, y: -1 },
|
|
2007
|
+
{ x: 0, y: 1 }
|
|
1300
2008
|
];
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
}),
|
|
1311
|
-
1
|
|
1312
|
-
);
|
|
1313
|
-
adjacentTilesKeys.forEach((key) => {
|
|
1314
|
-
const tile = getTile(kingdom, key);
|
|
1315
|
-
if (isOk(tile)) {
|
|
1316
|
-
if (tile.value.type !== "empty") {
|
|
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") {
|
|
1317
2018
|
adjacentTiles.push(tile.value);
|
|
1318
2019
|
}
|
|
1319
2020
|
}
|
|
1320
|
-
}
|
|
2021
|
+
}
|
|
1321
2022
|
return adjacentTiles;
|
|
1322
2023
|
};
|
|
1323
2024
|
var hasValidAdjacent = (adjacentTiles, tiles) => {
|
|
@@ -1341,21 +2042,43 @@ var checkCastleIsInMiddle = (kingdom) => {
|
|
|
1341
2042
|
if (castleX === void 0 || castleY === void 0) {
|
|
1342
2043
|
return false;
|
|
1343
2044
|
}
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
+
}
|
|
1350
2064
|
}
|
|
1351
2065
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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;
|
|
1357
2080
|
}
|
|
1358
|
-
return
|
|
2081
|
+
return maxX - minX + 1 > maxSize || maxY - minY + 1 > maxSize;
|
|
1359
2082
|
};
|
|
1360
2083
|
var countDominoes = (kingdom) => {
|
|
1361
2084
|
const notEmpties = kingdom.flat().filter((tile) => tile.type !== "empty");
|
|
@@ -1365,233 +2088,142 @@ var deepCopyKingdom = (kingdom) => {
|
|
|
1365
2088
|
return kingdom.map((line) => line.map((tile) => ({ ...tile })));
|
|
1366
2089
|
};
|
|
1367
2090
|
|
|
1368
|
-
// src/core/
|
|
1369
|
-
var
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
for (let x = 0; x < GRIDSIZE; x++) {
|
|
1374
|
-
const position = { x, y };
|
|
1375
|
-
for (const rotation of rotations) {
|
|
1376
|
-
const result = placeDomino(kingdom, position, rotation, domino);
|
|
1377
|
-
if (isOk(result)) {
|
|
1378
|
-
validPlacements.push({ position, rotation });
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
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);
|
|
1382
2096
|
}
|
|
1383
|
-
return
|
|
2097
|
+
return ok(name);
|
|
1384
2098
|
};
|
|
1385
|
-
var
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
const position = { x, y };
|
|
1390
|
-
for (const rotation of rotations) {
|
|
1391
|
-
const result = placeDomino(kingdom, position, rotation, domino);
|
|
1392
|
-
if (isOk(result)) {
|
|
1393
|
-
return true;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
2099
|
+
var createPlayer = (playerName, id, kingdom, bot) => {
|
|
2100
|
+
const name = validatePlayer(playerName);
|
|
2101
|
+
if (isErr(name)) {
|
|
2102
|
+
return name;
|
|
1397
2103
|
}
|
|
1398
|
-
return
|
|
2104
|
+
return ok({
|
|
2105
|
+
name: name.value,
|
|
2106
|
+
id,
|
|
2107
|
+
kingdom,
|
|
2108
|
+
...bot && { bot }
|
|
2109
|
+
});
|
|
1399
2110
|
};
|
|
1400
2111
|
|
|
1401
|
-
// src/
|
|
1402
|
-
var
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
return placements;
|
|
1408
|
-
};
|
|
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 };
|
|
1409
2118
|
};
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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"
|
|
1418
2149
|
};
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2150
|
+
const updatedGame = {
|
|
2151
|
+
...game,
|
|
2152
|
+
players: newPlayers,
|
|
2153
|
+
dominoes: splicedDominoes,
|
|
2154
|
+
rules: {
|
|
2155
|
+
...game.rules,
|
|
2156
|
+
basic: basicRules
|
|
2157
|
+
},
|
|
2158
|
+
nextAction: next
|
|
1428
2159
|
};
|
|
1429
|
-
return
|
|
1430
|
-
};
|
|
1431
|
-
var deserializeGame = (json) => {
|
|
1432
|
-
try {
|
|
1433
|
-
const parsed = JSON.parse(json);
|
|
1434
|
-
if (!parsed.version || !parsed.data) {
|
|
1435
|
-
return err("Invalid serialized game format");
|
|
1436
|
-
}
|
|
1437
|
-
if (parsed.version > SERIALIZATION_VERSION) {
|
|
1438
|
-
return err(
|
|
1439
|
-
`Unsupported serialization version: ${parsed.version}. Current version: ${SERIALIZATION_VERSION}`
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
const game = parsed.data;
|
|
1443
|
-
if (!game.id || !game.players || !game.lords || !game.nextAction) {
|
|
1444
|
-
return err("Invalid game state structure");
|
|
1445
|
-
}
|
|
1446
|
-
return ok(game);
|
|
1447
|
-
} catch (error) {
|
|
1448
|
-
return err(`Failed to parse game JSON: ${error.message}`);
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1451
|
-
var createSavePoint = (game) => {
|
|
1452
|
-
return {
|
|
1453
|
-
serialized: serializeGame(game),
|
|
1454
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1455
|
-
gameId: game.id,
|
|
1456
|
-
turn: game.turn
|
|
1457
|
-
};
|
|
1458
|
-
};
|
|
1459
|
-
var restoreFromSavePoint = (savePoint) => {
|
|
1460
|
-
return deserializeGame(savePoint.serialized);
|
|
2160
|
+
return ok(updatedGame);
|
|
1461
2161
|
};
|
|
1462
2162
|
|
|
1463
|
-
// src/
|
|
1464
|
-
var
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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 });
|
|
1479
2201
|
}
|
|
1480
|
-
const game = unwrap(result);
|
|
1481
|
-
logger.info(`Game deserialized successfully: ${game.id}`);
|
|
1482
|
-
return game;
|
|
1483
|
-
};
|
|
1484
|
-
};
|
|
1485
|
-
|
|
1486
|
-
// src/core/domain/entities/rule.ts
|
|
1487
|
-
var getAvailablesExtraRules = (extraRules, mode, numberOfPlayers) => {
|
|
1488
|
-
const availableExtraRules = extraRules.filter((rule) => {
|
|
1489
|
-
return rule.mode.filter((m) => m.name === mode).length > 0 && (!rule.playersLimit || rule.playersLimit === numberOfPlayers);
|
|
1490
|
-
});
|
|
1491
|
-
if (availableExtraRules.length === 0) {
|
|
1492
|
-
return err("No extra rules found");
|
|
1493
2202
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
2203
|
+
if (properties.length === 0) {
|
|
2204
|
+
return ok({
|
|
2205
|
+
points: 0,
|
|
2206
|
+
maxPropertiesSize: 0,
|
|
2207
|
+
totalCrowns: 0
|
|
2208
|
+
});
|
|
1500
2209
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const newExtraRules = [];
|
|
1509
|
-
for (const rule of rules) {
|
|
1510
|
-
const result = toExtraRule(rule, availableExtraRules);
|
|
1511
|
-
if (isErr(result)) {
|
|
1512
|
-
return result;
|
|
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;
|
|
1513
2217
|
}
|
|
1514
|
-
|
|
1515
|
-
}
|
|
1516
|
-
const updatedRules = {
|
|
1517
|
-
basic: game.rules.basic,
|
|
1518
|
-
extra: newExtraRules
|
|
1519
|
-
};
|
|
1520
|
-
const next = {
|
|
1521
|
-
type: "step",
|
|
1522
|
-
step: gameSteps.start
|
|
1523
|
-
};
|
|
1524
|
-
const updatedGame = {
|
|
1525
|
-
...game,
|
|
1526
|
-
rules: updatedRules,
|
|
1527
|
-
nextAction: next
|
|
1528
|
-
};
|
|
1529
|
-
return ok(updatedGame);
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
// src/core/domain/entities/player.ts
|
|
1533
|
-
var MINPLAYERSNAMELENGTH = 3;
|
|
1534
|
-
var validatePlayer = (name) => {
|
|
1535
|
-
if (!name || name.trim().length < MINPLAYERSNAMELENGTH) {
|
|
1536
|
-
return err(`Invalid player name: ${name}`);
|
|
1537
|
-
}
|
|
1538
|
-
return ok(name);
|
|
1539
|
-
};
|
|
1540
|
-
var createPlayer = (playerName, id, kingdom) => {
|
|
1541
|
-
const name = validatePlayer(playerName);
|
|
1542
|
-
if (isErr(name)) {
|
|
1543
|
-
return name;
|
|
2218
|
+
totalCrowns += property.crowns;
|
|
1544
2219
|
}
|
|
1545
2220
|
return ok({
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2221
|
+
points,
|
|
2222
|
+
maxPropertiesSize,
|
|
2223
|
+
totalCrowns
|
|
1549
2224
|
});
|
|
1550
2225
|
};
|
|
1551
2226
|
|
|
1552
|
-
// src/core/useCases/addPlayers.ts
|
|
1553
|
-
var addPlayersUseCase = (deps) => (game, players) => {
|
|
1554
|
-
const { uuidMethod: uuidMethod2, shuffleMethod: shuffleMethod2, ruleRepository } = deps;
|
|
1555
|
-
if (!players || players.length < 2 || players.length > 4) {
|
|
1556
|
-
return err("Invalid number of players");
|
|
1557
|
-
}
|
|
1558
|
-
const newPlayers = [];
|
|
1559
|
-
for (const player of players) {
|
|
1560
|
-
let kingdom = createEmptyKingdom();
|
|
1561
|
-
kingdom = placeCastle(kingdom);
|
|
1562
|
-
let id = uuidMethod2();
|
|
1563
|
-
const newPlayer = createPlayer(player, id, kingdom);
|
|
1564
|
-
if (isErr(newPlayer)) {
|
|
1565
|
-
return newPlayer;
|
|
1566
|
-
}
|
|
1567
|
-
newPlayers.push(newPlayer.value);
|
|
1568
|
-
}
|
|
1569
|
-
const rules = ruleRepository.getAll();
|
|
1570
|
-
const basicRules = rules.basic[newPlayers.length];
|
|
1571
|
-
if (!basicRules) {
|
|
1572
|
-
return err("No base rules found");
|
|
1573
|
-
}
|
|
1574
|
-
const dominoes = game.dominoes;
|
|
1575
|
-
const { maxDominoes } = basicRules;
|
|
1576
|
-
const shuffledDominoes = shuffleMethod2(dominoes);
|
|
1577
|
-
const splicedDominoes = shuffledDominoes.slice(0, maxDominoes);
|
|
1578
|
-
const next = {
|
|
1579
|
-
type: "step",
|
|
1580
|
-
step: "options"
|
|
1581
|
-
};
|
|
1582
|
-
const updatedGame = {
|
|
1583
|
-
...game,
|
|
1584
|
-
players: newPlayers,
|
|
1585
|
-
dominoes: splicedDominoes,
|
|
1586
|
-
rules: {
|
|
1587
|
-
...game.rules,
|
|
1588
|
-
basic: basicRules
|
|
1589
|
-
},
|
|
1590
|
-
nextAction: next
|
|
1591
|
-
};
|
|
1592
|
-
return ok(updatedGame);
|
|
1593
|
-
};
|
|
1594
|
-
|
|
1595
2227
|
// src/core/domain/entities/lord.ts
|
|
1596
2228
|
var createLord = (id, playerId) => {
|
|
1597
2229
|
return {
|
|
@@ -1615,18 +2247,17 @@ var canPlaceAndDominoPickedIsDefined = (lord) => {
|
|
|
1615
2247
|
return !hasPick(lord) && !hasPlace(lord) && !hasTurnEnded(lord) && lord.dominoPicked !== void 0;
|
|
1616
2248
|
};
|
|
1617
2249
|
var updateLordOrder = (lords) => {
|
|
1618
|
-
return lords.sort((a, b) => {
|
|
1619
|
-
return a.dominoPicked
|
|
2250
|
+
return [...lords].sort((a, b) => {
|
|
2251
|
+
return (a.dominoPicked?.number ?? 0) - (b.dominoPicked?.number ?? 0);
|
|
1620
2252
|
});
|
|
1621
2253
|
};
|
|
1622
|
-
var resetLordsActions = (
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}
|
|
1629
|
-
return lordsCopy;
|
|
2254
|
+
var resetLordsActions = (lords) => {
|
|
2255
|
+
return lords.map((lord) => ({
|
|
2256
|
+
...lord,
|
|
2257
|
+
turnEnded: false,
|
|
2258
|
+
hasPick: false,
|
|
2259
|
+
hasPlace: false
|
|
2260
|
+
}));
|
|
1630
2261
|
};
|
|
1631
2262
|
var nextLordWithAction = (lords) => {
|
|
1632
2263
|
const nextLord = findNextLord(lords);
|
|
@@ -1653,38 +2284,29 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
|
1653
2284
|
(lord) => lord.id === game.nextAction.nextLord
|
|
1654
2285
|
);
|
|
1655
2286
|
if (!currentLord) {
|
|
1656
|
-
return err(
|
|
2287
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1657
2288
|
}
|
|
1658
2289
|
if (currentLord.id !== lordId) {
|
|
1659
|
-
return err(
|
|
2290
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1660
2291
|
}
|
|
1661
2292
|
if (!canPick(currentLord)) {
|
|
1662
|
-
return err(
|
|
2293
|
+
return err(ErrorCode.CANNOT_PICK);
|
|
1663
2294
|
}
|
|
1664
2295
|
const selectedDomino = game.currentDominoes.find(
|
|
1665
2296
|
(domino) => domino.domino.number === dominoPick
|
|
1666
2297
|
);
|
|
1667
2298
|
if (!selectedDomino) {
|
|
1668
|
-
return err(
|
|
2299
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
1669
2300
|
}
|
|
1670
2301
|
if (selectedDomino.picked) {
|
|
1671
|
-
return err(
|
|
2302
|
+
return err(ErrorCode.DOMINO_ALREADY_PICKED);
|
|
1672
2303
|
}
|
|
1673
|
-
const updatedCurrentDominoes = game.currentDominoes.map(
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
});
|
|
1680
|
-
const updatedLords = game.lords.map((lord) => {
|
|
1681
|
-
if (lord.id === currentLord.id) {
|
|
1682
|
-
lord.hasPick = true;
|
|
1683
|
-
lord.turnEnded = true;
|
|
1684
|
-
lord.dominoPicked = selectedDomino.domino;
|
|
1685
|
-
}
|
|
1686
|
-
return lord;
|
|
1687
|
-
});
|
|
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
|
+
);
|
|
1688
2310
|
const turnEnded = allLordsHavePlayed(updatedLords);
|
|
1689
2311
|
if (turnEnded) {
|
|
1690
2312
|
const lords = resetLordsActions(updateLordOrder(updatedLords));
|
|
@@ -1725,7 +2347,7 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
|
1725
2347
|
|
|
1726
2348
|
// src/core/domain/entities/game.ts
|
|
1727
2349
|
var create = (payload) => {
|
|
1728
|
-
const { id, mode, dominoes } = payload;
|
|
2350
|
+
const { id, mode, dominoes, seed } = payload;
|
|
1729
2351
|
return {
|
|
1730
2352
|
id,
|
|
1731
2353
|
dominoes,
|
|
@@ -1742,11 +2364,13 @@ var create = (payload) => {
|
|
|
1742
2364
|
lords: 0,
|
|
1743
2365
|
maxDominoes: 0,
|
|
1744
2366
|
dominoesPerTurn: 0,
|
|
1745
|
-
maxTurns: 0
|
|
2367
|
+
maxTurns: 0,
|
|
2368
|
+
maxKingdomSize: 5
|
|
1746
2369
|
},
|
|
1747
2370
|
extra: []
|
|
1748
2371
|
},
|
|
1749
|
-
mode
|
|
2372
|
+
mode,
|
|
2373
|
+
...seed !== void 0 && { seed }
|
|
1750
2374
|
};
|
|
1751
2375
|
};
|
|
1752
2376
|
|
|
@@ -1759,13 +2383,14 @@ var createMode = (mode, availableMode) => {
|
|
|
1759
2383
|
if (foundMode) {
|
|
1760
2384
|
return ok(foundMode);
|
|
1761
2385
|
}
|
|
1762
|
-
return err(
|
|
2386
|
+
return err(ErrorCode.MODE_NOT_FOUND);
|
|
1763
2387
|
};
|
|
1764
2388
|
|
|
1765
2389
|
// src/core/useCases/createGame.ts
|
|
1766
|
-
var createGameUseCase = (deps) => (mode) => {
|
|
2390
|
+
var createGameUseCase = (deps) => (mode, seed) => {
|
|
1767
2391
|
const { modeRepository, dominoesRepository, uuidMethod: uuidMethod2 } = deps;
|
|
1768
2392
|
const id = uuidMethod2();
|
|
2393
|
+
const gameSeed = seed ?? uuidMethod2();
|
|
1769
2394
|
const availableMode = modeRepository.getAvailables();
|
|
1770
2395
|
const newMode = createMode(mode, availableMode);
|
|
1771
2396
|
if (isErr(newMode)) {
|
|
@@ -1773,17 +2398,51 @@ var createGameUseCase = (deps) => (mode) => {
|
|
|
1773
2398
|
}
|
|
1774
2399
|
const dominoes = dominoesRepository.getForMode(newMode.value);
|
|
1775
2400
|
if (!dominoes || dominoes.length === 0) {
|
|
1776
|
-
return err(
|
|
2401
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
1777
2402
|
}
|
|
1778
2403
|
const payload = {
|
|
1779
2404
|
id,
|
|
1780
2405
|
mode: newMode.value,
|
|
1781
|
-
dominoes
|
|
2406
|
+
dominoes,
|
|
2407
|
+
seed: gameSeed
|
|
1782
2408
|
};
|
|
1783
2409
|
const newGame = create(payload);
|
|
1784
2410
|
return ok(newGame);
|
|
1785
2411
|
};
|
|
1786
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
|
+
|
|
1787
2446
|
// src/core/useCases/discardDomino.ts
|
|
1788
2447
|
var discardDominoUseCase = (game, lordId) => {
|
|
1789
2448
|
const nextAction = game.nextAction;
|
|
@@ -1791,13 +2450,21 @@ var discardDominoUseCase = (game, lordId) => {
|
|
|
1791
2450
|
(lord) => lord.id === nextAction.nextLord
|
|
1792
2451
|
);
|
|
1793
2452
|
if (!currentLord) {
|
|
1794
|
-
return err(
|
|
2453
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1795
2454
|
}
|
|
1796
2455
|
if (currentLord.id !== lordId) {
|
|
1797
|
-
return err(
|
|
2456
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1798
2457
|
}
|
|
1799
2458
|
if (!canPass(currentLord)) {
|
|
1800
|
-
return err(
|
|
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
|
+
}
|
|
1801
2468
|
}
|
|
1802
2469
|
const maxTurns = game.rules.basic.maxTurns;
|
|
1803
2470
|
const currentTurn = game.turn;
|
|
@@ -1839,12 +2506,48 @@ var discardDominoUseCase = (game, lordId) => {
|
|
|
1839
2506
|
return ok(updatedGame);
|
|
1840
2507
|
};
|
|
1841
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
|
+
|
|
1842
2545
|
// src/core/useCases/getExtraRules.ts
|
|
1843
2546
|
var getExtraRulesUseCase = (deps) => (mode, players) => {
|
|
1844
2547
|
const { ruleRepository } = deps;
|
|
1845
2548
|
const allExtraRules = ruleRepository.getAllExtra();
|
|
1846
2549
|
if (!allExtraRules) {
|
|
1847
|
-
return err(
|
|
2550
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1848
2551
|
}
|
|
1849
2552
|
return getAvailablesExtraRules(allExtraRules, mode, players);
|
|
1850
2553
|
};
|
|
@@ -1861,11 +2564,11 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1861
2564
|
const { basic, extra } = rules;
|
|
1862
2565
|
const finalScoreResult = scoreResult.map((score) => {
|
|
1863
2566
|
let finalScore = score.details.points;
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
}
|
|
2567
|
+
const player = game.players.find((p) => p.id === score.playerId);
|
|
2568
|
+
if (!player) {
|
|
2569
|
+
return score;
|
|
2570
|
+
}
|
|
2571
|
+
const { kingdom } = player;
|
|
1869
2572
|
if (extra.length > 0) {
|
|
1870
2573
|
extra.forEach((extraRule) => {
|
|
1871
2574
|
if (extraRule.name === "The middle Kingdom") {
|
|
@@ -1897,10 +2600,7 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1897
2600
|
if (a.details.points !== b.details.points) {
|
|
1898
2601
|
return b.details.points - a.details.points;
|
|
1899
2602
|
}
|
|
1900
|
-
|
|
1901
|
-
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
1902
|
-
}
|
|
1903
|
-
return b.details.totalCrowns - a.details.totalCrowns;
|
|
2603
|
+
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
1904
2604
|
});
|
|
1905
2605
|
const result = [];
|
|
1906
2606
|
let isTie = false;
|
|
@@ -1913,7 +2613,7 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1913
2613
|
for (let i = 1; i < finalScoreResult.length; i++) {
|
|
1914
2614
|
const currentScore = finalScoreResult[i];
|
|
1915
2615
|
const previousScore = finalScoreResult[i - 1];
|
|
1916
|
-
if (previousScore && currentScore && currentScore.details.points === previousScore.details.points && currentScore.details.maxPropertiesSize === previousScore.details.maxPropertiesSize
|
|
2616
|
+
if (previousScore && currentScore && currentScore.details.points === previousScore.details.points && currentScore.details.maxPropertiesSize === previousScore.details.maxPropertiesSize) {
|
|
1917
2617
|
isTie = true;
|
|
1918
2618
|
}
|
|
1919
2619
|
if (isTie) {
|
|
@@ -1942,26 +2642,27 @@ var placeDominoUseCase = (game, lordId, position, rotation) => {
|
|
|
1942
2642
|
(lord) => lord.id === nextAction.nextLord
|
|
1943
2643
|
);
|
|
1944
2644
|
if (!currentLord) {
|
|
1945
|
-
return err(
|
|
2645
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1946
2646
|
}
|
|
1947
2647
|
if (currentLord.id !== lordId) {
|
|
1948
|
-
return err(
|
|
2648
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1949
2649
|
}
|
|
1950
2650
|
if (!canPlaceAndDominoPickedIsDefined(currentLord)) {
|
|
1951
|
-
return err(
|
|
2651
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
1952
2652
|
}
|
|
1953
2653
|
const domino = currentLord.dominoPicked;
|
|
1954
2654
|
const currentPlayer = game.players.find(
|
|
1955
2655
|
(player) => player.id === currentLord.playerId
|
|
1956
2656
|
);
|
|
1957
2657
|
if (!currentPlayer) {
|
|
1958
|
-
return err(
|
|
2658
|
+
return err(ErrorCode.PLAYER_NOT_FOUND);
|
|
1959
2659
|
}
|
|
1960
2660
|
const updatedKingdom = placeDomino(
|
|
1961
2661
|
currentPlayer.kingdom,
|
|
1962
2662
|
position,
|
|
1963
2663
|
rotation,
|
|
1964
|
-
domino
|
|
2664
|
+
domino,
|
|
2665
|
+
game.rules.basic.maxKingdomSize
|
|
1965
2666
|
);
|
|
1966
2667
|
if (isErr(updatedKingdom)) {
|
|
1967
2668
|
return updatedKingdom;
|
|
@@ -2032,7 +2733,8 @@ var startGameUseCase = (deps) => (game) => {
|
|
|
2032
2733
|
newLords.push(lord);
|
|
2033
2734
|
}
|
|
2034
2735
|
}
|
|
2035
|
-
const
|
|
2736
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "lords") : shuffleMethod2;
|
|
2737
|
+
const shuffledLords = shuffle(newLords);
|
|
2036
2738
|
const dominoesCopy = [...dominoes];
|
|
2037
2739
|
const dominoesDrawn = dominoesCopy.splice(0, dominoesPerTurn);
|
|
2038
2740
|
dominoesDrawn.sort((a, b) => a.number - b.number);
|
|
@@ -2058,82 +2760,6 @@ var startGameUseCase = (deps) => (game) => {
|
|
|
2058
2760
|
return ok(newState);
|
|
2059
2761
|
};
|
|
2060
2762
|
|
|
2061
|
-
// src/core/useCases/calculateScore.ts
|
|
2062
|
-
var calculateScoreUseCase = (kingdom) => {
|
|
2063
|
-
const visited = kingdom.map((row) => row.map(() => false));
|
|
2064
|
-
const properties = [];
|
|
2065
|
-
const directions = [
|
|
2066
|
-
[-1, 0],
|
|
2067
|
-
[1, 0],
|
|
2068
|
-
[0, -1],
|
|
2069
|
-
[0, 1]
|
|
2070
|
-
];
|
|
2071
|
-
const markZone = (x, y, type, crowns) => {
|
|
2072
|
-
if (x < 0 || y < 0 || x >= kingdom[0].length || y >= kingdom.length || visited[y][x] || kingdom[y][x].type !== type || kingdom[y][x].type === "empty" || kingdom[y][x].type === "castle") {
|
|
2073
|
-
return;
|
|
2074
|
-
}
|
|
2075
|
-
visited[y][x] = true;
|
|
2076
|
-
const currentZone = properties.find(
|
|
2077
|
-
(property) => property.type === type && property.tiles.some(([tx, ty]) => {
|
|
2078
|
-
for (let [dx, dy] of directions) {
|
|
2079
|
-
if (tx + dx === x && ty + dy === y) {
|
|
2080
|
-
return true;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
return false;
|
|
2084
|
-
})
|
|
2085
|
-
);
|
|
2086
|
-
if (currentZone) {
|
|
2087
|
-
currentZone.size += 1;
|
|
2088
|
-
currentZone.tiles.push([x, y]);
|
|
2089
|
-
currentZone.crowns += crowns;
|
|
2090
|
-
} else {
|
|
2091
|
-
properties.push({
|
|
2092
|
-
type,
|
|
2093
|
-
crowns,
|
|
2094
|
-
size: 1,
|
|
2095
|
-
tiles: [[x, y]]
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
directions.forEach(([dx, dy]) => {
|
|
2099
|
-
const nx = x + dx;
|
|
2100
|
-
const ny = y + dy;
|
|
2101
|
-
if (nx >= 0 && ny >= 0 && nx < kingdom[0].length && ny < kingdom.length && kingdom[ny][nx].type === type) {
|
|
2102
|
-
markZone(nx, ny, kingdom[ny][nx].type, kingdom[ny][nx].crowns);
|
|
2103
|
-
}
|
|
2104
|
-
});
|
|
2105
|
-
};
|
|
2106
|
-
kingdom.forEach((row, y) => {
|
|
2107
|
-
row.forEach((tile, x) => {
|
|
2108
|
-
if (!visited[y][x]) {
|
|
2109
|
-
markZone(x, y, tile.type, tile.crowns);
|
|
2110
|
-
}
|
|
2111
|
-
});
|
|
2112
|
-
});
|
|
2113
|
-
if (properties.length === 0) {
|
|
2114
|
-
return ok({
|
|
2115
|
-
points: 0,
|
|
2116
|
-
maxPropertiesSize: 0,
|
|
2117
|
-
totalCrowns: 0
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
let points = properties.reduce((acc, property) => {
|
|
2121
|
-
return acc + property.size * property.crowns;
|
|
2122
|
-
}, 0);
|
|
2123
|
-
const maxPropertiesSize = Math.max(
|
|
2124
|
-
...properties.map((property) => property.size)
|
|
2125
|
-
);
|
|
2126
|
-
const totalCrowns = properties.reduce(
|
|
2127
|
-
(acc, property) => acc + property.crowns,
|
|
2128
|
-
0
|
|
2129
|
-
);
|
|
2130
|
-
return ok({
|
|
2131
|
-
points,
|
|
2132
|
-
maxPropertiesSize,
|
|
2133
|
-
totalCrowns
|
|
2134
|
-
});
|
|
2135
|
-
};
|
|
2136
|
-
|
|
2137
2763
|
// src/config.ts
|
|
2138
2764
|
var configureEngine = (config) => {
|
|
2139
2765
|
const modeRepository = jsonModes_default();
|
|
@@ -2141,10 +2767,12 @@ var configureEngine = (config) => {
|
|
|
2141
2767
|
const ruleRepository = jsonRules_default();
|
|
2142
2768
|
const uuid = config.uuidMethod || uuidMethod;
|
|
2143
2769
|
const shuffle = config.shuffleMethod || shuffleMethod;
|
|
2770
|
+
const translator = config.translator || defaultTranslator;
|
|
2144
2771
|
const logger = config.logger ? config.logger : config.logging ? consoleLogger(true) : silentLogger;
|
|
2145
2772
|
return {
|
|
2146
2773
|
createGameHandler: createGameHandler(
|
|
2147
2774
|
logger,
|
|
2775
|
+
translator,
|
|
2148
2776
|
createGameUseCase({
|
|
2149
2777
|
modeRepository,
|
|
2150
2778
|
dominoesRepository,
|
|
@@ -2153,14 +2781,17 @@ var configureEngine = (config) => {
|
|
|
2153
2781
|
),
|
|
2154
2782
|
getModesHandler: getModesHandler(
|
|
2155
2783
|
logger,
|
|
2784
|
+
translator,
|
|
2156
2785
|
getModesUseCase({ modeRepository })
|
|
2157
2786
|
),
|
|
2158
2787
|
getExtraRulesHandler: getExtraRulesHandler(
|
|
2159
2788
|
logger,
|
|
2789
|
+
translator,
|
|
2160
2790
|
getExtraRulesUseCase({ ruleRepository })
|
|
2161
2791
|
),
|
|
2162
2792
|
addPlayersHandler: addPlayersHandler(
|
|
2163
2793
|
logger,
|
|
2794
|
+
translator,
|
|
2164
2795
|
addPlayersUseCase({
|
|
2165
2796
|
uuidMethod: uuid,
|
|
2166
2797
|
shuffleMethod: shuffle,
|
|
@@ -2169,188 +2800,903 @@ var configureEngine = (config) => {
|
|
|
2169
2800
|
),
|
|
2170
2801
|
addExtraRulesHandler: addExtraRulesHandler(
|
|
2171
2802
|
logger,
|
|
2172
|
-
|
|
2803
|
+
translator,
|
|
2804
|
+
addExtraRulesUseCase({
|
|
2805
|
+
ruleRepository,
|
|
2806
|
+
dominoesRepository,
|
|
2807
|
+
shuffleMethod: shuffle
|
|
2808
|
+
})
|
|
2173
2809
|
),
|
|
2174
2810
|
startGameHandler: startGameHandler(
|
|
2175
2811
|
logger,
|
|
2812
|
+
translator,
|
|
2176
2813
|
startGameUseCase({ uuidMethod: uuid, shuffleMethod: shuffle })
|
|
2177
2814
|
),
|
|
2178
|
-
chooseDominoHandler: chooseDominoHandler(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
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, {
|
|
2182
2831
|
getResultUseCase,
|
|
2183
2832
|
calculateScoreUseCase
|
|
2184
2833
|
}),
|
|
2185
|
-
calculateScoreHandler: calculateScoreHandler(
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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
|
+
)
|
|
2190
2856
|
};
|
|
2191
2857
|
};
|
|
2192
2858
|
|
|
2193
|
-
// src/core/
|
|
2194
|
-
var
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
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;
|
|
2222
2957
|
}
|
|
2223
|
-
|
|
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
|
+
}
|
|
2224
3036
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
clear(eventType) {
|
|
2242
|
-
if (eventType) {
|
|
2243
|
-
this.listeners.delete(eventType);
|
|
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}`));
|
|
2244
3053
|
} else {
|
|
2245
|
-
|
|
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"));
|
|
2246
3063
|
}
|
|
2247
3064
|
}
|
|
3065
|
+
return issues;
|
|
2248
3066
|
};
|
|
2249
|
-
var
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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;
|
|
2256
3127
|
};
|
|
2257
3128
|
|
|
2258
|
-
// src/core/
|
|
2259
|
-
var
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
ERROR_DOMINO_NOT_FOUND: "error.dominoNotFound",
|
|
2265
|
-
ERROR_INVALID_PLACEMENT: "error.invalidPlacement",
|
|
2266
|
-
ERROR_PLACEMENT_NOT_EMPTY: "error.placementNotEmpty",
|
|
2267
|
-
ERROR_PLACEMENT_NOT_ADJACENT: "error.placementNotAdjacent",
|
|
2268
|
-
ERROR_PLACEMENT_INVALID_TERRAIN: "error.placementInvalidTerrain",
|
|
2269
|
-
ERROR_PLACEMENT_OUT_OF_BOUNDS: "error.placementOutOfBounds",
|
|
2270
|
-
ERROR_NOT_YOUR_TURN: "error.notYourTurn",
|
|
2271
|
-
ERROR_CANNOT_PICK: "error.cannotPick",
|
|
2272
|
-
ERROR_CANNOT_PLACE: "error.cannotPlace",
|
|
2273
|
-
ERROR_DOMINO_ALREADY_PICKED: "error.dominoAlreadyPicked",
|
|
2274
|
-
ERROR_INVALID_PLAYER_COUNT: "error.invalidPlayerCount",
|
|
2275
|
-
ERROR_INVALID_PLAYER_NAME: "error.invalidPlayerName",
|
|
2276
|
-
// Game mode names
|
|
2277
|
-
MODE_CLASSIC: "mode.classic",
|
|
2278
|
-
// Extra rule names
|
|
2279
|
-
RULE_MIDDLE_KINGDOM: "rule.middleKingdom",
|
|
2280
|
-
RULE_HARMONY: "rule.harmony",
|
|
2281
|
-
// Ground types
|
|
2282
|
-
GROUND_CASTLE: "ground.castle",
|
|
2283
|
-
GROUND_WHEAT: "ground.wheat",
|
|
2284
|
-
GROUND_FOREST: "ground.forest",
|
|
2285
|
-
GROUND_SEA: "ground.sea",
|
|
2286
|
-
GROUND_PLAIN: "ground.plain",
|
|
2287
|
-
GROUND_SWAMP: "ground.swamp",
|
|
2288
|
-
GROUND_MINE: "ground.mine",
|
|
2289
|
-
// Game phases
|
|
2290
|
-
PHASE_ADD_PLAYERS: "phase.addPlayers",
|
|
2291
|
-
PHASE_OPTIONS: "phase.options",
|
|
2292
|
-
PHASE_START: "phase.start",
|
|
2293
|
-
PHASE_RESULT: "phase.result",
|
|
2294
|
-
// Actions
|
|
2295
|
-
ACTION_PICK_DOMINO: "action.pickDomino",
|
|
2296
|
-
ACTION_PLACE_DOMINO: "action.placeDomino",
|
|
2297
|
-
ACTION_PASS: "action.pass"
|
|
3129
|
+
// src/core/useCases/botRegistry.ts
|
|
3130
|
+
var defaultStrategies = {
|
|
3131
|
+
random: randomStrategy,
|
|
3132
|
+
greedy: greedyStrategy,
|
|
3133
|
+
advanced: advancedStrategy,
|
|
3134
|
+
expert: expertStrategy
|
|
2298
3135
|
};
|
|
2299
|
-
var
|
|
2300
|
-
|
|
2301
|
-
[TranslationKey.ERROR_INVALID_STEP]: "Invalid game step",
|
|
2302
|
-
[TranslationKey.ERROR_LORD_NOT_FOUND]: "Lord not found",
|
|
2303
|
-
[TranslationKey.ERROR_PLAYER_NOT_FOUND]: "Player not found",
|
|
2304
|
-
[TranslationKey.ERROR_DOMINO_NOT_FOUND]: "Domino not found",
|
|
2305
|
-
[TranslationKey.ERROR_INVALID_PLACEMENT]: "Invalid placement",
|
|
2306
|
-
[TranslationKey.ERROR_PLACEMENT_NOT_EMPTY]: "Position is not empty",
|
|
2307
|
-
[TranslationKey.ERROR_PLACEMENT_NOT_ADJACENT]: "Not adjacent to existing tiles",
|
|
2308
|
-
[TranslationKey.ERROR_PLACEMENT_INVALID_TERRAIN]: "Terrain type does not match",
|
|
2309
|
-
[TranslationKey.ERROR_PLACEMENT_OUT_OF_BOUNDS]: "Placement is out of bounds",
|
|
2310
|
-
[TranslationKey.ERROR_NOT_YOUR_TURN]: "It is not your turn",
|
|
2311
|
-
[TranslationKey.ERROR_CANNOT_PICK]: "Cannot pick a domino now",
|
|
2312
|
-
[TranslationKey.ERROR_CANNOT_PLACE]: "Cannot place a domino now",
|
|
2313
|
-
[TranslationKey.ERROR_DOMINO_ALREADY_PICKED]: "Domino has already been picked",
|
|
2314
|
-
[TranslationKey.ERROR_INVALID_PLAYER_COUNT]: "Invalid number of players (2-4 required)",
|
|
2315
|
-
[TranslationKey.ERROR_INVALID_PLAYER_NAME]: "Player name must be at least 3 characters",
|
|
2316
|
-
// Game mode names
|
|
2317
|
-
[TranslationKey.MODE_CLASSIC]: "Classic",
|
|
2318
|
-
// Extra rule names
|
|
2319
|
-
[TranslationKey.RULE_MIDDLE_KINGDOM]: "The Middle Kingdom",
|
|
2320
|
-
[TranslationKey.RULE_HARMONY]: "Harmony",
|
|
2321
|
-
// Ground types
|
|
2322
|
-
[TranslationKey.GROUND_CASTLE]: "Castle",
|
|
2323
|
-
[TranslationKey.GROUND_WHEAT]: "Wheat Field",
|
|
2324
|
-
[TranslationKey.GROUND_FOREST]: "Forest",
|
|
2325
|
-
[TranslationKey.GROUND_SEA]: "Sea",
|
|
2326
|
-
[TranslationKey.GROUND_PLAIN]: "Plain",
|
|
2327
|
-
[TranslationKey.GROUND_SWAMP]: "Swamp",
|
|
2328
|
-
[TranslationKey.GROUND_MINE]: "Mine",
|
|
2329
|
-
// Game phases
|
|
2330
|
-
[TranslationKey.PHASE_ADD_PLAYERS]: "Add Players",
|
|
2331
|
-
[TranslationKey.PHASE_OPTIONS]: "Game Options",
|
|
2332
|
-
[TranslationKey.PHASE_START]: "Start Game",
|
|
2333
|
-
[TranslationKey.PHASE_RESULT]: "Game Results",
|
|
2334
|
-
// Actions
|
|
2335
|
-
[TranslationKey.ACTION_PICK_DOMINO]: "Pick Domino",
|
|
2336
|
-
[TranslationKey.ACTION_PLACE_DOMINO]: "Place Domino",
|
|
2337
|
-
[TranslationKey.ACTION_PASS]: "Pass"
|
|
3136
|
+
var getStrategy = (name, custom) => {
|
|
3137
|
+
return custom?.[name] ?? defaultStrategies[name];
|
|
2338
3138
|
};
|
|
2339
|
-
var
|
|
2340
|
-
|
|
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
|
+
};
|
|
2341
3580
|
return {
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
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
|
|
2347
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;
|
|
2348
3650
|
}
|
|
2349
|
-
return text;
|
|
2350
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]
|
|
2351
3676
|
};
|
|
2352
3677
|
};
|
|
2353
|
-
var
|
|
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
|
+
});
|
|
2354
3700
|
|
|
2355
3701
|
// src/index.ts
|
|
2356
3702
|
var createGameEngine = (config) => {
|
|
@@ -2369,9 +3715,10 @@ var createGameEngine = (config) => {
|
|
|
2369
3715
|
getValidPlacementsHandler: getValidPlacementsHandler2,
|
|
2370
3716
|
canPlaceDominoHandler: canPlaceDominoHandler2,
|
|
2371
3717
|
serializeGameHandler: serializeGameHandler2,
|
|
2372
|
-
deserializeGameHandler: deserializeGameHandler2
|
|
3718
|
+
deserializeGameHandler: deserializeGameHandler2,
|
|
3719
|
+
getDynastyResultHandler: getDynastyResultHandler2
|
|
2373
3720
|
} = configureEngine(config);
|
|
2374
|
-
|
|
3721
|
+
let engine = {
|
|
2375
3722
|
getModes: (command) => getModesHandler2(command),
|
|
2376
3723
|
getExtraRules: (command) => getExtraRulesHandler2(command),
|
|
2377
3724
|
createGame: (command) => createGameHandler2(command),
|
|
@@ -2386,8 +3733,17 @@ var createGameEngine = (config) => {
|
|
|
2386
3733
|
getValidPlacements: (command) => getValidPlacementsHandler2(command),
|
|
2387
3734
|
canPlaceDomino: (command) => canPlaceDominoHandler2(command),
|
|
2388
3735
|
serialize: (command) => serializeGameHandler2(command),
|
|
2389
|
-
deserialize: (command) => deserializeGameHandler2(command)
|
|
3736
|
+
deserialize: (command) => deserializeGameHandler2(command),
|
|
3737
|
+
getDynastyResults: (command) => getDynastyResultHandler2(command)
|
|
2390
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;
|
|
2391
3747
|
};
|
|
2392
3748
|
export {
|
|
2393
3749
|
ActionExecutionError,
|
|
@@ -2395,26 +3751,56 @@ export {
|
|
|
2395
3751
|
ErrorCode,
|
|
2396
3752
|
GRIDSIZE,
|
|
2397
3753
|
GROUND,
|
|
2398
|
-
GameEventEmitter,
|
|
2399
|
-
GameEventType,
|
|
2400
3754
|
InvalidStepError,
|
|
3755
|
+
MAX_KINGDOM_SIZE,
|
|
2401
3756
|
NotFoundError,
|
|
3757
|
+
RedoError,
|
|
2402
3758
|
StepExecutionError,
|
|
2403
3759
|
TranslationKey,
|
|
3760
|
+
UndoError,
|
|
2404
3761
|
ValidationError,
|
|
3762
|
+
advancedStrategy,
|
|
3763
|
+
appendAction,
|
|
3764
|
+
canRedo,
|
|
3765
|
+
canUndo,
|
|
3766
|
+
clearHistory,
|
|
3767
|
+
createActionLog,
|
|
2405
3768
|
createDomainError,
|
|
2406
|
-
createEvent,
|
|
2407
3769
|
createGameEngine,
|
|
3770
|
+
createGameHistory,
|
|
2408
3771
|
createSavePoint,
|
|
2409
3772
|
createTranslator,
|
|
2410
3773
|
defaultTranslations,
|
|
2411
3774
|
defaultTranslator,
|
|
2412
3775
|
deserializeGame,
|
|
3776
|
+
errorCodeToTranslationKey,
|
|
3777
|
+
expertStrategy,
|
|
2413
3778
|
gameSteps,
|
|
3779
|
+
getActions,
|
|
3780
|
+
getActionsByTurn,
|
|
3781
|
+
getActionsByType,
|
|
3782
|
+
getHistorySize,
|
|
3783
|
+
getStrategy,
|
|
3784
|
+
getStrategyNames,
|
|
3785
|
+
greedyStrategy,
|
|
3786
|
+
isBotTurn,
|
|
2414
3787
|
isGameWithNextAction,
|
|
2415
3788
|
isGameWithNextStep,
|
|
3789
|
+
playBotTurn,
|
|
3790
|
+
playBotTurns,
|
|
2416
3791
|
playerActions,
|
|
3792
|
+
pushState,
|
|
3793
|
+
randomStrategy,
|
|
3794
|
+
redo,
|
|
3795
|
+
replayActions,
|
|
2417
3796
|
restoreFromSavePoint,
|
|
2418
|
-
serializeGame
|
|
3797
|
+
serializeGame,
|
|
3798
|
+
summarizeGameState,
|
|
3799
|
+
translateErrorCode,
|
|
3800
|
+
undo,
|
|
3801
|
+
validateGameState,
|
|
3802
|
+
wrapWithActionLog,
|
|
3803
|
+
wrapWithDebug,
|
|
3804
|
+
wrapWithEvents
|
|
2419
3805
|
};
|
|
2420
3806
|
//# sourceMappingURL=index.js.map
|