@pompidup/kingdomino-engine 3.0.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 +1137 -2
- package/dist/index.js +2103 -325
- package/dist/index.js.map +1 -1
- package/package.json +31 -21
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,90 +960,274 @@ var uuidMethod = () => {
|
|
|
673
960
|
return randomUUID();
|
|
674
961
|
};
|
|
675
962
|
|
|
676
|
-
// src/adapterServerside/winstonLogger.ts
|
|
677
|
-
import winston from "winston";
|
|
678
|
-
var winstonLogger = (isLoggingEnabled) => {
|
|
679
|
-
const myFormat = winston.format.printf(
|
|
680
|
-
({ level, message, label, timestamp }) => {
|
|
681
|
-
return `${timestamp} [${label}] ${level}: ${message}`;
|
|
682
|
-
}
|
|
683
|
-
);
|
|
684
|
-
const logger = winston.createLogger({
|
|
685
|
-
level: "info",
|
|
686
|
-
format: winston.format.combine(
|
|
687
|
-
winston.format.label({ label: "KDomino_Engine" }),
|
|
688
|
-
winston.format.timestamp(),
|
|
689
|
-
winston.format.simple(),
|
|
690
|
-
winston.format.json(),
|
|
691
|
-
winston.format.colorize(),
|
|
692
|
-
myFormat
|
|
693
|
-
),
|
|
694
|
-
transports: [new winston.transports.Console()]
|
|
695
|
-
});
|
|
696
|
-
return {
|
|
697
|
-
info: (message) => {
|
|
698
|
-
if (isLoggingEnabled) {
|
|
699
|
-
logger.info(message);
|
|
700
|
-
}
|
|
701
|
-
return void 0;
|
|
702
|
-
},
|
|
703
|
-
error: (message) => {
|
|
704
|
-
if (isLoggingEnabled) {
|
|
705
|
-
logger.error(message);
|
|
706
|
-
}
|
|
707
|
-
return void 0;
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
};
|
|
711
|
-
|
|
712
963
|
// src/core/domain/errors/domainErrors.ts
|
|
713
|
-
var
|
|
714
|
-
|
|
715
|
-
|
|
964
|
+
var ErrorCode = {
|
|
965
|
+
// Game flow errors
|
|
966
|
+
INVALID_STEP: "INVALID_STEP",
|
|
967
|
+
STEP_EXECUTION_FAILED: "STEP_EXECUTION_FAILED",
|
|
968
|
+
ACTION_EXECUTION_FAILED: "ACTION_EXECUTION_FAILED",
|
|
969
|
+
// Entity not found errors
|
|
970
|
+
LORD_NOT_FOUND: "LORD_NOT_FOUND",
|
|
971
|
+
PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND",
|
|
972
|
+
DOMINO_NOT_FOUND: "DOMINO_NOT_FOUND",
|
|
973
|
+
MODE_NOT_FOUND: "MODE_NOT_FOUND",
|
|
974
|
+
// Validation errors
|
|
975
|
+
INVALID_PLAYER_COUNT: "INVALID_PLAYER_COUNT",
|
|
976
|
+
INVALID_PLAYER_NAME: "INVALID_PLAYER_NAME",
|
|
977
|
+
INVALID_PLACEMENT: "INVALID_PLACEMENT",
|
|
978
|
+
PLACEMENT_NOT_EMPTY: "PLACEMENT_NOT_EMPTY",
|
|
979
|
+
PLACEMENT_NOT_ADJACENT: "PLACEMENT_NOT_ADJACENT",
|
|
980
|
+
PLACEMENT_INVALID_TERRAIN: "PLACEMENT_INVALID_TERRAIN",
|
|
981
|
+
PLACEMENT_OUT_OF_BOUNDS: "PLACEMENT_OUT_OF_BOUNDS",
|
|
982
|
+
PLACEMENT_EXCEEDS_KINGDOM_SIZE: "PLACEMENT_EXCEEDS_KINGDOM_SIZE",
|
|
983
|
+
// Action errors
|
|
984
|
+
NOT_YOUR_TURN: "NOT_YOUR_TURN",
|
|
985
|
+
CANNOT_PICK: "CANNOT_PICK",
|
|
986
|
+
CANNOT_PLACE: "CANNOT_PLACE",
|
|
987
|
+
DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED",
|
|
988
|
+
// History errors
|
|
989
|
+
UNDO_UNAVAILABLE: "UNDO_UNAVAILABLE",
|
|
990
|
+
REDO_UNAVAILABLE: "REDO_UNAVAILABLE"
|
|
991
|
+
};
|
|
992
|
+
var createDomainError = (code, message, context) => ({
|
|
993
|
+
code,
|
|
994
|
+
message,
|
|
995
|
+
context
|
|
996
|
+
});
|
|
997
|
+
var DomainException = class extends Error {
|
|
998
|
+
code;
|
|
999
|
+
context;
|
|
1000
|
+
constructor(error) {
|
|
1001
|
+
super(error.message);
|
|
1002
|
+
this.name = "DomainException";
|
|
1003
|
+
this.code = error.code;
|
|
1004
|
+
this.context = error.context;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
var InvalidStepError = class extends DomainException {
|
|
1008
|
+
constructor(message, context) {
|
|
1009
|
+
super({
|
|
1010
|
+
code: ErrorCode.INVALID_STEP,
|
|
1011
|
+
message,
|
|
1012
|
+
context
|
|
1013
|
+
});
|
|
716
1014
|
this.name = "InvalidStepError";
|
|
717
1015
|
}
|
|
718
1016
|
};
|
|
719
|
-
var StepExecutionError = class extends
|
|
720
|
-
constructor(message) {
|
|
721
|
-
super(
|
|
1017
|
+
var StepExecutionError = class extends DomainException {
|
|
1018
|
+
constructor(message, code = ErrorCode.STEP_EXECUTION_FAILED, context) {
|
|
1019
|
+
super({
|
|
1020
|
+
code,
|
|
1021
|
+
message,
|
|
1022
|
+
context
|
|
1023
|
+
});
|
|
722
1024
|
this.name = "StepExecutionError";
|
|
723
1025
|
}
|
|
724
1026
|
};
|
|
725
|
-
var ActionExecutionError = class extends
|
|
726
|
-
constructor(message) {
|
|
727
|
-
super(
|
|
1027
|
+
var ActionExecutionError = class extends DomainException {
|
|
1028
|
+
constructor(message, code = ErrorCode.ACTION_EXECUTION_FAILED, context) {
|
|
1029
|
+
super({
|
|
1030
|
+
code,
|
|
1031
|
+
message,
|
|
1032
|
+
context
|
|
1033
|
+
});
|
|
728
1034
|
this.name = "ActionExecutionError";
|
|
729
1035
|
}
|
|
730
1036
|
};
|
|
731
|
-
var NotFoundError = class extends
|
|
732
|
-
constructor(message) {
|
|
733
|
-
super(
|
|
1037
|
+
var NotFoundError = class extends DomainException {
|
|
1038
|
+
constructor(message, code = ErrorCode.LORD_NOT_FOUND, context) {
|
|
1039
|
+
super({
|
|
1040
|
+
code,
|
|
1041
|
+
message,
|
|
1042
|
+
context
|
|
1043
|
+
});
|
|
734
1044
|
this.name = "NotFoundError";
|
|
735
1045
|
}
|
|
736
1046
|
};
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1047
|
+
var ValidationError = class extends DomainException {
|
|
1048
|
+
constructor(message, code, context) {
|
|
1049
|
+
super({
|
|
1050
|
+
code,
|
|
1051
|
+
message,
|
|
1052
|
+
context
|
|
1053
|
+
});
|
|
1054
|
+
this.name = "ValidationError";
|
|
1055
|
+
}
|
|
744
1056
|
};
|
|
745
|
-
var
|
|
746
|
-
|
|
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";
|
|
1065
|
+
}
|
|
747
1066
|
};
|
|
748
|
-
var
|
|
749
|
-
|
|
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";
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
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
|
+
};
|
|
750
1214
|
};
|
|
1215
|
+
var defaultTranslator = createTranslator();
|
|
751
1216
|
|
|
752
1217
|
// src/utils/result.ts
|
|
753
1218
|
var ok = (value) => ({ tag: "Ok", value });
|
|
754
1219
|
var err = (error) => ({ tag: "Err", error });
|
|
755
1220
|
var isOk = (result) => result.tag === "Ok";
|
|
756
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
|
+
};
|
|
757
1228
|
|
|
758
1229
|
// src/application/handlers/addExtraRulesHandler.ts
|
|
759
|
-
var addExtraRulesHandler = (logger, useCase) => (command) => {
|
|
1230
|
+
var addExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
760
1231
|
const { game, extraRules } = command;
|
|
761
1232
|
logger.info(`Adding extra rules to game: ${game.id}`);
|
|
762
1233
|
logger.info(`Extra rules: ${extraRules}`);
|
|
@@ -764,50 +1235,101 @@ var addExtraRulesHandler = (logger, useCase) => (command) => {
|
|
|
764
1235
|
logger.error(
|
|
765
1236
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
766
1237
|
);
|
|
767
|
-
throw new InvalidStepError(
|
|
1238
|
+
throw new InvalidStepError(
|
|
1239
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1240
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1241
|
+
);
|
|
768
1242
|
}
|
|
769
1243
|
if (game.nextAction.step !== "options") {
|
|
770
1244
|
logger.error(
|
|
771
1245
|
`Required game with options step but got: ${game.nextAction.step}`
|
|
772
1246
|
);
|
|
773
|
-
throw new InvalidStepError(
|
|
1247
|
+
throw new InvalidStepError(
|
|
1248
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1249
|
+
{ expected: "options", actual: game.nextAction.step, gameId: game.id }
|
|
1250
|
+
);
|
|
774
1251
|
}
|
|
775
1252
|
const result = useCase(game, extraRules);
|
|
776
1253
|
if (isErr(result)) {
|
|
777
1254
|
logger.error(`Error adding extra rules: ${result.error}`);
|
|
778
|
-
|
|
1255
|
+
const code = result.error;
|
|
1256
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1257
|
+
gameId: game.id,
|
|
1258
|
+
extraRules: command.extraRules
|
|
1259
|
+
});
|
|
779
1260
|
}
|
|
780
1261
|
logger.info(`Extra rules added to game: ${game.id}`);
|
|
781
1262
|
return result.value;
|
|
782
1263
|
};
|
|
783
1264
|
|
|
784
1265
|
// src/application/handlers/addPlayersHandler.ts
|
|
785
|
-
var addPlayersHandler = (logger, useCase) => (command) => {
|
|
1266
|
+
var addPlayersHandler = (logger, translator, useCase) => (command) => {
|
|
786
1267
|
const { game, players } = command;
|
|
787
1268
|
logger.info(`Adding players to game: ${game.id}`);
|
|
788
1269
|
if (!isGameWithNextStep(game)) {
|
|
789
1270
|
logger.error(
|
|
790
1271
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
791
1272
|
);
|
|
792
|
-
throw new InvalidStepError(
|
|
1273
|
+
throw new InvalidStepError(
|
|
1274
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1275
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1276
|
+
);
|
|
793
1277
|
}
|
|
794
1278
|
if (game.nextAction.step !== "addPlayers") {
|
|
795
1279
|
logger.error(
|
|
796
1280
|
`Required game with addPlayers step but got: ${game.nextAction.step}`
|
|
797
1281
|
);
|
|
798
|
-
throw new InvalidStepError(
|
|
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
|
+
);
|
|
799
1290
|
}
|
|
800
1291
|
const result = useCase(game, players);
|
|
801
1292
|
if (isErr(result)) {
|
|
802
1293
|
logger.error(`Error adding players: ${result.error}`);
|
|
803
|
-
|
|
1294
|
+
const code = result.error;
|
|
1295
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1296
|
+
gameId: game.id,
|
|
1297
|
+
players: command.players
|
|
1298
|
+
});
|
|
804
1299
|
}
|
|
805
1300
|
logger.info(`Players added to game: ${game.id}`);
|
|
806
1301
|
return result.value;
|
|
807
1302
|
};
|
|
808
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
|
+
|
|
809
1331
|
// src/application/handlers/chooseDominoHandler.ts
|
|
810
|
-
var chooseDominoHandler = (logger, useCase) => (command) => {
|
|
1332
|
+
var chooseDominoHandler = (logger, translator, useCase) => (command) => {
|
|
811
1333
|
const { game, lordId, dominoPick } = command;
|
|
812
1334
|
logger.info(
|
|
813
1335
|
`Choosing domino: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
@@ -817,19 +1339,36 @@ var chooseDominoHandler = (logger, useCase) => (command) => {
|
|
|
817
1339
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
818
1340
|
);
|
|
819
1341
|
throw new InvalidStepError(
|
|
820
|
-
|
|
1342
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1343
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
821
1344
|
);
|
|
822
1345
|
}
|
|
823
1346
|
if (game.nextAction.nextAction !== "pickDomino") {
|
|
824
1347
|
logger.error(
|
|
825
1348
|
`Required game with pickDomino action but got: ${game.nextAction.nextAction}`
|
|
826
1349
|
);
|
|
827
|
-
throw new InvalidStepError(
|
|
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
|
+
);
|
|
828
1358
|
}
|
|
829
1359
|
const result = useCase(game, lordId, dominoPick);
|
|
830
1360
|
if (isErr(result)) {
|
|
831
1361
|
logger.error(`Error choosing domino: ${result.error}`);
|
|
832
|
-
|
|
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
|
+
);
|
|
833
1372
|
}
|
|
834
1373
|
logger.info(
|
|
835
1374
|
`Domino chosen: ${dominoPick} for lord: ${lordId} in game: ${game.id}`
|
|
@@ -838,19 +1377,22 @@ var chooseDominoHandler = (logger, useCase) => (command) => {
|
|
|
838
1377
|
};
|
|
839
1378
|
|
|
840
1379
|
// src/application/handlers/createGameHandler.ts
|
|
841
|
-
var createGameHandler = (logger, useCase) => (command) => {
|
|
1380
|
+
var createGameHandler = (logger, translator, useCase) => (command) => {
|
|
842
1381
|
logger.info(`Creating game with mode: ${command.mode}`);
|
|
843
|
-
const result = useCase(command.mode);
|
|
1382
|
+
const result = useCase(command.mode, command.seed);
|
|
844
1383
|
if (isErr(result)) {
|
|
845
1384
|
logger.error(`Error creating game: ${result.error}`);
|
|
846
|
-
|
|
1385
|
+
const code = result.error;
|
|
1386
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1387
|
+
mode: command.mode
|
|
1388
|
+
});
|
|
847
1389
|
}
|
|
848
1390
|
logger.info(`Game created: ${result.value.id}`);
|
|
849
1391
|
return result.value;
|
|
850
1392
|
};
|
|
851
1393
|
|
|
852
1394
|
// src/application/handlers/discardDominoHandler.ts
|
|
853
|
-
var discardDominoHandler = (logger, useCase) => (command) => {
|
|
1395
|
+
var discardDominoHandler = (logger, translator, useCase) => (command) => {
|
|
854
1396
|
const { game, lordId } = command;
|
|
855
1397
|
logger.info(`Discarding domino for lord: ${lordId} in game: ${game.id}`);
|
|
856
1398
|
if (!isGameWithNextAction(game)) {
|
|
@@ -858,19 +1400,32 @@ var discardDominoHandler = (logger, useCase) => (command) => {
|
|
|
858
1400
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
859
1401
|
);
|
|
860
1402
|
throw new InvalidStepError(
|
|
861
|
-
|
|
1403
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1404
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
862
1405
|
);
|
|
863
1406
|
}
|
|
864
1407
|
if (game.nextAction.nextAction !== "placeDomino") {
|
|
865
1408
|
logger.error(
|
|
866
1409
|
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
867
1410
|
);
|
|
868
|
-
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
|
+
);
|
|
869
1419
|
}
|
|
870
1420
|
const result = useCase(game, lordId);
|
|
871
1421
|
if (isErr(result)) {
|
|
872
1422
|
logger.error(`Error discarding domino: ${result.error}`);
|
|
873
|
-
|
|
1423
|
+
const code = result.error;
|
|
1424
|
+
throw new ActionExecutionError(
|
|
1425
|
+
translateErrorCode(translator, code),
|
|
1426
|
+
code,
|
|
1427
|
+
{ gameId: game.id, lordId: command.lordId }
|
|
1428
|
+
);
|
|
874
1429
|
}
|
|
875
1430
|
if (isGameWithNextAction(result.value)) {
|
|
876
1431
|
logger.info(`Domino discarded: ${lordId} in game: ${game.id}`);
|
|
@@ -880,8 +1435,20 @@ var discardDominoHandler = (logger, useCase) => (command) => {
|
|
|
880
1435
|
return result.value;
|
|
881
1436
|
};
|
|
882
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
|
+
|
|
883
1450
|
// src/application/handlers/getExtraRulesHandler.ts
|
|
884
|
-
var getExtraRulesHandler = (logger, useCase) => (command) => {
|
|
1451
|
+
var getExtraRulesHandler = (logger, translator, useCase) => (command) => {
|
|
885
1452
|
const { mode, players } = command;
|
|
886
1453
|
logger.info(
|
|
887
1454
|
`Getting extra rules for mode: ${mode} and players: ${players}`
|
|
@@ -889,26 +1456,30 @@ var getExtraRulesHandler = (logger, useCase) => (command) => {
|
|
|
889
1456
|
const result = useCase(mode, players);
|
|
890
1457
|
if (isErr(result)) {
|
|
891
1458
|
logger.error(`Error getting extra rules: ${result.error}`);
|
|
892
|
-
|
|
1459
|
+
const code = result.error;
|
|
1460
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1461
|
+
mode: command.mode
|
|
1462
|
+
});
|
|
893
1463
|
}
|
|
894
1464
|
logger.info(`Extra rules retrieved: ${result.value}`);
|
|
895
1465
|
return result.value;
|
|
896
1466
|
};
|
|
897
1467
|
|
|
898
1468
|
// src/application/handlers/getModesHandler.ts
|
|
899
|
-
var getModesHandler = (logger, useCase) => (_command) => {
|
|
1469
|
+
var getModesHandler = (logger, translator, useCase) => (_command) => {
|
|
900
1470
|
logger.info("Getting modes");
|
|
901
1471
|
const result = useCase();
|
|
902
1472
|
if (isErr(result)) {
|
|
903
1473
|
logger.error(`Error getting modes: ${result.error}`);
|
|
904
|
-
|
|
1474
|
+
const code = result.error;
|
|
1475
|
+
throw new NotFoundError(translateErrorCode(translator, code), code);
|
|
905
1476
|
}
|
|
906
1477
|
logger.info(`Modes retrieved: ${result.value}`);
|
|
907
1478
|
return result.value;
|
|
908
1479
|
};
|
|
909
1480
|
|
|
910
1481
|
// src/application/handlers/getResultHandler.ts
|
|
911
|
-
var getResultHandler = (logger, useCases) => (command) => {
|
|
1482
|
+
var getResultHandler = (logger, translator, useCases) => (command) => {
|
|
912
1483
|
const { getResultUseCase: getResultUseCase2, calculateScoreUseCase: calculateScoreUseCase2 } = useCases;
|
|
913
1484
|
const { game } = command;
|
|
914
1485
|
logger.info(`Getting result for game: ${game.id}`);
|
|
@@ -916,22 +1487,34 @@ var getResultHandler = (logger, useCases) => (command) => {
|
|
|
916
1487
|
logger.error(
|
|
917
1488
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
918
1489
|
);
|
|
919
|
-
throw new InvalidStepError(
|
|
1490
|
+
throw new InvalidStepError(
|
|
1491
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1492
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1493
|
+
);
|
|
920
1494
|
}
|
|
921
1495
|
if (game.nextAction.step !== "result") {
|
|
922
1496
|
logger.error(
|
|
923
1497
|
`Required game with result step but got: ${game.nextAction.step}`
|
|
924
1498
|
);
|
|
925
|
-
throw new InvalidStepError(
|
|
1499
|
+
throw new InvalidStepError(
|
|
1500
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1501
|
+
{ expected: "result", actual: game.nextAction.step, gameId: game.id }
|
|
1502
|
+
);
|
|
926
1503
|
}
|
|
927
|
-
let scoreResult;
|
|
928
1504
|
const { players } = game;
|
|
929
|
-
scoreResult = players.map((player) => {
|
|
1505
|
+
const scoreResult = players.map((player) => {
|
|
930
1506
|
const { kingdom } = player;
|
|
931
1507
|
const result2 = calculateScoreUseCase2(kingdom);
|
|
932
1508
|
if (isErr(result2)) {
|
|
933
1509
|
logger.error(`Error when calculating score: ${result2.error}`);
|
|
934
|
-
|
|
1510
|
+
const code = result2.error;
|
|
1511
|
+
throw new StepExecutionError(
|
|
1512
|
+
translateErrorCode(translator, code),
|
|
1513
|
+
code,
|
|
1514
|
+
{
|
|
1515
|
+
gameId: game.id
|
|
1516
|
+
}
|
|
1517
|
+
);
|
|
935
1518
|
}
|
|
936
1519
|
return {
|
|
937
1520
|
playerId: player.id,
|
|
@@ -944,14 +1527,33 @@ var getResultHandler = (logger, useCases) => (command) => {
|
|
|
944
1527
|
const result = getResultUseCase2(game, scoreResult);
|
|
945
1528
|
if (isErr(result)) {
|
|
946
1529
|
logger.error(`Error getting result: ${result.error}`);
|
|
947
|
-
|
|
1530
|
+
const code = result.error;
|
|
1531
|
+
throw new NotFoundError(translateErrorCode(translator, code), code, {
|
|
1532
|
+
gameId: game.id
|
|
1533
|
+
});
|
|
948
1534
|
}
|
|
949
1535
|
logger.info(`Result retrieved for game: ${game.id}`);
|
|
950
1536
|
return result.value;
|
|
951
1537
|
};
|
|
952
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
|
+
|
|
953
1555
|
// src/application/handlers/placeDominoHandler.ts
|
|
954
|
-
var placeDominoHandler = (logger, useCase) => (command) => {
|
|
1556
|
+
var placeDominoHandler = (logger, translator, useCase) => (command) => {
|
|
955
1557
|
const { game, lordId, position, rotation } = command;
|
|
956
1558
|
logger.info(
|
|
957
1559
|
`Placing domino: ${lordId} in position: ${JSON.stringify(
|
|
@@ -963,19 +1565,37 @@ var placeDominoHandler = (logger, useCase) => (command) => {
|
|
|
963
1565
|
`Invalid game, required nextAction type: 'action' but got: ${game.nextAction.type}`
|
|
964
1566
|
);
|
|
965
1567
|
throw new InvalidStepError(
|
|
966
|
-
|
|
1568
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1569
|
+
{ expected: "action", actual: game.nextAction.type, gameId: game.id }
|
|
967
1570
|
);
|
|
968
1571
|
}
|
|
969
1572
|
if (game.nextAction.nextAction !== "placeDomino") {
|
|
970
1573
|
logger.error(
|
|
971
1574
|
`Required game with placeDomino action but got: ${game.nextAction.nextAction}`
|
|
972
1575
|
);
|
|
973
|
-
throw new InvalidStepError(
|
|
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
|
+
);
|
|
974
1584
|
}
|
|
975
1585
|
const result = useCase(game, lordId, position, rotation);
|
|
976
1586
|
if (isErr(result)) {
|
|
977
1587
|
logger.error(`Error placing domino: ${result.error}`);
|
|
978
|
-
|
|
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
|
+
);
|
|
979
1599
|
}
|
|
980
1600
|
if (isGameWithNextAction(result.value)) {
|
|
981
1601
|
logger.info(
|
|
@@ -993,48 +1613,113 @@ var placeDominoHandler = (logger, useCase) => (command) => {
|
|
|
993
1613
|
return result.value;
|
|
994
1614
|
};
|
|
995
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
|
+
|
|
996
1682
|
// src/application/handlers/startGameHandler.ts
|
|
997
|
-
var startGameHandler = (logger, useCase) => (command) => {
|
|
1683
|
+
var startGameHandler = (logger, translator, useCase) => (command) => {
|
|
998
1684
|
const { game } = command;
|
|
999
1685
|
logger.info(`Starting game: ${game.id}`);
|
|
1000
1686
|
if (!isGameWithNextStep(game)) {
|
|
1001
1687
|
logger.error(
|
|
1002
1688
|
`Invalid game, required nextAction type: 'step' but got: ${game.nextAction.type}`
|
|
1003
1689
|
);
|
|
1004
|
-
throw new InvalidStepError(
|
|
1690
|
+
throw new InvalidStepError(
|
|
1691
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1692
|
+
{ expected: "step", actual: game.nextAction.type, gameId: game.id }
|
|
1693
|
+
);
|
|
1005
1694
|
}
|
|
1006
1695
|
if (game.nextAction.step !== "start" && game.nextAction.step !== "options") {
|
|
1007
1696
|
logger.error(
|
|
1008
1697
|
`Required game with start step or options step but got: ${game.nextAction.step}`
|
|
1009
1698
|
);
|
|
1010
1699
|
throw new InvalidStepError(
|
|
1011
|
-
|
|
1700
|
+
translateErrorCode(translator, ErrorCode.INVALID_STEP),
|
|
1701
|
+
{
|
|
1702
|
+
expected: "start|options",
|
|
1703
|
+
actual: game.nextAction.step,
|
|
1704
|
+
gameId: game.id
|
|
1705
|
+
}
|
|
1012
1706
|
);
|
|
1013
1707
|
}
|
|
1014
1708
|
const result = useCase(game);
|
|
1015
1709
|
if (isErr(result)) {
|
|
1016
1710
|
logger.error(`Error starting game: ${result.error}`);
|
|
1017
|
-
|
|
1711
|
+
const code = result.error;
|
|
1712
|
+
throw new StepExecutionError(translateErrorCode(translator, code), code, {
|
|
1713
|
+
gameId: game.id
|
|
1714
|
+
});
|
|
1018
1715
|
}
|
|
1019
1716
|
logger.info(`Game started: ${game.id}`);
|
|
1020
1717
|
return result.value;
|
|
1021
1718
|
};
|
|
1022
1719
|
|
|
1023
|
-
// src/application/handlers/calculateScoreHandler.ts
|
|
1024
|
-
var calculateScoreHandler = (logger, useCase) => (command) => {
|
|
1025
|
-
const { kingdom } = command;
|
|
1026
|
-
logger.info(`Calculating score for kingdom`);
|
|
1027
|
-
const result = useCase(kingdom);
|
|
1028
|
-
if (isErr(result)) {
|
|
1029
|
-
logger.error(`Error calculating score: ${result.error}`);
|
|
1030
|
-
throw new Error(result.error);
|
|
1031
|
-
}
|
|
1032
|
-
logger.info(`Score calculated: ${result.value}`);
|
|
1033
|
-
return result.value;
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
1720
|
// src/core/domain/types/kingdom.ts
|
|
1037
1721
|
var GRIDSIZE = 9;
|
|
1722
|
+
var MAX_KINGDOM_SIZE = 5;
|
|
1038
1723
|
|
|
1039
1724
|
// src/core/domain/types/domino.ts
|
|
1040
1725
|
var GROUND = [
|
|
@@ -1049,8 +1734,11 @@ var GROUND = [
|
|
|
1049
1734
|
|
|
1050
1735
|
// src/core/domain/types/player.ts
|
|
1051
1736
|
var playerActions = {
|
|
1737
|
+
/** Place a previously picked domino on the kingdom */
|
|
1052
1738
|
placeDomino: "placeDomino",
|
|
1739
|
+
/** Pick a domino from the revealed set */
|
|
1053
1740
|
pickDomino: "pickDomino",
|
|
1741
|
+
/** Pass/discard when no valid placement exists */
|
|
1054
1742
|
pass: "pass"
|
|
1055
1743
|
};
|
|
1056
1744
|
|
|
@@ -1060,21 +1748,53 @@ var getAvailablesExtraRules = (extraRules, mode, numberOfPlayers) => {
|
|
|
1060
1748
|
return rule.mode.filter((m) => m.name === mode).length > 0 && (!rule.playersLimit || rule.playersLimit === numberOfPlayers);
|
|
1061
1749
|
});
|
|
1062
1750
|
if (availableExtraRules.length === 0) {
|
|
1063
|
-
return err(
|
|
1751
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1064
1752
|
}
|
|
1065
1753
|
return ok(availableExtraRules);
|
|
1066
1754
|
};
|
|
1067
1755
|
var toExtraRule = (rule, extraRules) => {
|
|
1068
1756
|
const foundRule = extraRules.find((r) => r.name === rule);
|
|
1069
1757
|
if (!foundRule) {
|
|
1070
|
-
return err(
|
|
1758
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1071
1759
|
}
|
|
1072
1760
|
return ok(foundRule);
|
|
1073
1761
|
};
|
|
1074
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
|
+
|
|
1075
1795
|
// src/core/useCases/addExtraRules.ts
|
|
1076
1796
|
var addExtraRulesUseCase = (deps) => (game, rules) => {
|
|
1077
|
-
const { ruleRepository } = deps;
|
|
1797
|
+
const { ruleRepository, dominoesRepository, shuffleMethod: shuffleMethod2 } = deps;
|
|
1078
1798
|
const availableExtraRules = ruleRepository.getAllExtra();
|
|
1079
1799
|
const newExtraRules = [];
|
|
1080
1800
|
for (const rule of rules) {
|
|
@@ -1084,8 +1804,24 @@ var addExtraRulesUseCase = (deps) => (game, rules) => {
|
|
|
1084
1804
|
}
|
|
1085
1805
|
newExtraRules.push(result.value);
|
|
1086
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
|
+
}
|
|
1087
1823
|
const updatedRules = {
|
|
1088
|
-
basic:
|
|
1824
|
+
basic: updatedBasic,
|
|
1089
1825
|
extra: newExtraRules
|
|
1090
1826
|
};
|
|
1091
1827
|
const next = {
|
|
@@ -1094,6 +1830,7 @@ var addExtraRulesUseCase = (deps) => (game, rules) => {
|
|
|
1094
1830
|
};
|
|
1095
1831
|
const updatedGame = {
|
|
1096
1832
|
...game,
|
|
1833
|
+
dominoes: updatedDominoes,
|
|
1097
1834
|
rules: updatedRules,
|
|
1098
1835
|
nextAction: next
|
|
1099
1836
|
};
|
|
@@ -1133,7 +1870,7 @@ var placeTile = (kingdom, position, tile) => {
|
|
|
1133
1870
|
var getTile = (kingdom, position) => {
|
|
1134
1871
|
const tile = kingdom[position.y]?.[position.x];
|
|
1135
1872
|
if (!tile) {
|
|
1136
|
-
return err(
|
|
1873
|
+
return err(ErrorCode.PLACEMENT_OUT_OF_BOUNDS);
|
|
1137
1874
|
}
|
|
1138
1875
|
return ok(tile);
|
|
1139
1876
|
};
|
|
@@ -1208,7 +1945,7 @@ var calculateDominoPosition = (position, rotation, domino) => {
|
|
|
1208
1945
|
}
|
|
1209
1946
|
return [firstTile, secondTile];
|
|
1210
1947
|
};
|
|
1211
|
-
var placeDomino = (kingdom, position, rotation, domino) => {
|
|
1948
|
+
var placeDomino = (kingdom, position, rotation, domino, maxKingdomSize = MAX_KINGDOM_SIZE) => {
|
|
1212
1949
|
const [firstTile, secondTile] = calculateDominoPosition(
|
|
1213
1950
|
position,
|
|
1214
1951
|
rotation,
|
|
@@ -1222,20 +1959,23 @@ var placeDomino = (kingdom, position, rotation, domino) => {
|
|
|
1222
1959
|
if (isErr(isFreePlaceResult)) {
|
|
1223
1960
|
return isFreePlaceResult;
|
|
1224
1961
|
}
|
|
1962
|
+
if (exceedsMaxSize(kingdom, firstTile.position, secondTile.position, maxKingdomSize)) {
|
|
1963
|
+
return err(ErrorCode.PLACEMENT_EXCEEDS_KINGDOM_SIZE);
|
|
1964
|
+
}
|
|
1225
1965
|
const adjacentTiles = isAdjacent(
|
|
1226
1966
|
kingdom,
|
|
1227
1967
|
firstTile.position,
|
|
1228
1968
|
secondTile.position
|
|
1229
1969
|
);
|
|
1230
1970
|
if (adjacentTiles.length === 0) {
|
|
1231
|
-
return err(
|
|
1971
|
+
return err(ErrorCode.PLACEMENT_NOT_ADJACENT);
|
|
1232
1972
|
}
|
|
1233
1973
|
const hasValidAdjacentTiles = hasValidAdjacent(adjacentTiles, [
|
|
1234
1974
|
firstTile.tile,
|
|
1235
1975
|
secondTile.tile
|
|
1236
1976
|
]);
|
|
1237
1977
|
if (!hasValidAdjacentTiles) {
|
|
1238
|
-
return err(
|
|
1978
|
+
return err(ErrorCode.PLACEMENT_INVALID_TERRAIN);
|
|
1239
1979
|
}
|
|
1240
1980
|
return ok(placeTiles(kingdom, [firstTile, secondTile]));
|
|
1241
1981
|
};
|
|
@@ -1256,46 +1996,30 @@ var isFreePlace = (kingdom, firstPosition, secondPosition) => {
|
|
|
1256
1996
|
return secondTile;
|
|
1257
1997
|
}
|
|
1258
1998
|
if (firstTile.value.type !== "empty" || secondTile.value.type !== "empty") {
|
|
1259
|
-
return err(
|
|
1999
|
+
return err(ErrorCode.PLACEMENT_NOT_EMPTY);
|
|
1260
2000
|
}
|
|
1261
2001
|
return ok(true);
|
|
1262
2002
|
};
|
|
1263
2003
|
var isAdjacent = (kingdom, firstPosition, secondPosition) => {
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const adjacentTilesKeys = [
|
|
1270
|
-
{ x: xFirstPosition - 1, y: yFirstPosition },
|
|
1271
|
-
{ x: xFirstPosition, y: yFirstPosition - 1 },
|
|
1272
|
-
{ x: xFirstPosition + 1, y: yFirstPosition },
|
|
1273
|
-
{ x: xFirstPosition, y: yFirstPosition + 1 },
|
|
1274
|
-
{ x: xSecondPosition - 1, y: ySecondPosition },
|
|
1275
|
-
{ x: xSecondPosition, y: ySecondPosition - 1 },
|
|
1276
|
-
{ x: xSecondPosition + 1, y: ySecondPosition },
|
|
1277
|
-
{ 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 }
|
|
1278
2009
|
];
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
}),
|
|
1289
|
-
1
|
|
1290
|
-
);
|
|
1291
|
-
adjacentTilesKeys.forEach((key) => {
|
|
1292
|
-
const tile = getTile(kingdom, key);
|
|
1293
|
-
if (isOk(tile)) {
|
|
1294
|
-
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") {
|
|
1295
2019
|
adjacentTiles.push(tile.value);
|
|
1296
2020
|
}
|
|
1297
2021
|
}
|
|
1298
|
-
}
|
|
2022
|
+
}
|
|
1299
2023
|
return adjacentTiles;
|
|
1300
2024
|
};
|
|
1301
2025
|
var hasValidAdjacent = (adjacentTiles, tiles) => {
|
|
@@ -1306,22 +2030,56 @@ var hasValidAdjacent = (adjacentTiles, tiles) => {
|
|
|
1306
2030
|
});
|
|
1307
2031
|
};
|
|
1308
2032
|
var checkCastleIsInMiddle = (kingdom) => {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
(tile) => tile.type
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
)
|
|
1324
|
-
|
|
2033
|
+
let castleX;
|
|
2034
|
+
let castleY;
|
|
2035
|
+
for (let y = 0; y < kingdom.length; y++) {
|
|
2036
|
+
const x = kingdom[y].findIndex((tile) => tile.type === "castle");
|
|
2037
|
+
if (x !== -1) {
|
|
2038
|
+
castleX = x;
|
|
2039
|
+
castleY = y;
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (castleX === void 0 || castleY === void 0) {
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
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
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
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;
|
|
2081
|
+
}
|
|
2082
|
+
return maxX - minX + 1 > maxSize || maxY - minY + 1 > maxSize;
|
|
1325
2083
|
};
|
|
1326
2084
|
var countDominoes = (kingdom) => {
|
|
1327
2085
|
const notEmpties = kingdom.flat().filter((tile) => tile.type !== "empty");
|
|
@@ -1335,11 +2093,11 @@ var deepCopyKingdom = (kingdom) => {
|
|
|
1335
2093
|
var MINPLAYERSNAMELENGTH = 3;
|
|
1336
2094
|
var validatePlayer = (name) => {
|
|
1337
2095
|
if (!name || name.trim().length < MINPLAYERSNAMELENGTH) {
|
|
1338
|
-
return err(
|
|
2096
|
+
return err(ErrorCode.INVALID_PLAYER_NAME);
|
|
1339
2097
|
}
|
|
1340
2098
|
return ok(name);
|
|
1341
2099
|
};
|
|
1342
|
-
var createPlayer = (playerName, id, kingdom) => {
|
|
2100
|
+
var createPlayer = (playerName, id, kingdom, bot) => {
|
|
1343
2101
|
const name = validatePlayer(playerName);
|
|
1344
2102
|
if (isErr(name)) {
|
|
1345
2103
|
return name;
|
|
@@ -1347,22 +2105,30 @@ var createPlayer = (playerName, id, kingdom) => {
|
|
|
1347
2105
|
return ok({
|
|
1348
2106
|
name: name.value,
|
|
1349
2107
|
id,
|
|
1350
|
-
kingdom
|
|
2108
|
+
kingdom,
|
|
2109
|
+
...bot && { bot }
|
|
1351
2110
|
});
|
|
1352
2111
|
};
|
|
1353
2112
|
|
|
1354
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 };
|
|
2119
|
+
};
|
|
1355
2120
|
var addPlayersUseCase = (deps) => (game, players) => {
|
|
1356
2121
|
const { uuidMethod: uuidMethod2, shuffleMethod: shuffleMethod2, ruleRepository } = deps;
|
|
1357
|
-
if (!players || players.length <
|
|
1358
|
-
return err(
|
|
2122
|
+
if (!players || players.length < 1 || players.length > 4) {
|
|
2123
|
+
return err(ErrorCode.INVALID_PLAYER_COUNT);
|
|
1359
2124
|
}
|
|
1360
2125
|
const newPlayers = [];
|
|
1361
2126
|
for (const player of players) {
|
|
2127
|
+
const { name, bot } = normalizePlayerInput(player);
|
|
1362
2128
|
let kingdom = createEmptyKingdom();
|
|
1363
2129
|
kingdom = placeCastle(kingdom);
|
|
1364
2130
|
let id = uuidMethod2();
|
|
1365
|
-
const newPlayer = createPlayer(
|
|
2131
|
+
const newPlayer = createPlayer(name, id, kingdom, bot);
|
|
1366
2132
|
if (isErr(newPlayer)) {
|
|
1367
2133
|
return newPlayer;
|
|
1368
2134
|
}
|
|
@@ -1371,11 +2137,12 @@ var addPlayersUseCase = (deps) => (game, players) => {
|
|
|
1371
2137
|
const rules = ruleRepository.getAll();
|
|
1372
2138
|
const basicRules = rules.basic[newPlayers.length];
|
|
1373
2139
|
if (!basicRules) {
|
|
1374
|
-
return err(
|
|
2140
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1375
2141
|
}
|
|
1376
2142
|
const dominoes = game.dominoes;
|
|
1377
2143
|
const { maxDominoes } = basicRules;
|
|
1378
|
-
const
|
|
2144
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "dominoes") : shuffleMethod2;
|
|
2145
|
+
const shuffledDominoes = shuffle(dominoes);
|
|
1379
2146
|
const splicedDominoes = shuffledDominoes.slice(0, maxDominoes);
|
|
1380
2147
|
const next = {
|
|
1381
2148
|
type: "step",
|
|
@@ -1394,6 +2161,70 @@ var addPlayersUseCase = (deps) => (game, players) => {
|
|
|
1394
2161
|
return ok(updatedGame);
|
|
1395
2162
|
};
|
|
1396
2163
|
|
|
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 });
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (properties.length === 0) {
|
|
2205
|
+
return ok({
|
|
2206
|
+
points: 0,
|
|
2207
|
+
maxPropertiesSize: 0,
|
|
2208
|
+
totalCrowns: 0
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
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;
|
|
2218
|
+
}
|
|
2219
|
+
totalCrowns += property.crowns;
|
|
2220
|
+
}
|
|
2221
|
+
return ok({
|
|
2222
|
+
points,
|
|
2223
|
+
maxPropertiesSize,
|
|
2224
|
+
totalCrowns
|
|
2225
|
+
});
|
|
2226
|
+
};
|
|
2227
|
+
|
|
1397
2228
|
// src/core/domain/entities/lord.ts
|
|
1398
2229
|
var createLord = (id, playerId) => {
|
|
1399
2230
|
return {
|
|
@@ -1417,18 +2248,17 @@ var canPlaceAndDominoPickedIsDefined = (lord) => {
|
|
|
1417
2248
|
return !hasPick(lord) && !hasPlace(lord) && !hasTurnEnded(lord) && lord.dominoPicked !== void 0;
|
|
1418
2249
|
};
|
|
1419
2250
|
var updateLordOrder = (lords) => {
|
|
1420
|
-
return lords.sort((a, b) => {
|
|
1421
|
-
return a.dominoPicked
|
|
2251
|
+
return [...lords].sort((a, b) => {
|
|
2252
|
+
return (a.dominoPicked?.number ?? 0) - (b.dominoPicked?.number ?? 0);
|
|
1422
2253
|
});
|
|
1423
2254
|
};
|
|
1424
|
-
var resetLordsActions = (
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1431
|
-
return lordsCopy;
|
|
2255
|
+
var resetLordsActions = (lords) => {
|
|
2256
|
+
return lords.map((lord) => ({
|
|
2257
|
+
...lord,
|
|
2258
|
+
turnEnded: false,
|
|
2259
|
+
hasPick: false,
|
|
2260
|
+
hasPlace: false
|
|
2261
|
+
}));
|
|
1432
2262
|
};
|
|
1433
2263
|
var nextLordWithAction = (lords) => {
|
|
1434
2264
|
const nextLord = findNextLord(lords);
|
|
@@ -1455,38 +2285,29 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
|
1455
2285
|
(lord) => lord.id === game.nextAction.nextLord
|
|
1456
2286
|
);
|
|
1457
2287
|
if (!currentLord) {
|
|
1458
|
-
return err(
|
|
2288
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1459
2289
|
}
|
|
1460
2290
|
if (currentLord.id !== lordId) {
|
|
1461
|
-
return err(
|
|
2291
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1462
2292
|
}
|
|
1463
2293
|
if (!canPick(currentLord)) {
|
|
1464
|
-
return err(
|
|
2294
|
+
return err(ErrorCode.CANNOT_PICK);
|
|
1465
2295
|
}
|
|
1466
2296
|
const selectedDomino = game.currentDominoes.find(
|
|
1467
2297
|
(domino) => domino.domino.number === dominoPick
|
|
1468
2298
|
);
|
|
1469
2299
|
if (!selectedDomino) {
|
|
1470
|
-
return err(
|
|
2300
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
1471
2301
|
}
|
|
1472
2302
|
if (selectedDomino.picked) {
|
|
1473
|
-
return err(
|
|
2303
|
+
return err(ErrorCode.DOMINO_ALREADY_PICKED);
|
|
1474
2304
|
}
|
|
1475
|
-
const updatedCurrentDominoes = game.currentDominoes.map(
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
});
|
|
1482
|
-
const updatedLords = game.lords.map((lord) => {
|
|
1483
|
-
if (lord.id === currentLord.id) {
|
|
1484
|
-
lord.hasPick = true;
|
|
1485
|
-
lord.turnEnded = true;
|
|
1486
|
-
lord.dominoPicked = selectedDomino.domino;
|
|
1487
|
-
}
|
|
1488
|
-
return lord;
|
|
1489
|
-
});
|
|
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
|
+
);
|
|
1490
2311
|
const turnEnded = allLordsHavePlayed(updatedLords);
|
|
1491
2312
|
if (turnEnded) {
|
|
1492
2313
|
const lords = resetLordsActions(updateLordOrder(updatedLords));
|
|
@@ -1527,7 +2348,7 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
|
|
|
1527
2348
|
|
|
1528
2349
|
// src/core/domain/entities/game.ts
|
|
1529
2350
|
var create = (payload) => {
|
|
1530
|
-
const { id, mode, dominoes } = payload;
|
|
2351
|
+
const { id, mode, dominoes, seed } = payload;
|
|
1531
2352
|
return {
|
|
1532
2353
|
id,
|
|
1533
2354
|
dominoes,
|
|
@@ -1544,11 +2365,13 @@ var create = (payload) => {
|
|
|
1544
2365
|
lords: 0,
|
|
1545
2366
|
maxDominoes: 0,
|
|
1546
2367
|
dominoesPerTurn: 0,
|
|
1547
|
-
maxTurns: 0
|
|
2368
|
+
maxTurns: 0,
|
|
2369
|
+
maxKingdomSize: 5
|
|
1548
2370
|
},
|
|
1549
2371
|
extra: []
|
|
1550
2372
|
},
|
|
1551
|
-
mode
|
|
2373
|
+
mode,
|
|
2374
|
+
...seed !== void 0 && { seed }
|
|
1552
2375
|
};
|
|
1553
2376
|
};
|
|
1554
2377
|
|
|
@@ -1561,13 +2384,14 @@ var createMode = (mode, availableMode) => {
|
|
|
1561
2384
|
if (foundMode) {
|
|
1562
2385
|
return ok(foundMode);
|
|
1563
2386
|
}
|
|
1564
|
-
return err(
|
|
2387
|
+
return err(ErrorCode.MODE_NOT_FOUND);
|
|
1565
2388
|
};
|
|
1566
2389
|
|
|
1567
2390
|
// src/core/useCases/createGame.ts
|
|
1568
|
-
var createGameUseCase = (deps) => (mode) => {
|
|
2391
|
+
var createGameUseCase = (deps) => (mode, seed) => {
|
|
1569
2392
|
const { modeRepository, dominoesRepository, uuidMethod: uuidMethod2 } = deps;
|
|
1570
2393
|
const id = uuidMethod2();
|
|
2394
|
+
const gameSeed = seed ?? uuidMethod2();
|
|
1571
2395
|
const availableMode = modeRepository.getAvailables();
|
|
1572
2396
|
const newMode = createMode(mode, availableMode);
|
|
1573
2397
|
if (isErr(newMode)) {
|
|
@@ -1575,17 +2399,51 @@ var createGameUseCase = (deps) => (mode) => {
|
|
|
1575
2399
|
}
|
|
1576
2400
|
const dominoes = dominoesRepository.getForMode(newMode.value);
|
|
1577
2401
|
if (!dominoes || dominoes.length === 0) {
|
|
1578
|
-
return err(
|
|
2402
|
+
return err(ErrorCode.DOMINO_NOT_FOUND);
|
|
1579
2403
|
}
|
|
1580
2404
|
const payload = {
|
|
1581
2405
|
id,
|
|
1582
2406
|
mode: newMode.value,
|
|
1583
|
-
dominoes
|
|
2407
|
+
dominoes,
|
|
2408
|
+
seed: gameSeed
|
|
1584
2409
|
};
|
|
1585
2410
|
const newGame = create(payload);
|
|
1586
2411
|
return ok(newGame);
|
|
1587
2412
|
};
|
|
1588
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
|
+
|
|
1589
2447
|
// src/core/useCases/discardDomino.ts
|
|
1590
2448
|
var discardDominoUseCase = (game, lordId) => {
|
|
1591
2449
|
const nextAction = game.nextAction;
|
|
@@ -1593,13 +2451,21 @@ var discardDominoUseCase = (game, lordId) => {
|
|
|
1593
2451
|
(lord) => lord.id === nextAction.nextLord
|
|
1594
2452
|
);
|
|
1595
2453
|
if (!currentLord) {
|
|
1596
|
-
return err(
|
|
2454
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1597
2455
|
}
|
|
1598
2456
|
if (currentLord.id !== lordId) {
|
|
1599
|
-
return err(
|
|
2457
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1600
2458
|
}
|
|
1601
2459
|
if (!canPass(currentLord)) {
|
|
1602
|
-
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
|
+
}
|
|
1603
2469
|
}
|
|
1604
2470
|
const maxTurns = game.rules.basic.maxTurns;
|
|
1605
2471
|
const currentTurn = game.turn;
|
|
@@ -1641,12 +2507,48 @@ var discardDominoUseCase = (game, lordId) => {
|
|
|
1641
2507
|
return ok(updatedGame);
|
|
1642
2508
|
};
|
|
1643
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
|
+
|
|
1644
2546
|
// src/core/useCases/getExtraRules.ts
|
|
1645
2547
|
var getExtraRulesUseCase = (deps) => (mode, players) => {
|
|
1646
2548
|
const { ruleRepository } = deps;
|
|
1647
2549
|
const allExtraRules = ruleRepository.getAllExtra();
|
|
1648
2550
|
if (!allExtraRules) {
|
|
1649
|
-
return err(
|
|
2551
|
+
return err(ErrorCode.STEP_EXECUTION_FAILED);
|
|
1650
2552
|
}
|
|
1651
2553
|
return getAvailablesExtraRules(allExtraRules, mode, players);
|
|
1652
2554
|
};
|
|
@@ -1663,11 +2565,11 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1663
2565
|
const { basic, extra } = rules;
|
|
1664
2566
|
const finalScoreResult = scoreResult.map((score) => {
|
|
1665
2567
|
let finalScore = score.details.points;
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
}
|
|
2568
|
+
const player = game.players.find((p) => p.id === score.playerId);
|
|
2569
|
+
if (!player) {
|
|
2570
|
+
return score;
|
|
2571
|
+
}
|
|
2572
|
+
const { kingdom } = player;
|
|
1671
2573
|
if (extra.length > 0) {
|
|
1672
2574
|
extra.forEach((extraRule) => {
|
|
1673
2575
|
if (extraRule.name === "The middle Kingdom") {
|
|
@@ -1699,10 +2601,7 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1699
2601
|
if (a.details.points !== b.details.points) {
|
|
1700
2602
|
return b.details.points - a.details.points;
|
|
1701
2603
|
}
|
|
1702
|
-
|
|
1703
|
-
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
1704
|
-
}
|
|
1705
|
-
return b.details.totalCrowns - a.details.totalCrowns;
|
|
2604
|
+
return b.details.maxPropertiesSize - a.details.maxPropertiesSize;
|
|
1706
2605
|
});
|
|
1707
2606
|
const result = [];
|
|
1708
2607
|
let isTie = false;
|
|
@@ -1715,7 +2614,7 @@ var getResultUseCase = (game, scoreResult) => {
|
|
|
1715
2614
|
for (let i = 1; i < finalScoreResult.length; i++) {
|
|
1716
2615
|
const currentScore = finalScoreResult[i];
|
|
1717
2616
|
const previousScore = finalScoreResult[i - 1];
|
|
1718
|
-
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) {
|
|
1719
2618
|
isTie = true;
|
|
1720
2619
|
}
|
|
1721
2620
|
if (isTie) {
|
|
@@ -1744,26 +2643,27 @@ var placeDominoUseCase = (game, lordId, position, rotation) => {
|
|
|
1744
2643
|
(lord) => lord.id === nextAction.nextLord
|
|
1745
2644
|
);
|
|
1746
2645
|
if (!currentLord) {
|
|
1747
|
-
return err(
|
|
2646
|
+
return err(ErrorCode.LORD_NOT_FOUND);
|
|
1748
2647
|
}
|
|
1749
2648
|
if (currentLord.id !== lordId) {
|
|
1750
|
-
return err(
|
|
2649
|
+
return err(ErrorCode.NOT_YOUR_TURN);
|
|
1751
2650
|
}
|
|
1752
2651
|
if (!canPlaceAndDominoPickedIsDefined(currentLord)) {
|
|
1753
|
-
return err(
|
|
2652
|
+
return err(ErrorCode.CANNOT_PLACE);
|
|
1754
2653
|
}
|
|
1755
2654
|
const domino = currentLord.dominoPicked;
|
|
1756
2655
|
const currentPlayer = game.players.find(
|
|
1757
2656
|
(player) => player.id === currentLord.playerId
|
|
1758
2657
|
);
|
|
1759
2658
|
if (!currentPlayer) {
|
|
1760
|
-
return err(
|
|
2659
|
+
return err(ErrorCode.PLAYER_NOT_FOUND);
|
|
1761
2660
|
}
|
|
1762
2661
|
const updatedKingdom = placeDomino(
|
|
1763
2662
|
currentPlayer.kingdom,
|
|
1764
2663
|
position,
|
|
1765
2664
|
rotation,
|
|
1766
|
-
domino
|
|
2665
|
+
domino,
|
|
2666
|
+
game.rules.basic.maxKingdomSize
|
|
1767
2667
|
);
|
|
1768
2668
|
if (isErr(updatedKingdom)) {
|
|
1769
2669
|
return updatedKingdom;
|
|
@@ -1834,7 +2734,8 @@ var startGameUseCase = (deps) => (game) => {
|
|
|
1834
2734
|
newLords.push(lord);
|
|
1835
2735
|
}
|
|
1836
2736
|
}
|
|
1837
|
-
const
|
|
2737
|
+
const shuffle = game.seed ? createSeededShuffle(game.seed, "lords") : shuffleMethod2;
|
|
2738
|
+
const shuffledLords = shuffle(newLords);
|
|
1838
2739
|
const dominoesCopy = [...dominoes];
|
|
1839
2740
|
const dominoesDrawn = dominoesCopy.splice(0, dominoesPerTurn);
|
|
1840
2741
|
dominoesDrawn.sort((a, b) => a.number - b.number);
|
|
@@ -1860,82 +2761,6 @@ var startGameUseCase = (deps) => (game) => {
|
|
|
1860
2761
|
return ok(newState);
|
|
1861
2762
|
};
|
|
1862
2763
|
|
|
1863
|
-
// src/core/useCases/calculateScore.ts
|
|
1864
|
-
var calculateScoreUseCase = (kingdom) => {
|
|
1865
|
-
const visited = kingdom.map((row) => row.map(() => false));
|
|
1866
|
-
const properties = [];
|
|
1867
|
-
const directions = [
|
|
1868
|
-
[-1, 0],
|
|
1869
|
-
[1, 0],
|
|
1870
|
-
[0, -1],
|
|
1871
|
-
[0, 1]
|
|
1872
|
-
];
|
|
1873
|
-
const markZone = (x, y, type, crowns) => {
|
|
1874
|
-
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") {
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
visited[y][x] = true;
|
|
1878
|
-
const currentZone = properties.find(
|
|
1879
|
-
(property) => property.type === type && property.tiles.some(([tx, ty]) => {
|
|
1880
|
-
for (let [dx, dy] of directions) {
|
|
1881
|
-
if (tx + dx === x && ty + dy === y) {
|
|
1882
|
-
return true;
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
return false;
|
|
1886
|
-
})
|
|
1887
|
-
);
|
|
1888
|
-
if (currentZone) {
|
|
1889
|
-
currentZone.size += 1;
|
|
1890
|
-
currentZone.tiles.push([x, y]);
|
|
1891
|
-
currentZone.crowns += crowns;
|
|
1892
|
-
} else {
|
|
1893
|
-
properties.push({
|
|
1894
|
-
type,
|
|
1895
|
-
crowns,
|
|
1896
|
-
size: 1,
|
|
1897
|
-
tiles: [[x, y]]
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
directions.forEach(([dx, dy]) => {
|
|
1901
|
-
const nx = x + dx;
|
|
1902
|
-
const ny = y + dy;
|
|
1903
|
-
if (nx >= 0 && ny >= 0 && nx < kingdom[0].length && ny < kingdom.length && kingdom[ny][nx].type === type) {
|
|
1904
|
-
markZone(nx, ny, kingdom[ny][nx].type, kingdom[ny][nx].crowns);
|
|
1905
|
-
}
|
|
1906
|
-
});
|
|
1907
|
-
};
|
|
1908
|
-
kingdom.forEach((row, y) => {
|
|
1909
|
-
row.forEach((tile, x) => {
|
|
1910
|
-
if (!visited[y][x]) {
|
|
1911
|
-
markZone(x, y, tile.type, tile.crowns);
|
|
1912
|
-
}
|
|
1913
|
-
});
|
|
1914
|
-
});
|
|
1915
|
-
if (properties.length === 0) {
|
|
1916
|
-
return ok({
|
|
1917
|
-
points: 0,
|
|
1918
|
-
maxPropertiesSize: 0,
|
|
1919
|
-
totalCrowns: 0
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
let points = properties.reduce((acc, property) => {
|
|
1923
|
-
return acc + property.size * property.crowns;
|
|
1924
|
-
}, 0);
|
|
1925
|
-
const maxPropertiesSize = Math.max(
|
|
1926
|
-
...properties.map((property) => property.size)
|
|
1927
|
-
);
|
|
1928
|
-
const totalCrowns = properties.reduce(
|
|
1929
|
-
(acc, property) => acc + property.crowns,
|
|
1930
|
-
0
|
|
1931
|
-
);
|
|
1932
|
-
return ok({
|
|
1933
|
-
points,
|
|
1934
|
-
maxPropertiesSize,
|
|
1935
|
-
totalCrowns
|
|
1936
|
-
});
|
|
1937
|
-
};
|
|
1938
|
-
|
|
1939
2764
|
// src/config.ts
|
|
1940
2765
|
var configureEngine = (config) => {
|
|
1941
2766
|
const modeRepository = jsonModes_default();
|
|
@@ -1943,10 +2768,12 @@ var configureEngine = (config) => {
|
|
|
1943
2768
|
const ruleRepository = jsonRules_default();
|
|
1944
2769
|
const uuid = config.uuidMethod || uuidMethod;
|
|
1945
2770
|
const shuffle = config.shuffleMethod || shuffleMethod;
|
|
1946
|
-
const
|
|
2771
|
+
const translator = config.translator || defaultTranslator;
|
|
2772
|
+
const logger = config.logger ? config.logger : config.logging ? consoleLogger(true) : silentLogger;
|
|
1947
2773
|
return {
|
|
1948
2774
|
createGameHandler: createGameHandler(
|
|
1949
2775
|
logger,
|
|
2776
|
+
translator,
|
|
1950
2777
|
createGameUseCase({
|
|
1951
2778
|
modeRepository,
|
|
1952
2779
|
dominoesRepository,
|
|
@@ -1955,14 +2782,17 @@ var configureEngine = (config) => {
|
|
|
1955
2782
|
),
|
|
1956
2783
|
getModesHandler: getModesHandler(
|
|
1957
2784
|
logger,
|
|
2785
|
+
translator,
|
|
1958
2786
|
getModesUseCase({ modeRepository })
|
|
1959
2787
|
),
|
|
1960
2788
|
getExtraRulesHandler: getExtraRulesHandler(
|
|
1961
2789
|
logger,
|
|
2790
|
+
translator,
|
|
1962
2791
|
getExtraRulesUseCase({ ruleRepository })
|
|
1963
2792
|
),
|
|
1964
2793
|
addPlayersHandler: addPlayersHandler(
|
|
1965
2794
|
logger,
|
|
2795
|
+
translator,
|
|
1966
2796
|
addPlayersUseCase({
|
|
1967
2797
|
uuidMethod: uuid,
|
|
1968
2798
|
shuffleMethod: shuffle,
|
|
@@ -1971,22 +2801,903 @@ var configureEngine = (config) => {
|
|
|
1971
2801
|
),
|
|
1972
2802
|
addExtraRulesHandler: addExtraRulesHandler(
|
|
1973
2803
|
logger,
|
|
1974
|
-
|
|
2804
|
+
translator,
|
|
2805
|
+
addExtraRulesUseCase({
|
|
2806
|
+
ruleRepository,
|
|
2807
|
+
dominoesRepository,
|
|
2808
|
+
shuffleMethod: shuffle
|
|
2809
|
+
})
|
|
1975
2810
|
),
|
|
1976
2811
|
startGameHandler: startGameHandler(
|
|
1977
2812
|
logger,
|
|
2813
|
+
translator,
|
|
1978
2814
|
startGameUseCase({ uuidMethod: uuid, shuffleMethod: shuffle })
|
|
1979
2815
|
),
|
|
1980
|
-
chooseDominoHandler: chooseDominoHandler(
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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, {
|
|
1984
2832
|
getResultUseCase,
|
|
1985
2833
|
calculateScoreUseCase
|
|
1986
2834
|
}),
|
|
1987
|
-
calculateScoreHandler: calculateScoreHandler(
|
|
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
|
+
)
|
|
2857
|
+
};
|
|
2858
|
+
};
|
|
2859
|
+
|
|
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;
|
|
2958
|
+
}
|
|
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
|
+
}
|
|
3037
|
+
}
|
|
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}`));
|
|
3054
|
+
} else {
|
|
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"));
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return issues;
|
|
3067
|
+
};
|
|
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;
|
|
3128
|
+
};
|
|
3129
|
+
|
|
3130
|
+
// src/core/useCases/botRegistry.ts
|
|
3131
|
+
var defaultStrategies = {
|
|
3132
|
+
random: randomStrategy,
|
|
3133
|
+
greedy: greedyStrategy,
|
|
3134
|
+
advanced: advancedStrategy,
|
|
3135
|
+
expert: expertStrategy
|
|
3136
|
+
};
|
|
3137
|
+
var getStrategy = (name, custom) => {
|
|
3138
|
+
return custom?.[name] ?? defaultStrategies[name];
|
|
3139
|
+
};
|
|
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
|
+
};
|
|
3581
|
+
return {
|
|
3582
|
+
engine: wrappedEngine,
|
|
3583
|
+
getLog: () => log
|
|
1988
3584
|
};
|
|
1989
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
|
|
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;
|
|
3651
|
+
}
|
|
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]
|
|
3677
|
+
};
|
|
3678
|
+
};
|
|
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
|
+
});
|
|
1990
3701
|
|
|
1991
3702
|
// src/index.ts
|
|
1992
3703
|
var createGameEngine = (config) => {
|
|
@@ -2001,9 +3712,14 @@ var createGameEngine = (config) => {
|
|
|
2001
3712
|
placeDominoHandler: placeDominoHandler2,
|
|
2002
3713
|
discardDominoHandler: discardDominoHandler2,
|
|
2003
3714
|
getResultHandler: getResultHandler2,
|
|
2004
|
-
calculateScoreHandler: calculateScoreHandler2
|
|
3715
|
+
calculateScoreHandler: calculateScoreHandler2,
|
|
3716
|
+
getValidPlacementsHandler: getValidPlacementsHandler2,
|
|
3717
|
+
canPlaceDominoHandler: canPlaceDominoHandler2,
|
|
3718
|
+
serializeGameHandler: serializeGameHandler2,
|
|
3719
|
+
deserializeGameHandler: deserializeGameHandler2,
|
|
3720
|
+
getDynastyResultHandler: getDynastyResultHandler2
|
|
2005
3721
|
} = configureEngine(config);
|
|
2006
|
-
|
|
3722
|
+
let engine = {
|
|
2007
3723
|
getModes: (command) => getModesHandler2(command),
|
|
2008
3724
|
getExtraRules: (command) => getExtraRulesHandler2(command),
|
|
2009
3725
|
createGame: (command) => createGameHandler2(command),
|
|
@@ -2014,16 +3730,78 @@ var createGameEngine = (config) => {
|
|
|
2014
3730
|
placeDomino: (command) => placeDominoHandler2(command),
|
|
2015
3731
|
discardDomino: (command) => discardDominoHandler2(command),
|
|
2016
3732
|
getResults: (command) => getResultHandler2(command),
|
|
2017
|
-
calculateScore: (command) => calculateScoreHandler2(command)
|
|
3733
|
+
calculateScore: (command) => calculateScoreHandler2(command),
|
|
3734
|
+
getValidPlacements: (command) => getValidPlacementsHandler2(command),
|
|
3735
|
+
canPlaceDomino: (command) => canPlaceDominoHandler2(command),
|
|
3736
|
+
serialize: (command) => serializeGameHandler2(command),
|
|
3737
|
+
deserialize: (command) => deserializeGameHandler2(command),
|
|
3738
|
+
getDynastyResults: (command) => getDynastyResultHandler2(command)
|
|
2018
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;
|
|
2019
3748
|
};
|
|
2020
3749
|
export {
|
|
3750
|
+
ActionExecutionError,
|
|
3751
|
+
DomainException,
|
|
3752
|
+
ErrorCode,
|
|
2021
3753
|
GRIDSIZE,
|
|
2022
3754
|
GROUND,
|
|
3755
|
+
InvalidStepError,
|
|
3756
|
+
MAX_KINGDOM_SIZE,
|
|
3757
|
+
NotFoundError,
|
|
3758
|
+
RedoError,
|
|
3759
|
+
StepExecutionError,
|
|
3760
|
+
TranslationKey,
|
|
3761
|
+
UndoError,
|
|
3762
|
+
ValidationError,
|
|
3763
|
+
advancedStrategy,
|
|
3764
|
+
appendAction,
|
|
3765
|
+
canRedo,
|
|
3766
|
+
canUndo,
|
|
3767
|
+
clearHistory,
|
|
3768
|
+
createActionLog,
|
|
3769
|
+
createDomainError,
|
|
2023
3770
|
createGameEngine,
|
|
3771
|
+
createGameHistory,
|
|
3772
|
+
createSavePoint,
|
|
3773
|
+
createTranslator,
|
|
3774
|
+
defaultTranslations,
|
|
3775
|
+
defaultTranslator,
|
|
3776
|
+
deserializeGame,
|
|
3777
|
+
errorCodeToTranslationKey,
|
|
3778
|
+
expertStrategy,
|
|
2024
3779
|
gameSteps,
|
|
3780
|
+
getActions,
|
|
3781
|
+
getActionsByTurn,
|
|
3782
|
+
getActionsByType,
|
|
3783
|
+
getHistorySize,
|
|
3784
|
+
getStrategy,
|
|
3785
|
+
getStrategyNames,
|
|
3786
|
+
greedyStrategy,
|
|
3787
|
+
isBotTurn,
|
|
2025
3788
|
isGameWithNextAction,
|
|
2026
3789
|
isGameWithNextStep,
|
|
2027
|
-
|
|
3790
|
+
playBotTurn,
|
|
3791
|
+
playBotTurns,
|
|
3792
|
+
playerActions,
|
|
3793
|
+
pushState,
|
|
3794
|
+
randomStrategy,
|
|
3795
|
+
redo,
|
|
3796
|
+
replayActions,
|
|
3797
|
+
restoreFromSavePoint,
|
|
3798
|
+
serializeGame,
|
|
3799
|
+
summarizeGameState,
|
|
3800
|
+
translateErrorCode,
|
|
3801
|
+
undo,
|
|
3802
|
+
validateGameState,
|
|
3803
|
+
wrapWithActionLog,
|
|
3804
|
+
wrapWithDebug,
|
|
3805
|
+
wrapWithEvents
|
|
2028
3806
|
};
|
|
2029
3807
|
//# sourceMappingURL=index.js.map
|