@dimcool/sdk 0.1.30 → 0.1.32

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 CHANGED
@@ -1572,6 +1572,8 @@ const newFlag = await sdk.featureFlags.createFeatureFlag('new-feature', false);
1572
1572
 
1573
1573
  Create a new game lobby.
1574
1574
 
1575
+ > **Note:** Creating a lobby automatically closes and refunds any existing open or queued lobby for this user. You do not need to manually leave or cancel a previous lobby first.
1576
+
1575
1577
  ```typescript
1576
1578
  const lobby = await sdk.lobbies.createLobby('rock-paper-scissors');
1577
1579
  // lobby: { id, gameType, status, creatorId, maxPlayers, players: [...], ... }
@@ -1729,51 +1731,6 @@ const lobby = await sdk.lobbies.cancelQueue('lobby-id');
1729
1731
 
1730
1732
  **Note:** The queue is automatically cancelled if any player leaves the lobby while it's in queue.
1731
1733
 
1732
- #### `playAgain(gameType: string, betAmount: number, escrow: Escrow): Promise<{ lobby: Lobby; unsignedTransaction: string }>`
1733
-
1734
- Create a new lobby, start deposits, and prepare a deposit transaction for playing again with the same bet amount. This is a convenience method that combines lobby creation, deposit initialization, and transaction preparation.
1735
-
1736
- ```typescript
1737
- const { lobby, unsignedTransaction } = await sdk.lobbies.playAgain(
1738
- 'rock-paper-scissors',
1739
- 25,
1740
- sdk.escrow,
1741
- );
1742
- ```
1743
-
1744
- **Parameters:**
1745
-
1746
- - `gameType: string` - The game type to play again (e.g., 'rock-paper-scissors')
1747
- - `betAmount: number` - The bet amount (same as previous game)
1748
- - `escrow: Escrow` - The escrow service instance (from `sdk.escrow`)
1749
-
1750
- **Returns:**
1751
-
1752
- - `Promise<{ lobby: Lobby; unsignedTransaction: string }>` - The created lobby and unsigned transaction (base64 encoded) that needs to be signed
1753
-
1754
- **Behavior:**
1755
-
1756
- 1. Creates a new lobby with the specified game type and bet amount
1757
- 2. Starts the deposit flow (transitions lobby to 'preparing' state)
1758
- 3. Prepares the deposit transaction
1759
- 4. Returns the lobby and unsigned transaction
1760
-
1761
- **Example:**
1762
-
1763
- ```typescript
1764
- // Play again with same bet amount
1765
- const { lobby, unsignedTransaction } = await sdk.lobbies.playAgain(
1766
- 'rock-paper-scissors',
1767
- 25,
1768
- sdk.escrow,
1769
- );
1770
-
1771
- // Deposit and wait for your own deposit to confirm (polls status endpoint)
1772
- const result = await sdk.escrow.depositForLobbySync(lobby.id);
1773
- ```
1774
-
1775
- **Note:** After `playAgain()`, use `depositForLobbySync()` (for agents) or `depositForLobby()` (for React hooks) to complete the deposit flow. The server auto-joins the matchmaking queue when all deposits are confirmed.
1776
-
1777
1734
  ### Admin Methods
1778
1735
 
1779
1736
  #### `getActiveLobbies(): Promise<Lobby[]>` (Admin Only)
@@ -2971,6 +2928,59 @@ const sdk = new SDK({
2971
2928
  These stores are updated by the `WsRouter` and are the single source of truth for
2972
2929
  realtime UI. Room membership is ref-counted and mapped to backend join/leave emits.
2973
2930
 
2931
+ ### `gameActionsStore` Selectors
2932
+
2933
+ Pure functions that extract derived state from `GameActionsStoreState` without any React dependency. Pass them to `useSdkSelector` in React or call them directly in Node.
2934
+
2935
+ #### `selectGameLifecycleState(gameState)`
2936
+
2937
+ Extracts the common lifecycle fields from any `GameStateResponse`. Accepts the state directly so it can be called after a stable `useSdkSelector` call — the selector itself returns the existing store reference, then you project outside it. This avoids creating a new object inside `getSnapshot`, which would break `useSyncExternalStore`'s referential-equality contract.
2938
+
2939
+ ```ts
2940
+ import { selectGameLifecycleState } from '@dimcool/sdk';
2941
+
2942
+ // Correct: selector returns stable store ref, projection happens outside
2943
+ const rawState = useSdkSelector(store, (s) => s.statesByGameId[gameId] ?? null);
2944
+ const lifecycle = selectGameLifecycleState(rawState);
2945
+ // { status, winnerId, betAmount, wonAmount, totalPotMinor, currentPlayerId }
2946
+ ```
2947
+
2948
+ #### `getCountdownDigit(gameId, nowMs)`
2949
+
2950
+ Returns the current pre-game countdown digit (`3 | 2 | 1`) or `null` when the buffer has elapsed. Works for all game types (turn-based: reads `bufferEndsAt`; RPS: reads `roundState.selectionEndsAt` during the `starting` phase).
2951
+
2952
+ ```ts
2953
+ const digit = sdk.gameActionsStore.getCountdownDigit(gameId, Date.now());
2954
+ ```
2955
+
2956
+ #### `getChessClockTimes(gameId, nowMs)` / `getTicTacToeClockTimes` / `getConnect4ClockTimes`
2957
+
2958
+ Returns live per-color clock values deducting elapsed time from the current player's budget.
2959
+
2960
+ ```ts
2961
+ const clocks = sdk.gameActionsStore.getChessClockTimes(gameId, Date.now());
2962
+ // { whiteTimeMs, blackTimeMs }
2963
+
2964
+ const tttClocks = sdk.gameActionsStore.getTicTacToeClockTimes(
2965
+ gameId,
2966
+ Date.now(),
2967
+ );
2968
+ // { xTimeMs, oTimeMs }
2969
+
2970
+ const c4Clocks = sdk.gameActionsStore.getConnect4ClockTimes(gameId, Date.now());
2971
+ // { redTimeMs, yellowTimeMs }
2972
+ ```
2973
+
2974
+ #### `getChessCapturedPieces(gameId)`
2975
+
2976
+ Derives captured pieces from the FEN placement string — no external chess library needed.
2977
+
2978
+ ```ts
2979
+ const pieces = sdk.gameActionsStore.getChessCapturedPieces(gameId);
2980
+ // { capturedByWhite: string[], capturedByBlack: string[] }
2981
+ // e.g. capturedByWhite: ['bp', 'bp', 'br'] means white captured 2 black pawns + 1 rook
2982
+ ```
2983
+
2974
2984
  ### Event Bus (low-level)
2975
2985
 
2976
2986
  If you need raw event streams, use `sdk.events.subscribe(eventName, handler)`.
package/dist/index.cjs CHANGED
@@ -35,7 +35,7 @@ var require_money = __commonJS({
35
35
  "../utils/dist/money.js"(exports2) {
36
36
  "use strict";
37
37
  Object.defineProperty(exports2, "__esModule", { value: true });
38
- exports2.SOL_LAMPORTS = exports2.MICRO_UNITS = void 0;
38
+ exports2.SOL_MINT = exports2.SOL_LAMPORTS = exports2.MICRO_UNITS = void 0;
39
39
  exports2.isSolMint = isSolMint;
40
40
  exports2.formatMoneyMinor = formatMoneyMinor2;
41
41
  exports2.toMajor = toMajor2;
@@ -48,12 +48,12 @@ var require_money = __commonJS({
48
48
  exports2.formatMoneyMinorCompact = formatMoneyMinorCompact;
49
49
  exports2.MICRO_UNITS = 1e6;
50
50
  exports2.SOL_LAMPORTS = 1e9;
51
- var SOL_MINT_LABEL = "SOL";
51
+ exports2.SOL_MINT = "SOL";
52
52
  var SOL_SYSTEM_PROGRAM = "11111111111111111111111111111111";
53
53
  function isSolMint(mint) {
54
54
  if (!mint)
55
55
  return false;
56
- return mint === SOL_MINT_LABEL || mint === SOL_SYSTEM_PROGRAM;
56
+ return mint === exports2.SOL_MINT || mint === SOL_SYSTEM_PROGRAM;
57
57
  }
58
58
  function formatMoneyMinor2(amountMinor, fractionDigits = 2) {
59
59
  return (amountMinor / exports2.MICRO_UNITS).toFixed(fractionDigits);
@@ -159,7 +159,7 @@ var require_dist = __commonJS({
159
159
  "../utils/dist/index.js"(exports2) {
160
160
  "use strict";
161
161
  Object.defineProperty(exports2, "__esModule", { value: true });
162
- exports2.formatMoneyMinorCompact = exports2.formatCurrencyPartsForToken = exports2.formatCurrencyParts = exports2.kformatMoney = exports2.kformat = exports2.formatSolMinor = exports2.toSolMajor = exports2.toMajor = exports2.formatMoneyMinor = exports2.isSolMint = exports2.SOL_LAMPORTS = exports2.MICRO_UNITS = void 0;
162
+ exports2.formatMoneyMinorCompact = exports2.formatCurrencyPartsForToken = exports2.formatCurrencyParts = exports2.kformatMoney = exports2.kformat = exports2.formatSolMinor = exports2.toSolMajor = exports2.toMajor = exports2.formatMoneyMinor = exports2.isSolMint = exports2.SOL_MINT = exports2.SOL_LAMPORTS = exports2.MICRO_UNITS = void 0;
163
163
  var money_1 = require_money();
164
164
  Object.defineProperty(exports2, "MICRO_UNITS", { enumerable: true, get: function() {
165
165
  return money_1.MICRO_UNITS;
@@ -167,6 +167,9 @@ var require_dist = __commonJS({
167
167
  Object.defineProperty(exports2, "SOL_LAMPORTS", { enumerable: true, get: function() {
168
168
  return money_1.SOL_LAMPORTS;
169
169
  } });
170
+ Object.defineProperty(exports2, "SOL_MINT", { enumerable: true, get: function() {
171
+ return money_1.SOL_MINT;
172
+ } });
170
173
  Object.defineProperty(exports2, "isSolMint", { enumerable: true, get: function() {
171
174
  return money_1.isSolMint;
172
175
  } });
@@ -209,7 +212,7 @@ __export(index_exports, {
209
212
  ESTIMATED_SOL_FEE_LAMPORTS: () => ESTIMATED_SOL_FEE_LAMPORTS,
210
213
  HttpClient: () => HttpClient,
211
214
  Leaderboards: () => Leaderboards,
212
- MICRO_UNITS: () => import_utils.MICRO_UNITS,
215
+ MICRO_UNITS: () => import_utils2.MICRO_UNITS,
213
216
  MIN_SOL_TRANSFER_AMOUNT: () => MIN_SOL_TRANSFER_AMOUNT,
214
217
  MIN_TRANSFER_AMOUNT: () => MIN_TRANSFER_AMOUNT,
215
218
  Markets: () => Markets,
@@ -220,7 +223,7 @@ __export(index_exports, {
220
223
  SDK: () => SDK,
221
224
  SDK_VERSION: () => SDK_VERSION,
222
225
  SOL_DECIMALS: () => SOL_DECIMALS,
223
- SOL_MINT: () => SOL_MINT,
226
+ SOL_MINT: () => import_utils.SOL_MINT,
224
227
  SharedWorkerTransport: () => SharedWorkerTransport,
225
228
  Spectate: () => Spectate,
226
229
  StandaloneWsTransport: () => StandaloneWsTransport,
@@ -239,10 +242,11 @@ __export(index_exports, {
239
242
  createLogger: () => createLogger,
240
243
  createNotificationsStore: () => createNotificationsStore,
241
244
  createSdkStore: () => createSdkStore,
242
- formatMoneyMinor: () => import_utils.formatMoneyMinor,
245
+ formatMoneyMinor: () => import_utils2.formatMoneyMinor,
243
246
  isRetryableError: () => isRetryableError,
244
247
  logger: () => logger,
245
- toMajor: () => import_utils.toMajor,
248
+ selectGameLifecycleState: () => selectGameLifecycleState,
249
+ toMajor: () => import_utils2.toMajor,
246
250
  withRetry: () => withRetry
247
251
  });
248
252
  module.exports = __toCommonJS(index_exports);
@@ -796,6 +800,9 @@ var Admin = class {
796
800
  this.http = http;
797
801
  this.logger = logger2;
798
802
  }
803
+ async getInternalBots() {
804
+ return this.http.get("/admin/internal-bots");
805
+ }
799
806
  async getUserById(id) {
800
807
  return this.http.get(`/admin/users/${id}`);
801
808
  }
@@ -1153,6 +1160,13 @@ var Lobbies = class {
1153
1160
  setLobbyStore(store) {
1154
1161
  this.lobbyStore = store;
1155
1162
  }
1163
+ /**
1164
+ * Create a new game lobby.
1165
+ *
1166
+ * **Important:** Creating a lobby automatically closes and refunds any
1167
+ * existing open or queued lobby for this user. You do not need to manually
1168
+ * leave or cancel a previous lobby before calling this.
1169
+ */
1156
1170
  async createLobby(gameType, betAmount) {
1157
1171
  return this.http.post("/lobbies", { gameType, betAmount });
1158
1172
  }
@@ -1216,21 +1230,6 @@ var Lobbies = class {
1216
1230
  async deleteLobby(lobbyId) {
1217
1231
  return this.http.delete(`/lobbies/admin/${lobbyId}`);
1218
1232
  }
1219
- /**
1220
- * Play again: Create a new lobby and prepare deposit in one flow.
1221
- * Returns the lobby and unsigned transaction that needs to be signed.
1222
- * @param gameType - The game type to play again
1223
- * @param betAmount - The bet amount (same as previous game)
1224
- * @param escrow - The escrow service instance (from sdk.escrow)
1225
- */
1226
- async playAgain(gameType, betAmount, escrow) {
1227
- const lobby = await this.createLobby(gameType, betAmount);
1228
- const { transaction } = await escrow.prepareAndStartDeposit(lobby.id);
1229
- return {
1230
- lobby,
1231
- unsignedTransaction: transaction
1232
- };
1233
- }
1234
1233
  };
1235
1234
 
1236
1235
  // src/games.ts
@@ -1769,11 +1768,11 @@ var Achievements = class {
1769
1768
 
1770
1769
  // src/wallet.ts
1771
1770
  var import_web32 = require("@solana/web3.js");
1771
+ var import_utils = __toESM(require_dist(), 1);
1772
1772
  var TRANSFER_FEE_MINOR = 1e4;
1773
1773
  var MIN_TRANSFER_AMOUNT = 5e4;
1774
1774
  var MIN_SOL_TRANSFER_AMOUNT = 1e6;
1775
1775
  var SOL_DECIMALS = 9;
1776
- var SOL_MINT = "SOL";
1777
1776
  var ESTIMATED_SOL_FEE_LAMPORTS = 1e4;
1778
1777
  var Wallet = class {
1779
1778
  constructor(http, logger2) {
@@ -3463,6 +3462,19 @@ function createGameStore(transport) {
3463
3462
  }
3464
3463
 
3465
3464
  // src/stores/game-actions-store.ts
3465
+ function selectGameLifecycleState(gameState) {
3466
+ const s = gameState;
3467
+ if (!s) return null;
3468
+ const raw = s;
3469
+ return {
3470
+ status: raw.status,
3471
+ winnerId: raw.winnerId ?? null,
3472
+ betAmount: raw.betAmount ?? 0,
3473
+ wonAmount: raw.wonAmount ?? null,
3474
+ totalPotMinor: raw.totalPotMinor,
3475
+ currentPlayerId: raw.currentPlayerId ?? null
3476
+ };
3477
+ }
3466
3478
  var isRpsCompletionPayload = (payload) => payload.gameType === "rock-paper-scissors";
3467
3479
  function createGameActionsStore(transport) {
3468
3480
  const store = createSdkStore({
@@ -3484,8 +3496,21 @@ function createGameActionsStore(transport) {
3484
3496
  });
3485
3497
  };
3486
3498
  const isNonRpsState = (state) => Boolean(state && !isRpsState(state));
3499
+ const pendingEvents = /* @__PURE__ */ new Map();
3500
+ function enqueue(gameId, event) {
3501
+ const q = pendingEvents.get(gameId) ?? [];
3502
+ q.push(event);
3503
+ pendingEvents.set(gameId, q);
3504
+ }
3505
+ function drainQueue(gameId) {
3506
+ const q = pendingEvents.get(gameId);
3507
+ if (!q?.length) return;
3508
+ pendingEvents.delete(gameId);
3509
+ for (const ev of q) applyWsEvent(ev);
3510
+ }
3487
3511
  const setBaseState = (gameId, state) => {
3488
3512
  updateState(gameId, state);
3513
+ drainQueue(gameId);
3489
3514
  };
3490
3515
  const clearState = (gameId) => {
3491
3516
  store.updateState((state) => {
@@ -3495,6 +3520,7 @@ function createGameActionsStore(transport) {
3495
3520
  const { [gameId]: _, ...rest } = state.statesByGameId;
3496
3521
  return { ...state, statesByGameId: rest };
3497
3522
  });
3523
+ pendingEvents.delete(gameId);
3498
3524
  };
3499
3525
  const applyWsEvent = (event) => {
3500
3526
  switch (event.event) {
@@ -3513,7 +3539,10 @@ function createGameActionsStore(transport) {
3513
3539
  }
3514
3540
  case "game:rps:starting": {
3515
3541
  const current = store.getState().statesByGameId[event.payload.gameId];
3516
- if (!current || !isRpsState(current)) return;
3542
+ if (!current || !isRpsState(current)) {
3543
+ enqueue(event.payload.gameId, event);
3544
+ return;
3545
+ }
3517
3546
  const betAmount = typeof event.payload.betAmount === "number" ? event.payload.betAmount : void 0;
3518
3547
  const startedAt = typeof event.payload.startedAt === "string" ? event.payload.startedAt : current.roundState.startedAt;
3519
3548
  const bufferEndsAt = typeof event.payload.bufferEndsAt === "string" ? event.payload.bufferEndsAt : current.roundState.selectionEndsAt;
@@ -3534,7 +3563,10 @@ function createGameActionsStore(transport) {
3534
3563
  }
3535
3564
  case "game:rps:round:started": {
3536
3565
  const current = store.getState().statesByGameId[event.payload.gameId];
3537
- if (!current || !isRpsState(current)) return;
3566
+ if (!current || !isRpsState(current)) {
3567
+ enqueue(event.payload.gameId, event);
3568
+ return;
3569
+ }
3538
3570
  const actions = {};
3539
3571
  const baseUsers = /* @__PURE__ */ new Set();
3540
3572
  Object.keys(current.roundState.actions).forEach(
@@ -3568,7 +3600,10 @@ function createGameActionsStore(transport) {
3568
3600
  }
3569
3601
  case "game:rps:action:received": {
3570
3602
  const current = store.getState().statesByGameId[event.payload.gameId];
3571
- if (!current || !isRpsState(current)) return;
3603
+ if (!current || !isRpsState(current)) {
3604
+ enqueue(event.payload.gameId, event);
3605
+ return;
3606
+ }
3572
3607
  const updated = {
3573
3608
  ...current,
3574
3609
  roundState: {
@@ -3587,7 +3622,10 @@ function createGameActionsStore(transport) {
3587
3622
  }
3588
3623
  case "game:rps:timer:cutoff": {
3589
3624
  const current = store.getState().statesByGameId[event.payload.gameId];
3590
- if (!current || !isRpsState(current)) return;
3625
+ if (!current || !isRpsState(current)) {
3626
+ enqueue(event.payload.gameId, event);
3627
+ return;
3628
+ }
3591
3629
  const updated = {
3592
3630
  ...current,
3593
3631
  roundState: {
@@ -3601,7 +3639,10 @@ function createGameActionsStore(transport) {
3601
3639
  }
3602
3640
  case "game:rps:round:reveal": {
3603
3641
  const current = store.getState().statesByGameId[event.payload.gameId];
3604
- if (!current || !isRpsState(current)) return;
3642
+ if (!current || !isRpsState(current)) {
3643
+ enqueue(event.payload.gameId, event);
3644
+ return;
3645
+ }
3605
3646
  const actions = {};
3606
3647
  const payloadActions = event.payload.actions;
3607
3648
  Object.keys(payloadActions || {}).forEach((userId) => {
@@ -3625,7 +3666,10 @@ function createGameActionsStore(transport) {
3625
3666
  }
3626
3667
  case "game:rps:round:completed": {
3627
3668
  const current = store.getState().statesByGameId[event.payload.gameId];
3628
- if (!current || !isRpsState(current)) return;
3669
+ if (!current || !isRpsState(current)) {
3670
+ enqueue(event.payload.gameId, event);
3671
+ return;
3672
+ }
3629
3673
  const roundHistory = [
3630
3674
  ...current.roundHistory || [],
3631
3675
  {
@@ -3650,7 +3694,10 @@ function createGameActionsStore(transport) {
3650
3694
  }
3651
3695
  case "game:rps:timeout": {
3652
3696
  const current = store.getState().statesByGameId[event.payload.gameId];
3653
- if (!current || !isRpsState(current)) return;
3697
+ if (!current || !isRpsState(current)) {
3698
+ enqueue(event.payload.gameId, event);
3699
+ return;
3700
+ }
3654
3701
  const timedOutUser = event.payload.playerId;
3655
3702
  const action = event.payload.action;
3656
3703
  const updated = {
@@ -3675,7 +3722,10 @@ function createGameActionsStore(transport) {
3675
3722
  const payload = event.payload;
3676
3723
  const { gameId } = payload;
3677
3724
  const current = store.getState().statesByGameId[gameId];
3678
- if (!current) return;
3725
+ if (!current) {
3726
+ enqueue(gameId, event);
3727
+ return;
3728
+ }
3679
3729
  const updated = isRpsCompletionPayload(payload) && isRpsState(current) ? {
3680
3730
  ...current,
3681
3731
  status: "completed",
@@ -3724,11 +3774,15 @@ function createGameActionsStore(transport) {
3724
3774
  const current = store.getState().statesByGameId[gameId];
3725
3775
  const updated = current ? { ...current, ...incoming } : incoming;
3726
3776
  updateState(gameId, updated);
3777
+ drainQueue(gameId);
3727
3778
  break;
3728
3779
  }
3729
3780
  case "game:rematch:requested": {
3730
3781
  const current = store.getState().statesByGameId[event.payload.gameId];
3731
- if (!current) return;
3782
+ if (!current) {
3783
+ enqueue(event.payload.gameId, event);
3784
+ return;
3785
+ }
3732
3786
  const requestedBy = event.payload.requestedBy;
3733
3787
  const userId = event.payload.userId;
3734
3788
  const requested = new Set(
@@ -3744,7 +3798,10 @@ function createGameActionsStore(transport) {
3744
3798
  }
3745
3799
  case "game:rematch:cancelled": {
3746
3800
  const current = store.getState().statesByGameId[event.payload.gameId];
3747
- if (!current) return;
3801
+ if (!current) {
3802
+ enqueue(event.payload.gameId, event);
3803
+ return;
3804
+ }
3748
3805
  const requestedBy = event.payload.requestedBy ?? [];
3749
3806
  const updated = {
3750
3807
  ...current,
@@ -3755,7 +3812,10 @@ function createGameActionsStore(transport) {
3755
3812
  }
3756
3813
  case "game:rematch:started": {
3757
3814
  const current = store.getState().statesByGameId[event.payload.gameId];
3758
- if (!current) return;
3815
+ if (!current) {
3816
+ enqueue(event.payload.gameId, event);
3817
+ return;
3818
+ }
3759
3819
  const updated = {
3760
3820
  ...current,
3761
3821
  rematchRequestedBy: event.payload.playerIds ?? []
@@ -3766,7 +3826,10 @@ function createGameActionsStore(transport) {
3766
3826
  case "game:pot:updated": {
3767
3827
  const { gameId, totalPotMinor } = event.payload;
3768
3828
  const current = store.getState().statesByGameId[gameId];
3769
- if (!current) return;
3829
+ if (!current) {
3830
+ enqueue(gameId, event);
3831
+ return;
3832
+ }
3770
3833
  const updated = {
3771
3834
  ...current,
3772
3835
  totalPotMinor
@@ -3779,12 +3842,123 @@ function createGameActionsStore(transport) {
3779
3842
  }
3780
3843
  };
3781
3844
  const joinGame = (gameId) => transport.joinRoom(`game:${gameId}`);
3845
+ const getCountdownDigit = (gameId, nowMs) => {
3846
+ const state = store.getState().statesByGameId[gameId];
3847
+ if (!state) return null;
3848
+ if (isRpsState(state)) {
3849
+ if (state.roundState.phase !== "starting") return null;
3850
+ const remaining = new Date(state.roundState.selectionEndsAt).getTime() - nowMs;
3851
+ if (remaining <= 0) return null;
3852
+ return Math.ceil(remaining / 1e3);
3853
+ }
3854
+ const bufferEndsAt = state.bufferEndsAt;
3855
+ if (bufferEndsAt) {
3856
+ const remaining = new Date(bufferEndsAt).getTime() - nowMs;
3857
+ if (remaining <= 0) return null;
3858
+ return Math.ceil(remaining / 1e3);
3859
+ }
3860
+ return null;
3861
+ };
3862
+ const getChessClockTimes = (gameId, nowMs) => {
3863
+ const state = store.getState().statesByGameId[gameId];
3864
+ if (!state || state.gameType !== "chess") return null;
3865
+ const s = state;
3866
+ let whiteMs = s.whiteTimeMs ?? 0;
3867
+ let blackMs = s.blackTimeMs ?? 0;
3868
+ if (s.status === "active" && s.currentPlayerId) {
3869
+ const startedAt = Date.parse(s.turnStartedAt);
3870
+ if (!Number.isNaN(startedAt)) {
3871
+ const elapsed = Math.max(0, nowMs - startedAt);
3872
+ if (s.currentPlayerId === s.whitePlayerId) {
3873
+ whiteMs = Math.max(0, whiteMs - elapsed);
3874
+ } else if (s.currentPlayerId === s.blackPlayerId) {
3875
+ blackMs = Math.max(0, blackMs - elapsed);
3876
+ }
3877
+ }
3878
+ }
3879
+ return { whiteTimeMs: whiteMs, blackTimeMs: blackMs };
3880
+ };
3881
+ const getChessCapturedPieces = (gameId) => {
3882
+ const state = store.getState().statesByGameId[gameId];
3883
+ if (!state || state.gameType !== "chess") return null;
3884
+ const fen = state.fen;
3885
+ if (!fen) return { capturedByWhite: [], capturedByBlack: [] };
3886
+ const placement = fen.split(" ")[0];
3887
+ const white = {};
3888
+ const black = {};
3889
+ for (const char of placement) {
3890
+ if (char === "/" || char >= "1" && char <= "8") continue;
3891
+ const lower = char.toLowerCase();
3892
+ if (char === lower) {
3893
+ black[lower] = (black[lower] ?? 0) + 1;
3894
+ } else {
3895
+ white[lower] = (white[lower] ?? 0) + 1;
3896
+ }
3897
+ }
3898
+ const INITIAL = {
3899
+ p: 8,
3900
+ r: 2,
3901
+ n: 2,
3902
+ b: 2,
3903
+ q: 1,
3904
+ k: 1
3905
+ };
3906
+ const capturedByWhite = [];
3907
+ const capturedByBlack = [];
3908
+ for (const [type, initial] of Object.entries(INITIAL)) {
3909
+ const missingBlack = initial - (black[type] ?? 0);
3910
+ for (let i = 0; i < missingBlack; i++) capturedByWhite.push(`b${type}`);
3911
+ const missingWhite = initial - (white[type] ?? 0);
3912
+ for (let i = 0; i < missingWhite; i++) capturedByBlack.push(`w${type}`);
3913
+ }
3914
+ return { capturedByWhite, capturedByBlack };
3915
+ };
3916
+ const getTicTacToeClockTimes = (gameId, nowMs) => {
3917
+ const state = store.getState().statesByGameId[gameId];
3918
+ if (!state || state.gameType !== "tic-tac-toe") return null;
3919
+ const s = state;
3920
+ let xMs = s.xTimeMs ?? 0;
3921
+ let oMs = s.oTimeMs ?? 0;
3922
+ if (s.status === "active" && s.currentPlayerId) {
3923
+ const currentMark = s.playerMarks[s.currentPlayerId];
3924
+ const startedAt = Date.parse(s.turnStartedAt);
3925
+ if (!Number.isNaN(startedAt)) {
3926
+ const elapsed = Math.max(0, nowMs - startedAt);
3927
+ if (currentMark === "X") xMs = Math.max(0, xMs - elapsed);
3928
+ else if (currentMark === "O") oMs = Math.max(0, oMs - elapsed);
3929
+ }
3930
+ }
3931
+ return { xTimeMs: xMs, oTimeMs: oMs };
3932
+ };
3933
+ const getConnect4ClockTimes = (gameId, nowMs) => {
3934
+ const state = store.getState().statesByGameId[gameId];
3935
+ if (!state || state.gameType !== "connect-four") return null;
3936
+ const s = state;
3937
+ let redMs = s.redTimeMs ?? 0;
3938
+ let yellowMs = s.yellowTimeMs ?? 0;
3939
+ if (s.status === "active" && s.currentPlayerId) {
3940
+ const currentColor = s.playerColors[s.currentPlayerId];
3941
+ const startedAt = Date.parse(s.turnStartedAt);
3942
+ if (!Number.isNaN(startedAt)) {
3943
+ const elapsed = Math.max(0, nowMs - startedAt);
3944
+ if (currentColor === "RED") redMs = Math.max(0, redMs - elapsed);
3945
+ else if (currentColor === "YELLOW")
3946
+ yellowMs = Math.max(0, yellowMs - elapsed);
3947
+ }
3948
+ }
3949
+ return { redTimeMs: redMs, yellowTimeMs: yellowMs };
3950
+ };
3782
3951
  return {
3783
3952
  store,
3784
3953
  setBaseState,
3785
3954
  clearState,
3786
3955
  applyWsEvent,
3787
- joinGame
3956
+ joinGame,
3957
+ getCountdownDigit,
3958
+ getChessClockTimes,
3959
+ getChessCapturedPieces,
3960
+ getTicTacToeClockTimes,
3961
+ getConnect4ClockTimes
3788
3962
  };
3789
3963
  }
3790
3964
  function isRpsState(state) {
@@ -4454,12 +4628,14 @@ var WsRouter = class {
4454
4628
  }
4455
4629
  const decoded = decodeWsEvent(eventName, payload);
4456
4630
  if (!decoded) return;
4457
- this.deps.lobbyStore.applyWsEvent(decoded);
4458
- this.deps.gameStore.applyWsEvent(decoded);
4459
- this.deps.gameActionsStore.applyWsEvent(decoded);
4460
- this.deps.chatStore.applyWsEvent(decoded);
4461
- this.deps.dmThreadsStore.applyWsEvent(decoded);
4462
- this.deps.notificationsStore.applyWsEvent(decoded);
4631
+ const serverTs = payload !== null && typeof payload === "object" ? payload._serverTs : void 0;
4632
+ const event = serverTs !== void 0 ? { ...decoded, _serverTs: serverTs } : decoded;
4633
+ this.deps.lobbyStore.applyWsEvent(event);
4634
+ this.deps.gameStore.applyWsEvent(event);
4635
+ this.deps.gameActionsStore.applyWsEvent(event);
4636
+ this.deps.chatStore.applyWsEvent(event);
4637
+ this.deps.dmThreadsStore.applyWsEvent(event);
4638
+ this.deps.notificationsStore.applyWsEvent(event);
4463
4639
  });
4464
4640
  }
4465
4641
  stop() {
@@ -4471,12 +4647,14 @@ var WsRouter = class {
4471
4647
  this.transport.subscribeEvent(eventName, (payload) => {
4472
4648
  const decoded = decodeWsEvent(eventName, payload);
4473
4649
  if (!decoded) return;
4474
- this.deps.lobbyStore.applyWsEvent(decoded);
4475
- this.deps.gameStore.applyWsEvent(decoded);
4476
- this.deps.gameActionsStore.applyWsEvent(decoded);
4477
- this.deps.chatStore.applyWsEvent(decoded);
4478
- this.deps.dmThreadsStore.applyWsEvent(decoded);
4479
- this.deps.notificationsStore.applyWsEvent(decoded);
4650
+ const serverTs = payload !== null && typeof payload === "object" ? payload._serverTs : void 0;
4651
+ const event = serverTs !== void 0 ? { ...decoded, _serverTs: serverTs } : decoded;
4652
+ this.deps.lobbyStore.applyWsEvent(event);
4653
+ this.deps.gameStore.applyWsEvent(event);
4654
+ this.deps.gameActionsStore.applyWsEvent(event);
4655
+ this.deps.chatStore.applyWsEvent(event);
4656
+ this.deps.dmThreadsStore.applyWsEvent(event);
4657
+ this.deps.notificationsStore.applyWsEvent(event);
4480
4658
  });
4481
4659
  }
4482
4660
  }
@@ -4835,7 +5013,7 @@ var SharedWorkerTransport = class extends BaseWsTransport {
4835
5013
  };
4836
5014
 
4837
5015
  // src/utils/money.ts
4838
- var import_utils = __toESM(require_dist(), 1);
5016
+ var import_utils2 = __toESM(require_dist(), 1);
4839
5017
  // Annotate the CommonJS export names for ESM import in node:
4840
5018
  0 && (module.exports = {
4841
5019
  Admin,
@@ -4877,6 +5055,7 @@ var import_utils = __toESM(require_dist(), 1);
4877
5055
  formatMoneyMinor,
4878
5056
  isRetryableError,
4879
5057
  logger,
5058
+ selectGameLifecycleState,
4880
5059
  toMajor,
4881
5060
  withRetry
4882
5061
  });