@pompidup/kingdomino-engine 3.1.0 → 3.2.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 +2132 -745
- package/dist/index.js.map +1 -1
- package/package.json +22 -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 {
|
|
@@ -673,32 +960,6 @@ var uuidMethod = () => {
|
|
|
673
960
|
return randomUUID();
|
|
674
961
|
};
|
|
675
962
|
|
|
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
|
-
}
|
|
700
|
-
};
|
|
701
|
-
|
|
702
963
|
// src/core/domain/errors/domainErrors.ts
|
|
703
964
|
var ErrorCode = {
|
|
704
965
|
// Game flow errors
|
|
@@ -718,11 +979,15 @@ var ErrorCode = {
|
|
|
718
979
|
PLACEMENT_NOT_ADJACENT: "PLACEMENT_NOT_ADJACENT",
|
|
719
980
|
PLACEMENT_INVALID_TERRAIN: "PLACEMENT_INVALID_TERRAIN",
|
|
720
981
|
PLACEMENT_OUT_OF_BOUNDS: "PLACEMENT_OUT_OF_BOUNDS",
|
|
982
|
+
PLACEMENT_EXCEEDS_KINGDOM_SIZE: "PLACEMENT_EXCEEDS_KINGDOM_SIZE",
|
|
721
983
|
// Action errors
|
|
722
984
|
NOT_YOUR_TURN: "NOT_YOUR_TURN",
|
|
723
985
|
CANNOT_PICK: "CANNOT_PICK",
|
|
724
986
|
CANNOT_PLACE: "CANNOT_PLACE",
|
|
725
|
-
DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED"
|
|
987
|
+
DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED",
|
|
988
|
+
// History errors
|
|
989
|
+
UNDO_UNAVAILABLE: "UNDO_UNAVAILABLE",
|
|
990
|
+
REDO_UNAVAILABLE: "REDO_UNAVAILABLE"
|
|
726
991
|
};
|
|
727
992
|
var createDomainError = (code, message, context) => ({
|
|
728
993
|
code,
|
|
@@ -750,9 +1015,9 @@ var InvalidStepError = class extends DomainException {
|
|
|
750
1015
|
}
|
|
751
1016
|
};
|
|
752
1017
|
var StepExecutionError = class extends DomainException {
|
|
753
|
-
constructor(message, context) {
|
|
1018
|
+
constructor(message, code = ErrorCode.STEP_EXECUTION_FAILED, context) {
|
|
754
1019
|
super({
|
|
755
|
-
code
|
|
1020
|
+
code,
|
|
756
1021
|
message,
|
|
757
1022
|
context
|
|
758
1023
|
});
|
|
@@ -789,133 +1054,345 @@ var ValidationError = class extends DomainException {
|
|
|
789
1054
|
this.name = "ValidationError";
|
|
790
1055
|
}
|
|
791
1056
|
};
|
|
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);
|
|
1057
|
+
var UndoError = class extends DomainException {
|
|
1058
|
+
constructor(message = "No state to undo", context) {
|
|
1059
|
+
super({
|
|
1060
|
+
code: ErrorCode.UNDO_UNAVAILABLE,
|
|
1061
|
+
message,
|
|
1062
|
+
context
|
|
1063
|
+
});
|
|
1064
|
+
this.name = "UndoError";
|
|
898
1065
|
}
|
|
899
|
-
logger.info(
|
|
900
|
-
`Domino chosen: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
901
|
-
);
|
|
902
|
-
return result.value;
|
|
903
1066
|
};
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1067
|
+
var RedoError = class extends DomainException {
|
|
1068
|
+
constructor(message = "No state to redo", context) {
|
|
1069
|
+
super({
|
|
1070
|
+
code: ErrorCode.REDO_UNAVAILABLE,
|
|
1071
|
+
message,
|
|
1072
|
+
context
|
|
1073
|
+
});
|
|
1074
|
+
this.name = "RedoError";
|
|
912
1075
|
}
|
|
913
|
-
logger.info(`Game created: ${result.value.id}`);
|
|
914
|
-
return result.value;
|
|
915
1076
|
};
|
|
916
1077
|
|
|
917
|
-
// src/
|
|
918
|
-
var
|
|
1078
|
+
// src/core/i18n/translations.ts
|
|
1079
|
+
var TranslationKey = {
|
|
1080
|
+
// Error messages
|
|
1081
|
+
ERROR_INVALID_STEP: "error.invalidStep",
|
|
1082
|
+
ERROR_STEP_EXECUTION_FAILED: "error.stepExecutionFailed",
|
|
1083
|
+
ERROR_ACTION_EXECUTION_FAILED: "error.actionExecutionFailed",
|
|
1084
|
+
ERROR_LORD_NOT_FOUND: "error.lordNotFound",
|
|
1085
|
+
ERROR_PLAYER_NOT_FOUND: "error.playerNotFound",
|
|
1086
|
+
ERROR_DOMINO_NOT_FOUND: "error.dominoNotFound",
|
|
1087
|
+
ERROR_MODE_NOT_FOUND: "error.modeNotFound",
|
|
1088
|
+
ERROR_INVALID_PLACEMENT: "error.invalidPlacement",
|
|
1089
|
+
ERROR_PLACEMENT_NOT_EMPTY: "error.placementNotEmpty",
|
|
1090
|
+
ERROR_PLACEMENT_NOT_ADJACENT: "error.placementNotAdjacent",
|
|
1091
|
+
ERROR_PLACEMENT_INVALID_TERRAIN: "error.placementInvalidTerrain",
|
|
1092
|
+
ERROR_PLACEMENT_OUT_OF_BOUNDS: "error.placementOutOfBounds",
|
|
1093
|
+
ERROR_PLACEMENT_EXCEEDS_KINGDOM_SIZE: "error.placementExceedsKingdomSize",
|
|
1094
|
+
ERROR_NOT_YOUR_TURN: "error.notYourTurn",
|
|
1095
|
+
ERROR_CANNOT_PICK: "error.cannotPick",
|
|
1096
|
+
ERROR_CANNOT_PLACE: "error.cannotPlace",
|
|
1097
|
+
ERROR_DOMINO_ALREADY_PICKED: "error.dominoAlreadyPicked",
|
|
1098
|
+
ERROR_INVALID_PLAYER_COUNT: "error.invalidPlayerCount",
|
|
1099
|
+
ERROR_INVALID_PLAYER_NAME: "error.invalidPlayerName",
|
|
1100
|
+
ERROR_UNDO_UNAVAILABLE: "error.undoUnavailable",
|
|
1101
|
+
ERROR_REDO_UNAVAILABLE: "error.redoUnavailable",
|
|
1102
|
+
// Game mode names
|
|
1103
|
+
MODE_CLASSIC: "mode.classic",
|
|
1104
|
+
// Extra rule names
|
|
1105
|
+
RULE_MIDDLE_KINGDOM: "rule.middleKingdom",
|
|
1106
|
+
RULE_HARMONY: "rule.harmony",
|
|
1107
|
+
// Ground types
|
|
1108
|
+
GROUND_CASTLE: "ground.castle",
|
|
1109
|
+
GROUND_WHEAT: "ground.wheat",
|
|
1110
|
+
GROUND_FOREST: "ground.forest",
|
|
1111
|
+
GROUND_SEA: "ground.sea",
|
|
1112
|
+
GROUND_PLAIN: "ground.plain",
|
|
1113
|
+
GROUND_SWAMP: "ground.swamp",
|
|
1114
|
+
GROUND_MINE: "ground.mine",
|
|
1115
|
+
// Game phases
|
|
1116
|
+
PHASE_ADD_PLAYERS: "phase.addPlayers",
|
|
1117
|
+
PHASE_OPTIONS: "phase.options",
|
|
1118
|
+
PHASE_START: "phase.start",
|
|
1119
|
+
PHASE_RESULT: "phase.result",
|
|
1120
|
+
// Actions
|
|
1121
|
+
ACTION_PICK_DOMINO: "action.pickDomino",
|
|
1122
|
+
ACTION_PLACE_DOMINO: "action.placeDomino",
|
|
1123
|
+
ACTION_PASS: "action.pass"
|
|
1124
|
+
};
|
|
1125
|
+
var defaultTranslations = {
|
|
1126
|
+
// Error messages
|
|
1127
|
+
[TranslationKey.ERROR_INVALID_STEP]: "Invalid game step",
|
|
1128
|
+
[TranslationKey.ERROR_STEP_EXECUTION_FAILED]: "Step execution failed",
|
|
1129
|
+
[TranslationKey.ERROR_ACTION_EXECUTION_FAILED]: "Action execution failed",
|
|
1130
|
+
[TranslationKey.ERROR_LORD_NOT_FOUND]: "Lord not found",
|
|
1131
|
+
[TranslationKey.ERROR_PLAYER_NOT_FOUND]: "Player not found",
|
|
1132
|
+
[TranslationKey.ERROR_DOMINO_NOT_FOUND]: "Domino not found",
|
|
1133
|
+
[TranslationKey.ERROR_MODE_NOT_FOUND]: "Game mode not found",
|
|
1134
|
+
[TranslationKey.ERROR_INVALID_PLACEMENT]: "Invalid placement",
|
|
1135
|
+
[TranslationKey.ERROR_PLACEMENT_NOT_EMPTY]: "Position is not empty",
|
|
1136
|
+
[TranslationKey.ERROR_PLACEMENT_NOT_ADJACENT]: "Not adjacent to existing tiles",
|
|
1137
|
+
[TranslationKey.ERROR_PLACEMENT_INVALID_TERRAIN]: "Terrain type does not match",
|
|
1138
|
+
[TranslationKey.ERROR_PLACEMENT_OUT_OF_BOUNDS]: "Placement is out of bounds",
|
|
1139
|
+
[TranslationKey.ERROR_PLACEMENT_EXCEEDS_KINGDOM_SIZE]: "Placement exceeds kingdom size",
|
|
1140
|
+
[TranslationKey.ERROR_NOT_YOUR_TURN]: "It is not your turn",
|
|
1141
|
+
[TranslationKey.ERROR_CANNOT_PICK]: "Cannot pick a domino now",
|
|
1142
|
+
[TranslationKey.ERROR_CANNOT_PLACE]: "Cannot place a domino now",
|
|
1143
|
+
[TranslationKey.ERROR_DOMINO_ALREADY_PICKED]: "Domino has already been picked",
|
|
1144
|
+
[TranslationKey.ERROR_INVALID_PLAYER_COUNT]: "Invalid number of players (1-4 required)",
|
|
1145
|
+
[TranslationKey.ERROR_INVALID_PLAYER_NAME]: "Player name must be at least 3 characters",
|
|
1146
|
+
[TranslationKey.ERROR_UNDO_UNAVAILABLE]: "No state to undo",
|
|
1147
|
+
[TranslationKey.ERROR_REDO_UNAVAILABLE]: "No state to redo",
|
|
1148
|
+
// Game mode names
|
|
1149
|
+
[TranslationKey.MODE_CLASSIC]: "Classic",
|
|
1150
|
+
// Extra rule names
|
|
1151
|
+
[TranslationKey.RULE_MIDDLE_KINGDOM]: "The Middle Kingdom",
|
|
1152
|
+
[TranslationKey.RULE_HARMONY]: "Harmony",
|
|
1153
|
+
// Ground types
|
|
1154
|
+
[TranslationKey.GROUND_CASTLE]: "Castle",
|
|
1155
|
+
[TranslationKey.GROUND_WHEAT]: "Wheat Field",
|
|
1156
|
+
[TranslationKey.GROUND_FOREST]: "Forest",
|
|
1157
|
+
[TranslationKey.GROUND_SEA]: "Sea",
|
|
1158
|
+
[TranslationKey.GROUND_PLAIN]: "Plain",
|
|
1159
|
+
[TranslationKey.GROUND_SWAMP]: "Swamp",
|
|
1160
|
+
[TranslationKey.GROUND_MINE]: "Mine",
|
|
1161
|
+
// Game phases
|
|
1162
|
+
[TranslationKey.PHASE_ADD_PLAYERS]: "Add Players",
|
|
1163
|
+
[TranslationKey.PHASE_OPTIONS]: "Game Options",
|
|
1164
|
+
[TranslationKey.PHASE_START]: "Start Game",
|
|
1165
|
+
[TranslationKey.PHASE_RESULT]: "Game Results",
|
|
1166
|
+
// Actions
|
|
1167
|
+
[TranslationKey.ACTION_PICK_DOMINO]: "Pick Domino",
|
|
1168
|
+
[TranslationKey.ACTION_PLACE_DOMINO]: "Place Domino",
|
|
1169
|
+
[TranslationKey.ACTION_PASS]: "Pass"
|
|
1170
|
+
};
|
|
1171
|
+
var errorCodeToTranslationKey = {
|
|
1172
|
+
[ErrorCode.INVALID_STEP]: TranslationKey.ERROR_INVALID_STEP,
|
|
1173
|
+
[ErrorCode.STEP_EXECUTION_FAILED]: TranslationKey.ERROR_STEP_EXECUTION_FAILED,
|
|
1174
|
+
[ErrorCode.ACTION_EXECUTION_FAILED]: TranslationKey.ERROR_ACTION_EXECUTION_FAILED,
|
|
1175
|
+
[ErrorCode.LORD_NOT_FOUND]: TranslationKey.ERROR_LORD_NOT_FOUND,
|
|
1176
|
+
[ErrorCode.PLAYER_NOT_FOUND]: TranslationKey.ERROR_PLAYER_NOT_FOUND,
|
|
1177
|
+
[ErrorCode.DOMINO_NOT_FOUND]: TranslationKey.ERROR_DOMINO_NOT_FOUND,
|
|
1178
|
+
[ErrorCode.MODE_NOT_FOUND]: TranslationKey.ERROR_MODE_NOT_FOUND,
|
|
1179
|
+
[ErrorCode.INVALID_PLAYER_COUNT]: TranslationKey.ERROR_INVALID_PLAYER_COUNT,
|
|
1180
|
+
[ErrorCode.INVALID_PLAYER_NAME]: TranslationKey.ERROR_INVALID_PLAYER_NAME,
|
|
1181
|
+
[ErrorCode.INVALID_PLACEMENT]: TranslationKey.ERROR_INVALID_PLACEMENT,
|
|
1182
|
+
[ErrorCode.PLACEMENT_NOT_EMPTY]: TranslationKey.ERROR_PLACEMENT_NOT_EMPTY,
|
|
1183
|
+
[ErrorCode.PLACEMENT_NOT_ADJACENT]: TranslationKey.ERROR_PLACEMENT_NOT_ADJACENT,
|
|
1184
|
+
[ErrorCode.PLACEMENT_INVALID_TERRAIN]: TranslationKey.ERROR_PLACEMENT_INVALID_TERRAIN,
|
|
1185
|
+
[ErrorCode.PLACEMENT_OUT_OF_BOUNDS]: TranslationKey.ERROR_PLACEMENT_OUT_OF_BOUNDS,
|
|
1186
|
+
[ErrorCode.PLACEMENT_EXCEEDS_KINGDOM_SIZE]: TranslationKey.ERROR_PLACEMENT_EXCEEDS_KINGDOM_SIZE,
|
|
1187
|
+
[ErrorCode.NOT_YOUR_TURN]: TranslationKey.ERROR_NOT_YOUR_TURN,
|
|
1188
|
+
[ErrorCode.CANNOT_PICK]: TranslationKey.ERROR_CANNOT_PICK,
|
|
1189
|
+
[ErrorCode.CANNOT_PLACE]: TranslationKey.ERROR_CANNOT_PLACE,
|
|
1190
|
+
[ErrorCode.DOMINO_ALREADY_PICKED]: TranslationKey.ERROR_DOMINO_ALREADY_PICKED,
|
|
1191
|
+
[ErrorCode.UNDO_UNAVAILABLE]: TranslationKey.ERROR_UNDO_UNAVAILABLE,
|
|
1192
|
+
[ErrorCode.REDO_UNAVAILABLE]: TranslationKey.ERROR_REDO_UNAVAILABLE
|
|
1193
|
+
};
|
|
1194
|
+
var translateErrorCode = (translator, code, params) => {
|
|
1195
|
+
const key = errorCodeToTranslationKey[code];
|
|
1196
|
+
return translator.t(key, params);
|
|
1197
|
+
};
|
|
1198
|
+
var createTranslator = (translations = {}) => {
|
|
1199
|
+
const mergedTranslations = { ...defaultTranslations, ...translations };
|
|
1200
|
+
return {
|
|
1201
|
+
t(key, params) {
|
|
1202
|
+
let text = mergedTranslations[key] || key;
|
|
1203
|
+
if (params) {
|
|
1204
|
+
for (const [paramKey, value] of Object.entries(params)) {
|
|
1205
|
+
text = text.replace(
|
|
1206
|
+
new RegExp(`\\{\\{${paramKey}\\}\\}`, "g"),
|
|
1207
|
+
String(value)
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return text;
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
};
|
|
1215
|
+
var defaultTranslator = createTranslator();
|
|
1216
|
+
|
|
1217
|
+
// src/utils/result.ts
|
|
1218
|
+
var ok = (value) => ({ tag: "Ok", value });
|
|
1219
|
+
var err = (error) => ({ tag: "Err", error });
|
|
1220
|
+
var isOk = (result) => result.tag === "Ok";
|
|
1221
|
+
var isErr = (result) => result.tag === "Err";
|
|
1222
|
+
var unwrap = (result) => {
|
|
1223
|
+
if (result.tag === "Err") {
|
|
1224
|
+
throw new Error(result.error);
|
|
1225
|
+
}
|
|
1226
|
+
return result.value;
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
// src/application/handlers/addExtraRulesHandler.ts
|
|
1230
|
+
var addExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
1231
|
+
const { game, extraRules } = command;
|
|
1232
|
+
logger.info(`Adding extra rules to game: ${game.id}`);
|
|
1233
|
+
logger.info(`Extra rules: ${extraRules}`);
|
|
1234
|
+
if (!isGameWithNextStep(game)) {
|
|
1235
|
+
logger.error(
|
|
1236
|
+
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1237
|
+
);
|
|
1238
|
+
throw new InvalidStepError(
|
|
1239
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1240
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
if (game.nextAction.step !== "options") {
|
|
1244
|
+
logger.error(
|
|
1245
|
+
`Required game with options step but got: ${game.nextAction.step}`
|
|
1246
|
+
);
|
|
1247
|
+
throw new InvalidStepError(
|
|
1248
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1249
|
+
{ expected: "options", actual: game.nextAction.step, gameId: game.id }
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
const result = useCase(game, extraRules);
|
|
1253
|
+
if (isErr(result)) {
|
|
1254
|
+
logger.error(`Error adding extra rules: ${result.error}`);
|
|
1255
|
+
const code = result.error;
|
|
1256
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1257
|
+
gameId: game.id,
|
|
1258
|
+
extraRules: command.extraRules
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
logger.info(`Extra rules added to game: ${game.id}`);
|
|
1262
|
+
return result.value;
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/application/handlers/addPlayersHandler.ts
|
|
1266
|
+
var addPlayersHandler = (logger, translator, useCase) => (command) => {
|
|
1267
|
+
const { game, players } = command;
|
|
1268
|
+
logger.info(`Adding players to game: ${game.id}`);
|
|
1269
|
+
if (!isGameWithNextStep(game)) {
|
|
1270
|
+
logger.error(
|
|
1271
|
+
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1272
|
+
);
|
|
1273
|
+
throw new InvalidStepError(
|
|
1274
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1275
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (game.nextAction.step !== "addPlayers") {
|
|
1279
|
+
logger.error(
|
|
1280
|
+
`Required game with addPlayers step but got: ${game.nextAction.step}`
|
|
1281
|
+
);
|
|
1282
|
+
throw new InvalidStepError(
|
|
1283
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1284
|
+
{
|
|
1285
|
+
expected: "addPlayers",
|
|
1286
|
+
actual: game.nextAction.step,
|
|
1287
|
+
gameId: game.id
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
const result = useCase(game, players);
|
|
1292
|
+
if (isErr(result)) {
|
|
1293
|
+
logger.error(`Error adding players: ${result.error}`);
|
|
1294
|
+
const code = result.error;
|
|
1295
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1296
|
+
gameId: game.id,
|
|
1297
|
+
players: command.players
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
logger.info(`Players added to game: ${game.id}`);
|
|
1301
|
+
return result.value;
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
// src/application/handlers/calculateScoreHandler.ts
|
|
1305
|
+
var calculateScoreHandler = (logger, translator, useCase) => (command) => {
|
|
1306
|
+
const { kingdom } = command;
|
|
1307
|
+
logger.info(`Calculating score for kingdom`);
|
|
1308
|
+
const result = useCase(kingdom);
|
|
1309
|
+
if (isErr(result)) {
|
|
1310
|
+
logger.error(`Error calculating score: ${result.error}`);
|
|
1311
|
+
throw new Error(result.error);
|
|
1312
|
+
}
|
|
1313
|
+
logger.info(`Score calculated: ${result.value}`);
|
|
1314
|
+
return result.value;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// src/application/handlers/canPlaceDominoHandler.ts
|
|
1318
|
+
var canPlaceDominoHandler = (logger, translator, useCase) => {
|
|
1319
|
+
return (command) => {
|
|
1320
|
+
logger.info(`Checking if domino ${command.domino.number} can be placed`);
|
|
1321
|
+
const canPlace = useCase(
|
|
1322
|
+
command.kingdom,
|
|
1323
|
+
command.domino,
|
|
1324
|
+
command.maxKingdomSize
|
|
1325
|
+
);
|
|
1326
|
+
logger.info(`Can place domino: ${canPlace}`);
|
|
1327
|
+
return canPlace;
|
|
1328
|
+
};
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// src/application/handlers/chooseDominoHandler.ts
|
|
1332
|
+
var chooseDominoHandler = (logger, translator, useCase) => (command) => {
|
|
1333
|
+
const { game, lordId, dominoPick } = command;
|
|
1334
|
+
logger.info(
|
|
1335
|
+
`Choosing domino: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
1336
|
+
);
|
|
1337
|
+
if (!isGameWithNextAction(game)) {
|
|
1338
|
+
logger.error(
|
|
1339
|
+
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
1340
|
+
);
|
|
1341
|
+
throw new InvalidStepError(
|
|
1342
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1343
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
if (game.nextAction.nextAction !== "pickDomino") {
|
|
1347
|
+
logger.error(
|
|
1348
|
+
`Required game with pickDomino action but got: ${game.nextAction.nextAction}`
|
|
1349
|
+
);
|
|
1350
|
+
throw new InvalidStepError(
|
|
1351
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1352
|
+
{
|
|
1353
|
+
expected: "pickDomino",
|
|
1354
|
+
actual: game.nextAction.nextAction,
|
|
1355
|
+
gameId: game.id
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
const result = useCase(game, lordId, dominoPick);
|
|
1360
|
+
if (isErr(result)) {
|
|
1361
|
+
logger.error(`Error choosing domino: ${result.error}`);
|
|
1362
|
+
const code = result.error;
|
|
1363
|
+
throw new ActionExecutionError(
|
|
1364
|
+
translateErrorCode(translator, code),
|
|
1365
|
+
code,
|
|
1366
|
+
{
|
|
1367
|
+
gameId: game.id,
|
|
1368
|
+
lordId: command.lordId,
|
|
1369
|
+
dominoPick: command.dominoPick
|
|
1370
|
+
}
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
logger.info(
|
|
1374
|
+
`Domino chosen: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
1375
|
+
);
|
|
1376
|
+
return result.value;
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// src/application/handlers/createGameHandler.ts
|
|
1380
|
+
var createGameHandler = (logger, translator, useCase) => (command) => {
|
|
1381
|
+
logger.info(`Creating game with mode: ${command.mode}`);
|
|
1382
|
+
const result = useCase(command.mode, command.seed);
|
|
1383
|
+
if (isErr(result)) {
|
|
1384
|
+
logger.error(`Error creating game: ${result.error}`);
|
|
1385
|
+
const code = result.error;
|
|
1386
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1387
|
+
mode: command.mode
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
logger.info(`Game created: ${result.value.id}`);
|
|
1391
|
+
return result.value;
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/application/handlers/discardDominoHandler.ts
|
|
1395
|
+
var discardDominoHandler = (logger, translator, useCase) => (command) => {
|
|
919
1396
|
const { game, lordId } = command;
|
|
920
1397
|
logger.info(`Discarding domino for lord: ${lordId} in game: ${game.id}`);
|
|
921
1398
|
if (!isGameWithNextAction(game)) {
|
|
@@ -923,19 +1400,32 @@ var discardDominoHandler = (logger, useCase) => (command) => {
|
|
|
923
1400
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
924
1401
|
);
|
|
925
1402
|
throw new InvalidStepError(
|
|
926
|
-
|
|
1403
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1404
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
927
1405
|
);
|
|
928
1406
|
}
|
|
929
1407
|
if (game.nextAction.nextAction !== "placeDomino") {
|
|
930
1408
|
logger.error(
|
|
931
1409
|
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
932
1410
|
);
|
|
933
|
-
throw new InvalidStepError(
|
|
1411
|
+
throw new InvalidStepError(
|
|
1412
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1413
|
+
{
|
|
1414
|
+
expected: "placeDomino",
|
|
1415
|
+
actual: game.nextAction.nextAction,
|
|
1416
|
+
gameId: game.id
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
934
1419
|
}
|
|
935
1420
|
const result = useCase(game, lordId);
|
|
936
1421
|
if (isErr(result)) {
|
|
937
1422
|
logger.error(`Error discarding domino: ${result.error}`);
|
|
938
|
-
|
|
1423
|
+
const code = result.error;
|
|
1424
|
+
throw new ActionExecutionError(
|
|
1425
|
+
translateErrorCode(translator, code),
|
|
1426
|
+
code,
|
|
1427
|
+
{ gameId: game.id, lordId: command.lordId }
|
|
1428
|
+
);
|
|
939
1429
|
}
|
|
940
1430
|
if (isGameWithNextAction(result.value)) {
|
|
941
1431
|
logger.info(`Domino discarded: ${lordId} in game: ${game.id}`);
|
|
@@ -945,8 +1435,20 @@ var discardDominoHandler = (logger, useCase) => (command) => {
|
|
|
945
1435
|
return result.value;
|
|
946
1436
|
};
|
|
947
1437
|
|
|
1438
|
+
// src/application/handlers/getDynastyResultHandler.ts
|
|
1439
|
+
var getDynastyResultHandler = (logger, translator, useCase) => {
|
|
1440
|
+
return (command) => {
|
|
1441
|
+
logger.info(
|
|
1442
|
+
`Calculating dynasty results for ${command.games.length} games`
|
|
1443
|
+
);
|
|
1444
|
+
const results = useCase(command.games);
|
|
1445
|
+
logger.info(`Dynasty results calculated`);
|
|
1446
|
+
return results;
|
|
1447
|
+
};
|
|
1448
|
+
};
|
|
1449
|
+
|
|
948
1450
|
// src/application/handlers/getExtraRulesHandler.ts
|
|
949
|
-
var getExtraRulesHandler = (logger, useCase) => (command) => {
|
|
1451
|
+
var getExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
950
1452
|
const { mode, players } = command;
|
|
951
1453
|
logger.info(
|
|
952
1454
|
`Getting extra rules for mode: ${mode} and players: ${players}`
|
|
@@ -954,26 +1456,30 @@ var getExtraRulesHandler = (logger, useCase) => (command) => {
|
|
|
954
1456
|
const result = useCase(mode, players);
|
|
955
1457
|
if (isErr(result)) {
|
|
956
1458
|
logger.error(`Error getting extra rules: ${result.error}`);
|
|
957
|
-
|
|
1459
|
+
const code = result.error;
|
|
1460
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1461
|
+
mode: command.mode
|
|
1462
|
+
});
|
|
958
1463
|
}
|
|
959
1464
|
logger.info(`Extra rules retrieved: ${result.value}`);
|
|
960
1465
|
return result.value;
|
|
961
1466
|
};
|
|
962
1467
|
|
|
963
1468
|
// src/application/handlers/getModesHandler.ts
|
|
964
|
-
var getModesHandler = (logger, useCase) => (_command) => {
|
|
1469
|
+
var getModesHandler = (logger, translator, useCase) => (_command) => {
|
|
965
1470
|
logger.info("Getting modes");
|
|
966
1471
|
const result = useCase();
|
|
967
1472
|
if (isErr(result)) {
|
|
968
1473
|
logger.error(`Error getting modes: ${result.error}`);
|
|
969
|
-
|
|
1474
|
+
const code = result.error;
|
|
1475
|
+
throw new NotFoundError(translateErrorCode(translator, code), code);
|
|
970
1476
|
}
|
|
971
1477
|
logger.info(`Modes retrieved: ${result.value}`);
|
|
972
1478
|
return result.value;
|
|
973
1479
|
};
|
|
974
1480
|
|
|
975
1481
|
// src/application/handlers/getResultHandler.ts
|
|
976
|
-
var getResultHandler = (logger, useCases) => (command) => {
|
|
1482
|
+
var getResultHandler = (logger, translator, useCases) => (command) => {
|
|
977
1483
|
const { getResultUseCase: getResultUseCase2, calculateScoreUseCase: calculateScoreUseCase2 } = useCases;
|
|
978
1484
|
const { game } = command;
|
|
979
1485
|
logger.info(`Getting result for game: ${game.id}`);
|
|
@@ -981,22 +1487,34 @@ var getResultHandler = (logger, useCases) => (command) => {
|
|
|
981
1487
|
logger.error(
|
|
982
1488
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
983
1489
|
);
|
|
984
|
-
throw new InvalidStepError(
|
|
1490
|
+
throw new InvalidStepError(
|
|
1491
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1492
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1493
|
+
);
|
|
985
1494
|
}
|
|
986
1495
|
if (game.nextAction.step !== "result") {
|
|
987
1496
|
logger.error(
|
|
988
1497
|
`Required game with result step but got: ${game.nextAction.step}`
|
|
989
1498
|
);
|
|
990
|
-
throw new InvalidStepError(
|
|
1499
|
+
throw new InvalidStepError(
|
|
1500
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1501
|
+
{ expected: "result", actual: game.nextAction.step, gameId: game.id }
|
|
1502
|
+
);
|
|
991
1503
|
}
|
|
992
|
-
let scoreResult;
|
|
993
1504
|
const { players } = game;
|
|
994
|
-
scoreResult = players.map((player) => {
|
|
1505
|
+
const scoreResult = players.map((player) => {
|
|
995
1506
|
const { kingdom } = player;
|
|
996
1507
|
const result2 = calculateScoreUseCase2(kingdom);
|
|
997
1508
|
if (isErr(result2)) {
|
|
998
1509
|
logger.error(`Error when calculating score: ${result2.error}`);
|
|
999
|
-
|
|
1510
|
+
const code = result2.error;
|
|
1511
|
+
throw new StepExecutionError(
|
|
1512
|
+
translateErrorCode(translator, code),
|
|
1513
|
+
code,
|
|
1514
|
+
{
|
|
1515
|
+
gameId: game.id
|
|
1516
|
+
}
|
|
1517
|
+
);
|
|
1000
1518
|
}
|
|
1001
1519
|
return {
|
|
1002
1520
|
playerId: player.id,
|
|
@@ -1009,14 +1527,33 @@ var getResultHandler = (logger, useCases) => (command) => {
|
|
|
1009
1527
|
const result = getResultUseCase2(game, scoreResult);
|
|
1010
1528
|
if (isErr(result)) {
|
|
1011
1529
|
logger.error(`Error getting result: ${result.error}`);
|
|
1012
|
-
|
|
1530
|
+
const code = result.error;
|
|
1531
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1532
|
+
gameId: game.id
|
|
1533
|
+
});
|
|
1013
1534
|
}
|
|
1014
1535
|
logger.info(`Result retrieved for game: ${game.id}`);
|
|
1015
1536
|
return result.value;
|
|
1016
1537
|
};
|
|
1017
1538
|
|
|
1539
|
+
// src/application/handlers/getValidPlacementsHandler.ts
|
|
1540
|
+
var getValidPlacementsHandler = (logger, translator, useCase) => {
|
|
1541
|
+
return (command) => {
|
|
1542
|
+
logger.info(
|
|
1543
|
+
`Getting valid placements for domino: ${command.domino.number}`
|
|
1544
|
+
);
|
|
1545
|
+
const placements = useCase(
|
|
1546
|
+
command.kingdom,
|
|
1547
|
+
command.domino,
|
|
1548
|
+
command.maxKingdomSize
|
|
1549
|
+
);
|
|
1550
|
+
logger.info(`Found ${placements.length} valid placements`);
|
|
1551
|
+
return placements;
|
|
1552
|
+
};
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1018
1555
|
// src/application/handlers/placeDominoHandler.ts
|
|
1019
|
-
var placeDominoHandler = (logger, useCase) => (command) => {
|
|
1556
|
+
var placeDominoHandler = (logger, translator, useCase) => (command) => {
|
|
1020
1557
|
const { game, lordId, position, rotation } = command;
|
|
1021
1558
|
logger.info(
|
|
1022
1559
|
`Placing domino: ${lordId} in position: ${JSON.stringify(
|
|
@@ -1028,19 +1565,37 @@ var placeDominoHandler = (logger, useCase) => (command) => {
|
|
|
1028
1565
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
1029
1566
|
);
|
|
1030
1567
|
throw new InvalidStepError(
|
|
1031
|
-
|
|
1568
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1569
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
1032
1570
|
);
|
|
1033
1571
|
}
|
|
1034
1572
|
if (game.nextAction.nextAction !== "placeDomino") {
|
|
1035
1573
|
logger.error(
|
|
1036
1574
|
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
1037
1575
|
);
|
|
1038
|
-
throw new InvalidStepError(
|
|
1039
|
-
|
|
1576
|
+
throw new InvalidStepError(
|
|
1577
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1578
|
+
{
|
|
1579
|
+
expected: "placeDomino",
|
|
1580
|
+
actual: game.nextAction.nextAction,
|
|
1581
|
+
gameId: game.id
|
|
1582
|
+
}
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1040
1585
|
const result = useCase(game, lordId, position, rotation);
|
|
1041
1586
|
if (isErr(result)) {
|
|
1042
1587
|
logger.error(`Error placing domino: ${result.error}`);
|
|
1043
|
-
|
|
1588
|
+
const code = result.error;
|
|
1589
|
+
throw new ActionExecutionError(
|
|
1590
|
+
translateErrorCode(translator, code),
|
|
1591
|
+
code,
|
|
1592
|
+
{
|
|
1593
|
+
gameId: game.id,
|
|
1594
|
+
lordId: command.lordId,
|
|
1595
|
+
position: command.position,
|
|
1596
|
+
rotation: command.rotation
|
|
1597
|
+
}
|
|
1598
|
+
);
|
|
1044
1599
|
}
|
|
1045
1600
|
if (isGameWithNextAction(result.value)) {
|
|
1046
1601
|
logger.info(
|
|
@@ -1058,48 +1613,113 @@ var placeDominoHandler = (logger, useCase) => (command) => {
|
|
|
1058
1613
|
return result.value;
|
|
1059
1614
|
};
|
|
1060
1615
|
|
|
1616
|
+
// src/core/useCases/serialization.ts
|
|
1617
|
+
var SERIALIZATION_VERSION = 1;
|
|
1618
|
+
var serializeGame = (game) => {
|
|
1619
|
+
const serialized = {
|
|
1620
|
+
version: SERIALIZATION_VERSION,
|
|
1621
|
+
data: game,
|
|
1622
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1623
|
+
};
|
|
1624
|
+
return JSON.stringify(serialized);
|
|
1625
|
+
};
|
|
1626
|
+
var deserializeGame = (json) => {
|
|
1627
|
+
try {
|
|
1628
|
+
const parsed = JSON.parse(json);
|
|
1629
|
+
if (!parsed.version || !parsed.data) {
|
|
1630
|
+
return err("Invalid serialized game format");
|
|
1631
|
+
}
|
|
1632
|
+
if (parsed.version > SERIALIZATION_VERSION) {
|
|
1633
|
+
return err(
|
|
1634
|
+
`Unsupported serialization version: ${parsed.version}. Current version: ${SERIALIZATION_VERSION}`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const game = parsed.data;
|
|
1638
|
+
if (!game.id || !game.players || !game.lords || !game.nextAction) {
|
|
1639
|
+
return err("Invalid game state structure");
|
|
1640
|
+
}
|
|
1641
|
+
return ok(game);
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
return err(`Failed to parse game JSON: ${error.message}`);
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
var createSavePoint = (game) => {
|
|
1647
|
+
return {
|
|
1648
|
+
serialized: serializeGame(game),
|
|
1649
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1650
|
+
gameId: game.id,
|
|
1651
|
+
turn: game.turn
|
|
1652
|
+
};
|
|
1653
|
+
};
|
|
1654
|
+
var restoreFromSavePoint = (savePoint) => {
|
|
1655
|
+
return deserializeGame(savePoint.serialized);
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
// src/application/handlers/serializeGameHandler.ts
|
|
1659
|
+
var serializeGameHandler = (logger, _translator) => {
|
|
1660
|
+
return (command) => {
|
|
1661
|
+
logger.info(`Serializing game: ${command.game.id}`);
|
|
1662
|
+
const serialized = serializeGame(command.game);
|
|
1663
|
+
logger.info(`Game serialized successfully`);
|
|
1664
|
+
return serialized;
|
|
1665
|
+
};
|
|
1666
|
+
};
|
|
1667
|
+
var deserializeGameHandler = (logger, translator) => {
|
|
1668
|
+
return (command) => {
|
|
1669
|
+
logger.info(`Deserializing game from JSON`);
|
|
1670
|
+
const result = deserializeGame(command.json);
|
|
1671
|
+
if (isErr(result)) {
|
|
1672
|
+
logger.error(`Failed to deserialize game: ${result.error}`);
|
|
1673
|
+
const code = result.error;
|
|
1674
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code);
|
|
1675
|
+
}
|
|
1676
|
+
const game = unwrap(result);
|
|
1677
|
+
logger.info(`Game deserialized successfully: ${game.id}`);
|
|
1678
|
+
return game;
|
|
1679
|
+
};
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1061
1682
|
// src/application/handlers/startGameHandler.ts
|
|
1062
|
-
var startGameHandler = (logger, useCase) => (command) => {
|
|
1683
|
+
var startGameHandler = (logger, translator, useCase) => (command) => {
|
|
1063
1684
|
const { game } = command;
|
|
1064
1685
|
logger.info(`Starting game: ${game.id}`);
|
|
1065
1686
|
if (!isGameWithNextStep(game)) {
|
|
1066
1687
|
logger.error(
|
|
1067
1688
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1068
1689
|
);
|
|
1069
|
-
throw new InvalidStepError(
|
|
1690
|
+
throw new InvalidStepError(
|
|
1691
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1692
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1693
|
+
);
|
|
1070
1694
|
}
|
|
1071
1695
|
if (game.nextAction.step !== "start" && game.nextAction.step !== "options") {
|
|
1072
1696
|
logger.error(
|
|
1073
1697
|
`Required game with start step or options step but got: ${game.nextAction.step}`
|
|
1074
1698
|
);
|
|
1075
1699
|
throw new InvalidStepError(
|
|
1076
|
-
|
|
1700
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1701
|
+
{
|
|
1702
|
+
expected: "start|options",
|
|
1703
|
+
actual: game.nextAction.step,
|
|
1704
|
+
gameId: game.id
|
|
1705
|
+
}
|
|
1077
1706
|
);
|
|
1078
1707
|
}
|
|
1079
1708
|
const result = useCase(game);
|
|
1080
1709
|
if (isErr(result)) {
|
|
1081
1710
|
logger.error(`Error starting game: ${result.error}`);
|
|
1082
|
-
|
|
1711
|
+
const code = result.error;
|
|
1712
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1713
|
+
gameId: game.id
|
|
1714
|
+
});
|
|
1083
1715
|
}
|
|
1084
1716
|
logger.info(`Game started: ${game.id}`);
|
|
1085
1717
|
return result.value;
|
|
1086
1718
|
};
|
|
1087
1719
|
|
|
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
1720
|
// src/core/domain/types/kingdom.ts
|
|
1102
1721
|
var GRIDSIZE = 9;
|
|
1722
|
+
var MAX_KINGDOM_SIZE = 5;
|
|
1103
1723
|
|
|
1104
1724
|
// src/core/domain/types/domino.ts
|
|
1105
1725
|
var GROUND = [
|
|
@@ -1122,6 +1742,101 @@ var playerActions = {
|
|
|
1122
1742
|
pass: "pass"
|
|
1123
1743
|
};
|
|
1124
1744
|
|
|
1745
|
+
// src/core/domain/entities/rule.ts
|
|
1746
|
+
var getAvailablesExtraRules = (extraRules, mode, numberOfPlayers) => {
|
|
1747
|
+
const availableExtraRules = extraRules.filter((rule) => {
|
|
1748
|
+
return rule.mode.filter((m) => m.name === mode).length > 0 && (!rule.playersLimit || rule.playersLimit === numberOfPlayers);
|
|
1749
|
+
});
|
|
1750
|
+
if (availableExtraRules.length === 0) {
|
|
1751
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1752
|
+
}
|
|
1753
|
+
return ok(availableExtraRules);
|
|
1754
|
+
};
|
|
1755
|
+
var toExtraRule = (rule, extraRules) => {
|
|
1756
|
+
const foundRule = extraRules.find((r) => r.name === rule);
|
|
1757
|
+
if (!foundRule) {
|
|
1758
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1759
|
+
}
|
|
1760
|
+
return ok(foundRule);
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// src/utils/seededShuffle.ts
|
|
1764
|
+
var hashSeed = (str) => {
|
|
1765
|
+
let hash = 2166136261;
|
|
1766
|
+
for (let i = 0; i < str.length; i++) {
|
|
1767
|
+
hash ^= str.charCodeAt(i);
|
|
1768
|
+
hash = Math.imul(hash, 16777619);
|
|
1769
|
+
}
|
|
1770
|
+
return hash >>> 0;
|
|
1771
|
+
};
|
|
1772
|
+
var mulberry32 = (seed) => {
|
|
1773
|
+
let state = seed;
|
|
1774
|
+
return () => {
|
|
1775
|
+
state |= 0;
|
|
1776
|
+
state = state + 1831565813 | 0;
|
|
1777
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
1778
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1779
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1780
|
+
};
|
|
1781
|
+
};
|
|
1782
|
+
var createSeededShuffle = (seed, salt) => {
|
|
1783
|
+
const numericSeed = hashSeed(`${seed}|${salt}`);
|
|
1784
|
+
const random = mulberry32(numericSeed);
|
|
1785
|
+
return (array) => {
|
|
1786
|
+
const result = [...array];
|
|
1787
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
1788
|
+
const j = Math.floor(random() * (i + 1));
|
|
1789
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
1790
|
+
}
|
|
1791
|
+
return result;
|
|
1792
|
+
};
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// src/core/useCases/addExtraRules.ts
|
|
1796
|
+
var addExtraRulesUseCase = (deps) => (game, rules) => {
|
|
1797
|
+
const { ruleRepository, dominoesRepository, shuffleMethod: shuffleMethod2 } = deps;
|
|
1798
|
+
const availableExtraRules = ruleRepository.getAllExtra();
|
|
1799
|
+
const newExtraRules = [];
|
|
1800
|
+
for (const rule of rules) {
|
|
1801
|
+
const result = toExtraRule(rule, availableExtraRules);
|
|
1802
|
+
if (isErr(result)) {
|
|
1803
|
+
return result;
|
|
1804
|
+
}
|
|
1805
|
+
newExtraRules.push(result.value);
|
|
1806
|
+
}
|
|
1807
|
+
let updatedBasic = game.rules.basic;
|
|
1808
|
+
let updatedDominoes = game.dominoes;
|
|
1809
|
+
const hasMightyDuel = newExtraRules.some((r) => r.name === "The Mighty Duel");
|
|
1810
|
+
if (hasMightyDuel) {
|
|
1811
|
+
updatedBasic = {
|
|
1812
|
+
...updatedBasic,
|
|
1813
|
+
maxDominoes: 48,
|
|
1814
|
+
maxTurns: 12,
|
|
1815
|
+
maxKingdomSize: 7
|
|
1816
|
+
};
|
|
1817
|
+
const allDominoes = dominoesRepository.getForMode(game.mode);
|
|
1818
|
+
if (allDominoes) {
|
|
1819
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "mighty-duel") : shuffleMethod2;
|
|
1820
|
+
updatedDominoes = shuffle(allDominoes);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const updatedRules = {
|
|
1824
|
+
basic: updatedBasic,
|
|
1825
|
+
extra: newExtraRules
|
|
1826
|
+
};
|
|
1827
|
+
const next = {
|
|
1828
|
+
type: "step",
|
|
1829
|
+
step: gameSteps.start
|
|
1830
|
+
};
|
|
1831
|
+
const updatedGame = {
|
|
1832
|
+
...game,
|
|
1833
|
+
dominoes: updatedDominoes,
|
|
1834
|
+
rules: updatedRules,
|
|
1835
|
+
nextAction: next
|
|
1836
|
+
};
|
|
1837
|
+
return ok(updatedGame);
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1125
1840
|
// src/core/domain/entities/domino.ts
|
|
1126
1841
|
var isGround = (ground) => {
|
|
1127
1842
|
return GROUND.includes(ground);
|
|
@@ -1155,7 +1870,7 @@ var placeTile = (kingdom, position, tile) => {
|
|
|
1155
1870
|
var getTile = (kingdom, position) => {
|
|
1156
1871
|
const tile = kingdom[position.y]?.[position.x];
|
|
1157
1872
|
if (!tile) {
|
|
1158
|
-
return err(
|
|
1873
|
+
return err(ErrorCode.PLACEMENT_OUT_OF_BOUNDS);
|
|
1159
1874
|
}
|
|
1160
1875
|
return ok(tile);
|
|
1161
1876
|
};
|
|
@@ -1230,7 +1945,7 @@ var calculateDominoPosition = (position, rotation, domino) => {
|
|
|
1230
1945
|
}
|
|
1231
1946
|
return [firstTile, secondTile];
|
|
1232
1947
|
};
|
|
1233
|
-
var placeDomino = (kingdom, position, rotation, domino) => {
|
|
1948
|
+
var placeDomino = (kingdom, position, rotation, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
1234
1949
|
const [firstTile, secondTile] = calculateDominoPosition(
|
|
1235
1950
|
position,
|
|
1236
1951
|
rotation,
|
|
@@ -1244,20 +1959,23 @@ var placeDomino = (kingdom, position, rotation, domino) => {
|
|
|
1244
1959
|
if (isErr(isFreePlaceResult)) {
|
|
1245
1960
|
return isFreePlaceResult;
|
|
1246
1961
|
}
|
|
1962
|
+
if (exceedsMaxSize(kingdom, firstTile.position, secondTile.position, maxKingdomSize)) {
|
|
1963
|
+
return err(ErrorCode.PLACEMENT_EXCEEDS_KINGDOM_SIZE);
|
|
1964
|
+
}
|
|
1247
1965
|
const adjacentTiles = isAdjacent(
|
|
1248
1966
|
kingdom,
|
|
1249
1967
|
firstTile.position,
|
|
1250
1968
|
secondTile.position
|
|
1251
1969
|
);
|
|
1252
1970
|
if (adjacentTiles.length === 0) {
|
|
1253
|
-
return err(
|
|
1971
|
+
return err(ErrorCode.PLACEMENT_NOT_ADJACENT);
|
|
1254
1972
|
}
|
|
1255
1973
|
const hasValidAdjacentTiles = hasValidAdjacent(adjacentTiles, [
|
|
1256
1974
|
firstTile.tile,
|
|
1257
1975
|
secondTile.tile
|
|
1258
1976
|
]);
|
|
1259
1977
|
if (!hasValidAdjacentTiles) {
|
|
1260
|
-
return err(
|
|
1978
|
+
return err(ErrorCode.PLACEMENT_INVALID_TERRAIN);
|
|
1261
1979
|
}
|
|
1262
1980
|
return ok(placeTiles(kingdom, [firstTile, secondTile]));
|
|
1263
1981
|
};
|
|
@@ -1278,46 +1996,30 @@ var isFreePlace = (kingdom, firstPosition, secondPosition) => {
|
|
|
1278
1996
|
return secondTile;
|
|
1279
1997
|
}
|
|
1280
1998
|
if (firstTile.value.type !== "empty" || secondTile.value.type !== "empty") {
|
|
1281
|
-
return err(
|
|
1999
|
+
return err(ErrorCode.PLACEMENT_NOT_EMPTY);
|
|
1282
2000
|
}
|
|
1283
2001
|
return ok(true);
|
|
1284
2002
|
};
|
|
1285
2003
|
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 }
|
|
2004
|
+
const offsets = [
|
|
2005
|
+
{ x: -1, y: 0 },
|
|
2006
|
+
{ x: 1, y: 0 },
|
|
2007
|
+
{ x: 0, y: -1 },
|
|
2008
|
+
{ x: 0, y: 1 }
|
|
1300
2009
|
];
|
|
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") {
|
|
2010
|
+
const isOwnPosition = (x, y) => x === firstPosition.x && y === firstPosition.y || x === secondPosition.x && y === secondPosition.y;
|
|
2011
|
+
const adjacentTiles = [];
|
|
2012
|
+
for (const pos of [firstPosition, secondPosition]) {
|
|
2013
|
+
for (const offset of offsets) {
|
|
2014
|
+
const nx = pos.x + offset.x;
|
|
2015
|
+
const ny = pos.y + offset.y;
|
|
2016
|
+
if (isOwnPosition(nx, ny)) continue;
|
|
2017
|
+
const tile = getTile(kingdom, { x: nx, y: ny });
|
|
2018
|
+
if (isOk(tile) && tile.value.type !== "empty") {
|
|
1317
2019
|
adjacentTiles.push(tile.value);
|
|
1318
2020
|
}
|
|
1319
2021
|
}
|
|
1320
|
-
}
|
|
2022
|
+
}
|
|
1321
2023
|
return adjacentTiles;
|
|
1322
2024
|
};
|
|
1323
2025
|
var hasValidAdjacent = (adjacentTiles, tiles) => {
|
|
@@ -1341,21 +2043,43 @@ var checkCastleIsInMiddle = (kingdom) => {
|
|
|
1341
2043
|
if (castleX === void 0 || castleY === void 0) {
|
|
1342
2044
|
return false;
|
|
1343
2045
|
}
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
2046
|
+
const bbox = getBoundingBox(kingdom);
|
|
2047
|
+
if (!bbox) return false;
|
|
2048
|
+
const centerX = (bbox.minX + bbox.maxX) / 2;
|
|
2049
|
+
const centerY = (bbox.minY + bbox.maxY) / 2;
|
|
2050
|
+
return castleX === centerX && castleY === centerY;
|
|
2051
|
+
};
|
|
2052
|
+
var getBoundingBox = (kingdom) => {
|
|
2053
|
+
let minX = Infinity;
|
|
2054
|
+
let maxX = -Infinity;
|
|
2055
|
+
let minY = Infinity;
|
|
2056
|
+
let maxY = -Infinity;
|
|
2057
|
+
for (let y = 0; y < kingdom.length; y++) {
|
|
2058
|
+
for (let x = 0; x < kingdom[y].length; x++) {
|
|
2059
|
+
if (kingdom[y][x].type !== "empty") {
|
|
2060
|
+
if (x < minX) minX = x;
|
|
2061
|
+
if (x > maxX) maxX = x;
|
|
2062
|
+
if (y < minY) minY = y;
|
|
2063
|
+
if (y > maxY) maxY = y;
|
|
2064
|
+
}
|
|
1350
2065
|
}
|
|
1351
2066
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2067
|
+
if (minX === Infinity) return null;
|
|
2068
|
+
return { minX, maxX, minY, maxY };
|
|
2069
|
+
};
|
|
2070
|
+
var exceedsMaxSize = (kingdom, firstPosition, secondPosition, maxSize = MAX_KINGDOM_SIZE) => {
|
|
2071
|
+
const bbox = getBoundingBox(kingdom);
|
|
2072
|
+
let minX = bbox?.minX ?? Infinity;
|
|
2073
|
+
let maxX = bbox?.maxX ?? -Infinity;
|
|
2074
|
+
let minY = bbox?.minY ?? Infinity;
|
|
2075
|
+
let maxY = bbox?.maxY ?? -Infinity;
|
|
2076
|
+
for (const pos of [firstPosition, secondPosition]) {
|
|
2077
|
+
if (pos.x < minX) minX = pos.x;
|
|
2078
|
+
if (pos.x > maxX) maxX = pos.x;
|
|
2079
|
+
if (pos.y < minY) minY = pos.y;
|
|
2080
|
+
if (pos.y > maxY) maxY = pos.y;
|
|
1357
2081
|
}
|
|
1358
|
-
return
|
|
2082
|
+
return maxX - minX + 1 > maxSize || maxY - minY + 1 > maxSize;
|
|
1359
2083
|
};
|
|
1360
2084
|
var countDominoes = (kingdom) => {
|
|
1361
2085
|
const notEmpties = kingdom.flat().filter((tile) => tile.type !== "empty");
|
|
@@ -1365,233 +2089,142 @@ var deepCopyKingdom = (kingdom) => {
|
|
|
1365
2089
|
return kingdom.map((line) => line.map((tile) => ({ ...tile })));
|
|
1366
2090
|
};
|
|
1367
2091
|
|
|
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
|
-
}
|
|
2092
|
+
// src/core/domain/entities/player.ts
|
|
2093
|
+
var MINPLAYERSNAMELENGTH = 3;
|
|
2094
|
+
var validatePlayer = (name) => {
|
|
2095
|
+
if (!name || name.trim().length < MINPLAYERSNAMELENGTH) {
|
|
2096
|
+
return err(ErrorCode.INVALID_PLAYER_NAME);
|
|
1382
2097
|
}
|
|
1383
|
-
return
|
|
2098
|
+
return ok(name);
|
|
1384
2099
|
};
|
|
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
|
-
}
|
|
2100
|
+
var createPlayer = (playerName, id, kingdom, bot) => {
|
|
2101
|
+
const name = validatePlayer(playerName);
|
|
2102
|
+
if (isErr(name)) {
|
|
2103
|
+
return name;
|
|
1397
2104
|
}
|
|
1398
|
-
return
|
|
2105
|
+
return ok({
|
|
2106
|
+
name: name.value,
|
|
2107
|
+
id,
|
|
2108
|
+
kingdom,
|
|
2109
|
+
...bot && { bot }
|
|
2110
|
+
});
|
|
1399
2111
|
};
|
|
1400
2112
|
|
|
1401
|
-
// src/
|
|
1402
|
-
var
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
return placements;
|
|
1408
|
-
};
|
|
2113
|
+
// src/core/useCases/addPlayers.ts
|
|
2114
|
+
var normalizePlayerInput = (input) => {
|
|
2115
|
+
if (typeof input === "string") {
|
|
2116
|
+
return { name: input };
|
|
2117
|
+
}
|
|
2118
|
+
return { name: input.name, bot: input.bot };
|
|
1409
2119
|
};
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
2120
|
+
var addPlayersUseCase = (deps) => (game, players) => {
|
|
2121
|
+
const { uuidMethod: uuidMethod2, shuffleMethod: shuffleMethod2, ruleRepository } = deps;
|
|
2122
|
+
if (!players || players.length < 1 || players.length > 4) {
|
|
2123
|
+
return err(ErrorCode.INVALID_PLAYER_COUNT);
|
|
2124
|
+
}
|
|
2125
|
+
const newPlayers = [];
|
|
2126
|
+
for (const player of players) {
|
|
2127
|
+
const { name, bot } = normalizePlayerInput(player);
|
|
2128
|
+
let kingdom = createEmptyKingdom();
|
|
2129
|
+
kingdom = placeCastle(kingdom);
|
|
2130
|
+
let id = uuidMethod2();
|
|
2131
|
+
const newPlayer = createPlayer(name, id, kingdom, bot);
|
|
2132
|
+
if (isErr(newPlayer)) {
|
|
2133
|
+
return newPlayer;
|
|
2134
|
+
}
|
|
2135
|
+
newPlayers.push(newPlayer.value);
|
|
2136
|
+
}
|
|
2137
|
+
const rules = ruleRepository.getAll();
|
|
2138
|
+
const basicRules = rules.basic[newPlayers.length];
|
|
2139
|
+
if (!basicRules) {
|
|
2140
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
2141
|
+
}
|
|
2142
|
+
const dominoes = game.dominoes;
|
|
2143
|
+
const { maxDominoes } = basicRules;
|
|
2144
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "dominoes") : shuffleMethod2;
|
|
2145
|
+
const shuffledDominoes = shuffle(dominoes);
|
|
2146
|
+
const splicedDominoes = shuffledDominoes.slice(0, maxDominoes);
|
|
2147
|
+
const next = {
|
|
2148
|
+
type: "step",
|
|
2149
|
+
step: "options"
|
|
1418
2150
|
};
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2151
|
+
const updatedGame = {
|
|
2152
|
+
...game,
|
|
2153
|
+
players: newPlayers,
|
|
2154
|
+
dominoes: splicedDominoes,
|
|
2155
|
+
rules: {
|
|
2156
|
+
...game.rules,
|
|
2157
|
+
basic: basicRules
|
|
2158
|
+
},
|
|
2159
|
+
nextAction: next
|
|
1428
2160
|
};
|
|
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);
|
|
2161
|
+
return ok(updatedGame);
|
|
1461
2162
|
};
|
|
1462
2163
|
|
|
1463
|
-
// src/
|
|
1464
|
-
var
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
2164
|
+
// src/core/useCases/calculateScore.ts
|
|
2165
|
+
var calculateScoreUseCase = (kingdom) => {
|
|
2166
|
+
const rows = kingdom.length;
|
|
2167
|
+
const cols = kingdom[0].length;
|
|
2168
|
+
const visited = kingdom.map((row) => row.map(() => false));
|
|
2169
|
+
const properties = [];
|
|
2170
|
+
const directions = [
|
|
2171
|
+
[-1, 0],
|
|
2172
|
+
[1, 0],
|
|
2173
|
+
[0, -1],
|
|
2174
|
+
[0, 1]
|
|
2175
|
+
];
|
|
2176
|
+
for (let y = 0; y < rows; y++) {
|
|
2177
|
+
for (let x = 0; x < cols; x++) {
|
|
2178
|
+
const tile = kingdom[y][x];
|
|
2179
|
+
if (visited[y][x] || tile.type === "empty" || tile.type === "castle") {
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
const type = tile.type;
|
|
2183
|
+
let size = 0;
|
|
2184
|
+
let crowns = 0;
|
|
2185
|
+
const queue = [[x, y]];
|
|
2186
|
+
visited[y][x] = true;
|
|
2187
|
+
while (queue.length > 0) {
|
|
2188
|
+
const [cx, cy] = queue.pop();
|
|
2189
|
+
const current = kingdom[cy][cx];
|
|
2190
|
+
size++;
|
|
2191
|
+
crowns += current.crowns;
|
|
2192
|
+
for (const [dx, dy] of directions) {
|
|
2193
|
+
const nx = cx + dx;
|
|
2194
|
+
const ny = cy + dy;
|
|
2195
|
+
if (nx >= 0 && ny >= 0 && nx < cols && ny < rows && !visited[ny][nx] && kingdom[ny][nx].type === type) {
|
|
2196
|
+
visited[ny][nx] = true;
|
|
2197
|
+
queue.push([nx, ny]);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
properties.push({ size, crowns });
|
|
1479
2202
|
}
|
|
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
2203
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
2204
|
+
if (properties.length === 0) {
|
|
2205
|
+
return ok({
|
|
2206
|
+
points: 0,
|
|
2207
|
+
maxPropertiesSize: 0,
|
|
2208
|
+
totalCrowns: 0
|
|
2209
|
+
});
|
|
1500
2210
|
}
|
|
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;
|
|
2211
|
+
let points = 0;
|
|
2212
|
+
let maxPropertiesSize = 0;
|
|
2213
|
+
let totalCrowns = 0;
|
|
2214
|
+
for (const property of properties) {
|
|
2215
|
+
points += property.size * property.crowns;
|
|
2216
|
+
if (property.size > maxPropertiesSize) {
|
|
2217
|
+
maxPropertiesSize = property.size;
|
|
1513
2218
|
}
|
|
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;
|
|
2219
|
+
totalCrowns += property.crowns;
|
|
1544
2220
|
}
|
|
1545
2221
|
return ok({
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2222
|
+
points,
|
|
2223
|
+
maxPropertiesSize,
|
|
2224
|
+
totalCrowns
|
|
1549
2225
|
});
|
|
1550
2226
|
};
|
|
1551
2227
|
|
|
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
2228
|
// src/core/domain/entities/lord.ts
|
|
1596
2229
|
var createLord = (id, playerId) => {
|
|
1597
2230
|
return {
|
|
@@ -1615,18 +2248,17 @@ var canPlaceAndDominoPickedIsDefined = (lord) => {
|
|
|
1615
2248
|
return !hasPick(lord) && !hasPlace(lord) && !hasTurnEnded(lord) && lord.dominoPicked !== void 0;
|
|
1616
2249
|
};
|
|
1617
2250
|
var updateLordOrder = (lords) => {
|
|
1618
|
-
return lords.sort((a, b) => {
|
|
1619
|
-
return a.dominoPicked
|
|
2251
|
+
return [...lords].sort((a, b) => {
|
|
2252
|
+
return (a.dominoPicked?.number ?? 0) - (b.dominoPicked?.number ?? 0);
|
|
1620
2253
|
});
|
|
1621
2254
|
};
|
|
1622
|
-
var resetLordsActions = (
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}
|
|
1629
|
-
return lordsCopy;
|
|
2255
|
+
var resetLordsActions = (lords) => {
|
|
2256
|
+
return lords.map((lord) => ({
|
|
2257
|
+
...lord,
|
|
2258
|
+
turnEnded: false,
|
|
2259
|
+
hasPick: false,
|
|
2260
|
+
hasPlace: false
|
|
2261
|
+
}));
|
|
1630
2262
|
};
|
|
1631
2263
|
var nextLordWithAction = (lords) => {
|
|
1632
2264
|
const nextLord = findNextLord(lords);
|
|
@@ -1653,38 +2285,29 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
|
1653
2285
|
(lord) => lord.id === game.nextAction.nextLord
|
|
1654
2286
|
);
|
|
1655
2287
|
if (!currentLord) {
|
|
1656
|
-
return err(
|
|
2288
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1657
2289
|
}
|
|
1658
2290
|
if (currentLord.id !== lordId) {
|
|
1659
|
-
return err(
|
|
2291
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1660
2292
|
}
|
|
1661
2293
|
if (!canPick(currentLord)) {
|
|
1662
|
-
return err(
|
|
2294
|
+
return err(ErrorCode.CANNOT_PICK);
|
|
1663
2295
|
}
|
|
1664
2296
|
const selectedDomino = game.currentDominoes.find(
|
|
1665
2297
|
(domino) => domino.domino.number === dominoPick
|
|
1666
2298
|
);
|
|
1667
2299
|
if (!selectedDomino) {
|
|
1668
|
-
return err(
|
|
2300
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
1669
2301
|
}
|
|
1670
2302
|
if (selectedDomino.picked) {
|
|
1671
|
-
return err(
|
|
2303
|
+
return err(ErrorCode.DOMINO_ALREADY_PICKED);
|
|
1672
2304
|
}
|
|
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
|
-
});
|
|
2305
|
+
const updatedCurrentDominoes = game.currentDominoes.map(
|
|
2306
|
+
(domino) => domino.domino.number === dominoPick ? { ...domino, picked: true, lordId: currentLord.id } : domino
|
|
2307
|
+
);
|
|
2308
|
+
const updatedLords = game.lords.map(
|
|
2309
|
+
(lord) => lord.id === currentLord.id ? { ...lord, hasPick: true, turnEnded: true, dominoPicked: selectedDomino.domino } : lord
|
|
2310
|
+
);
|
|
1688
2311
|
const turnEnded = allLordsHavePlayed(updatedLords);
|
|
1689
2312
|
if (turnEnded) {
|
|
1690
2313
|
const lords = resetLordsActions(updateLordOrder(updatedLords));
|
|
@@ -1725,7 +2348,7 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
|
1725
2348
|
|
|
1726
2349
|
// src/core/domain/entities/game.ts
|
|
1727
2350
|
var create = (payload) => {
|
|
1728
|
-
const { id, mode, dominoes } = payload;
|
|
2351
|
+
const { id, mode, dominoes, seed } = payload;
|
|
1729
2352
|
return {
|
|
1730
2353
|
id,
|
|
1731
2354
|
dominoes,
|
|
@@ -1742,11 +2365,13 @@ var create = (payload) => {
|
|
|
1742
2365
|
lords: 0,
|
|
1743
2366
|
maxDominoes: 0,
|
|
1744
2367
|
dominoesPerTurn: 0,
|
|
1745
|
-
maxTurns: 0
|
|
2368
|
+
maxTurns: 0,
|
|
2369
|
+
maxKingdomSize: 5
|
|
1746
2370
|
},
|
|
1747
2371
|
extra: []
|
|
1748
2372
|
},
|
|
1749
|
-
mode
|
|
2373
|
+
mode,
|
|
2374
|
+
...seed !== void 0 && { seed }
|
|
1750
2375
|
};
|
|
1751
2376
|
};
|
|
1752
2377
|
|
|
@@ -1759,13 +2384,14 @@ var createMode = (mode, availableMode) => {
|
|
|
1759
2384
|
if (foundMode) {
|
|
1760
2385
|
return ok(foundMode);
|
|
1761
2386
|
}
|
|
1762
|
-
return err(
|
|
2387
|
+
return err(ErrorCode.MODE_NOT_FOUND);
|
|
1763
2388
|
};
|
|
1764
2389
|
|
|
1765
2390
|
// src/core/useCases/createGame.ts
|
|
1766
|
-
var createGameUseCase = (deps) => (mode) => {
|
|
2391
|
+
var createGameUseCase = (deps) => (mode, seed) => {
|
|
1767
2392
|
const { modeRepository, dominoesRepository, uuidMethod: uuidMethod2 } = deps;
|
|
1768
2393
|
const id = uuidMethod2();
|
|
2394
|
+
const gameSeed = seed ?? uuidMethod2();
|
|
1769
2395
|
const availableMode = modeRepository.getAvailables();
|
|
1770
2396
|
const newMode = createMode(mode, availableMode);
|
|
1771
2397
|
if (isErr(newMode)) {
|
|
@@ -1773,17 +2399,51 @@ var createGameUseCase = (deps) => (mode) => {
|
|
|
1773
2399
|
}
|
|
1774
2400
|
const dominoes = dominoesRepository.getForMode(newMode.value);
|
|
1775
2401
|
if (!dominoes || dominoes.length === 0) {
|
|
1776
|
-
return err(
|
|
2402
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
1777
2403
|
}
|
|
1778
2404
|
const payload = {
|
|
1779
2405
|
id,
|
|
1780
2406
|
mode: newMode.value,
|
|
1781
|
-
dominoes
|
|
2407
|
+
dominoes,
|
|
2408
|
+
seed: gameSeed
|
|
1782
2409
|
};
|
|
1783
2410
|
const newGame = create(payload);
|
|
1784
2411
|
return ok(newGame);
|
|
1785
2412
|
};
|
|
1786
2413
|
|
|
2414
|
+
// src/core/useCases/getValidPlacements.ts
|
|
2415
|
+
var getValidPlacementsUseCase = (kingdom, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
2416
|
+
const validPlacements = [];
|
|
2417
|
+
const rotations = [0, 90, 180, 270];
|
|
2418
|
+
for (let y = 0; y < GRIDSIZE; y++) {
|
|
2419
|
+
for (let x = 0; x < GRIDSIZE; x++) {
|
|
2420
|
+
const position = { x, y };
|
|
2421
|
+
for (const rotation of rotations) {
|
|
2422
|
+
const result = placeDomino(kingdom, position, rotation, domino, maxKingdomSize);
|
|
2423
|
+
if (isOk(result)) {
|
|
2424
|
+
validPlacements.push({ position, rotation });
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return validPlacements;
|
|
2430
|
+
};
|
|
2431
|
+
var canPlaceDominoUseCase = (kingdom, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
2432
|
+
const rotations = [0, 90, 180, 270];
|
|
2433
|
+
for (let y = 0; y < GRIDSIZE; y++) {
|
|
2434
|
+
for (let x = 0; x < GRIDSIZE; x++) {
|
|
2435
|
+
const position = { x, y };
|
|
2436
|
+
for (const rotation of rotations) {
|
|
2437
|
+
const result = placeDomino(kingdom, position, rotation, domino, maxKingdomSize);
|
|
2438
|
+
if (isOk(result)) {
|
|
2439
|
+
return true;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
return false;
|
|
2445
|
+
};
|
|
2446
|
+
|
|
1787
2447
|
// src/core/useCases/discardDomino.ts
|
|
1788
2448
|
var discardDominoUseCase = (game, lordId) => {
|
|
1789
2449
|
const nextAction = game.nextAction;
|
|
@@ -1791,13 +2451,21 @@ var discardDominoUseCase = (game, lordId) => {
|
|
|
1791
2451
|
(lord) => lord.id === nextAction.nextLord
|
|
1792
2452
|
);
|
|
1793
2453
|
if (!currentLord) {
|
|
1794
|
-
return err(
|
|
2454
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1795
2455
|
}
|
|
1796
2456
|
if (currentLord.id !== lordId) {
|
|
1797
|
-
return err(
|
|
2457
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1798
2458
|
}
|
|
1799
2459
|
if (!canPass(currentLord)) {
|
|
1800
|
-
return err(
|
|
2460
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
2461
|
+
}
|
|
2462
|
+
if (currentLord.dominoPicked) {
|
|
2463
|
+
const currentPlayer = game.players.find(
|
|
2464
|
+
(player) => player.id === currentLord.playerId
|
|
2465
|
+
);
|
|
2466
|
+
if (currentPlayer && canPlaceDominoUseCase(currentPlayer.kingdom, currentLord.dominoPicked, game.rules.basic.maxKingdomSize)) {
|
|
2467
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
2468
|
+
}
|
|
1801
2469
|
}
|
|
1802
2470
|
const maxTurns = game.rules.basic.maxTurns;
|
|
1803
2471
|
const currentTurn = game.turn;
|
|
@@ -1839,12 +2507,48 @@ var discardDominoUseCase = (game, lordId) => {
|
|
|
1839
2507
|
return ok(updatedGame);
|
|
1840
2508
|
};
|
|
1841
2509
|
|
|
2510
|
+
// src/core/useCases/getDynastyResult.ts
|
|
2511
|
+
var getDynastyResultUseCase = (games) => {
|
|
2512
|
+
const playerMap = /* @__PURE__ */ new Map();
|
|
2513
|
+
for (const game of games) {
|
|
2514
|
+
for (const result of game.result) {
|
|
2515
|
+
const existing = playerMap.get(result.playerId);
|
|
2516
|
+
if (existing) {
|
|
2517
|
+
existing.points.push(result.details.points);
|
|
2518
|
+
} else {
|
|
2519
|
+
playerMap.set(result.playerId, {
|
|
2520
|
+
name: result.playerName,
|
|
2521
|
+
points: [result.details.points]
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
const results = [];
|
|
2527
|
+
for (const [playerId, data] of playerMap) {
|
|
2528
|
+
results.push({
|
|
2529
|
+
playerId,
|
|
2530
|
+
playerName: data.name,
|
|
2531
|
+
totalPoints: data.points.reduce((sum, p) => sum + p, 0),
|
|
2532
|
+
gamesPoints: data.points
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
results.sort((a, b) => b.totalPoints - a.totalPoints);
|
|
2536
|
+
const finalResults = [];
|
|
2537
|
+
for (let i = 0; i < results.length; i++) {
|
|
2538
|
+
const current = results[i];
|
|
2539
|
+
const previous = i > 0 ? finalResults[i - 1] : null;
|
|
2540
|
+
const position = previous && current.totalPoints === previous.totalPoints ? previous.position : i + 1;
|
|
2541
|
+
finalResults.push({ ...current, position });
|
|
2542
|
+
}
|
|
2543
|
+
return finalResults;
|
|
2544
|
+
};
|
|
2545
|
+
|
|
1842
2546
|
// src/core/useCases/getExtraRules.ts
|
|
1843
2547
|
var getExtraRulesUseCase = (deps) => (mode, players) => {
|
|
1844
2548
|
const { ruleRepository } = deps;
|
|
1845
2549
|
const allExtraRules = ruleRepository.getAllExtra();
|
|
1846
2550
|
if (!allExtraRules) {
|
|
1847
|
-
return err(
|
|
2551
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1848
2552
|
}
|
|
1849
2553
|
return getAvailablesExtraRules(allExtraRules, mode, players);
|
|
1850
2554
|
};
|
|
@@ -1861,11 +2565,11 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1861
2565
|
const { basic, extra } = rules;
|
|
1862
2566
|
const finalScoreResult = scoreResult.map((score) => {
|
|
1863
2567
|
let finalScore = score.details.points;
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
}
|
|
2568
|
+
const player = game.players.find((p) => p.id === score.playerId);
|
|
2569
|
+
if (!player) {
|
|
2570
|
+
return score;
|
|
2571
|
+
}
|
|
2572
|
+
const { kingdom } = player;
|
|
1869
2573
|
if (extra.length > 0) {
|
|
1870
2574
|
extra.forEach((extraRule) => {
|
|
1871
2575
|
if (extraRule.name === "The middle Kingdom") {
|
|
@@ -1897,10 +2601,7 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1897
2601
|
if (a.details.points !== b.details.points) {
|
|
1898
2602
|
return b.details.points - a.details.points;
|
|
1899
2603
|
}
|
|
1900
|
-
|
|
1901
|
-
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
1902
|
-
}
|
|
1903
|
-
return b.details.totalCrowns - a.details.totalCrowns;
|
|
2604
|
+
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
1904
2605
|
});
|
|
1905
2606
|
const result = [];
|
|
1906
2607
|
let isTie = false;
|
|
@@ -1913,7 +2614,7 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1913
2614
|
for (let i = 1; i < finalScoreResult.length; i++) {
|
|
1914
2615
|
const currentScore = finalScoreResult[i];
|
|
1915
2616
|
const previousScore = finalScoreResult[i - 1];
|
|
1916
|
-
if (previousScore && currentScore && currentScore.details.points === previousScore.details.points && currentScore.details.maxPropertiesSize === previousScore.details.maxPropertiesSize
|
|
2617
|
+
if (previousScore && currentScore && currentScore.details.points === previousScore.details.points && currentScore.details.maxPropertiesSize === previousScore.details.maxPropertiesSize) {
|
|
1917
2618
|
isTie = true;
|
|
1918
2619
|
}
|
|
1919
2620
|
if (isTie) {
|
|
@@ -1942,26 +2643,27 @@ var placeDominoUseCase = (game, lordId, position, rotation) => {
|
|
|
1942
2643
|
(lord) => lord.id === nextAction.nextLord
|
|
1943
2644
|
);
|
|
1944
2645
|
if (!currentLord) {
|
|
1945
|
-
return err(
|
|
2646
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1946
2647
|
}
|
|
1947
2648
|
if (currentLord.id !== lordId) {
|
|
1948
|
-
return err(
|
|
2649
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1949
2650
|
}
|
|
1950
2651
|
if (!canPlaceAndDominoPickedIsDefined(currentLord)) {
|
|
1951
|
-
return err(
|
|
2652
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
1952
2653
|
}
|
|
1953
2654
|
const domino = currentLord.dominoPicked;
|
|
1954
2655
|
const currentPlayer = game.players.find(
|
|
1955
2656
|
(player) => player.id === currentLord.playerId
|
|
1956
2657
|
);
|
|
1957
2658
|
if (!currentPlayer) {
|
|
1958
|
-
return err(
|
|
2659
|
+
return err(ErrorCode.PLAYER_NOT_FOUND);
|
|
1959
2660
|
}
|
|
1960
2661
|
const updatedKingdom = placeDomino(
|
|
1961
2662
|
currentPlayer.kingdom,
|
|
1962
2663
|
position,
|
|
1963
2664
|
rotation,
|
|
1964
|
-
domino
|
|
2665
|
+
domino,
|
|
2666
|
+
game.rules.basic.maxKingdomSize
|
|
1965
2667
|
);
|
|
1966
2668
|
if (isErr(updatedKingdom)) {
|
|
1967
2669
|
return updatedKingdom;
|
|
@@ -2032,7 +2734,8 @@ var startGameUseCase = (deps) => (game) => {
|
|
|
2032
2734
|
newLords.push(lord);
|
|
2033
2735
|
}
|
|
2034
2736
|
}
|
|
2035
|
-
const
|
|
2737
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "lords") : shuffleMethod2;
|
|
2738
|
+
const shuffledLords = shuffle(newLords);
|
|
2036
2739
|
const dominoesCopy = [...dominoes];
|
|
2037
2740
|
const dominoesDrawn = dominoesCopy.splice(0, dominoesPerTurn);
|
|
2038
2741
|
dominoesDrawn.sort((a, b) => a.number - b.number);
|
|
@@ -2058,82 +2761,6 @@ var startGameUseCase = (deps) => (game) => {
|
|
|
2058
2761
|
return ok(newState);
|
|
2059
2762
|
};
|
|
2060
2763
|
|
|
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
2764
|
// src/config.ts
|
|
2138
2765
|
var configureEngine = (config) => {
|
|
2139
2766
|
const modeRepository = jsonModes_default();
|
|
@@ -2141,10 +2768,12 @@ var configureEngine = (config) => {
|
|
|
2141
2768
|
const ruleRepository = jsonRules_default();
|
|
2142
2769
|
const uuid = config.uuidMethod || uuidMethod;
|
|
2143
2770
|
const shuffle = config.shuffleMethod || shuffleMethod;
|
|
2771
|
+
const translator = config.translator || defaultTranslator;
|
|
2144
2772
|
const logger = config.logger ? config.logger : config.logging ? consoleLogger(true) : silentLogger;
|
|
2145
2773
|
return {
|
|
2146
2774
|
createGameHandler: createGameHandler(
|
|
2147
2775
|
logger,
|
|
2776
|
+
translator,
|
|
2148
2777
|
createGameUseCase({
|
|
2149
2778
|
modeRepository,
|
|
2150
2779
|
dominoesRepository,
|
|
@@ -2153,14 +2782,17 @@ var configureEngine = (config) => {
|
|
|
2153
2782
|
),
|
|
2154
2783
|
getModesHandler: getModesHandler(
|
|
2155
2784
|
logger,
|
|
2785
|
+
translator,
|
|
2156
2786
|
getModesUseCase({ modeRepository })
|
|
2157
2787
|
),
|
|
2158
2788
|
getExtraRulesHandler: getExtraRulesHandler(
|
|
2159
2789
|
logger,
|
|
2790
|
+
translator,
|
|
2160
2791
|
getExtraRulesUseCase({ ruleRepository })
|
|
2161
2792
|
),
|
|
2162
2793
|
addPlayersHandler: addPlayersHandler(
|
|
2163
2794
|
logger,
|
|
2795
|
+
translator,
|
|
2164
2796
|
addPlayersUseCase({
|
|
2165
2797
|
uuidMethod: uuid,
|
|
2166
2798
|
shuffleMethod: shuffle,
|
|
@@ -2169,188 +2801,903 @@ var configureEngine = (config) => {
|
|
|
2169
2801
|
),
|
|
2170
2802
|
addExtraRulesHandler: addExtraRulesHandler(
|
|
2171
2803
|
logger,
|
|
2172
|
-
|
|
2804
|
+
translator,
|
|
2805
|
+
addExtraRulesUseCase({
|
|
2806
|
+
ruleRepository,
|
|
2807
|
+
dominoesRepository,
|
|
2808
|
+
shuffleMethod: shuffle
|
|
2809
|
+
})
|
|
2173
2810
|
),
|
|
2174
2811
|
startGameHandler: startGameHandler(
|
|
2175
2812
|
logger,
|
|
2813
|
+
translator,
|
|
2176
2814
|
startGameUseCase({ uuidMethod: uuid, shuffleMethod: shuffle })
|
|
2177
2815
|
),
|
|
2178
|
-
chooseDominoHandler: chooseDominoHandler(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2816
|
+
chooseDominoHandler: chooseDominoHandler(
|
|
2817
|
+
logger,
|
|
2818
|
+
translator,
|
|
2819
|
+
chooseDominoUseCase
|
|
2820
|
+
),
|
|
2821
|
+
placeDominoHandler: placeDominoHandler(
|
|
2822
|
+
logger,
|
|
2823
|
+
translator,
|
|
2824
|
+
placeDominoUseCase
|
|
2825
|
+
),
|
|
2826
|
+
discardDominoHandler: discardDominoHandler(
|
|
2827
|
+
logger,
|
|
2828
|
+
translator,
|
|
2829
|
+
discardDominoUseCase
|
|
2830
|
+
),
|
|
2831
|
+
getResultHandler: getResultHandler(logger, translator, {
|
|
2182
2832
|
getResultUseCase,
|
|
2183
2833
|
calculateScoreUseCase
|
|
2184
2834
|
}),
|
|
2185
|
-
calculateScoreHandler: calculateScoreHandler(
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2835
|
+
calculateScoreHandler: calculateScoreHandler(
|
|
2836
|
+
logger,
|
|
2837
|
+
translator,
|
|
2838
|
+
calculateScoreUseCase
|
|
2839
|
+
),
|
|
2840
|
+
getValidPlacementsHandler: getValidPlacementsHandler(
|
|
2841
|
+
logger,
|
|
2842
|
+
translator,
|
|
2843
|
+
getValidPlacementsUseCase
|
|
2844
|
+
),
|
|
2845
|
+
canPlaceDominoHandler: canPlaceDominoHandler(
|
|
2846
|
+
logger,
|
|
2847
|
+
translator,
|
|
2848
|
+
canPlaceDominoUseCase
|
|
2849
|
+
),
|
|
2850
|
+
serializeGameHandler: serializeGameHandler(logger, translator),
|
|
2851
|
+
deserializeGameHandler: deserializeGameHandler(logger, translator),
|
|
2852
|
+
getDynastyResultHandler: getDynastyResultHandler(
|
|
2853
|
+
logger,
|
|
2854
|
+
translator,
|
|
2855
|
+
getDynastyResultUseCase
|
|
2856
|
+
)
|
|
2190
2857
|
};
|
|
2191
2858
|
};
|
|
2192
2859
|
|
|
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
|
-
|
|
2860
|
+
// src/core/domain/entities/validateGameState.ts
|
|
2861
|
+
var issue = (path, code, message, severity = "error") => ({ path, code, message, severity });
|
|
2862
|
+
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
2863
|
+
var isNonNegativeInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
2864
|
+
var isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
2865
|
+
var validGrounds = /* @__PURE__ */ new Set([...GROUND, "empty"]);
|
|
2866
|
+
var validPlayerActions = new Set(Object.values(playerActions));
|
|
2867
|
+
var validGameSteps = new Set(Object.values(gameSteps));
|
|
2868
|
+
var validateGameStructure = (game) => {
|
|
2869
|
+
const issues = [];
|
|
2870
|
+
if (!isNonEmptyString(game.id)) {
|
|
2871
|
+
issues.push(issue("id", "INVALID_ID", "Game id must be a non-empty string"));
|
|
2872
|
+
}
|
|
2873
|
+
if (!isNonNegativeInteger(game.turn)) {
|
|
2874
|
+
issues.push(issue("turn", "INVALID_TURN", "Turn must be a non-negative integer"));
|
|
2875
|
+
}
|
|
2876
|
+
if (!Array.isArray(game.dominoes)) {
|
|
2877
|
+
issues.push(issue("dominoes", "INVALID_DOMINOES", "Dominoes must be an array"));
|
|
2878
|
+
}
|
|
2879
|
+
if (!Array.isArray(game.currentDominoes)) {
|
|
2880
|
+
issues.push(issue("currentDominoes", "INVALID_CURRENT_DOMINOES", "CurrentDominoes must be an array"));
|
|
2881
|
+
}
|
|
2882
|
+
if (!Array.isArray(game.players)) {
|
|
2883
|
+
issues.push(issue("players", "INVALID_PLAYERS", "Players must be an array"));
|
|
2884
|
+
}
|
|
2885
|
+
if (!Array.isArray(game.lords)) {
|
|
2886
|
+
issues.push(issue("lords", "INVALID_LORDS", "Lords must be an array"));
|
|
2887
|
+
}
|
|
2888
|
+
if (!game.rules || typeof game.rules !== "object") {
|
|
2889
|
+
issues.push(issue("rules", "INVALID_RULES", "Rules must be an object"));
|
|
2890
|
+
} else {
|
|
2891
|
+
if (!game.rules.basic || typeof game.rules.basic !== "object") {
|
|
2892
|
+
issues.push(issue("rules.basic", "INVALID_BASIC_RULES", "Rules.basic must be an object"));
|
|
2893
|
+
}
|
|
2894
|
+
if (!Array.isArray(game.rules.extra)) {
|
|
2895
|
+
issues.push(issue("rules.extra", "INVALID_EXTRA_RULES", "Rules.extra must be an array"));
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (!game.mode || typeof game.mode !== "object") {
|
|
2899
|
+
issues.push(issue("mode", "INVALID_MODE", "Mode must be an object"));
|
|
2900
|
+
} else {
|
|
2901
|
+
if (!isNonEmptyString(game.mode.name)) {
|
|
2902
|
+
issues.push(issue("mode.name", "INVALID_MODE_NAME", "Mode name must be a non-empty string"));
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (!game.nextAction || typeof game.nextAction !== "object") {
|
|
2906
|
+
issues.push(issue("nextAction", "INVALID_NEXT_ACTION", "NextAction must be an object"));
|
|
2907
|
+
} else if (game.nextAction.type !== "action" && game.nextAction.type !== "step") {
|
|
2908
|
+
issues.push(issue("nextAction.type", "INVALID_NEXT_ACTION_TYPE", "NextAction.type must be 'action' or 'step'"));
|
|
2909
|
+
}
|
|
2910
|
+
return issues;
|
|
2911
|
+
};
|
|
2912
|
+
var validateTile = (tile, path) => {
|
|
2913
|
+
const issues = [];
|
|
2914
|
+
if (!tile || typeof tile !== "object") {
|
|
2915
|
+
issues.push(issue(path, "INVALID_TILE", `Tile at ${path} must be an object`));
|
|
2916
|
+
return issues;
|
|
2917
|
+
}
|
|
2918
|
+
const t = tile;
|
|
2919
|
+
if (typeof t.type !== "string" || !validGrounds.has(t.type)) {
|
|
2920
|
+
issues.push(issue(`${path}.type`, "INVALID_TILE_TYPE", `Invalid terrain type at ${path}`));
|
|
2921
|
+
}
|
|
2922
|
+
if (typeof t.crowns !== "number" || t.crowns < 0) {
|
|
2923
|
+
issues.push(issue(`${path}.crowns`, "INVALID_TILE_CROWNS", `Invalid crowns at ${path}`));
|
|
2924
|
+
}
|
|
2925
|
+
return issues;
|
|
2926
|
+
};
|
|
2927
|
+
var validatePlayers = (game) => {
|
|
2928
|
+
const issues = [];
|
|
2929
|
+
if (!Array.isArray(game.players)) return issues;
|
|
2930
|
+
const playerIds = /* @__PURE__ */ new Set();
|
|
2931
|
+
for (let i = 0; i < game.players.length; i++) {
|
|
2932
|
+
const player = game.players[i];
|
|
2933
|
+
const path = `players[${i}]`;
|
|
2934
|
+
if (!isNonEmptyString(player.id)) {
|
|
2935
|
+
issues.push(issue(`${path}.id`, "INVALID_PLAYER_ID", `Player id must be a non-empty string`));
|
|
2936
|
+
} else if (playerIds.has(player.id)) {
|
|
2937
|
+
issues.push(issue(`${path}.id`, "DUPLICATE_PLAYER_ID", `Duplicate player id: ${player.id}`));
|
|
2938
|
+
} else {
|
|
2939
|
+
playerIds.add(player.id);
|
|
2940
|
+
}
|
|
2941
|
+
if (typeof player.name !== "string" || player.name.length < 3) {
|
|
2942
|
+
issues.push(issue(`${path}.name`, "INVALID_PLAYER_NAME", `Player name must be at least 3 characters`));
|
|
2943
|
+
}
|
|
2944
|
+
if (!Array.isArray(player.kingdom)) {
|
|
2945
|
+
issues.push(issue(`${path}.kingdom`, "INVALID_KINGDOM", `Kingdom must be a 2D array`));
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
if (player.kingdom.length !== GRIDSIZE) {
|
|
2949
|
+
issues.push(issue(`${path}.kingdom`, "INVALID_KINGDOM_GRID", `Kingdom must have ${GRIDSIZE} rows, got ${player.kingdom.length}`));
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
let castleCount = 0;
|
|
2953
|
+
for (let y = 0; y < player.kingdom.length; y++) {
|
|
2954
|
+
const row = player.kingdom[y];
|
|
2955
|
+
if (!Array.isArray(row) || row.length !== GRIDSIZE) {
|
|
2956
|
+
issues.push(issue(`${path}.kingdom[${y}]`, "INVALID_KINGDOM_ROW", `Kingdom row ${y} must have ${GRIDSIZE} columns`));
|
|
2957
|
+
continue;
|
|
2222
2958
|
}
|
|
2223
|
-
|
|
2959
|
+
for (let x = 0; x < row.length; x++) {
|
|
2960
|
+
const cell = row[x];
|
|
2961
|
+
issues.push(...validateTile(cell, `${path}.kingdom[${y}][${x}]`));
|
|
2962
|
+
if (cell && typeof cell === "object" && cell.type === "castle") {
|
|
2963
|
+
castleCount++;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
if (castleCount === 0) {
|
|
2968
|
+
issues.push(issue(`${path}.kingdom`, "MISSING_CASTLE", `Kingdom must have exactly one castle tile`, "warning"));
|
|
2969
|
+
} else if (castleCount > 1) {
|
|
2970
|
+
issues.push(issue(`${path}.kingdom`, "MULTIPLE_CASTLES", `Kingdom has ${castleCount} castle tiles, expected 1`));
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
return issues;
|
|
2974
|
+
};
|
|
2975
|
+
var validateDominoes = (game) => {
|
|
2976
|
+
const issues = [];
|
|
2977
|
+
const allNumbers = /* @__PURE__ */ new Set();
|
|
2978
|
+
if (Array.isArray(game.dominoes)) {
|
|
2979
|
+
for (let i = 0; i < game.dominoes.length; i++) {
|
|
2980
|
+
const domino = game.dominoes[i];
|
|
2981
|
+
const path = `dominoes[${i}]`;
|
|
2982
|
+
if (!isPositiveInteger(domino.number)) {
|
|
2983
|
+
issues.push(issue(`${path}.number`, "INVALID_DOMINO_NUMBER", `Domino number must be a positive integer`));
|
|
2984
|
+
} else if (allNumbers.has(domino.number)) {
|
|
2985
|
+
issues.push(issue(`${path}.number`, "DUPLICATE_DOMINO_NUMBER", `Duplicate domino number: ${domino.number}`));
|
|
2986
|
+
} else {
|
|
2987
|
+
allNumbers.add(domino.number);
|
|
2988
|
+
}
|
|
2989
|
+
issues.push(...validateTile(domino.left, `${path}.left`));
|
|
2990
|
+
issues.push(...validateTile(domino.right, `${path}.right`));
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (Array.isArray(game.currentDominoes)) {
|
|
2994
|
+
for (let i = 0; i < game.currentDominoes.length; i++) {
|
|
2995
|
+
const revealed = game.currentDominoes[i];
|
|
2996
|
+
const path = `currentDominoes[${i}]`;
|
|
2997
|
+
if (!revealed.domino || typeof revealed.domino !== "object") {
|
|
2998
|
+
issues.push(issue(`${path}.domino`, "INVALID_REVEALED_DOMINO", `Revealed domino must be an object`));
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
const domino = revealed.domino;
|
|
3002
|
+
if (!isPositiveInteger(domino.number)) {
|
|
3003
|
+
issues.push(issue(`${path}.domino.number`, "INVALID_DOMINO_NUMBER", `Domino number must be a positive integer`));
|
|
3004
|
+
} else if (allNumbers.has(domino.number)) {
|
|
3005
|
+
issues.push(issue(`${path}.domino.number`, "DUPLICATE_DOMINO_NUMBER", `Duplicate domino number: ${domino.number}`));
|
|
3006
|
+
} else {
|
|
3007
|
+
allNumbers.add(domino.number);
|
|
3008
|
+
}
|
|
3009
|
+
issues.push(...validateTile(domino.left, `${path}.domino.left`));
|
|
3010
|
+
issues.push(...validateTile(domino.right, `${path}.domino.right`));
|
|
3011
|
+
if (revealed.picked && revealed.lordId === null) {
|
|
3012
|
+
issues.push(issue(`${path}`, "PICKED_WITHOUT_LORD", `Domino is marked as picked but has no lordId`));
|
|
3013
|
+
}
|
|
3014
|
+
if (!revealed.picked && revealed.lordId !== null) {
|
|
3015
|
+
issues.push(issue(`${path}`, "UNPICKED_WITH_LORD", `Domino is not picked but has a lordId`));
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
if (Array.isArray(game.lords)) {
|
|
3020
|
+
const lordPickedNumbers = /* @__PURE__ */ new Set();
|
|
3021
|
+
for (let i = 0; i < game.lords.length; i++) {
|
|
3022
|
+
const lord = game.lords[i];
|
|
3023
|
+
if (lord.dominoPicked) {
|
|
3024
|
+
const path = `lords[${i}].dominoPicked`;
|
|
3025
|
+
const domino = lord.dominoPicked;
|
|
3026
|
+
if (!isPositiveInteger(domino.number)) {
|
|
3027
|
+
issues.push(issue(`${path}.number`, "INVALID_DOMINO_NUMBER", `Domino number must be a positive integer`));
|
|
3028
|
+
} else if (lordPickedNumbers.has(domino.number)) {
|
|
3029
|
+
issues.push(issue(`${path}.number`, "DUPLICATE_DOMINO_NUMBER", `Duplicate domino number among lords: ${domino.number}`));
|
|
3030
|
+
} else {
|
|
3031
|
+
lordPickedNumbers.add(domino.number);
|
|
3032
|
+
}
|
|
3033
|
+
issues.push(...validateTile(domino.left, `${path}.left`));
|
|
3034
|
+
issues.push(...validateTile(domino.right, `${path}.right`));
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
2224
3037
|
}
|
|
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);
|
|
3038
|
+
return issues;
|
|
3039
|
+
};
|
|
3040
|
+
var validateLords = (game) => {
|
|
3041
|
+
const issues = [];
|
|
3042
|
+
if (!Array.isArray(game.lords)) return issues;
|
|
3043
|
+
const playerIds = new Set(
|
|
3044
|
+
Array.isArray(game.players) ? game.players.map((p) => p.id) : []
|
|
3045
|
+
);
|
|
3046
|
+
const lordIds = /* @__PURE__ */ new Set();
|
|
3047
|
+
for (let i = 0; i < game.lords.length; i++) {
|
|
3048
|
+
const lord = game.lords[i];
|
|
3049
|
+
const path = `lords[${i}]`;
|
|
3050
|
+
if (!isNonEmptyString(lord.id)) {
|
|
3051
|
+
issues.push(issue(`${path}.id`, "INVALID_LORD_ID", `Lord id must be a non-empty string`));
|
|
3052
|
+
} else if (lordIds.has(lord.id)) {
|
|
3053
|
+
issues.push(issue(`${path}.id`, "DUPLICATE_LORD_ID", `Duplicate lord id: ${lord.id}`));
|
|
2244
3054
|
} else {
|
|
2245
|
-
|
|
3055
|
+
lordIds.add(lord.id);
|
|
3056
|
+
}
|
|
3057
|
+
if (!isNonEmptyString(lord.playerId)) {
|
|
3058
|
+
issues.push(issue(`${path}.playerId`, "INVALID_LORD_PLAYER_ID", `Lord playerId must be a non-empty string`));
|
|
3059
|
+
} else if (playerIds.size > 0 && !playerIds.has(lord.playerId)) {
|
|
3060
|
+
issues.push(issue(`${path}.playerId`, "LORD_PLAYER_NOT_FOUND", `Lord references non-existent player: ${lord.playerId}`));
|
|
3061
|
+
}
|
|
3062
|
+
if (lord.turnEnded && (!lord.hasPick || !lord.hasPlace)) {
|
|
3063
|
+
issues.push(issue(path, "INCONSISTENT_LORD_FLAGS", `Lord has turnEnded=true but hasPick=${lord.hasPick}, hasPlace=${lord.hasPlace}`, "warning"));
|
|
2246
3064
|
}
|
|
2247
3065
|
}
|
|
3066
|
+
return issues;
|
|
2248
3067
|
};
|
|
2249
|
-
var
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
3068
|
+
var validateGameFlow = (game) => {
|
|
3069
|
+
const issues = [];
|
|
3070
|
+
if (!game.nextAction || typeof game.nextAction !== "object") return issues;
|
|
3071
|
+
const nextAction = game.nextAction;
|
|
3072
|
+
if (nextAction.type === "action") {
|
|
3073
|
+
const action = nextAction;
|
|
3074
|
+
if (!isNonEmptyString(action.nextLord)) {
|
|
3075
|
+
issues.push(issue("nextAction.nextLord", "INVALID_NEXT_LORD", "nextLord must be a non-empty string"));
|
|
3076
|
+
} else if (Array.isArray(game.lords) && !game.lords.some((l) => l.id === action.nextLord)) {
|
|
3077
|
+
issues.push(issue("nextAction.nextLord", "NEXT_LORD_NOT_FOUND", `nextLord references non-existent lord: ${action.nextLord}`));
|
|
3078
|
+
}
|
|
3079
|
+
if (typeof action.nextAction !== "string" || !validPlayerActions.has(action.nextAction)) {
|
|
3080
|
+
issues.push(issue("nextAction.nextAction", "INVALID_PLAYER_ACTION", `nextAction must be one of: ${[...validPlayerActions].join(", ")}`));
|
|
3081
|
+
}
|
|
3082
|
+
} else if (nextAction.type === "step") {
|
|
3083
|
+
const step = nextAction;
|
|
3084
|
+
if (typeof step.step !== "string" || !validGameSteps.has(step.step)) {
|
|
3085
|
+
issues.push(issue("nextAction.step", "INVALID_GAME_STEP", `step must be one of: ${[...validGameSteps].join(", ")}`));
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
return issues;
|
|
3089
|
+
};
|
|
3090
|
+
var isSetupPhase = (game) => {
|
|
3091
|
+
if (game.nextAction?.type !== "step") return false;
|
|
3092
|
+
const step = game.nextAction.step;
|
|
3093
|
+
return step === "addPlayers" || step === "options" || step === "start";
|
|
3094
|
+
};
|
|
3095
|
+
var validateRules = (game) => {
|
|
3096
|
+
const issues = [];
|
|
3097
|
+
if (!game.rules || typeof game.rules !== "object") return issues;
|
|
3098
|
+
const allowZero = isSetupPhase(game);
|
|
3099
|
+
const basic = game.rules.basic;
|
|
3100
|
+
if (basic && typeof basic === "object") {
|
|
3101
|
+
const fields = ["lords", "maxDominoes", "dominoesPerTurn", "maxTurns", "maxKingdomSize"];
|
|
3102
|
+
for (const field of fields) {
|
|
3103
|
+
const check = allowZero ? isNonNegativeInteger : isPositiveInteger;
|
|
3104
|
+
if (!check(basic[field])) {
|
|
3105
|
+
issues.push(issue(`rules.basic.${field}`, "INVALID_BASIC_RULE_VALUE", `rules.basic.${field} must be a positive integer`));
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (Array.isArray(game.rules.extra)) {
|
|
3110
|
+
for (let i = 0; i < game.rules.extra.length; i++) {
|
|
3111
|
+
const rule = game.rules.extra[i];
|
|
3112
|
+
if (!isNonEmptyString(rule.name)) {
|
|
3113
|
+
issues.push(issue(`rules.extra[${i}].name`, "INVALID_EXTRA_RULE_NAME", `Extra rule name must be a non-empty string`));
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return issues;
|
|
3118
|
+
};
|
|
3119
|
+
var validateGameState = (game) => {
|
|
3120
|
+
const issues = [];
|
|
3121
|
+
issues.push(...validateGameStructure(game));
|
|
3122
|
+
issues.push(...validatePlayers(game));
|
|
3123
|
+
issues.push(...validateDominoes(game));
|
|
3124
|
+
issues.push(...validateLords(game));
|
|
3125
|
+
issues.push(...validateGameFlow(game));
|
|
3126
|
+
issues.push(...validateRules(game));
|
|
3127
|
+
return issues;
|
|
2256
3128
|
};
|
|
2257
3129
|
|
|
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"
|
|
3130
|
+
// src/core/useCases/botRegistry.ts
|
|
3131
|
+
var defaultStrategies = {
|
|
3132
|
+
random: randomStrategy,
|
|
3133
|
+
greedy: greedyStrategy,
|
|
3134
|
+
advanced: advancedStrategy,
|
|
3135
|
+
expert: expertStrategy
|
|
2298
3136
|
};
|
|
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"
|
|
3137
|
+
var getStrategy = (name, custom) => {
|
|
3138
|
+
return custom?.[name] ?? defaultStrategies[name];
|
|
2338
3139
|
};
|
|
2339
|
-
var
|
|
2340
|
-
|
|
3140
|
+
var getStrategyNames = () => Object.keys(defaultStrategies);
|
|
3141
|
+
|
|
3142
|
+
// src/core/useCases/bot.ts
|
|
3143
|
+
var scoreKingdom = (kingdom) => {
|
|
3144
|
+
const result = calculateScoreUseCase(kingdom);
|
|
3145
|
+
return isOk(result) ? result.value.points : 0;
|
|
3146
|
+
};
|
|
3147
|
+
var simulatePlacement = (kingdom, domino, placement, maxKingdomSize) => {
|
|
3148
|
+
const result = placeDomino(
|
|
3149
|
+
kingdom,
|
|
3150
|
+
placement.position,
|
|
3151
|
+
placement.rotation,
|
|
3152
|
+
domino,
|
|
3153
|
+
maxKingdomSize
|
|
3154
|
+
);
|
|
3155
|
+
return isOk(result) ? result.value : null;
|
|
3156
|
+
};
|
|
3157
|
+
var bestPlacementScore = (kingdom, domino, placements, maxKingdomSize) => {
|
|
3158
|
+
let best = null;
|
|
3159
|
+
for (const placement of placements) {
|
|
3160
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxKingdomSize);
|
|
3161
|
+
if (!newKingdom) continue;
|
|
3162
|
+
const score = scoreKingdom(newKingdom);
|
|
3163
|
+
if (!best || score > best.score) {
|
|
3164
|
+
best = { placement, score, kingdom: newKingdom };
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
return best;
|
|
3168
|
+
};
|
|
3169
|
+
var getMaxKingdomSize = (game) => game.rules.basic.maxKingdomSize;
|
|
3170
|
+
var getLordAndPlayer = (game, lordId) => {
|
|
3171
|
+
const lord = game.lords.find((l) => l.id === lordId);
|
|
3172
|
+
const player = lord ? game.players.find((p) => p.id === lord.playerId) : void 0;
|
|
3173
|
+
return { lord, player };
|
|
3174
|
+
};
|
|
3175
|
+
var getNextBatchDominoes = (game) => {
|
|
3176
|
+
const perTurn = game.rules.basic.dominoesPerTurn;
|
|
3177
|
+
return [...game.dominoes].slice(0, perTurn).sort((a, b) => a.number - b.number);
|
|
3178
|
+
};
|
|
3179
|
+
var randomStrategy = {
|
|
3180
|
+
chooseDomino: (context) => {
|
|
3181
|
+
const { availableDominoes } = context;
|
|
3182
|
+
const idx = Math.floor(Math.random() * availableDominoes.length);
|
|
3183
|
+
return availableDominoes[idx].domino.number;
|
|
3184
|
+
},
|
|
3185
|
+
choosePlacement: (context) => {
|
|
3186
|
+
const { validPlacements } = context;
|
|
3187
|
+
if (validPlacements.length === 0) return null;
|
|
3188
|
+
const idx = Math.floor(Math.random() * validPlacements.length);
|
|
3189
|
+
return validPlacements[idx];
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
var greedyStrategy = {
|
|
3193
|
+
chooseDomino: (context) => {
|
|
3194
|
+
const { game, lordId, availableDominoes } = context;
|
|
3195
|
+
const { player } = getLordAndPlayer(game, lordId);
|
|
3196
|
+
if (!player) return availableDominoes[0].domino.number;
|
|
3197
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3198
|
+
let bestScore = -1;
|
|
3199
|
+
let bestPick = availableDominoes[0].domino.number;
|
|
3200
|
+
for (const rd of availableDominoes) {
|
|
3201
|
+
const placements = getValidPlacementsUseCase(
|
|
3202
|
+
player.kingdom,
|
|
3203
|
+
rd.domino,
|
|
3204
|
+
maxSize
|
|
3205
|
+
);
|
|
3206
|
+
const result = bestPlacementScore(
|
|
3207
|
+
player.kingdom,
|
|
3208
|
+
rd.domino,
|
|
3209
|
+
placements,
|
|
3210
|
+
maxSize
|
|
3211
|
+
);
|
|
3212
|
+
const score = result ? result.score : 0;
|
|
3213
|
+
if (score > bestScore) {
|
|
3214
|
+
bestScore = score;
|
|
3215
|
+
bestPick = rd.domino.number;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
return bestPick;
|
|
3219
|
+
},
|
|
3220
|
+
choosePlacement: (context) => {
|
|
3221
|
+
const { game, domino, kingdom, validPlacements } = context;
|
|
3222
|
+
if (validPlacements.length === 0) return null;
|
|
3223
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3224
|
+
const result = bestPlacementScore(kingdom, domino, validPlacements, maxSize);
|
|
3225
|
+
return result ? result.placement : validPlacements[0];
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
var evaluateWithLookahead = (kingdom, maxSize, nextDominoes) => {
|
|
3229
|
+
if (nextDominoes.length === 0) return 0;
|
|
3230
|
+
let bestNextScore = 0;
|
|
3231
|
+
for (const nextDomino of nextDominoes) {
|
|
3232
|
+
const nextPlacements = getValidPlacementsUseCase(kingdom, nextDomino, maxSize);
|
|
3233
|
+
const result = bestPlacementScore(kingdom, nextDomino, nextPlacements, maxSize);
|
|
3234
|
+
if (result && result.score > bestNextScore) {
|
|
3235
|
+
bestNextScore = result.score;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return bestNextScore;
|
|
3239
|
+
};
|
|
3240
|
+
var advancedStrategy = {
|
|
3241
|
+
chooseDomino: (context) => {
|
|
3242
|
+
const { game, lordId, availableDominoes } = context;
|
|
3243
|
+
const { player } = getLordAndPlayer(game, lordId);
|
|
3244
|
+
if (!player) return availableDominoes[0].domino.number;
|
|
3245
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3246
|
+
const nextBatch = getNextBatchDominoes(game);
|
|
3247
|
+
let bestCombinedScore = -1;
|
|
3248
|
+
let bestPick = availableDominoes[0].domino.number;
|
|
3249
|
+
for (const rd of availableDominoes) {
|
|
3250
|
+
const placements = getValidPlacementsUseCase(
|
|
3251
|
+
player.kingdom,
|
|
3252
|
+
rd.domino,
|
|
3253
|
+
maxSize
|
|
3254
|
+
);
|
|
3255
|
+
const result = bestPlacementScore(
|
|
3256
|
+
player.kingdom,
|
|
3257
|
+
rd.domino,
|
|
3258
|
+
placements,
|
|
3259
|
+
maxSize
|
|
3260
|
+
);
|
|
3261
|
+
const immediateScore = result ? result.score : scoreKingdom(player.kingdom);
|
|
3262
|
+
const kingdomAfter = result ? result.kingdom : player.kingdom;
|
|
3263
|
+
const futureScore = evaluateWithLookahead(kingdomAfter, maxSize, nextBatch);
|
|
3264
|
+
const combined = immediateScore + futureScore * 0.8;
|
|
3265
|
+
if (combined > bestCombinedScore) {
|
|
3266
|
+
bestCombinedScore = combined;
|
|
3267
|
+
bestPick = rd.domino.number;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return bestPick;
|
|
3271
|
+
},
|
|
3272
|
+
choosePlacement: (context) => {
|
|
3273
|
+
const { game, domino, kingdom, validPlacements } = context;
|
|
3274
|
+
if (validPlacements.length === 0) return null;
|
|
3275
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3276
|
+
const nextBatch = getNextBatchDominoes(game);
|
|
3277
|
+
let bestCombinedScore = -1;
|
|
3278
|
+
let bestPlacement = validPlacements[0];
|
|
3279
|
+
for (const placement of validPlacements) {
|
|
3280
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxSize);
|
|
3281
|
+
if (!newKingdom) continue;
|
|
3282
|
+
const immediateScore = scoreKingdom(newKingdom);
|
|
3283
|
+
const futureScore = evaluateWithLookahead(newKingdom, maxSize, nextBatch);
|
|
3284
|
+
const combined = immediateScore + futureScore * 0.8;
|
|
3285
|
+
if (combined > bestCombinedScore) {
|
|
3286
|
+
bestCombinedScore = combined;
|
|
3287
|
+
bestPlacement = placement;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return bestPlacement;
|
|
3291
|
+
}
|
|
3292
|
+
};
|
|
3293
|
+
var EXPERT_BEAM_WIDTH = 2;
|
|
3294
|
+
var EXPERT_MAX_DEPTH = 2;
|
|
3295
|
+
var topPlacements = (kingdom, domino, placements, maxSize, limit) => {
|
|
3296
|
+
if (placements.length <= limit) return placements;
|
|
3297
|
+
const scored = placements.map((p) => {
|
|
3298
|
+
const k = simulatePlacement(kingdom, domino, p, maxSize);
|
|
3299
|
+
return { placement: p, score: k ? scoreKingdom(k) : 0 };
|
|
3300
|
+
}).sort((a, b) => b.score - a.score);
|
|
3301
|
+
return scored.slice(0, limit).map((s) => s.placement);
|
|
3302
|
+
};
|
|
3303
|
+
var expertEvaluate = (kingdom, remainingDominoes, dominoesPerTurn, maxSize, depth) => {
|
|
3304
|
+
if (depth <= 0 || remainingDominoes.length === 0) {
|
|
3305
|
+
return scoreKingdom(kingdom);
|
|
3306
|
+
}
|
|
3307
|
+
const batch = [...remainingDominoes].slice(0, dominoesPerTurn).sort((a, b) => a.number - b.number);
|
|
3308
|
+
const afterBatch = remainingDominoes.slice(dominoesPerTurn);
|
|
3309
|
+
let bestScore = scoreKingdom(kingdom);
|
|
3310
|
+
for (const domino of batch) {
|
|
3311
|
+
const allPlacements = getValidPlacementsUseCase(kingdom, domino, maxSize);
|
|
3312
|
+
if (allPlacements.length === 0) {
|
|
3313
|
+
const score = expertEvaluate(
|
|
3314
|
+
kingdom,
|
|
3315
|
+
afterBatch,
|
|
3316
|
+
dominoesPerTurn,
|
|
3317
|
+
maxSize,
|
|
3318
|
+
depth - 1
|
|
3319
|
+
);
|
|
3320
|
+
if (score > bestScore) bestScore = score;
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
const candidates = topPlacements(
|
|
3324
|
+
kingdom,
|
|
3325
|
+
domino,
|
|
3326
|
+
allPlacements,
|
|
3327
|
+
maxSize,
|
|
3328
|
+
EXPERT_BEAM_WIDTH
|
|
3329
|
+
);
|
|
3330
|
+
for (const placement of candidates) {
|
|
3331
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxSize);
|
|
3332
|
+
if (!newKingdom) continue;
|
|
3333
|
+
const score = expertEvaluate(
|
|
3334
|
+
newKingdom,
|
|
3335
|
+
afterBatch,
|
|
3336
|
+
dominoesPerTurn,
|
|
3337
|
+
maxSize,
|
|
3338
|
+
depth - 1
|
|
3339
|
+
);
|
|
3340
|
+
if (score > bestScore) bestScore = score;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return bestScore;
|
|
3344
|
+
};
|
|
3345
|
+
var expertStrategy = {
|
|
3346
|
+
chooseDomino: (context) => {
|
|
3347
|
+
const { game, lordId, availableDominoes } = context;
|
|
3348
|
+
const { player } = getLordAndPlayer(game, lordId);
|
|
3349
|
+
if (!player) return availableDominoes[0].domino.number;
|
|
3350
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3351
|
+
const perTurn = game.rules.basic.dominoesPerTurn;
|
|
3352
|
+
const turnsLeft = Math.min(
|
|
3353
|
+
game.rules.basic.maxTurns - game.turn,
|
|
3354
|
+
EXPERT_MAX_DEPTH
|
|
3355
|
+
);
|
|
3356
|
+
let bestScore = -1;
|
|
3357
|
+
let bestPick = availableDominoes[0].domino.number;
|
|
3358
|
+
for (const rd of availableDominoes) {
|
|
3359
|
+
const allPlacements = getValidPlacementsUseCase(
|
|
3360
|
+
player.kingdom,
|
|
3361
|
+
rd.domino,
|
|
3362
|
+
maxSize
|
|
3363
|
+
);
|
|
3364
|
+
if (allPlacements.length === 0) {
|
|
3365
|
+
const score = expertEvaluate(
|
|
3366
|
+
player.kingdom,
|
|
3367
|
+
game.dominoes,
|
|
3368
|
+
perTurn,
|
|
3369
|
+
maxSize,
|
|
3370
|
+
turnsLeft
|
|
3371
|
+
);
|
|
3372
|
+
if (score > bestScore) {
|
|
3373
|
+
bestScore = score;
|
|
3374
|
+
bestPick = rd.domino.number;
|
|
3375
|
+
}
|
|
3376
|
+
continue;
|
|
3377
|
+
}
|
|
3378
|
+
const candidates = topPlacements(
|
|
3379
|
+
player.kingdom,
|
|
3380
|
+
rd.domino,
|
|
3381
|
+
allPlacements,
|
|
3382
|
+
maxSize,
|
|
3383
|
+
EXPERT_BEAM_WIDTH
|
|
3384
|
+
);
|
|
3385
|
+
for (const placement of candidates) {
|
|
3386
|
+
const newKingdom = simulatePlacement(
|
|
3387
|
+
player.kingdom,
|
|
3388
|
+
rd.domino,
|
|
3389
|
+
placement,
|
|
3390
|
+
maxSize
|
|
3391
|
+
);
|
|
3392
|
+
if (!newKingdom) continue;
|
|
3393
|
+
const score = expertEvaluate(
|
|
3394
|
+
newKingdom,
|
|
3395
|
+
game.dominoes,
|
|
3396
|
+
perTurn,
|
|
3397
|
+
maxSize,
|
|
3398
|
+
turnsLeft
|
|
3399
|
+
);
|
|
3400
|
+
if (score > bestScore) {
|
|
3401
|
+
bestScore = score;
|
|
3402
|
+
bestPick = rd.domino.number;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
return bestPick;
|
|
3407
|
+
},
|
|
3408
|
+
choosePlacement: (context) => {
|
|
3409
|
+
const { game, domino, kingdom, validPlacements } = context;
|
|
3410
|
+
if (validPlacements.length === 0) return null;
|
|
3411
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3412
|
+
const perTurn = game.rules.basic.dominoesPerTurn;
|
|
3413
|
+
const turnsLeft = Math.min(
|
|
3414
|
+
game.rules.basic.maxTurns - game.turn,
|
|
3415
|
+
EXPERT_MAX_DEPTH
|
|
3416
|
+
);
|
|
3417
|
+
let bestScore = -1;
|
|
3418
|
+
let bestPlacement = validPlacements[0];
|
|
3419
|
+
for (const placement of validPlacements) {
|
|
3420
|
+
const newKingdom = simulatePlacement(kingdom, domino, placement, maxSize);
|
|
3421
|
+
if (!newKingdom) continue;
|
|
3422
|
+
const score = expertEvaluate(
|
|
3423
|
+
newKingdom,
|
|
3424
|
+
game.dominoes,
|
|
3425
|
+
perTurn,
|
|
3426
|
+
maxSize,
|
|
3427
|
+
turnsLeft
|
|
3428
|
+
);
|
|
3429
|
+
if (score > bestScore) {
|
|
3430
|
+
bestScore = score;
|
|
3431
|
+
bestPlacement = placement;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return bestPlacement;
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
var playBotTurn = (engine, game, strategy) => {
|
|
3438
|
+
const lordId = game.nextAction.nextLord;
|
|
3439
|
+
const action = game.nextAction.nextAction;
|
|
3440
|
+
if (action === "pickDomino") {
|
|
3441
|
+
const availableDominoes = game.currentDominoes.filter((d) => !d.picked);
|
|
3442
|
+
const dominoPick = strategy.chooseDomino({
|
|
3443
|
+
game,
|
|
3444
|
+
lordId,
|
|
3445
|
+
availableDominoes
|
|
3446
|
+
});
|
|
3447
|
+
return engine.chooseDomino({ game, lordId, dominoPick });
|
|
3448
|
+
}
|
|
3449
|
+
if (action === "placeDomino") {
|
|
3450
|
+
const { lord, player } = getLordAndPlayer(game, lordId);
|
|
3451
|
+
if (!lord?.dominoPicked || !player) {
|
|
3452
|
+
return engine.discardDomino({ game, lordId });
|
|
3453
|
+
}
|
|
3454
|
+
const domino = lord.dominoPicked;
|
|
3455
|
+
const kingdom = player.kingdom;
|
|
3456
|
+
const maxSize = getMaxKingdomSize(game);
|
|
3457
|
+
const validPlacements = getValidPlacementsUseCase(kingdom, domino, maxSize);
|
|
3458
|
+
const placement = strategy.choosePlacement({
|
|
3459
|
+
game,
|
|
3460
|
+
lordId,
|
|
3461
|
+
domino,
|
|
3462
|
+
kingdom,
|
|
3463
|
+
validPlacements
|
|
3464
|
+
});
|
|
3465
|
+
if (!placement) {
|
|
3466
|
+
return engine.discardDomino({ game, lordId });
|
|
3467
|
+
}
|
|
3468
|
+
return engine.placeDomino({
|
|
3469
|
+
game,
|
|
3470
|
+
lordId,
|
|
3471
|
+
position: placement.position,
|
|
3472
|
+
rotation: placement.rotation
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
return engine.discardDomino({ game, lordId });
|
|
3476
|
+
};
|
|
3477
|
+
var isBotTurn = (game) => {
|
|
3478
|
+
const lordId = game.nextAction.nextLord;
|
|
3479
|
+
const lord = game.lords.find((l) => l.id === lordId);
|
|
3480
|
+
const player = lord ? game.players.find((p) => p.id === lord.playerId) : void 0;
|
|
3481
|
+
return !!player?.bot;
|
|
3482
|
+
};
|
|
3483
|
+
var playBotTurns = (engine, game, customStrategies) => {
|
|
3484
|
+
let current = game;
|
|
3485
|
+
while (isGameWithNextAction(current) && isBotTurn(current)) {
|
|
3486
|
+
const lordId = current.nextAction.nextLord;
|
|
3487
|
+
const lord = current.lords.find((l) => l.id === lordId);
|
|
3488
|
+
const player = lord ? current.players.find((p) => p.id === lord.playerId) : void 0;
|
|
3489
|
+
const strategyName = player?.bot?.strategyName ?? "random";
|
|
3490
|
+
const strategy = getStrategy(strategyName, customStrategies) ?? randomStrategy;
|
|
3491
|
+
current = playBotTurn(engine, current, strategy);
|
|
3492
|
+
}
|
|
3493
|
+
return current;
|
|
3494
|
+
};
|
|
3495
|
+
|
|
3496
|
+
// src/core/useCases/gameActionLog.ts
|
|
3497
|
+
var createActionLog = (gameId) => ({
|
|
3498
|
+
gameId,
|
|
3499
|
+
entries: []
|
|
3500
|
+
});
|
|
3501
|
+
var appendAction = (log, entry) => ({
|
|
3502
|
+
...log,
|
|
3503
|
+
entries: [
|
|
3504
|
+
...log.entries,
|
|
3505
|
+
{
|
|
3506
|
+
...entry,
|
|
3507
|
+
id: log.entries.length,
|
|
3508
|
+
timestamp: Date.now()
|
|
3509
|
+
}
|
|
3510
|
+
]
|
|
3511
|
+
});
|
|
3512
|
+
var getActions = (log) => log.entries;
|
|
3513
|
+
var getActionsByType = (log, type) => log.entries.filter((e) => e.action === type);
|
|
3514
|
+
var getActionsByTurn = (log, turn) => log.entries.filter((e) => e.turn === turn);
|
|
3515
|
+
var wrapWithActionLog = (engine) => {
|
|
3516
|
+
let log = createActionLog("");
|
|
3517
|
+
const record = (action, payload, turn) => {
|
|
3518
|
+
log = appendAction(log, { action, payload, turn });
|
|
3519
|
+
};
|
|
3520
|
+
const wrappedEngine = {
|
|
3521
|
+
// Read-only methods — pass through
|
|
3522
|
+
getModes: (cmd) => engine.getModes(cmd),
|
|
3523
|
+
getExtraRules: (cmd) => engine.getExtraRules(cmd),
|
|
3524
|
+
calculateScore: (cmd) => engine.calculateScore(cmd),
|
|
3525
|
+
getValidPlacements: (cmd) => engine.getValidPlacements(cmd),
|
|
3526
|
+
canPlaceDomino: (cmd) => engine.canPlaceDomino(cmd),
|
|
3527
|
+
serialize: (cmd) => engine.serialize(cmd),
|
|
3528
|
+
deserialize: (cmd) => engine.deserialize(cmd),
|
|
3529
|
+
getResults: (cmd) => engine.getResults(cmd),
|
|
3530
|
+
getDynastyResults: (cmd) => engine.getDynastyResults(cmd),
|
|
3531
|
+
// State-changing methods — record after execution
|
|
3532
|
+
createGame: (cmd) => {
|
|
3533
|
+
const game = engine.createGame(cmd);
|
|
3534
|
+
log = createActionLog(game.id);
|
|
3535
|
+
record("createGame", { mode: cmd.mode, seed: cmd.seed }, game.turn);
|
|
3536
|
+
return game;
|
|
3537
|
+
},
|
|
3538
|
+
addPlayers: (cmd) => {
|
|
3539
|
+
const game = engine.addPlayers(cmd);
|
|
3540
|
+
record("addPlayers", { players: cmd.players }, game.turn);
|
|
3541
|
+
return game;
|
|
3542
|
+
},
|
|
3543
|
+
addExtraRules: (cmd) => {
|
|
3544
|
+
const game = engine.addExtraRules(cmd);
|
|
3545
|
+
record("addExtraRules", { extraRules: cmd.extraRules }, game.turn);
|
|
3546
|
+
return game;
|
|
3547
|
+
},
|
|
3548
|
+
startGame: (cmd) => {
|
|
3549
|
+
const game = engine.startGame(cmd);
|
|
3550
|
+
record("startGame", {}, game.turn);
|
|
3551
|
+
return game;
|
|
3552
|
+
},
|
|
3553
|
+
chooseDomino: (cmd) => {
|
|
3554
|
+
const game = engine.chooseDomino(cmd);
|
|
3555
|
+
record(
|
|
3556
|
+
"chooseDomino",
|
|
3557
|
+
{ lordId: cmd.lordId, dominoPick: cmd.dominoPick },
|
|
3558
|
+
game.turn
|
|
3559
|
+
);
|
|
3560
|
+
return game;
|
|
3561
|
+
},
|
|
3562
|
+
placeDomino: (cmd) => {
|
|
3563
|
+
const game = engine.placeDomino(cmd);
|
|
3564
|
+
record(
|
|
3565
|
+
"placeDomino",
|
|
3566
|
+
{
|
|
3567
|
+
lordId: cmd.lordId,
|
|
3568
|
+
position: cmd.position,
|
|
3569
|
+
rotation: cmd.rotation
|
|
3570
|
+
},
|
|
3571
|
+
game.turn
|
|
3572
|
+
);
|
|
3573
|
+
return game;
|
|
3574
|
+
},
|
|
3575
|
+
discardDomino: (cmd) => {
|
|
3576
|
+
const game = engine.discardDomino(cmd);
|
|
3577
|
+
record("discardDomino", { lordId: cmd.lordId }, game.turn);
|
|
3578
|
+
return game;
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
2341
3581
|
return {
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
3582
|
+
engine: wrappedEngine,
|
|
3583
|
+
getLog: () => log
|
|
3584
|
+
};
|
|
3585
|
+
};
|
|
3586
|
+
var replayActions = (engine, log) => {
|
|
3587
|
+
if (log.entries.length === 0) {
|
|
3588
|
+
throw new Error("Cannot replay an empty action log");
|
|
3589
|
+
}
|
|
3590
|
+
const firstEntry = log.entries[0];
|
|
3591
|
+
if (firstEntry?.action !== "createGame") {
|
|
3592
|
+
throw new Error("Action log must start with a createGame action");
|
|
3593
|
+
}
|
|
3594
|
+
const firstPayload = firstEntry.payload;
|
|
3595
|
+
let game = engine.createGame({
|
|
3596
|
+
mode: firstPayload.mode,
|
|
3597
|
+
seed: firstPayload.seed
|
|
3598
|
+
});
|
|
3599
|
+
for (let i = 1; i < log.entries.length; i++) {
|
|
3600
|
+
const entry = log.entries[i];
|
|
3601
|
+
if (!entry) continue;
|
|
3602
|
+
switch (entry.action) {
|
|
3603
|
+
case "createGame": {
|
|
3604
|
+
const payload = entry.payload;
|
|
3605
|
+
game = engine.createGame({ mode: payload.mode, seed: payload.seed });
|
|
3606
|
+
break;
|
|
3607
|
+
}
|
|
3608
|
+
case "addPlayers": {
|
|
3609
|
+
const payload = entry.payload;
|
|
3610
|
+
game = engine.addPlayers({ game, players: payload.players });
|
|
3611
|
+
break;
|
|
3612
|
+
}
|
|
3613
|
+
case "addExtraRules": {
|
|
3614
|
+
const payload = entry.payload;
|
|
3615
|
+
game = engine.addExtraRules({
|
|
3616
|
+
game,
|
|
3617
|
+
extraRules: payload.extraRules
|
|
2347
3618
|
});
|
|
3619
|
+
break;
|
|
3620
|
+
}
|
|
3621
|
+
case "startGame": {
|
|
3622
|
+
game = engine.startGame({ game });
|
|
3623
|
+
break;
|
|
3624
|
+
}
|
|
3625
|
+
case "chooseDomino": {
|
|
3626
|
+
const payload = entry.payload;
|
|
3627
|
+
game = engine.chooseDomino({
|
|
3628
|
+
game,
|
|
3629
|
+
lordId: payload.lordId,
|
|
3630
|
+
dominoPick: payload.dominoPick
|
|
3631
|
+
});
|
|
3632
|
+
break;
|
|
3633
|
+
}
|
|
3634
|
+
case "placeDomino": {
|
|
3635
|
+
const payload = entry.payload;
|
|
3636
|
+
game = engine.placeDomino({
|
|
3637
|
+
game,
|
|
3638
|
+
lordId: payload.lordId,
|
|
3639
|
+
position: payload.position,
|
|
3640
|
+
rotation: payload.rotation
|
|
3641
|
+
});
|
|
3642
|
+
break;
|
|
3643
|
+
}
|
|
3644
|
+
case "discardDomino": {
|
|
3645
|
+
const payload = entry.payload;
|
|
3646
|
+
game = engine.discardDomino({
|
|
3647
|
+
game,
|
|
3648
|
+
lordId: payload.lordId
|
|
3649
|
+
});
|
|
3650
|
+
break;
|
|
2348
3651
|
}
|
|
2349
|
-
return text;
|
|
2350
3652
|
}
|
|
3653
|
+
}
|
|
3654
|
+
return game;
|
|
3655
|
+
};
|
|
3656
|
+
|
|
3657
|
+
// src/core/useCases/gameHistory.ts
|
|
3658
|
+
var createGameHistory = (initialState) => ({
|
|
3659
|
+
current: initialState,
|
|
3660
|
+
past: [],
|
|
3661
|
+
future: []
|
|
3662
|
+
});
|
|
3663
|
+
var pushState = (history, newState) => ({
|
|
3664
|
+
current: newState,
|
|
3665
|
+
past: [...history.past, history.current],
|
|
3666
|
+
future: []
|
|
3667
|
+
});
|
|
3668
|
+
var undo = (history) => {
|
|
3669
|
+
if (history.past.length === 0) {
|
|
3670
|
+
throw new UndoError();
|
|
3671
|
+
}
|
|
3672
|
+
const previous = history.past[history.past.length - 1];
|
|
3673
|
+
return {
|
|
3674
|
+
current: previous,
|
|
3675
|
+
past: history.past.slice(0, -1),
|
|
3676
|
+
future: [...history.future, history.current]
|
|
2351
3677
|
};
|
|
2352
3678
|
};
|
|
2353
|
-
var
|
|
3679
|
+
var redo = (history) => {
|
|
3680
|
+
if (history.future.length === 0) {
|
|
3681
|
+
throw new RedoError();
|
|
3682
|
+
}
|
|
3683
|
+
const next = history.future[history.future.length - 1];
|
|
3684
|
+
return {
|
|
3685
|
+
current: next,
|
|
3686
|
+
past: [...history.past, history.current],
|
|
3687
|
+
future: history.future.slice(0, -1)
|
|
3688
|
+
};
|
|
3689
|
+
};
|
|
3690
|
+
var canUndo = (history) => history.past.length > 0;
|
|
3691
|
+
var canRedo = (history) => history.future.length > 0;
|
|
3692
|
+
var clearHistory = (history) => ({
|
|
3693
|
+
current: history.current,
|
|
3694
|
+
past: [],
|
|
3695
|
+
future: []
|
|
3696
|
+
});
|
|
3697
|
+
var getHistorySize = (history) => ({
|
|
3698
|
+
past: history.past.length,
|
|
3699
|
+
future: history.future.length
|
|
3700
|
+
});
|
|
2354
3701
|
|
|
2355
3702
|
// src/index.ts
|
|
2356
3703
|
var createGameEngine = (config) => {
|
|
@@ -2369,9 +3716,10 @@ var createGameEngine = (config) => {
|
|
|
2369
3716
|
getValidPlacementsHandler: getValidPlacementsHandler2,
|
|
2370
3717
|
canPlaceDominoHandler: canPlaceDominoHandler2,
|
|
2371
3718
|
serializeGameHandler: serializeGameHandler2,
|
|
2372
|
-
deserializeGameHandler: deserializeGameHandler2
|
|
3719
|
+
deserializeGameHandler: deserializeGameHandler2,
|
|
3720
|
+
getDynastyResultHandler: getDynastyResultHandler2
|
|
2373
3721
|
} = configureEngine(config);
|
|
2374
|
-
|
|
3722
|
+
let engine = {
|
|
2375
3723
|
getModes: (command) => getModesHandler2(command),
|
|
2376
3724
|
getExtraRules: (command) => getExtraRulesHandler2(command),
|
|
2377
3725
|
createGame: (command) => createGameHandler2(command),
|
|
@@ -2386,8 +3734,17 @@ var createGameEngine = (config) => {
|
|
|
2386
3734
|
getValidPlacements: (command) => getValidPlacementsHandler2(command),
|
|
2387
3735
|
canPlaceDomino: (command) => canPlaceDominoHandler2(command),
|
|
2388
3736
|
serialize: (command) => serializeGameHandler2(command),
|
|
2389
|
-
deserialize: (command) => deserializeGameHandler2(command)
|
|
3737
|
+
deserialize: (command) => deserializeGameHandler2(command),
|
|
3738
|
+
getDynastyResults: (command) => getDynastyResultHandler2(command)
|
|
2390
3739
|
};
|
|
3740
|
+
if (config.events) {
|
|
3741
|
+
engine = wrapWithEvents(engine, config.events);
|
|
3742
|
+
}
|
|
3743
|
+
if (config.debug) {
|
|
3744
|
+
const debugOptions = typeof config.debug === "object" ? config.debug : void 0;
|
|
3745
|
+
engine = wrapWithDebug(engine, debugOptions);
|
|
3746
|
+
}
|
|
3747
|
+
return engine;
|
|
2391
3748
|
};
|
|
2392
3749
|
export {
|
|
2393
3750
|
ActionExecutionError,
|
|
@@ -2395,26 +3752,56 @@ export {
|
|
|
2395
3752
|
ErrorCode,
|
|
2396
3753
|
GRIDSIZE,
|
|
2397
3754
|
GROUND,
|
|
2398
|
-
GameEventEmitter,
|
|
2399
|
-
GameEventType,
|
|
2400
3755
|
InvalidStepError,
|
|
3756
|
+
MAX_KINGDOM_SIZE,
|
|
2401
3757
|
NotFoundError,
|
|
3758
|
+
RedoError,
|
|
2402
3759
|
StepExecutionError,
|
|
2403
3760
|
TranslationKey,
|
|
3761
|
+
UndoError,
|
|
2404
3762
|
ValidationError,
|
|
3763
|
+
advancedStrategy,
|
|
3764
|
+
appendAction,
|
|
3765
|
+
canRedo,
|
|
3766
|
+
canUndo,
|
|
3767
|
+
clearHistory,
|
|
3768
|
+
createActionLog,
|
|
2405
3769
|
createDomainError,
|
|
2406
|
-
createEvent,
|
|
2407
3770
|
createGameEngine,
|
|
3771
|
+
createGameHistory,
|
|
2408
3772
|
createSavePoint,
|
|
2409
3773
|
createTranslator,
|
|
2410
3774
|
defaultTranslations,
|
|
2411
3775
|
defaultTranslator,
|
|
2412
3776
|
deserializeGame,
|
|
3777
|
+
errorCodeToTranslationKey,
|
|
3778
|
+
expertStrategy,
|
|
2413
3779
|
gameSteps,
|
|
3780
|
+
getActions,
|
|
3781
|
+
getActionsByTurn,
|
|
3782
|
+
getActionsByType,
|
|
3783
|
+
getHistorySize,
|
|
3784
|
+
getStrategy,
|
|
3785
|
+
getStrategyNames,
|
|
3786
|
+
greedyStrategy,
|
|
3787
|
+
isBotTurn,
|
|
2414
3788
|
isGameWithNextAction,
|
|
2415
3789
|
isGameWithNextStep,
|
|
3790
|
+
playBotTurn,
|
|
3791
|
+
playBotTurns,
|
|
2416
3792
|
playerActions,
|
|
3793
|
+
pushState,
|
|
3794
|
+
randomStrategy,
|
|
3795
|
+
redo,
|
|
3796
|
+
replayActions,
|
|
2417
3797
|
restoreFromSavePoint,
|
|
2418
|
-
serializeGame
|
|
3798
|
+
serializeGame,
|
|
3799
|
+
summarizeGameState,
|
|
3800
|
+
translateErrorCode,
|
|
3801
|
+
undo,
|
|
3802
|
+
validateGameState,
|
|
3803
|
+
wrapWithActionLog,
|
|
3804
|
+
wrapWithDebug,
|
|
3805
|
+
wrapWithEvents
|
|
2419
3806
|
};
|
|
2420
3807
|
//# sourceMappingURL=index.js.map
|