@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/README.md +155 -2
- package/dist/index.d.ts +701 -4
- package/dist/index.js +520 -132
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
package/dist/index.js
CHANGED
|
@@ -673,73 +673,132 @@ var uuidMethod = () => {
|
|
|
673
673
|
return randomUUID();
|
|
674
674
|
};
|
|
675
675
|
|
|
676
|
-
// src/adapterServerside/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
685
|
+
console.log(formatMessage("info", message));
|
|
700
686
|
}
|
|
701
|
-
return void 0;
|
|
702
687
|
},
|
|
703
688
|
error: (message) => {
|
|
704
689
|
if (isLoggingEnabled) {
|
|
705
|
-
|
|
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
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
720
|
-
constructor(message) {
|
|
721
|
-
super(
|
|
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
|
|
726
|
-
constructor(message) {
|
|
727
|
-
super(
|
|
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
|
|
732
|
-
constructor(message) {
|
|
733
|
-
super(
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
1162
|
+
var calculateDominoPosition = (position, rotation, domino) => {
|
|
1142
1163
|
let firstTile;
|
|
1143
1164
|
let secondTile;
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1181
|
+
break;
|
|
1182
|
+
case 90:
|
|
1161
1183
|
firstTile = {
|
|
1162
|
-
tile: domino.
|
|
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.
|
|
1191
|
+
tile: domino.right,
|
|
1170
1192
|
position: {
|
|
1171
|
-
x: position.x
|
|
1172
|
-
y: position.y
|
|
1193
|
+
x: position.x,
|
|
1194
|
+
y: position.y + 1
|
|
1173
1195
|
}
|
|
1174
1196
|
};
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (rotation === 0) {
|
|
1197
|
+
break;
|
|
1198
|
+
case 180:
|
|
1178
1199
|
firstTile = {
|
|
1179
|
-
tile: domino.
|
|
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.
|
|
1207
|
+
tile: domino.left,
|
|
1187
1208
|
position: {
|
|
1188
|
-
x: position.x,
|
|
1189
|
-
y: position.y
|
|
1209
|
+
x: position.x + 1,
|
|
1210
|
+
y: position.y
|
|
1190
1211
|
}
|
|
1191
1212
|
};
|
|
1192
|
-
|
|
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,
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
(tile) => tile.type
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
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,
|
|
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 ?
|
|
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
|