@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/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: 36,
602
- dominoesPerTurn: 3,
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 InvalidStepError = class extends Error {
714
- constructor(message) {
715
- super(message);
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 Error {
720
- constructor(message) {
721
- super(message);
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 Error {
726
- constructor(message) {
727
- super(message);
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 Error {
732
- constructor(message) {
733
- super(message);
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
- // src/core/domain/types/game.ts
739
- var gameSteps = {
740
- addPlayers: "addPlayers",
741
- options: "options",
742
- start: "start",
743
- result: "result"
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 isGameWithNextAction = (game) => {
746
- return game.nextAction.type === "action";
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 isGameWithNextStep = (game) => {
749
- return game.nextAction.type === "step";
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("Required game with nextAction type: 'step'");
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("Required game with options step");
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
- throw new StepExecutionError(result.error);
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("Required game with nextAction type: 'step'");
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("Required game with addPlayers step");
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
- throw new StepExecutionError(result.error);
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
- "Required game with nextAction type: 'action'"
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("Required game with pickDomino action");
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
- throw new ActionExecutionError(result.error);
1362
+ const code = result.error;
1363
+ throw new ActionExecutionError(
1364
+ translateErrorCode(translator, code),
1365
+ code,
1366
+ {
1367
+ gameId: game.id,
1368
+ lordId: command.lordId,
1369
+ dominoPick: command.dominoPick
1370
+ }
1371
+ );
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
- throw new StepExecutionError(result.error);
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
- "Required game with nextAction type: 'action'"
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("Required game with placeDomino action");
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
- throw new ActionExecutionError(result.error);
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
- throw new NotFoundError(result.error);
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
- throw new NotFoundError(result.error);
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("Required game with nextAction type: 'step'");
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("Required game with result step");
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
- throw new StepExecutionError(result2.error);
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
- throw new NotFoundError(result.error);
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
- "Required game with nextAction type: 'action'"
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("Required game with placeDomino step");
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
- throw new ActionExecutionError(result.error);
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("Required game with nextAction type: 'step'");
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
- "Required game with start step or options step"
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
- throw new StepExecutionError(result.error);
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("No extra rules found");
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(`Rule not found: ${rule}`);
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: game.rules.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("Invalid placement (not fit into the grid)");
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("Invalid placement (not adjacent)");
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("Invalid placement (not valid adjacent)");
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("Invalid placement (not empty)");
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 xFirstPosition = firstPosition.x;
1265
- const yFirstPosition = firstPosition.y;
1266
- const xSecondPosition = secondPosition.x;
1267
- const ySecondPosition = secondPosition.y;
1268
- const adjacentTiles = [];
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
- adjacentTilesKeys.splice(
1280
- adjacentTilesKeys.findIndex((pos) => {
1281
- return pos.x === xFirstPosition && pos.y === yFirstPosition;
1282
- }),
1283
- 1
1284
- );
1285
- adjacentTilesKeys.splice(
1286
- adjacentTilesKeys.findIndex((pos) => {
1287
- return pos.x === xSecondPosition && pos.y === ySecondPosition;
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
- const castlePosition = kingdom.map((row) => row.findIndex((tile) => tile.type === "castle")).find((index) => index !== -1);
1310
- const leftTiles = castlePosition !== void 0 ? kingdom[castlePosition].slice(0, castlePosition) : [];
1311
- const leftTilesCount = leftTiles.filter(
1312
- (tile) => tile.type !== "empty"
1313
- ).length;
1314
- const rightTiles = castlePosition !== void 0 ? kingdom[castlePosition].slice(castlePosition + 1) : [];
1315
- const rightTilesCount = rightTiles.filter(
1316
- (tile) => tile.type !== "empty"
1317
- ).length;
1318
- const topTiles = castlePosition !== void 0 ? kingdom[castlePosition].slice(0, castlePosition) : [];
1319
- const topTilesCount = topTiles.filter((tile) => tile.type !== "empty").length;
1320
- const bottomTiles = castlePosition != void 0 ? kingdom[castlePosition].slice(castlePosition + 1) : [];
1321
- const bottomTilesCount = bottomTiles.filter(
1322
- (tile) => tile.type !== "empty"
1323
- ).length;
1324
- return leftTilesCount === rightTilesCount && topTilesCount === bottomTilesCount && leftTilesCount === topTilesCount;
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(`Invalid player name: ${name}`);
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 < 2 || players.length > 4) {
1358
- return err("Invalid number of players");
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(player, id, kingdom);
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("No base rules found");
2140
+ return err(ErrorCode.STEP_EXECUTION_FAILED);
1375
2141
  }
1376
2142
  const dominoes = game.dominoes;
1377
2143
  const { maxDominoes } = basicRules;
1378
- const shuffledDominoes = shuffleMethod2(dominoes);
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.number - b.dominoPicked.number;
2251
+ return [...lords].sort((a, b) => {
2252
+ return (a.dominoPicked?.number ?? 0) - (b.dominoPicked?.number ?? 0);
1422
2253
  });
1423
2254
  };
1424
- var resetLordsActions = (lord) => {
1425
- const lordsCopy = [...lord];
1426
- for (const lord2 of lordsCopy) {
1427
- lord2.turnEnded = false;
1428
- lord2.hasPick = false;
1429
- lord2.hasPlace = false;
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("Lord not found");
2288
+ return err(ErrorCode.LORD_NOT_FOUND);
1459
2289
  }
1460
2290
  if (currentLord.id !== lordId) {
1461
- return err("Not your turn");
2291
+ return err(ErrorCode.NOT_YOUR_TURN);
1462
2292
  }
1463
2293
  if (!canPick(currentLord)) {
1464
- return err("Lord can't pick");
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("Domino not found");
2300
+ return err(ErrorCode.DOMINO_NOT_FOUND);
1471
2301
  }
1472
2302
  if (selectedDomino.picked) {
1473
- return err("Domino already picked");
2303
+ return err(ErrorCode.DOMINO_ALREADY_PICKED);
1474
2304
  }
1475
- const updatedCurrentDominoes = game.currentDominoes.map((domino) => {
1476
- if (domino.domino.number === dominoPick) {
1477
- domino.picked = true;
1478
- domino.lordId = currentLord.id;
1479
- }
1480
- return domino;
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(`Invalid mode: ${mode}`);
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(`No dominoes found for mode ${newMode.value.name}`);
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("Lord not found");
2454
+ return err(ErrorCode.LORD_NOT_FOUND);
1597
2455
  }
1598
2456
  if (currentLord.id !== lordId) {
1599
- return err("Not your turn");
2457
+ return err(ErrorCode.NOT_YOUR_TURN);
1600
2458
  }
1601
2459
  if (!canPass(currentLord)) {
1602
- return err("Lord can't pass");
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("No rules found");
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 kingdom = game.players.find((player) => {
1667
- if (player.id === score.playerId) {
1668
- return player;
1669
- }
1670
- })?.kingdom;
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
- if (a.details.maxPropertiesSize !== b.details.maxPropertiesSize) {
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 && currentScore.details.totalCrowns === previousScore.details.totalCrowns) {
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("Lord not found");
2646
+ return err(ErrorCode.LORD_NOT_FOUND);
1748
2647
  }
1749
2648
  if (currentLord.id !== lordId) {
1750
- return err("Not your turn");
2649
+ return err(ErrorCode.NOT_YOUR_TURN);
1751
2650
  }
1752
2651
  if (!canPlaceAndDominoPickedIsDefined(currentLord)) {
1753
- return err("Lord can't place");
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("Player not found");
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 shuffledLords = shuffleMethod2(newLords);
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 logger = config.logging ? winstonLogger(true) : winstonLogger(false);
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
- addExtraRulesUseCase({ ruleRepository })
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(logger, chooseDominoUseCase),
1981
- placeDominoHandler: placeDominoHandler(logger, placeDominoUseCase),
1982
- discardDominoHandler: discardDominoHandler(logger, discardDominoUseCase),
1983
- getResultHandler: getResultHandler(logger, {
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(logger, calculateScoreUseCase)
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
- return {
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
- playerActions
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