@pompidup/kingdomino-engine 3.6.0 → 3.7.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.
@@ -161,6 +161,12 @@ var wrapWithDebug = (engine2, options) => {
161
161
  logger,
162
162
  level
163
163
  ),
164
+ getValidDominoPicks: wrapMethod(
165
+ "getValidDominoPicks",
166
+ engine2.getValidDominoPicks,
167
+ logger,
168
+ level
169
+ ),
164
170
  serialize: wrapMethod("serialize", engine2.serialize, logger, level),
165
171
  deserialize: wrapMethod("deserialize", engine2.deserialize, logger, level),
166
172
  getDynastyResults: wrapMethod(
@@ -221,6 +227,12 @@ var wrapWithDebug = (engine2, options) => {
221
227
  logger,
222
228
  level
223
229
  ),
230
+ getValidDragonTargets: wrapMethod(
231
+ "getValidDragonTargets",
232
+ engine2.getValidDragonTargets,
233
+ logger,
234
+ level
235
+ ),
224
236
  getValidCavemanPlacements: wrapMethod(
225
237
  "getValidCavemanPlacements",
226
238
  engine2.getValidCavemanPlacements,
@@ -266,6 +278,23 @@ var wrapWithEvents = (engine2, callbacks) => {
266
278
  callbacks.onGameEnd?.({ game: after });
267
279
  }
268
280
  };
281
+ const emitNeolithicPair = (before, after, lordId) => {
282
+ if (!callbacks.onNeolithicPairChosen) return;
283
+ if (before.currentDominoes.some((d) => d.reservedFor !== void 0)) return;
284
+ const playerId = before.lords.find((l) => l.id === lordId)?.playerId;
285
+ const reserved = after.currentDominoes.filter(
286
+ (d) => d.reservedFor !== void 0
287
+ );
288
+ if (!playerId || reserved.length === 0) return;
289
+ const numbersFor = (own) => reserved.filter((d) => d.reservedFor === playerId === own).map((d) => d.domino.number);
290
+ callbacks.onNeolithicPairChosen({
291
+ game: after,
292
+ lordId,
293
+ playerId,
294
+ dominoNumbers: numbersFor(true),
295
+ opponentDominoNumbers: numbersFor(false)
296
+ });
297
+ };
269
298
  const gameplay = (run, emit) => (cmd) => {
270
299
  const game = run(cmd);
271
300
  emit(game, cmd);
@@ -279,6 +308,7 @@ var wrapWithEvents = (engine2, callbacks) => {
279
308
  calculateScore: (cmd) => engine2.calculateScore(cmd),
280
309
  getValidPlacements: (cmd) => engine2.getValidPlacements(cmd),
281
310
  canPlaceDomino: (cmd) => engine2.canPlaceDomino(cmd),
311
+ getValidDominoPicks: (cmd) => engine2.getValidDominoPicks(cmd),
282
312
  serialize: (cmd) => engine2.serialize(cmd),
283
313
  deserialize: (cmd) => engine2.deserialize(cmd),
284
314
  getResults: (cmd) => engine2.getResults(cmd),
@@ -287,6 +317,7 @@ var wrapWithEvents = (engine2, callbacks) => {
287
317
  getValidKnightPositions: (cmd) => engine2.getValidKnightPositions(cmd),
288
318
  getValidBuildingPositions: (cmd) => engine2.getValidBuildingPositions(cmd),
289
319
  getAffordableBuildings: (cmd) => engine2.getAffordableBuildings(cmd),
320
+ getValidDragonTargets: (cmd) => engine2.getValidDragonTargets(cmd),
290
321
  getValidCavemanPlacements: (cmd) => engine2.getValidCavemanPlacements(cmd),
291
322
  getValidGiantPositions: (cmd) => engine2.getValidGiantPositions(cmd),
292
323
  getValidGiantTargets: (cmd) => engine2.getValidGiantTargets(cmd),
@@ -317,11 +348,14 @@ var wrapWithEvents = (engine2, callbacks) => {
317
348
  // Gameplay methods — specific event, then derived turn / game-end events
318
349
  chooseDomino: gameplay(
319
350
  (cmd) => engine2.chooseDomino(cmd),
320
- (game, cmd) => callbacks.onDominoPicked?.({
321
- game,
322
- lordId: cmd.lordId,
323
- dominoNumber: cmd.dominoPick
324
- })
351
+ (game, cmd) => {
352
+ callbacks.onDominoPicked?.({
353
+ game,
354
+ lordId: cmd.lordId,
355
+ dominoNumber: cmd.dominoPick
356
+ });
357
+ emitNeolithicPair(cmd.game, game, cmd.lordId);
358
+ }
325
359
  ),
326
360
  placeDomino: gameplay(
327
361
  (cmd) => engine2.placeDomino(cmd),
@@ -3854,6 +3888,29 @@ var rules_default = {
3854
3888
  dominoesDiscardedPerTurn: 0
3855
3889
  }
3856
3890
  ],
3891
+ modes: {
3892
+ QueenDomino: [
3893
+ {
3894
+ playersLimit: 2,
3895
+ lordsPerPlayer: 2,
3896
+ maxDominoes: 48,
3897
+ dominoesPerTurn: 4,
3898
+ maxTurns: 12,
3899
+ maxKingdomSize: 7
3900
+ }
3901
+ ],
3902
+ "AgeOfGiants-QueenDomino": [
3903
+ {
3904
+ playersLimit: 2,
3905
+ lordsPerPlayer: 2,
3906
+ maxDominoes: 60,
3907
+ dominoesPerTurn: 5,
3908
+ maxTurns: 12,
3909
+ maxKingdomSize: 7,
3910
+ dominoesDiscardedPerTurn: 1
3911
+ }
3912
+ ]
3913
+ },
3857
3914
  extra: [
3858
3915
  {
3859
3916
  name: "The middle Kingdom",
@@ -3967,6 +4024,24 @@ var jsonRules = () => {
3967
4024
  };
3968
4025
  });
3969
4026
  }
4027
+ const modeRules = rules_default.modes ?? {};
4028
+ for (const [modeName, modeEntries] of Object.entries(modeRules)) {
4029
+ rules.modeBasic ??= {};
4030
+ const byPlayers = {};
4031
+ modeEntries.forEach((rule) => {
4032
+ byPlayers[rule.playersLimit] = {
4033
+ lords: rule.lordsPerPlayer,
4034
+ maxDominoes: rule.maxDominoes,
4035
+ dominoesPerTurn: rule.dominoesPerTurn,
4036
+ maxTurns: rule.maxTurns,
4037
+ maxKingdomSize: rule.maxKingdomSize,
4038
+ ...rule.dominoesDiscardedPerTurn !== void 0 && {
4039
+ dominoesDiscardedPerTurn: rule.dominoesDiscardedPerTurn
4040
+ }
4041
+ };
4042
+ });
4043
+ rules.modeBasic[modeName] = byPlayers;
4044
+ }
3970
4045
  extraRules.forEach((rule) => {
3971
4046
  rules.extraRules.push({
3972
4047
  name: rule.name,
@@ -4026,6 +4101,7 @@ var ErrorCode = {
4026
4101
  CANNOT_PICK: "CANNOT_PICK",
4027
4102
  CANNOT_PLACE: "CANNOT_PLACE",
4028
4103
  DOMINO_ALREADY_PICKED: "DOMINO_ALREADY_PICKED",
4104
+ DOMINO_RESERVED: "DOMINO_RESERVED",
4029
4105
  // History errors
4030
4106
  UNDO_UNAVAILABLE: "UNDO_UNAVAILABLE",
4031
4107
  REDO_UNAVAILABLE: "REDO_UNAVAILABLE",
@@ -4127,6 +4203,7 @@ var TranslationKey = {
4127
4203
  ERROR_CANNOT_PICK: "error.cannotPick",
4128
4204
  ERROR_CANNOT_PLACE: "error.cannotPlace",
4129
4205
  ERROR_DOMINO_ALREADY_PICKED: "error.dominoAlreadyPicked",
4206
+ ERROR_DOMINO_RESERVED: "error.dominoReserved",
4130
4207
  ERROR_INVALID_PLAYER_COUNT: "error.invalidPlayerCount",
4131
4208
  ERROR_INVALID_PLAYER_NAME: "error.invalidPlayerName",
4132
4209
  ERROR_EXTRA_RULE_NOT_AVAILABLE: "error.extraRuleNotAvailable",
@@ -4227,6 +4304,7 @@ var TranslationKey = {
4227
4304
  // hunter-gatherers reuse the CAVEMAN_* keys)
4228
4305
  SCORE_QUEEN: "score.queen",
4229
4306
  SCORE_BUILDING: "score.building",
4307
+ SCORE_WEALTH: "score.wealth",
4230
4308
  SCORE_TOTEM_MAMMOTH: "score.totemMammoth",
4231
4309
  SCORE_TOTEM_FISH: "score.totemFish",
4232
4310
  SCORE_TOTEM_MUSHROOM: "score.totemMushroom",
@@ -4261,6 +4339,7 @@ var defaultTranslations = {
4261
4339
  [TranslationKey.ERROR_CANNOT_PICK]: "Cannot pick a domino now",
4262
4340
  [TranslationKey.ERROR_CANNOT_PLACE]: "Cannot place a domino now",
4263
4341
  [TranslationKey.ERROR_DOMINO_ALREADY_PICKED]: "Domino has already been picked",
4342
+ [TranslationKey.ERROR_DOMINO_RESERVED]: "This domino is reserved for the other player (Neolithic: pairs 1 & 4 and 2 & 3)",
4264
4343
  [TranslationKey.ERROR_INVALID_PLAYER_COUNT]: "Invalid number of players (1-4 required)",
4265
4344
  [TranslationKey.ERROR_INVALID_PLAYER_NAME]: "Player name must be at least 3 characters",
4266
4345
  [TranslationKey.ERROR_EXTRA_RULE_NOT_AVAILABLE]: "Extra rule is not available for this game mode or number of players",
@@ -4359,6 +4438,7 @@ var defaultTranslations = {
4359
4438
  // Score breakdown labels
4360
4439
  [TranslationKey.SCORE_QUEEN]: "Queen",
4361
4440
  [TranslationKey.SCORE_BUILDING]: "Building: {{name}}",
4441
+ [TranslationKey.SCORE_WEALTH]: "Wealth",
4362
4442
  [TranslationKey.SCORE_TOTEM_MAMMOTH]: "Mammoth totem",
4363
4443
  [TranslationKey.SCORE_TOTEM_FISH]: "Fish totem",
4364
4444
  [TranslationKey.SCORE_TOTEM_MUSHROOM]: "Mushroom totem",
@@ -4393,6 +4473,7 @@ var frenchTranslations = {
4393
4473
  [TranslationKey.ERROR_CANNOT_PICK]: "Impossible de choisir un domino maintenant",
4394
4474
  [TranslationKey.ERROR_CANNOT_PLACE]: "Impossible de placer un domino maintenant",
4395
4475
  [TranslationKey.ERROR_DOMINO_ALREADY_PICKED]: "Ce domino a d\xE9j\xE0 \xE9t\xE9 choisi",
4476
+ [TranslationKey.ERROR_DOMINO_RESERVED]: "Ce domino est r\xE9serv\xE9 \xE0 l'autre joueur (N\xE9olithique : paires 1 et 4, 2 et 3)",
4396
4477
  [TranslationKey.ERROR_INVALID_PLAYER_COUNT]: "Nombre de joueurs invalide (1 \xE0 4 requis)",
4397
4478
  [TranslationKey.ERROR_INVALID_PLAYER_NAME]: "Le nom du joueur doit contenir au moins 3 caract\xE8res",
4398
4479
  [TranslationKey.ERROR_EXTRA_RULE_NOT_AVAILABLE]: "Cette r\xE8gle optionnelle n'est pas disponible pour ce mode ou ce nombre de joueurs",
@@ -4491,6 +4572,7 @@ var frenchTranslations = {
4491
4572
  // Score breakdown labels
4492
4573
  [TranslationKey.SCORE_QUEEN]: "Reine",
4493
4574
  [TranslationKey.SCORE_BUILDING]: "B\xE2timent : {{name}}",
4575
+ [TranslationKey.SCORE_WEALTH]: "Richesse",
4494
4576
  [TranslationKey.SCORE_TOTEM_MAMMOTH]: "Totem mammouth",
4495
4577
  [TranslationKey.SCORE_TOTEM_FISH]: "Totem poisson",
4496
4578
  [TranslationKey.SCORE_TOTEM_MUSHROOM]: "Totem champignon",
@@ -4597,6 +4679,7 @@ var errorCodeToTranslationKey = {
4597
4679
  [ErrorCode.CANNOT_PICK]: TranslationKey.ERROR_CANNOT_PICK,
4598
4680
  [ErrorCode.CANNOT_PLACE]: TranslationKey.ERROR_CANNOT_PLACE,
4599
4681
  [ErrorCode.DOMINO_ALREADY_PICKED]: TranslationKey.ERROR_DOMINO_ALREADY_PICKED,
4682
+ [ErrorCode.DOMINO_RESERVED]: TranslationKey.ERROR_DOMINO_RESERVED,
4600
4683
  [ErrorCode.UNDO_UNAVAILABLE]: TranslationKey.ERROR_UNDO_UNAVAILABLE,
4601
4684
  [ErrorCode.REDO_UNAVAILABLE]: TranslationKey.ERROR_REDO_UNAVAILABLE,
4602
4685
  [ErrorCode.CANNOT_PLACE_KNIGHT]: TranslationKey.ERROR_CANNOT_PLACE_KNIGHT,
@@ -5609,7 +5692,7 @@ var addExtraRulesUseCase = (deps) => (game, rules) => {
5609
5692
  }
5610
5693
  let updatedBasic = game.rules.basic;
5611
5694
  let updatedDominoes = game.dominoes;
5612
- const hasLargeKingdomRule = newExtraRules.some(isLargeKingdomExtraRule);
5695
+ const hasLargeKingdomRule = newExtraRules.some(isLargeKingdomExtraRule) && game.rules.basic.maxKingdomSize < 7;
5613
5696
  if (hasLargeKingdomRule) {
5614
5697
  updatedBasic = {
5615
5698
  ...updatedBasic,
@@ -5660,7 +5743,10 @@ var isFootprintDomino = (domino) => (domino.left.hasFootprint ?? false) || (domi
5660
5743
 
5661
5744
  // src/core/domain/entities/economy.ts
5662
5745
  var STARTING_COINS = 7;
5663
- var MAX_KNIGHTS = 3;
5746
+ var STARTING_KNIGHTS = 1;
5747
+ var DRAGON_COST = 1;
5748
+ var QUEEN_DISCOUNT = 1;
5749
+ var COINS_PER_WEALTH_POINT = 3;
5664
5750
  var addCoins = (player, amount) => ({
5665
5751
  ...player,
5666
5752
  coins: (player.coins ?? 0) + amount
@@ -5679,7 +5765,8 @@ var addBonusKnights = (player, amount) => ({
5679
5765
  ...player,
5680
5766
  bonusKnights: (player.bonusKnights ?? 0) + amount
5681
5767
  });
5682
- var getMaxKnights = (player) => MAX_KNIGHTS + (player.bonusKnights ?? 0);
5768
+ var getMaxKnights = (player) => STARTING_KNIGHTS + (player.bonusKnights ?? 0);
5769
+ var getWealthPoints = (player) => Math.floor(Math.max(0, player.coins ?? 0) / COINS_PER_WEALTH_POINT);
5683
5770
 
5684
5771
  // src/core/domain/entities/domino.ts
5685
5772
  var isGround = (ground) => {
@@ -6034,7 +6121,7 @@ var addPlayersUseCase = (deps) => (game, players) => {
6034
6121
  newPlayers.push(newPlayer.value);
6035
6122
  }
6036
6123
  const rules = ruleRepository.getAll();
6037
- const basicRules = isAoG && rules.aogBasic ? rules.aogBasic[newPlayers.length] : rules.basic[newPlayers.length];
6124
+ const basicRules = rules.modeBasic?.[game.mode.name]?.[newPlayers.length] ?? (isAoG && rules.aogBasic ? rules.aogBasic[newPlayers.length] : rules.basic[newPlayers.length]);
6038
6125
  if (!basicRules) {
6039
6126
  return err(ErrorCode.STEP_EXECUTION_FAILED);
6040
6127
  }
@@ -6293,7 +6380,15 @@ var calculateOriginsFireBonus = (kingdom, placedFireTokens) => {
6293
6380
  };
6294
6381
 
6295
6382
  // src/core/domain/entities/queen.ts
6296
- var determineQueenHolder = (players, currentHolderId) => {
6383
+ var determineQueenHolder = (players, currentHolderId, actingPlayerId) => {
6384
+ if (actingPlayerId !== void 0) {
6385
+ const acting = players.find((p) => p.id === actingPlayerId);
6386
+ if (!acting) return currentHolderId;
6387
+ const towers = getTowerCount(acting);
6388
+ const others = players.filter((p) => p.id !== actingPlayerId);
6389
+ const reachesMost = towers > 0 && others.every((p) => getTowerCount(p) <= towers);
6390
+ return reachesMost ? actingPlayerId : currentHolderId;
6391
+ }
6297
6392
  let maxTowers = 0;
6298
6393
  let leaderId = null;
6299
6394
  let isTied = false;
@@ -6805,6 +6900,12 @@ var queendominoBonuses = (game, player, provisional) => {
6805
6900
  provisional
6806
6901
  });
6807
6902
  }
6903
+ bonuses.push({
6904
+ type: "wealth",
6905
+ label: TranslationKey.SCORE_WEALTH,
6906
+ points: getWealthPoints(player),
6907
+ provisional
6908
+ });
6808
6909
  for (const placed of player.buildings ?? []) {
6809
6910
  if (!placed.building.endGameScoring) continue;
6810
6911
  bonuses.push({
@@ -7000,31 +7101,58 @@ var calculateScoreUseCase = (kingdom, giantPositions) => {
7000
7101
  };
7001
7102
 
7002
7103
  // src/core/domain/entities/building.ts
7003
- var BOARD_SLOTS = 4;
7104
+ var BUILDERS_BOARD_SLOT_COSTS = [5, 4, 3, 2, 1, 0];
7105
+ var BOARD_SLOTS = BUILDERS_BOARD_SLOT_COSTS.length;
7004
7106
  var createBuildersBoard = (tiles, shuffle) => {
7005
7107
  const shuffled = shuffle([...tiles]);
7006
7108
  const slots = shuffled.slice(0, BOARD_SLOTS);
7109
+ while (slots.length < BOARD_SLOTS) slots.push(null);
7007
7110
  const drawPile = shuffled.slice(BOARD_SLOTS);
7008
7111
  return {
7009
7112
  slots,
7010
- drawPile
7113
+ drawPile,
7114
+ slotCosts: [...BUILDERS_BOARD_SLOT_COSTS]
7011
7115
  };
7012
7116
  };
7013
7117
  var refillBuildersBoard = (board) => {
7014
- const newSlots = [...board.slots];
7015
7118
  const newDrawPile = [...board.drawPile];
7016
- for (let i = 0; i < newSlots.length; i++) {
7017
- if (newSlots[i] === null && newDrawPile.length > 0) {
7018
- newSlots[i] = newDrawPile.shift();
7119
+ if (!board.slotCosts) {
7120
+ const newSlots2 = [...board.slots];
7121
+ for (let i = 0; i < newSlots2.length; i++) {
7122
+ if (newSlots2[i] === null && newDrawPile.length > 0) {
7123
+ newSlots2[i] = newDrawPile.shift();
7124
+ }
7019
7125
  }
7126
+ return { slots: newSlots2, drawPile: newDrawPile };
7127
+ }
7128
+ const remaining = board.slots.filter(
7129
+ (slot) => slot !== null
7130
+ );
7131
+ const empty = board.slots.length - remaining.length;
7132
+ const newSlots = [
7133
+ ...Array.from({ length: empty }, () => null),
7134
+ ...remaining
7135
+ ];
7136
+ for (let i = empty - 1; i >= 0 && newDrawPile.length > 0; i--) {
7137
+ newSlots[i] = newDrawPile.shift();
7020
7138
  }
7021
- return { slots: newSlots, drawPile: newDrawPile };
7139
+ return { ...board, slots: newSlots, drawPile: newDrawPile };
7022
7140
  };
7023
7141
  var startNewQueenDominoRound = (state) => ({
7024
7142
  ...state,
7025
7143
  buildersBoard: refillBuildersBoard(state.buildersBoard),
7026
7144
  dragonUsedThisRound: false
7027
7145
  });
7146
+ var getSlotCost = (board, slotIndex) => {
7147
+ const building = board.slots[slotIndex];
7148
+ if (!building) return null;
7149
+ return board.slotCosts?.[slotIndex] ?? building.cost;
7150
+ };
7151
+ var getConstructionCost = (board, slotIndex, isQueenHost) => {
7152
+ const cost = getSlotCost(board, slotIndex);
7153
+ if (cost === null) return null;
7154
+ return Math.max(0, cost - (isQueenHost ? QUEEN_DISCOUNT : 0));
7155
+ };
7028
7156
  var findBuildingOnBoard = (board, buildingId) => {
7029
7157
  for (let i = 0; i < board.slots.length; i++) {
7030
7158
  const slot = board.slots[i];
@@ -7039,12 +7167,17 @@ var removeBuildingFromBoard = (board, slotIndex) => {
7039
7167
  newSlots[slotIndex] = null;
7040
7168
  return { ...board, slots: newSlots };
7041
7169
  };
7042
- var purchaseBuilding = (board, player, buildingId) => {
7170
+ var purchaseBuilding = (board, player, buildingId, queenHolderId = null) => {
7043
7171
  const found = findBuildingOnBoard(board, buildingId);
7044
7172
  if (!found) return null;
7045
7173
  const { building, slotIndex } = found;
7046
- if (!canAfford(player, building.cost)) return null;
7047
- let updatedPlayer = removeCoins(player, building.cost);
7174
+ const cost = getConstructionCost(
7175
+ board,
7176
+ slotIndex,
7177
+ queenHolderId !== null && queenHolderId === player.id
7178
+ );
7179
+ if (cost === null || !canAfford(player, cost)) return null;
7180
+ let updatedPlayer = removeCoins(player, cost);
7048
7181
  if (building.immediateBonus) {
7049
7182
  switch (building.immediateBonus.type) {
7050
7183
  case "coins":
@@ -7068,7 +7201,7 @@ var purchaseBuilding = (board, player, buildingId) => {
7068
7201
  updatedPlayer = addTowers(updatedPlayer, building.towers);
7069
7202
  }
7070
7203
  const updatedBoard = removeBuildingFromBoard(board, slotIndex);
7071
- return { updatedBoard, updatedPlayer, building };
7204
+ return { updatedBoard, updatedPlayer, building, cost };
7072
7205
  };
7073
7206
  var isValidConstructionPosition = (kingdom, placedBuildings, position) => {
7074
7207
  const tile = kingdom[position.y]?.[position.x];
@@ -7094,9 +7227,9 @@ var placeBuildingOnKingdom = (kingdom, building, position) => {
7094
7227
  var destroyWithDragon = (board, buildingId, player) => {
7095
7228
  const found = findBuildingOnBoard(board, buildingId);
7096
7229
  if (!found) return null;
7097
- const { building, slotIndex } = found;
7098
- if (!canAfford(player, building.cost)) return null;
7099
- const updatedPlayer = removeCoins(player, building.cost);
7230
+ const { slotIndex } = found;
7231
+ if (!canAfford(player, DRAGON_COST)) return null;
7232
+ const updatedPlayer = removeCoins(player, DRAGON_COST);
7100
7233
  const updatedBoard = removeBuildingFromBoard(board, slotIndex);
7101
7234
  return { updatedBoard, updatedPlayer };
7102
7235
  };
@@ -7215,6 +7348,30 @@ var nextOriginsAction = (currentAction, isTribe) => {
7215
7348
  return sequence[index + 1];
7216
7349
  };
7217
7350
 
7351
+ // src/core/domain/entities/neolithic.ts
7352
+ var NEOLITHIC_RULE_NAME = "Neolithic";
7353
+ var NEOLITHIC_PAIRS = [
7354
+ [1, 4],
7355
+ [2, 3]
7356
+ ];
7357
+ var NEOLITHIC_LINE_SIZE = 4;
7358
+ var isNeolithicGame = (game) => game.players.length === 2 && game.rules.extra.some((rule) => rule.name === NEOLITHIC_RULE_NAME);
7359
+ var neolithicPairOf = (position) => NEOLITHIC_PAIRS.find((pair) => pair.includes(position));
7360
+ var isNeolithicPairChoice = (game, line) => isNeolithicGame(game) && line.length === NEOLITHIC_LINE_SIZE && line.every((domino) => !domino.picked && domino.reservedFor === void 0);
7361
+ var canLordPickRevealed = (lord, domino) => !domino.picked && (domino.reservedFor === void 0 || domino.reservedFor === lord.playerId);
7362
+ var reserveNeolithicPairs = (game, line, lord, pickedNumber) => {
7363
+ if (!isNeolithicPairChoice(game, line)) return line;
7364
+ const picked = line.find((domino) => domino.domino.number === pickedNumber);
7365
+ const pair = picked && neolithicPairOf(picked.position);
7366
+ const opponent = game.players.find((player) => player.id !== lord.playerId);
7367
+ if (!pair || !opponent) return line;
7368
+ return line.map((domino) => ({
7369
+ ...domino,
7370
+ reservedFor: pair.includes(domino.position) ? lord.playerId : opponent.id
7371
+ }));
7372
+ };
7373
+ var pickableDominoNumbers = (line, lord) => line.filter((domino) => canLordPickRevealed(lord, domino)).map((domino) => domino.domino.number);
7374
+
7218
7375
  // src/core/useCases/chooseDomino.ts
7219
7376
  var chooseDominoUseCase = (game, lordId, dominoPick) => {
7220
7377
  const currentLord = game.lords.find(
@@ -7238,7 +7395,15 @@ var chooseDominoUseCase = (game, lordId, dominoPick) => {
7238
7395
  if (selectedDomino.picked) {
7239
7396
  return err(ErrorCode.DOMINO_ALREADY_PICKED);
7240
7397
  }
7241
- const updatedCurrentDominoes = game.currentDominoes.map(
7398
+ if (selectedDomino.reservedFor !== void 0 && selectedDomino.reservedFor !== currentLord.playerId) {
7399
+ return err(ErrorCode.DOMINO_RESERVED);
7400
+ }
7401
+ const updatedCurrentDominoes = reserveNeolithicPairs(
7402
+ game,
7403
+ game.currentDominoes,
7404
+ currentLord,
7405
+ dominoPick
7406
+ ).map(
7242
7407
  (domino) => domino.domino.number === dominoPick ? { ...domino, picked: true, lordId: currentLord.id } : domino
7243
7408
  );
7244
7409
  const isTribe = isTribeMode(game.mode.name);
@@ -7348,7 +7513,8 @@ var constructBuildingUseCase = (game, lordId, buildingId, position) => {
7348
7513
  const purchaseResult = purchaseBuilding(
7349
7514
  queendomino.buildersBoard,
7350
7515
  currentPlayer,
7351
- buildingId
7516
+ buildingId,
7517
+ queendomino.queenHolderId
7352
7518
  );
7353
7519
  if (!purchaseResult) {
7354
7520
  return err(ErrorCode.CANNOT_AFFORD_BUILDING);
@@ -7370,7 +7536,8 @@ var constructBuildingUseCase = (game, lordId, buildingId, position) => {
7370
7536
  );
7371
7537
  const newQueenHolder = determineQueenHolder(
7372
7538
  updatedPlayers,
7373
- queendomino.queenHolderId
7539
+ queendomino.queenHolderId,
7540
+ currentPlayer.id
7374
7541
  );
7375
7542
  const nextAction = nextQueenDominoAction(playerActions.constructBuilding);
7376
7543
  return ok({
@@ -7900,55 +8067,18 @@ var getResultUseCase = (game) => {
7900
8067
  };
7901
8068
 
7902
8069
  // src/core/domain/entities/knight.ts
7903
- var countConstructionSquaresInTerritory = (kingdom, position) => {
7904
- const rows = kingdom.length;
7905
- const cols = kingdom[0]?.length ?? 0;
8070
+ var countTerritorySquares = (kingdom, position) => {
7906
8071
  const startTile = kingdom[position.y]?.[position.x];
7907
8072
  if (!startTile || startTile.type === "empty" || startTile.type === "castle") {
7908
8073
  return 0;
7909
8074
  }
7910
- const terrainType = startTile.type;
7911
- const visited = kingdom.map((row) => row.map(() => false));
7912
- const queue = [[position.x, position.y]];
7913
- visited[position.y][position.x] = true;
7914
- let count = 0;
7915
- const directions = [
7916
- [-1, 0],
7917
- [1, 0],
7918
- [0, -1],
7919
- [0, 1]
7920
- ];
7921
- while (queue.length > 0) {
7922
- const [cx, cy] = queue.pop();
7923
- const tile = kingdom[cy][cx];
7924
- if (tile.type !== "empty" && tile.type !== "castle" && "hasConstructionSquare" in tile && tile.hasConstructionSquare) {
7925
- count++;
7926
- }
7927
- for (const [dx, dy] of directions) {
7928
- const nx = cx + dx;
7929
- const ny = cy + dy;
7930
- if (nx >= 0 && ny >= 0 && nx < cols && ny < rows && !visited[ny][nx] && kingdom[ny][nx].type === terrainType) {
7931
- visited[ny][nx] = true;
7932
- queue.push([nx, ny]);
7933
- }
7934
- }
7935
- }
7936
- return count;
7937
- };
7938
- var getTerritoryTerrain = (kingdom, position) => {
7939
- const tile = kingdom[position.y]?.[position.x];
7940
- if (!tile || tile.type === "empty" || tile.type === "castle") return null;
7941
- return tile.type;
7942
- };
7943
- var territoryHasKnight = (kingdom, position, knights) => {
7944
- const terrain = getTerritoryTerrain(kingdom, position);
7945
- if (!terrain) return false;
7946
8075
  const rows = kingdom.length;
7947
8076
  const cols = kingdom[0]?.length ?? 0;
8077
+ const terrain = startTile.type;
7948
8078
  const visited = kingdom.map((row) => row.map(() => false));
7949
8079
  const queue = [[position.x, position.y]];
7950
8080
  visited[position.y][position.x] = true;
7951
- const territoryPositions = /* @__PURE__ */ new Set();
8081
+ let count = 0;
7952
8082
  const directions = [
7953
8083
  [-1, 0],
7954
8084
  [1, 0],
@@ -7957,7 +8087,7 @@ var territoryHasKnight = (kingdom, position, knights) => {
7957
8087
  ];
7958
8088
  while (queue.length > 0) {
7959
8089
  const [cx, cy] = queue.pop();
7960
- territoryPositions.add(`${cx},${cy}`);
8090
+ count++;
7961
8091
  for (const [dx, dy] of directions) {
7962
8092
  const nx = cx + dx;
7963
8093
  const ny = cy + dy;
@@ -7967,10 +8097,9 @@ var territoryHasKnight = (kingdom, position, knights) => {
7967
8097
  }
7968
8098
  }
7969
8099
  }
7970
- return knights.some(
7971
- (k) => territoryPositions.has(`${k.position.x},${k.position.y}`)
7972
- );
8100
+ return count;
7973
8101
  };
8102
+ var calculateKnightTax = (kingdom, position) => countTerritorySquares(kingdom, position);
7974
8103
  var canPlaceKnight = (player, kingdom, position, allowedPositions) => {
7975
8104
  const knights = player.knights ?? [];
7976
8105
  if (knights.length >= getMaxKnights(player)) return false;
@@ -7979,13 +8108,17 @@ var canPlaceKnight = (player, kingdom, position, allowedPositions) => {
7979
8108
  }
7980
8109
  const tile = kingdom[position.y]?.[position.x];
7981
8110
  if (!tile || tile.type === "empty" || tile.type === "castle") return false;
7982
- if (territoryHasKnight(kingdom, position, knights)) return false;
8111
+ if (knights.some(
8112
+ (k) => k.position.x === position.x && k.position.y === position.y
8113
+ )) {
8114
+ return false;
8115
+ }
7983
8116
  return true;
7984
8117
  };
7985
8118
  var placeKnight = (player, kingdom, position) => {
7986
8119
  const knight = { playerId: player.id, position };
7987
8120
  const knights = [...player.knights ?? [], knight];
7988
- const taxAmount = countConstructionSquaresInTerritory(kingdom, position);
8121
+ const taxAmount = calculateKnightTax(kingdom, position);
7989
8122
  const updatedPlayer = addCoins({ ...player, knights }, taxAmount);
7990
8123
  return updatedPlayer;
7991
8124
  };
@@ -8321,7 +8454,15 @@ var placeKnightUseCase = (game, lordId, position) => {
8321
8454
  const updatedPlayers = game.players.map(
8322
8455
  (p) => p.id === currentPlayer.id ? updatedPlayer : p
8323
8456
  );
8324
- const nextAction = nextQueenDominoAction(playerActions.placeKnight);
8457
+ const canPlaceAnother = allowedPositions.some(
8458
+ (square) => canPlaceKnight(
8459
+ updatedPlayer,
8460
+ updatedPlayer.kingdom,
8461
+ square,
8462
+ allowedPositions
8463
+ )
8464
+ );
8465
+ const nextAction = canPlaceAnother ? playerActions.placeKnight : nextQueenDominoAction(playerActions.placeKnight);
8325
8466
  return ok({
8326
8467
  ...game,
8327
8468
  players: updatedPlayers,
@@ -8529,6 +8670,74 @@ var advanceToNextLordOrResult2 = (game) => {
8529
8670
  });
8530
8671
  };
8531
8672
 
8673
+ // src/core/useCases/useDragon.ts
8674
+ var resolveDragon = (game, lordId) => {
8675
+ const currentAction = game.nextAction;
8676
+ if (currentAction.nextLord !== lordId) {
8677
+ return err(ErrorCode.NOT_YOUR_TURN);
8678
+ }
8679
+ if (currentAction.nextAction !== playerActions.useDragon) {
8680
+ return err(ErrorCode.INVALID_OPTIONAL_ACTION);
8681
+ }
8682
+ if (!game.queendomino) {
8683
+ return err(ErrorCode.INVALID_OPTIONAL_ACTION);
8684
+ }
8685
+ if (!game.queendomino.dragonAvailable || game.queendomino.dragonUsedThisRound) {
8686
+ return err(ErrorCode.DRAGON_UNAVAILABLE);
8687
+ }
8688
+ const currentLord = game.lords.find((l) => l.id === lordId);
8689
+ if (!currentLord) return err(ErrorCode.LORD_NOT_FOUND);
8690
+ const currentPlayer = game.players.find((p) => p.id === currentLord.playerId);
8691
+ if (!currentPlayer) return err(ErrorCode.PLAYER_NOT_FOUND);
8692
+ if (game.queendomino.queenHolderId === currentPlayer.id) {
8693
+ return err(ErrorCode.QUEEN_HOLDER_CANNOT_USE_DRAGON);
8694
+ }
8695
+ return ok({ player: currentPlayer, queendomino: game.queendomino });
8696
+ };
8697
+ var useDragonUseCase = (game, lordId, buildingId) => {
8698
+ const context = resolveDragon(game, lordId);
8699
+ if (isErr(context)) {
8700
+ return context;
8701
+ }
8702
+ const { player: currentPlayer, queendomino } = context.value;
8703
+ const result = destroyWithDragon(
8704
+ queendomino.buildersBoard,
8705
+ buildingId,
8706
+ currentPlayer
8707
+ );
8708
+ if (!result) {
8709
+ return err(ErrorCode.BUILDING_NOT_FOUND);
8710
+ }
8711
+ const { updatedBoard, updatedPlayer } = result;
8712
+ const updatedPlayers = game.players.map(
8713
+ (p) => p.id === currentPlayer.id ? updatedPlayer : p
8714
+ );
8715
+ const maxTurns = game.rules.basic.maxTurns;
8716
+ const isLastTurn = game.turn === maxTurns;
8717
+ if (isLastTurn && allLordsHavePlayed(game.lords)) {
8718
+ return ok({
8719
+ ...game,
8720
+ players: updatedPlayers,
8721
+ queendomino: {
8722
+ ...queendomino,
8723
+ buildersBoard: updatedBoard,
8724
+ dragonUsedThisRound: true
8725
+ },
8726
+ nextAction: { type: "step", step: gameSteps.result }
8727
+ });
8728
+ }
8729
+ return ok({
8730
+ ...game,
8731
+ players: updatedPlayers,
8732
+ queendomino: {
8733
+ ...queendomino,
8734
+ buildersBoard: updatedBoard,
8735
+ dragonUsedThisRound: true
8736
+ },
8737
+ nextAction: nextLordWithAction(game.lords)
8738
+ });
8739
+ };
8740
+
8532
8741
  // src/core/useCases/getValidActionTargets.ts
8533
8742
  var turnFor = (game, action, lordId) => {
8534
8743
  if (!isGameWithNextAction(game) || game.nextAction.nextAction !== action) {
@@ -8579,13 +8788,24 @@ var getConstructionOptions = (game, lordId) => {
8579
8788
  )
8580
8789
  );
8581
8790
  const buildings = board.slots.filter(
8582
- (slot) => slot !== null && purchaseBuilding(board, player, slot.id) !== null
8791
+ (slot) => slot !== null && purchaseBuilding(board, player, slot.id, queendomino.queenHolderId) !== null
8583
8792
  );
8584
8793
  if (positions.length === 0 || buildings.length === 0) return noConstruction();
8585
8794
  return { positions, buildings };
8586
8795
  };
8587
8796
  var getValidBuildingPositionsUseCase = (game, lordId) => getConstructionOptions(game, lordId).positions;
8588
8797
  var getAffordableBuildingsUseCase = (game, lordId) => getConstructionOptions(game, lordId).buildings;
8798
+ var getValidDragonTargetsUseCase = (game, lordId) => {
8799
+ const turn = turnFor(game, playerActions.useDragon, lordId);
8800
+ if (!turn) return [];
8801
+ const context = resolveDragon(turn.game, turn.lordId);
8802
+ if (isErr(context)) return [];
8803
+ const { player, queendomino } = context.value;
8804
+ const board = queendomino.buildersBoard;
8805
+ return board.slots.filter(
8806
+ (slot) => slot !== null && destroyWithDragon(board, slot.id, player) !== null
8807
+ );
8808
+ };
8589
8809
  var noRecruitment = () => ({
8590
8810
  options: [],
8591
8811
  positions: []
@@ -8665,6 +8885,18 @@ var getValidGiantTargetsUseCase = (game, lordId) => {
8665
8885
  };
8666
8886
  var canSkipOptionalActionUseCase = (game, lordId) => isGameWithNextAction(game) && isOk(resolveSkip(game, lordId ?? game.nextAction.nextLord));
8667
8887
 
8888
+ // src/core/useCases/getValidDominoPicks.ts
8889
+ var getValidDominoPicksUseCase = (game, lordId) => {
8890
+ if (!isGameWithNextAction(game) || game.nextAction.nextAction !== playerActions.pickDomino) {
8891
+ return [];
8892
+ }
8893
+ const id = lordId ?? game.nextAction.nextLord;
8894
+ if (id !== game.nextAction.nextLord) return [];
8895
+ const lord = game.lords.find((l) => l.id === id);
8896
+ if (!lord || !canPick(lord)) return [];
8897
+ return pickableDominoNumbers(game.currentDominoes, lord);
8898
+ };
8899
+
8668
8900
  // src/core/useCases/placeDomino.ts
8669
8901
  var findVolcanoInDomino = (game, position, rotation) => {
8670
8902
  const lord = game.lords.find((l) => l.id === game.nextAction.nextLord);
@@ -8973,66 +9205,6 @@ var startGameUseCase = (deps) => (game) => {
8973
9205
  return ok(newState);
8974
9206
  };
8975
9207
 
8976
- // src/core/useCases/useDragon.ts
8977
- var useDragonUseCase = (game, lordId, buildingId) => {
8978
- const currentAction = game.nextAction;
8979
- if (currentAction.nextLord !== lordId) {
8980
- return err(ErrorCode.NOT_YOUR_TURN);
8981
- }
8982
- if (currentAction.nextAction !== playerActions.useDragon) {
8983
- return err(ErrorCode.INVALID_OPTIONAL_ACTION);
8984
- }
8985
- if (!game.queendomino) {
8986
- return err(ErrorCode.INVALID_OPTIONAL_ACTION);
8987
- }
8988
- if (!game.queendomino.dragonAvailable || game.queendomino.dragonUsedThisRound) {
8989
- return err(ErrorCode.DRAGON_UNAVAILABLE);
8990
- }
8991
- const currentLord = game.lords.find((l) => l.id === lordId);
8992
- if (!currentLord) return err(ErrorCode.LORD_NOT_FOUND);
8993
- const currentPlayer = game.players.find((p) => p.id === currentLord.playerId);
8994
- if (!currentPlayer) return err(ErrorCode.PLAYER_NOT_FOUND);
8995
- if (game.queendomino.queenHolderId === currentPlayer.id) {
8996
- return err(ErrorCode.QUEEN_HOLDER_CANNOT_USE_DRAGON);
8997
- }
8998
- const result = destroyWithDragon(
8999
- game.queendomino.buildersBoard,
9000
- buildingId,
9001
- currentPlayer
9002
- );
9003
- if (!result) {
9004
- return err(ErrorCode.BUILDING_NOT_FOUND);
9005
- }
9006
- const { updatedBoard, updatedPlayer } = result;
9007
- const updatedPlayers = game.players.map(
9008
- (p) => p.id === currentPlayer.id ? updatedPlayer : p
9009
- );
9010
- const maxTurns = game.rules.basic.maxTurns;
9011
- const isLastTurn = game.turn === maxTurns;
9012
- if (isLastTurn && allLordsHavePlayed(game.lords)) {
9013
- return ok({
9014
- ...game,
9015
- players: updatedPlayers,
9016
- queendomino: {
9017
- ...game.queendomino,
9018
- buildersBoard: updatedBoard,
9019
- dragonUsedThisRound: true
9020
- },
9021
- nextAction: { type: "step", step: gameSteps.result }
9022
- });
9023
- }
9024
- return ok({
9025
- ...game,
9026
- players: updatedPlayers,
9027
- queendomino: {
9028
- ...game.queendomino,
9029
- buildersBoard: updatedBoard,
9030
- dragonUsedThisRound: true
9031
- },
9032
- nextAction: nextLordWithAction(game.lords)
9033
- });
9034
- };
9035
-
9036
9208
  // src/config.ts
9037
9209
  var configureEngine = (config) => {
9038
9210
  const modeRepository = jsonModes_default();
@@ -9125,6 +9297,13 @@ var configureEngine = (config) => {
9125
9297
  translator,
9126
9298
  canPlaceDominoUseCase
9127
9299
  ),
9300
+ getValidDominoPicksHandler: getValidActionTargetsHandler(
9301
+ logger,
9302
+ translator,
9303
+ "getValidDominoPicks",
9304
+ getValidDominoPicksUseCase,
9305
+ (numbers) => `dominoes [${numbers.join(", ")}]`
9306
+ ),
9128
9307
  serializeGameHandler: serializeGameHandler(logger, translator),
9129
9308
  deserializeGameHandler: deserializeGameHandler(logger, translator),
9130
9309
  getDynastyResultHandler: getDynastyResultHandler(
@@ -9188,6 +9367,13 @@ var configureEngine = (config) => {
9188
9367
  getAffordableBuildingsUseCase,
9189
9368
  (buildings) => `${buildings.length} buildings`
9190
9369
  ),
9370
+ getValidDragonTargetsHandler: getValidActionTargetsHandler(
9371
+ logger,
9372
+ translator,
9373
+ "getValidDragonTargets",
9374
+ getValidDragonTargetsUseCase,
9375
+ (buildings) => `${buildings.length} buildings`
9376
+ ),
9191
9377
  getValidCavemanPlacementsHandler: getValidActionTargetsHandler(
9192
9378
  logger,
9193
9379
  translator,
@@ -9758,6 +9944,7 @@ var createGameEngine = (config) => {
9758
9944
  calculateScoreHandler: calculateScoreHandler2,
9759
9945
  getValidPlacementsHandler: getValidPlacementsHandler2,
9760
9946
  canPlaceDominoHandler: canPlaceDominoHandler2,
9947
+ getValidDominoPicksHandler,
9761
9948
  serializeGameHandler: serializeGameHandler2,
9762
9949
  deserializeGameHandler: deserializeGameHandler2,
9763
9950
  getDynastyResultHandler: getDynastyResultHandler2,
@@ -9773,6 +9960,7 @@ var createGameEngine = (config) => {
9773
9960
  getValidKnightPositionsHandler,
9774
9961
  getValidBuildingPositionsHandler,
9775
9962
  getAffordableBuildingsHandler,
9963
+ getValidDragonTargetsHandler,
9776
9964
  getValidCavemanPlacementsHandler,
9777
9965
  getValidGiantPositionsHandler,
9778
9966
  getValidGiantTargetsHandler,
@@ -9793,6 +9981,7 @@ var createGameEngine = (config) => {
9793
9981
  calculateScore: (command) => calculateScoreHandler2(command),
9794
9982
  getValidPlacements: (command) => getValidPlacementsHandler2(command),
9795
9983
  canPlaceDomino: (command) => canPlaceDominoHandler2(command),
9984
+ getValidDominoPicks: (command) => getValidDominoPicksHandler(command),
9796
9985
  serialize: (command) => serializeGameHandler2(command),
9797
9986
  deserialize: (command) => deserializeGameHandler2(command),
9798
9987
  getDynastyResults: (command) => getDynastyResultHandler2(command),
@@ -9808,6 +9997,7 @@ var createGameEngine = (config) => {
9808
9997
  getValidKnightPositions: (command) => getValidKnightPositionsHandler(command),
9809
9998
  getValidBuildingPositions: (command) => getValidBuildingPositionsHandler(command),
9810
9999
  getAffordableBuildings: (command) => getAffordableBuildingsHandler(command),
10000
+ getValidDragonTargets: (command) => getValidDragonTargetsHandler(command),
9811
10001
  getValidCavemanPlacements: (command) => getValidCavemanPlacementsHandler(command),
9812
10002
  getValidGiantPositions: (command) => getValidGiantPositionsHandler(command),
9813
10003
  getValidGiantTargets: (command) => getValidGiantTargetsHandler(command),