@pompidup/kingdomino-engine 2.3.0 → 3.1.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
@@ -673,73 +673,132 @@ var uuidMethod = () => {
673
673
  return randomUUID();
674
674
  };
675
675
 
676
- // src/adapterServerside/winstonLogger.ts
677
- import winston from "winston";
678
- var winstonLogger = (isLoggingEnabled) => {
679
- const myFormat = winston.format.printf(
680
- ({ level, message, label, timestamp }) => {
681
- return `${timestamp} [${label}] ${level}: ${message}`;
682
- }
683
- );
684
- const logger = winston.createLogger({
685
- level: "info",
686
- format: winston.format.combine(
687
- winston.format.label({ label: "KDomino_Engine" }),
688
- winston.format.timestamp(),
689
- winston.format.simple(),
690
- winston.format.json(),
691
- winston.format.colorize(),
692
- myFormat
693
- ),
694
- transports: [new winston.transports.Console()]
695
- });
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
+ };
696
682
  return {
697
683
  info: (message) => {
698
684
  if (isLoggingEnabled) {
699
- logger.info(message);
685
+ console.log(formatMessage("info", message));
700
686
  }
701
- return void 0;
702
687
  },
703
688
  error: (message) => {
704
689
  if (isLoggingEnabled) {
705
- logger.error(message);
690
+ console.error(formatMessage("error", message));
706
691
  }
707
- return void 0;
708
692
  }
709
693
  };
710
694
  };
695
+ var silentLogger = {
696
+ info: () => {
697
+ },
698
+ error: () => {
699
+ }
700
+ };
711
701
 
712
702
  // src/core/domain/errors/domainErrors.ts
