@pompidup/kingdomino-engine 3.1.0 → 3.3.0

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