@pompidup/kingdomino-engine 3.1.0 → 3.2.0

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