713
- var InvalidStepError = class extends Error {
714
- constructor(message) {
715
- super(message);
703
+ var ErrorCode = {
704
+ // Game flow errors
705
+ INVALID_STEP: "INVALID_STEP",
706
+ STEP_EXECUTION_FAILED: "STEP_EXECUTION_FAILED",
707
+ ACTION_EXECUTION_FAILED: "ACTION_EXECUTION_FAILED",
708
+ // Entity not found errors
709
+ LORD_NOT_FOUND: "LORD_NOT_FOUND",
710
+ PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND",
711
+ DOMINO_NOT_FOUND: "DOMINO_NOT_FOUND",
712
+ MODE_NOT_FOUND: "MODE_NOT_FOUND",
713
+ // Validation errors
714
+ INVALID_PLAYER_COUNT: "INVALID_PLAYER_COUNT",
715
+ INVALID_PLAYER_NAME: "INVALID_PLAYER_NAME",
716
+ INVALID_PLACEMENT: "INVALID_PLACEMENT",
717
+ PLACEMENT_NOT_EMPTY: "PLACEMENT_NOT_EMPTY",
718
+ PLACEMENT_NOT_ADJACENT: "PLACEMENT_NOT_ADJACENT",
719
+ PLACEMENT_INVALID_TERRAIN: "PLACEMENT_INVALID_TERRAIN",
720
+ PLACEMENT_OUT_OF_BOUNDS: "PLACEMENT_OUT_OF_BOUNDS",
721
+ // Action errors
722
+ NOT_YOUR_TURN: "NOT_YOUR_TURN",
723
+ CANNOT_PICK: "CANNOT_PICK",
724
+ CANNOT_PLACE: "CANNOT_PLACE",
725
+ DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED"
726
+ };
727
+ var createDomainError = (code, message, context) => ({
728
+ code,
729
+ message,
730
+ context
731
+ });
732
+ var DomainException = class extends Error {
733
+ code;
734
+ context;
735
+ constructor(error) {
736
+ super(error.message);
737
+ this.name = "DomainException";
738
+ this.code = error.code;
739
+ this.context = error.context;
740
+ }
741
+ };
742
+ var InvalidStepError = class extends DomainException {
743
+ constructor(message, context) {
744
+ super({
745
+ code: ErrorCode.INVALID_STEP,
746
+ message,
747
+ context
748
+ });
716
749
  this.name = "InvalidStepError";
717
750
  }
718
751
  };
719
- var StepExecutionError = class extends Error {
720
- constructor(message) {
721
- super(message);
752
+ var StepExecutionError = class extends DomainException {
753
+ constructor(message, context) {
754
+ super({
755
+ code: ErrorCode.STEP_EXECUTION_FAILED,
756
+ message,
757
+ context
758
+ });
722
759
  this.name = "StepExecutionError";
723
760
  }
724
761
  };
725
- var ActionExecutionError = class extends Error {
726
- constructor(message) {
727
- super(message);
762
+ var ActionExecutionError = class extends DomainException {
763
+ constructor(message, code = ErrorCode.ACTION_EXECUTION_FAILED, context) {
764
+ super({
765
+ code,
766
+ message,
767
+ context
768
+ });
728
769
  this.name = "ActionExecutionError";
729
770
  }
730
771
  };
731
- var NotFoundError = class extends Error {
732
- constructor(message) {
733
- super(message);
772
+ var NotFoundError = class extends DomainException {
773
+ constructor(message, code = ErrorCode.LORD_NOT_FOUND, context) {
774
+ super({
775
+ code,
776
+ message,
777
+ context
778
+ });
734
779
  this.name = "NotFoundError";
735
780
  }
736
781
  };
782
+ var ValidationError = class extends DomainException {
783
+ constructor(message, code, context) {
784
+ super({
785
+ code,
786
+ message,
787
+ context
788
+ });
789
+ this.name = "ValidationError";
790
+ }
791
+ };
737
792
 
738
793
  // src/core/domain/types/game.ts
739
794
  var gameSteps = {
795
+ /** Waiting for players to be added */
740
796
  addPlayers: "addPlayers",
797
+ /** Optional rules selection phase */
741
798
  options: "options",
799
+ /** Ready to start the game */
742
800
  start: "start",
801
+ /** Game has ended, results available */
743
802
  result: "result"
744
803
  };
745
804
  var isGameWithNextAction = (game) => {
@@ -754,6 +813,12 @@ var ok = (value) => ({ tag: "Ok", value });
754
813
  var err = (error) => ({ tag: "Err", error });
755
814
  var isOk = (result) => result.tag === "Ok";
756
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
+ };
757
822
 
758
823
  // src/application/handlers/addExtraRulesHandler.ts
759
824
  var addExtraRulesHandler = (logger, useCase) => (command) => {
@@ -952,11 +1017,11 @@ var getResultHandler = (logger, useCases) => (command) => {
952
1017
 
953
1018
  // src/application/handlers/placeDominoHandler.ts
954
1019
  var placeDominoHandler = (logger, useCase) => (command) => {
955
- const { game, lordId, position, orientation, rotation } = command;
1020
+ const { game, lordId, position, rotation } = command;
956
1021
  logger.info(
957
1022
  `Placing domino: ${lordId} in position: ${JSON.stringify(
958
1023
  position
959
- )} with orientation: ${orientation} and rotation: ${rotation} in game: ${game.id}`
1024
+ )} with rotation: ${rotation} in game: ${game.id}`
960
1025
  );
961
1026
  if (!isGameWithNextAction(game)) {
962
1027
  logger.error(
@@ -972,7 +1037,7 @@ var placeDominoHandler = (logger, useCase) => (command) => {
972
1037
  );
973
1038
  throw new InvalidStepError("Required game with placeDomino step");
974
1039
  }
975
- const result = useCase(game, lordId, position, orientation, rotation);
1040
+ const result = useCase(game, lordId, position, rotation);
976
1041
  if (isErr(result)) {
977
1042
  logger.error(`Error placing domino: ${result.error}`);
978
1043
  throw new ActionExecutionError(result.error);
@@ -981,14 +1046,14 @@ var placeDominoHandler = (logger, useCase) => (command) => {
981
1046
  logger.info(
982
1047
  `Domino placed: ${lordId} in position: ${JSON.stringify(
983
1048
  position
984
- )} with orientation: ${orientation} and rotation: ${rotation} in game: ${game.id}`
1049
+ )} with rotation: ${rotation} in game: ${game.id}`
985
1050
  );
986
1051
  return result.value;
987
1052
  }
988
1053
  logger.info(
989
1054
  `Domino placed: ${lordId} in position: ${JSON.stringify(
990
1055
  position
991
- )} with orientation: ${orientation} and rotation: ${rotation} in game: ${game.id}`
1056
+ )} with rotation: ${rotation} in game: ${game.id}`
992
1057
  );
993
1058
  return result.value;
994
1059
  };
@@ -1049,57 +1114,14 @@ var GROUND = [
1049
1114
 
1050
1115
  // src/core/domain/types/player.ts
1051
1116
  var playerActions = {
1117
+ /** Place a previously picked domino on the kingdom */
1052
1118
  placeDomino: "placeDomino",
1119
+ /** Pick a domino from the revealed set */
1053
1120
  pickDomino: "pickDomino",
1121
+ /** Pass/discard when no valid placement exists */
1054
1122
  pass: "pass"
1055
1123
  };
1056
1124
 
1057
- // src/core/domain/entities/rule.ts
1058
- var getAvailablesExtraRules = (extraRules, mode, numberOfPlayers) => {
1059
- const availableExtraRules = extraRules.filter((rule) => {
1060
- return rule.mode.filter((m) => m.name === mode).length > 0 && (!rule.playersLimit || rule.playersLimit === numberOfPlayers);
1061
- });
1062
- if (availableExtraRules.length === 0) {
1063
- return err("No extra rules found");
1064
- }
1065
- return ok(availableExtraRules);
1066
- };
1067
- var toExtraRule = (rule, extraRules) => {
1068
- const foundRule = extraRules.find((r) => r.name === rule);
1069
- if (!foundRule) {
1070
- return err(`Rule not found: ${rule}`);
1071
- }
1072
- return ok(foundRule);
1073
- };
1074
-
1075
- // src/core/useCases/addExtraRules.ts
1076
- var addExtraRulesUseCase = (deps) => (game, rules) => {
1077
- const { ruleRepository } = deps;
1078
- const availableExtraRules = ruleRepository.getAllExtra();
1079
- const newExtraRules = [];
1080
- for (const rule of rules) {
1081
- const result = toExtraRule(rule, availableExtraRules);
1082
- if (isErr(result)) {
1083
- return result;
1084
- }
1085
- newExtraRules.push(result.value);
1086
- }
1087
- const updatedRules = {
1088
- basic: game.rules.basic,
1089
- extra: newExtraRules
1090
- };
1091
- const next = {
1092
- type: "step",
1093
- step: gameSteps.start
1094
- };
1095
- const updatedGame = {
1096
- ...game,
1097
- rules: updatedRules,
1098
- nextAction: next
1099
- };
1100
- return ok(updatedGame);
1101
- };
1102
-
1103
1125
  // src/core/domain/entities/domino.ts
1104
1126
  var isGround = (ground) => {
1105
1127
  return GROUND.includes(ground);
@@ -1114,11 +1136,10 @@ var createTile = (ground, crowns = 0) => {
1114
1136
  // src/core/domain/entities/kingdom.ts
1115
1137
  var createEmptyKingdom = () => {
1116
1138
  const emptyTile = createTile("empty");
1117
- const grid = Array.from(
1139
+ return Array.from(
1118
1140
  { length: GRIDSIZE },
1119
1141
  () => Array.from({ length: GRIDSIZE }, () => emptyTile)
1120
1142
  );
1121
- return grid;
1122
1143
  };
1123
1144
  var placeCastle = (kingdom) => {
1124
1145
  const castle = createTile("castle");
@@ -1138,11 +1159,11 @@ var getTile = (kingdom, position) => {
1138
1159
  }
1139
1160
  return ok(tile);
1140
1161
  };
1141
- var calculateDominoPosition = (position, orientation, rotation, domino) => {
1162
+ var calculateDominoPosition = (position, rotation, domino) => {
1142
1163
  let firstTile;
1143
1164
  let secondTile;
1144
- if (orientation === "horizontal") {
1145
- if (rotation === 0) {
1165
+ switch (rotation) {
1166
+ case 0:
1146
1167
  firstTile = {
1147
1168
  tile: domino.left,
1148
1169
  position: {
@@ -1157,39 +1178,40 @@ var calculateDominoPosition = (position, orientation, rotation, domino) => {
1157
1178
  y: position.y
1158
1179
  }
1159
1180
  };
1160
- } else {
1181
+ break;
1182
+ case 90:
1161
1183
  firstTile = {
1162
- tile: domino.right,
1184
+ tile: domino.left,
1163
1185
  position: {
1164
1186
  x: position.x,
1165
1187
  y: position.y
1166
1188
  }
1167
1189
  };
1168
1190
  secondTile = {
1169
- tile: domino.left,
1191
+ tile: domino.right,
1170
1192
  position: {
1171
- x: position.x + 1,
1172
- y: position.y
1193
+ x: position.x,
1194
+ y: position.y + 1
1173
1195
  }
1174
1196
  };
1175
- }
1176
- } else {
1177
- if (rotation === 0) {
1197
+ break;
1198
+ case 180:
1178
1199
  firstTile = {
1179
- tile: domino.left,
1200
+ tile: domino.right,
1180
1201
  position: {
1181
1202
  x: position.x,
1182
1203
  y: position.y
1183
1204
  }
1184
1205
  };
1185
1206
  secondTile = {
1186
- tile: domino.right,
1207
+ tile: domino.left,
1187
1208
  position: {
1188
- x: position.x,
1189
- y: position.y + 1
1209
+ x: position.x + 1,
1210
+ y: position.y
1190
1211
  }
1191
1212
  };
1192
- } else {
1213
+ break;
1214
+ case 270:
1193
1215
  firstTile = {
1194
1216
  tile: domino.right,
1195
1217
  position: {
@@ -1204,14 +1226,13 @@ var calculateDominoPosition = (position, orientation, rotation, domino) => {
1204
1226
  y: position.y + 1
1205
1227
  }
1206
1228
  };
1207
- }
1229
+ break;
1208
1230
  }
1209
1231
  return [firstTile, secondTile];
1210
1232
  };
1211
- var placeDomino = (kingdom, position, orientation, rotation, domino) => {
1233
+ var placeDomino = (kingdom, position, rotation, domino) => {
1212
1234
  const [firstTile, secondTile] = calculateDominoPosition(
1213
1235
  position,
1214
- orientation,
1215
1236
  rotation,
1216
1237
  domino
1217
1238
  );
@@ -1307,32 +1328,207 @@ var hasValidAdjacent = (adjacentTiles, tiles) => {
1307
1328
  });
1308
1329
  };
1309
1330
  var checkCastleIsInMiddle = (kingdom) => {
1310
- const castlePosition = kingdom.map((row) => row.findIndex((tile) => tile.type === "castle")).find((index) => index !== -1);
1311
- const leftTiles = castlePosition !== void 0 ? kingdom[castlePosition].slice(0, castlePosition) : [];
1312
- const leftTilesCount = leftTiles.filter(
1313
- (tile) => tile.type !== "empty"
1314
- ).length;
1315
- const rightTiles = castlePosition !== void 0 ? kingdom[castlePosition].slice(castlePosition + 1) : [];
1316
- const rightTilesCount = rightTiles.filter(
1317
- (tile) => tile.type !== "empty"
1318
- ).length;
1319
- const topTiles = castlePosition !== void 0 ? kingdom[castlePosition].slice(0, castlePosition) : [];
1320
- const topTilesCount = topTiles.filter((tile) => tile.type !== "empty").length;
1321
- const bottomTiles = castlePosition != void 0 ? kingdom[castlePosition].slice(castlePosition + 1) : [];
1322
- const bottomTilesCount = bottomTiles.filter(
1323
- (tile) => tile.type !== "empty"
1324
- ).length;
1331
+ let castleX;
1332
+ let castleY;
1333
+ for (let y = 0; y < kingdom.length; y++) {
1334
+ const x = kingdom[y].findIndex((tile) => tile.type === "castle");
1335
+ if (x !== -1) {
1336
+ castleX = x;
1337
+ castleY = y;
1338
+ break;
1339
+ }
1340
+ }
1341
+ if (castleX === void 0 || castleY === void 0) {
1342
+ return false;
1343
+ }
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++;
1350
+ }
1351
+ }
1352
+ let bottomTilesCount = 0;
1353
+ for (let y = castleY + 1; y < kingdom.length; y++) {
1354
+ if (kingdom[y][castleX].type !== "empty") {
1355
+ bottomTilesCount++;
1356
+ }
1357
+ }
1325
1358
  return leftTilesCount === rightTilesCount && topTilesCount === bottomTilesCount && leftTilesCount === topTilesCount;
1326
1359
  };
1327
1360
  var countDominoes = (kingdom) => {
1328
1361
  const notEmpties = kingdom.flat().filter((tile) => tile.type !== "empty");
1329
- const totalDominoes = (notEmpties.length - 1) / 2;
1330
- return totalDominoes;
1362
+ return (notEmpties.length - 1) / 2;
1331
1363
  };
1332
1364
  var deepCopyKingdom = (kingdom) => {
1333
1365
  return kingdom.map((line) => line.map((tile) => ({ ...tile })));
1334
1366
  };
1335
1367
 
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
+ }
1382
+ }
1383
+ return validPlacements;
1384
+ };
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
+ }
1397
+ }
1398
+ return false;
1399
+ };
1400
+
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
+ };
1409
+ };
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;
1418
+ };
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()
1428
+ };
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);
1461
+ };
1462
+
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);
1479
+ }
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
+ }
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}`);
1500
+ }
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;
1513
+ }
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
+
1336
1532
  // src/core/domain/entities/player.ts
1337
1533
  var MINPLAYERSNAMELENGTH = 3;
1338
1534
  var validatePlayer = (name) => {
@@ -1740,7 +1936,7 @@ var getResultUseCase = (game, scoreResult) => {
1740
1936
  };
1741
1937
 
1742
1938
  // src/core/useCases/placeDomino.ts
1743
- var placeDominoUseCase = (game, lordId, position, orientation, rotation) => {
1939
+ var placeDominoUseCase = (game, lordId, position, rotation) => {
1744
1940
  const nextAction = game.nextAction;
1745
1941
  const currentLord = game.lords.find(
1746
1942
  (lord) => lord.id === nextAction.nextLord
@@ -1764,7 +1960,6 @@ var placeDominoUseCase = (game, lordId, position, orientation, rotation) => {
1764
1960
  const updatedKingdom = placeDomino(
1765
1961
  currentPlayer.kingdom,
1766
1962
  position,
1767
- orientation,
1768
1963
  rotation,
1769
1964
  domino
1770
1965
  );
@@ -1946,7 +2141,7 @@ var configureEngine = (config) => {
1946
2141
  const ruleRepository = jsonRules_default();
1947
2142
  const uuid = config.uuidMethod || uuidMethod;
1948
2143
  const shuffle = config.shuffleMethod || shuffleMethod;
1949
- const logger = config.logging ? winstonLogger(true) : winstonLogger(false);
2144
+ const logger = config.logger ? config.logger : config.logging ? consoleLogger(true) : silentLogger;
1950
2145
  return {
1951
2146
  createGameHandler: createGameHandler(
1952
2147
  logger,
@@ -1987,9 +2182,175 @@ var configureEngine = (config) => {
1987
2182
  getResultUseCase,
1988
2183
  calculateScoreUseCase
1989
2184
  }),
1990
- calculateScoreHandler: calculateScoreHandler(logger, calculateScoreUseCase)
2185
+ calculateScoreHandler: calculateScoreHandler(logger, calculateScoreUseCase),
2186
+ getValidPlacementsHandler: getValidPlacementsHandler(logger),
2187
+ canPlaceDominoHandler: canPlaceDominoHandler(logger),
2188
+ serializeGameHandler: serializeGameHandler(logger),
2189
+ deserializeGameHandler: deserializeGameHandler(logger)
2190
+ };
2191
+ };
2192
+
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);
2222
+ }
2223
+ };
2224
+ }
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);
2244
+ } else {
2245
+ this.listeners.clear();
2246
+ }
2247
+ }
2248
+ };
2249
+ var createEvent = (gameId, type, data) => {
2250
+ return {
2251
+ type,
2252
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2253
+ gameId,
2254
+ ...data
2255
+ };
2256
+ };
2257
+
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"
2298
+ };
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"
2338
+ };
2339
+ var createTranslator = (translations = {}) => {
2340
+ const mergedTranslations = { ...defaultTranslations, ...translations };
2341
+ 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));
2347
+ });
2348
+ }
2349
+ return text;
2350
+ }
1991
2351
  };
1992
2352
  };
2353
+ var defaultTranslator = createTranslator();
1993
2354
 
1994
2355
  // src/index.ts
1995
2356
  var createGameEngine = (config) => {
@@ -2004,7 +2365,11 @@ var createGameEngine = (config) => {
2004
2365
  placeDominoHandler: placeDominoHandler2,
2005
2366
  discardDominoHandler: discardDominoHandler2,
2006
2367
  getResultHandler: getResultHandler2,
2007
- calculateScoreHandler: calculateScoreHandler2
2368
+ calculateScoreHandler: calculateScoreHandler2,
2369
+ getValidPlacementsHandler: getValidPlacementsHandler2,
2370
+ canPlaceDominoHandler: canPlaceDominoHandler2,
2371
+ serializeGameHandler: serializeGameHandler2,
2372
+ deserializeGameHandler: deserializeGameHandler2
2008
2373
  } = configureEngine(config);
2009
2374
  return {
2010
2375
  getModes: (command) => getModesHandler2(command),
@@ -2017,16 +2382,39 @@ var createGameEngine = (config) => {
2017
2382
  placeDomino: (command) => placeDominoHandler2(command),
2018
2383
  discardDomino: (command) => discardDominoHandler2(command),
2019
2384
  getResults: (command) => getResultHandler2(command),
2020
- calculateScore: (command) => calculateScoreHandler2(command)
2385
+ calculateScore: (command) => calculateScoreHandler2(command),
2386
+ getValidPlacements: (command) => getValidPlacementsHandler2(command),
2387
+ canPlaceDomino: (command) => canPlaceDominoHandler2(command),
2388
+ serialize: (command) => serializeGameHandler2(command),
2389
+ deserialize: (command) => deserializeGameHandler2(command)
2021
2390
  };
2022
2391
  };
2023
2392
  export {
2393
+ ActionExecutionError,
2394
+ DomainException,
2395
+ ErrorCode,
2024
2396
  GRIDSIZE,
2025
2397
  GROUND,
2398
+ GameEventEmitter,
2399
+ GameEventType,
2400
+ InvalidStepError,
2401
+ NotFoundError,
2402
+ StepExecutionError,
2403
+ TranslationKey,
2404
+ ValidationError,
2405
+ createDomainError,
2406
+ createEvent,
2026
2407
  createGameEngine,
2408
+ createSavePoint,
2409
+ createTranslator,
2410
+ defaultTranslations,
2411
+ defaultTranslator,
2412
+ deserializeGame,
2027
2413
  gameSteps,
2028
2414
  isGameWithNextAction,
2029
2415
  isGameWithNextStep,
2030
- playerActions
2416
+ playerActions,
2417
+ restoreFromSavePoint,
2418
+ serializeGame
2031
2419
  };
2032
2420
  //# sourceMappingURL=index.js.map