@beta-gamer/react-native 0.1.22 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,10 +32,15 @@ __export(index_exports, {
32
32
  TictactoeBoard: () => TictactoeBoard,
33
33
  Timer: () => Timer,
34
34
  useBetaGamer: () => useBetaGamer,
35
+ useCheckersGame: () => useCheckersGame,
36
+ useChessGame: () => useChessGame,
37
+ useConnect: () => useConnect,
38
+ useConnect4Game: () => useConnect4Game,
35
39
  useGameState: () => useGameState,
36
40
  useSession: () => useSession,
37
41
  useSocket: () => useSocket,
38
- useTheme: () => useTheme
42
+ useTheme: () => useTheme,
43
+ useTictactoeGame: () => useTictactoeGame
39
44
  });
40
45
  module.exports = __toCommonJS(index_exports);
41
46
 
@@ -97,13 +102,13 @@ function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", so
97
102
  });
98
103
  (0, import_react.useEffect)(() => {
99
104
  if (!connectSocket) return;
100
- const s = (0, import_socket.io)(`${serverUrl}/${session.game}`, {
105
+ const s4 = (0, import_socket.io)(`${serverUrl}/${session.game}`, {
101
106
  auth: { token },
102
107
  path: socketPath,
103
108
  transports: ["websocket", "polling"]
104
109
  });
105
- setSocket(s);
106
- s.on("connect_error", (err) => {
110
+ setSocket(s4);
111
+ s4.on("connect_error", (err) => {
107
112
  const msg = err.message?.toLowerCase() ?? "";
108
113
  let reason = "Unable to connect to the game server.";
109
114
  if (msg.includes("websocket") || msg.includes("transport")) reason = "Connection failed: server does not accept WebSocket connections.";
@@ -114,14 +119,14 @@ function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", so
114
119
  else if (err.message) reason = `Connection error: ${err.message}`;
115
120
  import_react_native.Alert.alert("Connection Error", `[Beta Gamer] ${reason}`);
116
121
  });
117
- s.on("game:state", (state) => {
122
+ s4.on("game:state", (state) => {
118
123
  setGameState((prev) => ({ ...prev, ...state }));
119
124
  });
120
- s.on("game:over", ({ winner, reason }) => {
125
+ s4.on("game:over", ({ winner, reason }) => {
121
126
  setGameState((prev) => ({ ...prev, status: "ended", winner, reason }));
122
127
  });
123
128
  return () => {
124
- s.disconnect();
129
+ s4.disconnect();
125
130
  setSocket(null);
126
131
  };
127
132
  }, [token, serverUrl, socketPath, session.game]);
@@ -134,6 +139,8 @@ function useBetaGamer() {
134
139
  }
135
140
 
136
141
  // src/hooks/index.ts
142
+ var import_react2 = require("react");
143
+ var import_socket2 = require("socket.io-client");
137
144
  function useGameState() {
138
145
  return useBetaGamer().gameState;
139
146
  }
@@ -146,6 +153,26 @@ function useSocket() {
146
153
  function useTheme() {
147
154
  return useBetaGamer().theme;
148
155
  }
156
+ function useConnect() {
157
+ const { token, serverUrl, session } = useBetaGamer();
158
+ const [socket, setSocket] = (0, import_react2.useState)(null);
159
+ const socketRef = (0, import_react2.useRef)(null);
160
+ (0, import_react2.useEffect)(() => {
161
+ const s4 = (0, import_socket2.io)(`${serverUrl}/${session.game}`, {
162
+ auth: { token },
163
+ path: "/socket.io",
164
+ transports: ["websocket", "polling"]
165
+ });
166
+ socketRef.current = s4;
167
+ setSocket(s4);
168
+ return () => {
169
+ s4.disconnect();
170
+ socketRef.current = null;
171
+ setSocket(null);
172
+ };
173
+ }, [token, serverUrl, session.game]);
174
+ return socket;
175
+ }
149
176
 
150
177
  // src/components/PlayerCard.tsx
151
178
  var import_react_native2 = require("react-native");
@@ -163,14 +190,14 @@ function PlayerCard({ player, style, nameStyle, indicatorStyle }) {
163
190
  }
164
191
 
165
192
  // src/components/Timer.tsx
166
- var import_react2 = require("react");
193
+ var import_react3 = require("react");
167
194
  var import_react_native3 = require("react-native");
168
195
  var import_jsx_runtime3 = require("react/jsx-runtime");
169
196
  function Timer({ player, initialSeconds = 600, style, textStyle }) {
170
197
  const socket = useSocket();
171
198
  const { status } = useGameState();
172
- const [seconds, setSeconds] = (0, import_react2.useState)(initialSeconds);
173
- (0, import_react2.useEffect)(() => {
199
+ const [seconds, setSeconds] = (0, import_react3.useState)(initialSeconds);
200
+ (0, import_react3.useEffect)(() => {
174
201
  if (!socket) return;
175
202
  const handler = (clocks) => {
176
203
  if (clocks[player] !== void 0) setSeconds(clocks[player]);
@@ -180,73 +207,65 @@ function Timer({ player, initialSeconds = 600, style, textStyle }) {
180
207
  socket.off("game:clock", handler);
181
208
  };
182
209
  }, [socket, player]);
183
- (0, import_react2.useEffect)(() => {
210
+ (0, import_react3.useEffect)(() => {
184
211
  if (status !== "active") return;
185
- const id = setInterval(() => setSeconds((s2) => Math.max(0, s2 - 1)), 1e3);
212
+ const id = setInterval(() => setSeconds((s5) => Math.max(0, s5 - 1)), 1e3);
186
213
  return () => clearInterval(id);
187
214
  }, [status]);
188
215
  const m = Math.floor(seconds / 60).toString().padStart(2, "0");
189
- const s = (seconds % 60).toString().padStart(2, "0");
216
+ const s4 = (seconds % 60).toString().padStart(2, "0");
190
217
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.Text, { style: textStyle, children: [
191
218
  m,
192
219
  ":",
193
- s
220
+ s4
194
221
  ] }) });
195
222
  }
196
223
 
197
224
  // src/components/chess/ChessBoard.tsx
198
- var import_react3 = require("react");
199
225
  var import_react_native4 = require("react-native");
226
+
227
+ // src/components/chess/useChessGame.ts
228
+ var import_react4 = require("react");
229
+ var import_socket3 = require("socket.io-client");
200
230
  var import_chess = require("chess.js");
201
- var import_socket2 = require("socket.io-client");
202
- var import_jsx_runtime4 = require("react/jsx-runtime");
203
- var PIECES = {
204
- p: { white: "\u2659", black: "\u265F" },
205
- n: { white: "\u2658", black: "\u265E" },
206
- b: { white: "\u2657", black: "\u265D" },
207
- r: { white: "\u2656", black: "\u265C" },
208
- q: { white: "\u2655", black: "\u265B" },
209
- k: { white: "\u2654", black: "\u265A" }
210
- };
211
- function ChessBoard({ style, showAfkWarning = true, onLeave }) {
231
+ function useChessGame() {
212
232
  const { token, serverUrl, session } = useBetaGamer();
213
233
  const myPlayer = session.players[0];
214
- const socketRef = (0, import_react3.useRef)(null);
215
- const roomIdRef = (0, import_react3.useRef)(null);
216
- const myPlayerIdRef = (0, import_react3.useRef)(myPlayer?.id ?? "");
217
- const afkTimerRef = (0, import_react3.useRef)(null);
218
- const chessRef = (0, import_react3.useRef)(new import_chess.Chess());
219
- const [fen, setFen] = (0, import_react3.useState)("start");
220
- const [myColor, setMyColor] = (0, import_react3.useState)("white");
221
- const [currentTurn, setCurrentTurn] = (0, import_react3.useState)(0);
222
- const [players, setPlayers] = (0, import_react3.useState)([]);
223
- const [clocks, setClocks] = (0, import_react3.useState)({ self: 600, opponent: 600 });
224
- const [selected, setSelected] = (0, import_react3.useState)(null);
225
- const [legalMoves, setLegalMoves] = (0, import_react3.useState)([]);
226
- const [lastMove, setLastMove] = (0, import_react3.useState)(null);
227
- const [gameOver, setGameOver] = (0, import_react3.useState)(false);
228
- const [gameResult, setGameResult] = (0, import_react3.useState)(null);
229
- const [afkWarning, setAfkWarning] = (0, import_react3.useState)(null);
230
- const [promotionMove, setPromotionMove] = (0, import_react3.useState)(null);
231
- const [moveHistory, setMoveHistory] = (0, import_react3.useState)([]);
232
- if (session.game !== "chess") return null;
233
- const isMyTurn = players.length > 0 ? myColor === "white" ? currentTurn === 0 : currentTurn === 1 : false;
234
- (0, import_react3.useEffect)(() => {
235
- const s = (0, import_socket2.io)(`${serverUrl}/chess`, {
234
+ const socketRef = (0, import_react4.useRef)(null);
235
+ const roomIdRef = (0, import_react4.useRef)(null);
236
+ const myPlayerIdRef = (0, import_react4.useRef)(myPlayer?.id ?? "");
237
+ const afkTimerRef = (0, import_react4.useRef)(null);
238
+ const chessRef = (0, import_react4.useRef)(new import_chess.Chess());
239
+ const [fen, setFen] = (0, import_react4.useState)("start");
240
+ const [myColor, setMyColor] = (0, import_react4.useState)("white");
241
+ const [currentTurn, setCurrentTurn] = (0, import_react4.useState)(0);
242
+ const [players, setPlayers] = (0, import_react4.useState)([]);
243
+ const [clocks, setClocks] = (0, import_react4.useState)({ self: 600, opponent: 600 });
244
+ const [selected, setSelected] = (0, import_react4.useState)(null);
245
+ const [legalMoves, setLegalMoves] = (0, import_react4.useState)([]);
246
+ const [lastMove, setLastMove] = (0, import_react4.useState)(null);
247
+ const [moveHistory, setMoveHistory] = (0, import_react4.useState)([]);
248
+ const [promotionMove, setPromotionMove] = (0, import_react4.useState)(null);
249
+ const [gameOver, setGameOver] = (0, import_react4.useState)(false);
250
+ const [gameResult, setGameResult] = (0, import_react4.useState)(null);
251
+ const [afkWarning, setAfkWarning] = (0, import_react4.useState)(null);
252
+ const [, forceUpdate] = (0, import_react4.useState)(0);
253
+ (0, import_react4.useEffect)(() => {
254
+ const s4 = (0, import_socket3.io)(`${serverUrl}/chess`, {
236
255
  auth: { token },
237
256
  path: "/socket.io",
238
257
  transports: ["websocket", "polling"]
239
258
  });
240
- socketRef.current = s;
241
- const join = () => s.emit("matchmaking:join", {
259
+ socketRef.current = s4;
260
+ const join = () => s4.emit("matchmaking:join", {
242
261
  username: myPlayer.displayName,
243
262
  playerId: myPlayer.id,
244
- wantsBot: false,
263
+ wantsBot: session.matchType === "bot",
245
264
  sessionId: session.sessionId
246
265
  });
247
- if (s.connected) join();
248
- else s.once("connect", join);
249
- s.on("game:started", (d) => {
266
+ if (s4.connected) join();
267
+ else s4.once("connect", join);
268
+ s4.on("game:started", (d) => {
250
269
  roomIdRef.current = d.roomId;
251
270
  myPlayerIdRef.current = d.playerId;
252
271
  setMyColor(d.color);
@@ -259,7 +278,7 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
259
278
  setClocks({ self: Math.floor((vals[0] ?? 6e5) / 1e3), opponent: Math.floor((vals[1] ?? 6e5) / 1e3) });
260
279
  }
261
280
  });
262
- s.on("game:move:made", (d) => {
281
+ s4.on("game:move:made", (d) => {
263
282
  chessRef.current.load(d.fen);
264
283
  setFen(d.fen);
265
284
  setCurrentTurn(d.currentTurn ?? 0);
@@ -275,14 +294,14 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
275
294
  }
276
295
  setAfkWarning(null);
277
296
  });
278
- s.on("timer:update", (d) => {
297
+ s4.on("timer:update", (d) => {
279
298
  if (!d.playerTimers) return;
280
299
  Object.entries(d.playerTimers).forEach(([id, ms]) => {
281
300
  if (id === myPlayerIdRef.current) setClocks((c) => ({ ...c, self: Math.floor(ms / 1e3) }));
282
301
  else setClocks((c) => ({ ...c, opponent: Math.floor(ms / 1e3) }));
283
302
  });
284
303
  });
285
- s.on("chess:afk_warning", (d) => {
304
+ s4.on("chess:afk_warning", (d) => {
286
305
  setAfkWarning(d);
287
306
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
288
307
  afkTimerRef.current = setInterval(() => {
@@ -290,21 +309,21 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
290
309
  if (!prev || prev.secondsRemaining <= 1) {
291
310
  clearInterval(afkTimerRef.current);
292
311
  afkTimerRef.current = null;
293
- if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
312
+ if (roomIdRef.current) s4.emit("afk:check", { roomId: roomIdRef.current });
294
313
  return prev;
295
314
  }
296
315
  return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
297
316
  });
298
317
  }, 1e3);
299
318
  });
300
- s.on("chess:afk_warning_cleared", () => {
319
+ s4.on("chess:afk_warning_cleared", () => {
301
320
  if (afkTimerRef.current) {
302
321
  clearInterval(afkTimerRef.current);
303
322
  afkTimerRef.current = null;
304
323
  }
305
324
  setAfkWarning(null);
306
325
  });
307
- s.on("afk:status", (status) => {
326
+ s4.on("afk:status", (status) => {
308
327
  if (!status) {
309
328
  if (afkTimerRef.current) {
310
329
  clearInterval(afkTimerRef.current);
@@ -315,22 +334,31 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
315
334
  setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
316
335
  }
317
336
  });
318
- s.on("game:over", (d) => {
337
+ s4.on("game:over", (d) => {
319
338
  setGameResult({ winner: d.winner ?? null, reason: d.reason });
320
339
  setTimeout(() => setGameOver(true), 400);
321
340
  });
322
341
  return () => {
323
342
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
324
- s.disconnect();
343
+ s4.disconnect();
325
344
  socketRef.current = null;
326
345
  };
327
346
  }, [token, serverUrl]);
347
+ const isMyTurn = players.length > 0 ? myColor === "white" ? currentTurn === 0 : currentTurn === 1 : false;
348
+ const emitMove = (from, to, promotion) => {
349
+ if (!socketRef.current || !roomIdRef.current) return;
350
+ socketRef.current.emit("game:move", {
351
+ roomId: roomIdRef.current,
352
+ playerId: myPlayerIdRef.current,
353
+ move: promotion ? { from, to, promotion } : { from, to }
354
+ });
355
+ };
328
356
  const handleSquarePress = (square) => {
329
357
  if (gameOver || !isMyTurn) return;
330
- const chess2 = chessRef.current;
358
+ const chess = chessRef.current;
331
359
  if (selected) {
332
360
  if (legalMoves.includes(square)) {
333
- const piece = chess2.get(selected);
361
+ const piece = chess.get(selected);
334
362
  const toRank = square[1];
335
363
  if (piece?.type === "p" && (piece.color === "w" && toRank === "8" || piece.color === "b" && toRank === "1")) {
336
364
  setPromotionMove({ from: selected, to: square });
@@ -342,36 +370,83 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
342
370
  setSelected(null);
343
371
  setLegalMoves([]);
344
372
  } else {
345
- const piece = chess2.get(square);
373
+ const piece = chess.get(square);
346
374
  const myColorChar = myColor === "white" ? "w" : "b";
347
375
  if (piece && piece.color === myColorChar) {
348
- const moves = chess2.moves({ square, verbose: true });
349
376
  setSelected(square);
350
- setLegalMoves(moves.map((m) => m.to));
377
+ setLegalMoves(chess.moves({ square, verbose: true }).map((m) => m.to));
351
378
  } else {
352
379
  setSelected(null);
353
380
  setLegalMoves([]);
354
381
  }
355
382
  }
356
383
  } else {
357
- const piece = chess2.get(square);
384
+ const piece = chess.get(square);
358
385
  const myColorChar = myColor === "white" ? "w" : "b";
359
386
  if (piece && piece.color === myColorChar) {
360
- const moves = chess2.moves({ square, verbose: true });
361
387
  setSelected(square);
362
- setLegalMoves(moves.map((m) => m.to));
388
+ setLegalMoves(chess.moves({ square, verbose: true }).map((m) => m.to));
363
389
  }
364
390
  }
391
+ forceUpdate((n) => n + 1);
365
392
  };
366
- const emitMove = (from, to, promotion) => {
367
- if (!socketRef.current || !roomIdRef.current) return;
368
- socketRef.current.emit("game:move", {
369
- roomId: roomIdRef.current,
370
- playerId: myPlayerIdRef.current,
371
- move: promotion ? { from, to, promotion } : { from, to }
372
- });
393
+ return {
394
+ socket: socketRef.current,
395
+ roomId: roomIdRef.current,
396
+ myPlayerId: myPlayerIdRef.current,
397
+ myColor,
398
+ fen,
399
+ currentTurn,
400
+ players,
401
+ clocks,
402
+ selected,
403
+ legalMoves,
404
+ lastMove,
405
+ moveHistory,
406
+ promotionMove,
407
+ gameOver,
408
+ gameResult,
409
+ afkWarning,
410
+ isMyTurn,
411
+ chess: chessRef.current,
412
+ handleSquarePress,
413
+ emitMove,
414
+ setPromotionMove
373
415
  };
374
- const formatClock = (s) => `${Math.floor(s / 60).toString().padStart(2, "0")}:${(s % 60).toString().padStart(2, "0")}`;
416
+ }
417
+
418
+ // src/components/chess/ChessBoard.tsx
419
+ var import_jsx_runtime4 = require("react/jsx-runtime");
420
+ var PIECES = {
421
+ p: { white: "\u2659", black: "\u265F" },
422
+ n: { white: "\u2658", black: "\u265E" },
423
+ b: { white: "\u2657", black: "\u265D" },
424
+ r: { white: "\u2656", black: "\u265C" },
425
+ q: { white: "\u2655", black: "\u265B" },
426
+ k: { white: "\u2654", black: "\u265A" }
427
+ };
428
+ function ChessBoard({ style, layout = "board-only", showAfkWarning = true, onLeave }) {
429
+ const { session } = useBetaGamer();
430
+ if (session.game !== "chess") return null;
431
+ const game = useChessGame();
432
+ const {
433
+ myColor,
434
+ fen,
435
+ selected,
436
+ legalMoves,
437
+ lastMove,
438
+ promotionMove,
439
+ gameOver,
440
+ gameResult,
441
+ afkWarning,
442
+ isMyTurn,
443
+ chess,
444
+ handleSquarePress,
445
+ emitMove,
446
+ setPromotionMove
447
+ } = game;
448
+ const { session: { players: sessionPlayers } } = useBetaGamer();
449
+ const myPlayer = sessionPlayers[0];
375
450
  const boardSize = Math.min(import_react_native4.Dimensions.get("window").width, import_react_native4.Dimensions.get("window").height) - 32;
376
451
  const squareSize = boardSize / 8;
377
452
  const files = ["a", "b", "c", "d", "e", "f", "g", "h"];
@@ -380,8 +455,63 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
380
455
  files.reverse();
381
456
  ranks.reverse();
382
457
  }
383
- const chess = chessRef.current;
384
- const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
458
+ const board = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: { width: boardSize, height: boardSize }, children: ranks.map((rank, rowIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: { flexDirection: "row" }, children: files.map((file, colIdx) => {
459
+ const square = `${file}${rank}`;
460
+ const piece = chess.get(square);
461
+ const isLight = (rowIdx + colIdx) % 2 === 0;
462
+ const isSelected = selected === square;
463
+ const isLegal = legalMoves.includes(square);
464
+ const isLast = lastMove && (lastMove.from === square || lastMove.to === square);
465
+ let bg = isLight ? "#f0d9b5" : "#b58863";
466
+ if (isSelected) bg = "#7fc97f";
467
+ else if (isLast) bg = isLight ? "#cdd16f" : "#aaa23a";
468
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
469
+ import_react_native4.TouchableOpacity,
470
+ {
471
+ onPress: () => handleSquarePress(square),
472
+ activeOpacity: 0.85,
473
+ style: { width: squareSize, height: squareSize, backgroundColor: bg, alignItems: "center", justifyContent: "center" },
474
+ children: [
475
+ piece && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.piece, { fontSize: squareSize * 0.72 }, piece.color === "w" ? styles.whitePiece : styles.blackPiece], children: PIECES[piece.type]?.[piece.color === "w" ? "white" : "black"] }),
476
+ isLegal && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [
477
+ styles.legalDot,
478
+ piece ? { ...import_react_native4.StyleSheet.absoluteFillObject, borderRadius: squareSize / 2, borderWidth: 3, borderColor: "rgba(0,180,0,0.5)", backgroundColor: "transparent" } : { width: squareSize * 0.3, height: squareSize * 0.3, borderRadius: squareSize * 0.15, backgroundColor: "rgba(0,180,0,0.45)" }
479
+ ] })
480
+ ]
481
+ },
482
+ square
483
+ );
484
+ }) }, rank)) });
485
+ const promotionModal = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Modal, { visible: !!promotionMove, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.promotionBox, children: [
486
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.promotionTitle, children: "Promote pawn" }),
487
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: { flexDirection: "row", gap: 12 }, children: ["q", "r", "b", "n"].map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
488
+ import_react_native4.TouchableOpacity,
489
+ {
490
+ style: styles.promotionPiece,
491
+ onPress: () => {
492
+ if (promotionMove) emitMove(promotionMove.from, promotionMove.to, p);
493
+ setPromotionMove(null);
494
+ },
495
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: { fontSize: 36 }, children: PIECES[p]?.[myColor === "white" ? "white" : "black"] })
496
+ },
497
+ p
498
+ )) })
499
+ ] }) }) });
500
+ const gameOverModal = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.gameOverBox, children: [
501
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === game.myPlayerId ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.gameOverTitle, children: gameResult?.winner === game.myPlayerId ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
503
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.gameOverReason, children: gameResult?.reason }),
504
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.TouchableOpacity, { style: styles.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnPlayAgainText, children: "Play again" }) })
505
+ ] }) }) });
506
+ if (layout === "board-only") {
507
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style, children: [
508
+ board,
509
+ promotionModal,
510
+ gameOverModal
511
+ ] });
512
+ }
513
+ const opponentName = game.players.find((p) => p.id !== game.myPlayerId)?.username ?? "Waiting\u2026";
514
+ const formatClock = (s4) => `${Math.floor(s4 / 60).toString().padStart(2, "0")}:${(s4 % 60).toString().padStart(2, "0")}`;
385
515
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles.container, style], children: [
386
516
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.playerRow, children: [
387
517
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.avatarText, children: opponentName[0]?.toUpperCase() }) }),
@@ -389,57 +519,31 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
389
519
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerName, children: opponentName }),
390
520
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerColor, children: myColor === "white" ? "Black" : "White" })
391
521
  ] }),
392
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.clock, clocks.opponent < 30 && styles.clockDanger], children: formatClock(clocks.opponent) })
522
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.clock, game.clocks.opponent < 30 && styles.clockDanger], children: formatClock(game.clocks.opponent) })
393
523
  ] }),
394
- showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles.afkBannerSelf : styles.afkBannerOpponent], children: [
395
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F You are AFK \u2014 make a move!" : "\u23F3 Opponent is AFK" }),
524
+ showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles.afkBanner, afkWarning.playerId === game.myPlayerId ? styles.afkBannerSelf : styles.afkBannerOpponent], children: [
525
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.afkText, children: afkWarning.playerId === game.myPlayerId ? "\u26A0\uFE0F You are AFK \u2014 make a move!" : "\u23F3 Opponent is AFK" }),
396
526
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.Text, { style: styles.afkText, children: [
397
527
  afkWarning.secondsRemaining,
398
528
  "s"
399
529
  ] })
400
530
  ] }),
401
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: { width: boardSize, height: boardSize }, children: ranks.map((rank, rowIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: { flexDirection: "row" }, children: files.map((file, colIdx) => {
402
- const square = `${file}${rank}`;
403
- const piece = chess.get(square);
404
- const isLight = (rowIdx + colIdx) % 2 === 0;
405
- const isSelected = selected === square;
406
- const isLegal = legalMoves.includes(square);
407
- const isLast = lastMove && (lastMove.from === square || lastMove.to === square);
408
- let bg = isLight ? "#f0d9b5" : "#b58863";
409
- if (isSelected) bg = "#7fc97f";
410
- else if (isLast) bg = isLight ? "#cdd16f" : "#aaa23a";
411
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
412
- import_react_native4.TouchableOpacity,
413
- {
414
- onPress: () => handleSquarePress(square),
415
- activeOpacity: 0.85,
416
- style: [{ width: squareSize, height: squareSize, backgroundColor: bg, alignItems: "center", justifyContent: "center" }],
417
- children: [
418
- piece && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.piece, { fontSize: squareSize * 0.72 }, piece.color === "w" ? styles.whitePiece : styles.blackPiece], children: PIECES[piece.type]?.[piece.color === "w" ? "white" : "black"] }),
419
- isLegal && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [
420
- styles.legalDot,
421
- piece ? { ...import_react_native4.StyleSheet.absoluteFillObject, borderRadius: squareSize / 2, borderWidth: 3, borderColor: "rgba(0,180,0,0.5)", backgroundColor: "transparent" } : { width: squareSize * 0.3, height: squareSize * 0.3, borderRadius: squareSize * 0.15, backgroundColor: "rgba(0,180,0,0.45)" }
422
- ] })
423
- ]
424
- },
425
- square
426
- );
427
- }) }, rank)) }),
531
+ board,
428
532
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.playerRow, children: [
429
533
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [styles.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
430
534
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: { flex: 1, marginLeft: 10 }, children: [
431
535
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerName, children: myPlayer.displayName }),
432
536
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerColor, children: myColor })
433
537
  ] }),
434
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.clock, clocks.self < 30 && styles.clockDanger], children: formatClock(clocks.self) })
538
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.clock, game.clocks.self < 30 && styles.clockDanger], children: formatClock(game.clocks.self) })
435
539
  ] }),
436
540
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.actions, children: [
437
541
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
438
542
  import_react_native4.TouchableOpacity,
439
543
  {
440
544
  style: styles.btnResign,
441
- onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
442
545
  disabled: gameOver,
546
+ onPress: () => game.socket?.emit("game:resign", { roomId: game.roomId, playerId: game.myPlayerId }),
443
547
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnResignText, children: "Resign" })
444
548
  }
445
549
  ),
@@ -447,34 +551,15 @@ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
447
551
  import_react_native4.TouchableOpacity,
448
552
  {
449
553
  style: styles.btnDraw,
450
- onPress: () => socketRef.current?.emit("game:draw:offer", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
451
554
  disabled: gameOver,
555
+ onPress: () => game.socket?.emit("game:draw:offer", { roomId: game.roomId, playerId: game.myPlayerId }),
452
556
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnDrawText, children: "Offer draw" })
453
557
  }
454
558
  ),
455
559
  onLeave && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.TouchableOpacity, { style: styles.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnLeaveText, children: "Leave" }) })
456
560
  ] }),
457
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Modal, { visible: !!promotionMove, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.promotionBox, children: [
458
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.promotionTitle, children: "Promote pawn" }),
459
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: { flexDirection: "row", gap: 12 }, children: ["q", "r", "b", "n"].map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
460
- import_react_native4.TouchableOpacity,
461
- {
462
- style: styles.promotionPiece,
463
- onPress: () => {
464
- if (promotionMove) emitMove(promotionMove.from, promotionMove.to, p);
465
- setPromotionMove(null);
466
- },
467
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: { fontSize: 36 }, children: PIECES[p]?.[myColor === "white" ? "white" : "black"] })
468
- },
469
- p
470
- )) })
471
- ] }) }) }),
472
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.gameOverBox, children: [
473
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
474
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
475
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.gameOverReason, children: gameResult?.reason }),
476
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.TouchableOpacity, { style: styles.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnPlayAgainText, children: "Play again" }) })
477
- ] }) }) })
561
+ promotionModal,
562
+ gameOverModal
478
563
  ] });
479
564
  }
480
565
  var styles = import_react_native4.StyleSheet.create({
@@ -513,13 +598,13 @@ var styles = import_react_native4.StyleSheet.create({
513
598
  });
514
599
 
515
600
  // src/components/chess/ChessMoveHistory.tsx
516
- var import_react4 = require("react");
601
+ var import_react5 = require("react");
517
602
  var import_react_native5 = require("react-native");
518
603
  var import_jsx_runtime5 = require("react/jsx-runtime");
519
604
  function ChessMoveHistory({ style, rowStyle, textStyle }) {
520
605
  const socket = useSocket();
521
- const [moves, setMoves] = (0, import_react4.useState)([]);
522
- (0, import_react4.useEffect)(() => {
606
+ const [moves, setMoves] = (0, import_react5.useState)([]);
607
+ (0, import_react5.useEffect)(() => {
523
608
  if (!socket) return;
524
609
  const handler = ({ san, moveIndex }) => {
525
610
  const moveNumber = Math.floor(moveIndex / 2) + 1;
@@ -549,46 +634,33 @@ function ChessMoveHistory({ style, rowStyle, textStyle }) {
549
634
  }
550
635
 
551
636
  // src/components/checkers/index.tsx
552
- var import_react5 = require("react");
553
637
  var import_react_native6 = require("react-native");
554
- var import_socket3 = require("socket.io-client");
638
+ var import_socket4 = require("socket.io-client");
639
+ var import_react6 = require("react");
555
640
  var import_jsx_runtime6 = require("react/jsx-runtime");
556
- function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
641
+ function useCheckersGame() {
557
642
  const { token, serverUrl, session } = useBetaGamer();
558
643
  const myPlayer = session.players[0];
559
- const socketRef = (0, import_react5.useRef)(null);
560
- const roomIdRef = (0, import_react5.useRef)(null);
561
- const myPlayerIdRef = (0, import_react5.useRef)(myPlayer?.id ?? "");
562
- const afkTimerRef = (0, import_react5.useRef)(null);
563
- const [board, setBoard] = (0, import_react5.useState)(Array(64).fill(null));
564
- const [myColor, setMyColor] = (0, import_react5.useState)("red");
565
- const [currentTurn, setCurrentTurn] = (0, import_react5.useState)(0);
566
- const [players, setPlayers] = (0, import_react5.useState)([]);
567
- const [selectedPiece, setSelectedPiece] = (0, import_react5.useState)(null);
568
- const [validMoves, setValidMoves] = (0, import_react5.useState)([]);
569
- const [gameOver, setGameOver] = (0, import_react5.useState)(false);
570
- const [gameResult, setGameResult] = (0, import_react5.useState)(null);
571
- const [afkWarning, setAfkWarning] = (0, import_react5.useState)(null);
572
- if (session.game !== "checkers") return null;
573
- const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
574
- const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
575
- const opponentColor = myColor === "red" ? "black" : "red";
576
- (0, import_react5.useEffect)(() => {
577
- const s = (0, import_socket3.io)(`${serverUrl}/checkers`, {
578
- auth: { token },
579
- path: "/socket.io",
580
- transports: ["websocket", "polling"]
581
- });
582
- socketRef.current = s;
583
- const join = () => s.emit("matchmaking:join", {
584
- username: myPlayer.displayName,
585
- playerId: myPlayer.id,
586
- wantsBot: false,
587
- sessionId: session.sessionId
588
- });
589
- if (s.connected) join();
590
- else s.once("connect", join);
591
- s.on("game:started", (d) => {
644
+ const socketRef = (0, import_react6.useRef)(null);
645
+ const roomIdRef = (0, import_react6.useRef)(null);
646
+ const myPlayerIdRef = (0, import_react6.useRef)(myPlayer?.id ?? "");
647
+ const afkTimerRef = (0, import_react6.useRef)(null);
648
+ const [board, setBoard] = (0, import_react6.useState)(Array(64).fill(null));
649
+ const [myColor, setMyColor] = (0, import_react6.useState)("red");
650
+ const [currentTurn, setCurrentTurn] = (0, import_react6.useState)(0);
651
+ const [players, setPlayers] = (0, import_react6.useState)([]);
652
+ const [selectedPiece, setSelectedPiece] = (0, import_react6.useState)(null);
653
+ const [validMoves, setValidMoves] = (0, import_react6.useState)([]);
654
+ const [gameOver, setGameOver] = (0, import_react6.useState)(false);
655
+ const [gameResult, setGameResult] = (0, import_react6.useState)(null);
656
+ const [afkWarning, setAfkWarning] = (0, import_react6.useState)(null);
657
+ (0, import_react6.useEffect)(() => {
658
+ const s4 = (0, import_socket4.io)(`${serverUrl}/checkers`, { auth: { token }, path: "/socket.io", transports: ["websocket", "polling"] });
659
+ socketRef.current = s4;
660
+ const join = () => s4.emit("matchmaking:join", { username: myPlayer.displayName, playerId: myPlayer.id, wantsBot: session.matchType === "bot", sessionId: session.sessionId });
661
+ if (s4.connected) join();
662
+ else s4.once("connect", join);
663
+ s4.on("game:started", (d) => {
592
664
  roomIdRef.current = d.roomId;
593
665
  myPlayerIdRef.current = d.playerId;
594
666
  setMyColor(d.color ?? "red");
@@ -596,7 +668,7 @@ function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
596
668
  setCurrentTurn(d.currentTurn ?? 0);
597
669
  setPlayers(d.players ?? []);
598
670
  });
599
- s.on("game:move:made", (d) => {
671
+ s4.on("game:move:made", (d) => {
600
672
  setBoard(d.board);
601
673
  setCurrentTurn(d.currentTurn ?? 0);
602
674
  setSelectedPiece(null);
@@ -607,11 +679,11 @@ function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
607
679
  }
608
680
  setAfkWarning(null);
609
681
  });
610
- s.on("game:valid_moves", (d) => {
682
+ s4.on("game:valid_moves", (d) => {
611
683
  setSelectedPiece(d.position);
612
684
  setValidMoves(d.moves);
613
685
  });
614
- s.on("checkers:afk_warning", (d) => {
686
+ s4.on("checkers:afk_warning", (d) => {
615
687
  setAfkWarning(d);
616
688
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
617
689
  afkTimerRef.current = setInterval(() => {
@@ -619,66 +691,63 @@ function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
619
691
  if (!prev || prev.secondsRemaining <= 1) {
620
692
  clearInterval(afkTimerRef.current);
621
693
  afkTimerRef.current = null;
622
- if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
694
+ if (roomIdRef.current) s4.emit("afk:check", { roomId: roomIdRef.current });
623
695
  return prev;
624
696
  }
625
697
  return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
626
698
  });
627
699
  }, 1e3);
628
700
  });
629
- s.on("checkers:afk_warning_cleared", () => {
701
+ s4.on("checkers:afk_warning_cleared", () => {
630
702
  if (afkTimerRef.current) {
631
703
  clearInterval(afkTimerRef.current);
632
704
  afkTimerRef.current = null;
633
705
  }
634
706
  setAfkWarning(null);
635
707
  });
636
- s.on("afk:status", (status) => {
708
+ s4.on("afk:status", (status) => {
637
709
  if (!status) {
638
710
  if (afkTimerRef.current) {
639
711
  clearInterval(afkTimerRef.current);
640
712
  afkTimerRef.current = null;
641
713
  }
642
714
  setAfkWarning(null);
643
- } else {
644
- setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
645
- }
715
+ } else setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
646
716
  });
647
- s.on("game:over", (d) => {
717
+ s4.on("game:over", (d) => {
648
718
  setGameResult({ winner: d.winner ?? null, reason: d.reason });
649
719
  setTimeout(() => setGameOver(true), 400);
650
720
  });
651
721
  return () => {
652
722
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
653
- s.disconnect();
723
+ s4.disconnect();
654
724
  socketRef.current = null;
655
725
  };
656
726
  }, [token, serverUrl]);
727
+ const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
657
728
  const handleCellPress = (pos) => {
658
729
  if (gameOver || !isMyTurn) return;
659
730
  const piece = board[pos];
660
731
  const moveTarget = validMoves.find((m) => m.to === pos);
661
732
  if (moveTarget && selectedPiece !== null) {
662
- socketRef.current?.emit("game:move", {
663
- roomId: roomIdRef.current,
664
- playerId: myPlayerIdRef.current,
665
- move: { from: selectedPiece, to: pos, captures: moveTarget.captures }
666
- });
733
+ socketRef.current?.emit("game:move", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current, move: { from: selectedPiece, to: pos, captures: moveTarget.captures } });
667
734
  setSelectedPiece(null);
668
735
  setValidMoves([]);
669
- return;
670
- }
671
- if (piece && piece.player === myColor) {
672
- socketRef.current?.emit("game:get_moves", {
673
- roomId: roomIdRef.current,
674
- position: pos,
675
- playerId: myPlayerIdRef.current
676
- });
736
+ } else if (piece && piece.player === myColor) {
737
+ socketRef.current?.emit("game:get_moves", { roomId: roomIdRef.current, position: pos, playerId: myPlayerIdRef.current });
677
738
  } else {
678
739
  setSelectedPiece(null);
679
740
  setValidMoves([]);
680
741
  }
681
742
  };
743
+ return { socket: socketRef.current, roomId: roomIdRef.current, myPlayerId: myPlayerIdRef.current, myColor, board, currentTurn, players, selectedPiece, validMoves, gameOver, gameResult, afkWarning, isMyTurn, handleCellPress };
744
+ }
745
+ function CheckersBoard({ style, layout = "board-only", showAfkWarning = true, onLeave }) {
746
+ const { session } = useBetaGamer();
747
+ if (session.game !== "checkers") return null;
748
+ const game = useCheckersGame();
749
+ const { myColor, board, selectedPiece, validMoves, gameOver, gameResult, afkWarning, isMyTurn, handleCellPress } = game;
750
+ const myPlayer = session.players[0];
682
751
  const screenWidth = import_react_native6.Dimensions.get("window").width;
683
752
  const boardSize = screenWidth - 32;
684
753
  const cellSize = boardSize / 8;
@@ -688,87 +757,81 @@ function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
688
757
  rows.reverse();
689
758
  cols.reverse();
690
759
  }
691
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles2.container, style], children: [
692
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles2.playerRow, children: [
693
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: styles2.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.avatarText, children: opponentName[0]?.toUpperCase() }) }),
760
+ const boardEl = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: { width: boardSize, height: boardSize, borderWidth: 3, borderColor: "#92400e", borderRadius: 4 }, children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: { flexDirection: "row" }, children: cols.map((col) => {
761
+ const pos = row * 8 + col;
762
+ const piece = board[pos];
763
+ const isLight = (row + col) % 2 === 0;
764
+ const isSelected = selectedPiece === pos;
765
+ const moveTarget = validMoves.find((m) => m.to === pos);
766
+ const isCapture = (moveTarget?.captures?.length ?? 0) > 0;
767
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
768
+ import_react_native6.TouchableOpacity,
769
+ {
770
+ onPress: () => handleCellPress(pos),
771
+ activeOpacity: isLight ? 1 : 0.85,
772
+ style: [{ width: cellSize, height: cellSize, alignItems: "center", justifyContent: "center", backgroundColor: isLight ? "#f0d9b5" : "#b58863" }, isSelected && s.cellSelected, moveTarget && s.cellValidMove],
773
+ children: [
774
+ piece && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [s.piece, { width: cellSize * 0.78, height: cellSize * 0.78 }, piece.player === "red" ? s.pieceRed : s.pieceBlack, isSelected && s.pieceSelected], children: piece.type === "king" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: { fontSize: cellSize * 0.35 }, children: "\u{1F451}" }) }),
775
+ moveTarget && !piece && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [s.moveDot, { width: cellSize * 0.3, height: cellSize * 0.3, borderRadius: cellSize * 0.15 }, isCapture ? s.moveDotCapture : s.moveDotNormal] }),
776
+ moveTarget && piece && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [import_react_native6.StyleSheet.absoluteFill, s.captureRing, { borderRadius: cellSize / 2 }] })
777
+ ]
778
+ },
779
+ col
780
+ );
781
+ }) }, row)) });
782
+ const gameOverModal = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: s.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: s.gameOverBox, children: [
783
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === game.myPlayerId ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
784
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.gameOverTitle, children: gameResult?.winner === game.myPlayerId ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
785
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.gameOverReason, children: gameResult?.reason }),
786
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.TouchableOpacity, { style: s.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.btnPlayAgainText, children: "Play again" }) })
787
+ ] }) }) });
788
+ if (layout === "board-only") return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style, children: [
789
+ boardEl,
790
+ gameOverModal
791
+ ] });
792
+ const opponentName = game.players.find((p) => p.id !== game.myPlayerId)?.username ?? "Waiting\u2026";
793
+ const opponentColor = myColor === "red" ? "black" : "red";
794
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [s.container, style], children: [
795
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: s.playerRow, children: [
796
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: s.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.avatarText, children: opponentName[0]?.toUpperCase() }) }),
694
797
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: { flex: 1, marginLeft: 10 }, children: [
695
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.playerName, children: opponentName }),
696
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles2.playerColor, { color: opponentColor === "red" ? "#f87171" : "#94a3b8" }], children: opponentColor })
798
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.playerName, children: opponentName }),
799
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [s.playerColor, { color: opponentColor === "red" ? "#f87171" : "#94a3b8" }], children: opponentColor })
697
800
  ] }),
698
- !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.turnIndicator, children: "\u25CF thinking" })
801
+ !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.turnIndicator, children: "\u25CF thinking" })
699
802
  ] }),
700
- showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles2.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles2.afkSelf : styles2.afkOpponent], children: [
701
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F Make a move!" : "\u23F3 Opponent is AFK" }),
702
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Text, { style: styles2.afkText, children: [
803
+ showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [s.afkBanner, afkWarning.playerId === game.myPlayerId ? s.afkSelf : s.afkOpponent], children: [
804
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.afkText, children: afkWarning.playerId === game.myPlayerId ? "\u26A0\uFE0F Make a move!" : "\u23F3 Opponent is AFK" }),
805
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Text, { style: s.afkText, children: [
703
806
  afkWarning.secondsRemaining,
704
807
  "s"
705
808
  ] })
706
809
  ] }),
707
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: { width: boardSize, height: boardSize, borderWidth: 3, borderColor: "#92400e", borderRadius: 4 }, children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: { flexDirection: "row" }, children: cols.map((col) => {
708
- const pos = row * 8 + col;
709
- const piece = board[pos];
710
- const isLight = (row + col) % 2 === 0;
711
- const isSelected = selectedPiece === pos;
712
- const moveTarget = validMoves.find((m) => m.to === pos);
713
- const isCapture = (moveTarget?.captures?.length ?? 0) > 0;
714
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
715
- import_react_native6.TouchableOpacity,
716
- {
717
- onPress: () => handleCellPress(pos),
718
- activeOpacity: isLight ? 1 : 0.85,
719
- style: [
720
- { width: cellSize, height: cellSize, alignItems: "center", justifyContent: "center" },
721
- { backgroundColor: isLight ? "#f0d9b5" : "#b58863" },
722
- isSelected && styles2.cellSelected,
723
- moveTarget && styles2.cellValidMove
724
- ],
725
- children: [
726
- piece && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [
727
- styles2.piece,
728
- { width: cellSize * 0.78, height: cellSize * 0.78 },
729
- piece.player === "red" ? styles2.pieceRed : styles2.pieceBlack,
730
- isSelected && styles2.pieceSelected
731
- ], children: piece.type === "king" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: { fontSize: cellSize * 0.35 }, children: "\u{1F451}" }) }),
732
- moveTarget && !piece && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [
733
- styles2.moveDot,
734
- { width: cellSize * 0.3, height: cellSize * 0.3, borderRadius: cellSize * 0.15 },
735
- isCapture ? styles2.moveDotCapture : styles2.moveDotNormal
736
- ] }),
737
- moveTarget && piece && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [import_react_native6.StyleSheet.absoluteFill, styles2.captureRing, { borderRadius: cellSize / 2 }] })
738
- ]
739
- },
740
- col
741
- );
742
- }) }, row)) }),
743
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles2.playerRow, children: [
744
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [styles2.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
810
+ boardEl,
811
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: s.playerRow, children: [
812
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [s.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
745
813
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: { flex: 1, marginLeft: 10 }, children: [
746
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.playerName, children: myPlayer.displayName }),
747
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles2.playerColor, { color: myColor === "red" ? "#f87171" : "#94a3b8" }], children: myColor })
814
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.playerName, children: myPlayer.displayName }),
815
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [s.playerColor, { color: myColor === "red" ? "#f87171" : "#94a3b8" }], children: myColor })
748
816
  ] }),
749
- isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.turnIndicator, children: "\u25CF your turn" })
817
+ isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.turnIndicator, children: "\u25CF your turn" })
750
818
  ] }),
751
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles2.actions, children: [
819
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: s.actions, children: [
752
820
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
753
821
  import_react_native6.TouchableOpacity,
754
822
  {
755
- style: styles2.btnResign,
823
+ style: s.btnResign,
756
824
  disabled: gameOver,
757
- onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
758
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.btnResignText, children: "Resign" })
825
+ onPress: () => game.socket?.emit("game:resign", { roomId: game.roomId, playerId: game.myPlayerId }),
826
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.btnResignText, children: "Resign" })
759
827
  }
760
828
  ),
761
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.TouchableOpacity, { style: styles2.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.btnLeaveText, children: "Leave" }) })
829
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.TouchableOpacity, { style: s.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: s.btnLeaveText, children: "Leave" }) })
762
830
  ] }),
763
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: styles2.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles2.gameOverBox, children: [
764
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
765
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
766
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.gameOverReason, children: gameResult?.reason }),
767
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.TouchableOpacity, { style: styles2.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.btnPlayAgainText, children: "Play again" }) })
768
- ] }) }) })
831
+ gameOverModal
769
832
  ] });
770
833
  }
771
- var styles2 = import_react_native6.StyleSheet.create({
834
+ var s = import_react_native6.StyleSheet.create({
772
835
  container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
773
836
  playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
774
837
  avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
@@ -804,48 +867,35 @@ var styles2 = import_react_native6.StyleSheet.create({
804
867
  });
805
868
 
806
869
  // src/components/connect4/index.tsx
807
- var import_react6 = require("react");
870
+ var import_react7 = require("react");
808
871
  var import_react_native7 = require("react-native");
809
- var import_socket4 = require("socket.io-client");
872
+ var import_socket5 = require("socket.io-client");
810
873
  var import_jsx_runtime7 = require("react/jsx-runtime");
811
- function Connect4Board({ style, showAfkWarning = true, onLeave }) {
874
+ var ROWS = 6;
875
+ var COLS = 7;
876
+ function useConnect4Game() {
812
877
  const { token, serverUrl, session } = useBetaGamer();
813
878
  const myPlayer = session.players[0];
814
- const socketRef = (0, import_react6.useRef)(null);
815
- const roomIdRef = (0, import_react6.useRef)(null);
816
- const myPlayerIdRef = (0, import_react6.useRef)(myPlayer?.id ?? "");
817
- const afkTimerRef = (0, import_react6.useRef)(null);
818
- const ROWS = 6;
819
- const COLS = 7;
820
- const [board, setBoard] = (0, import_react6.useState)(Array(ROWS * COLS).fill(null));
821
- const [myColor, setMyColor] = (0, import_react6.useState)("red");
822
- const [currentTurn, setCurrentTurn] = (0, import_react6.useState)(0);
823
- const [players, setPlayers] = (0, import_react6.useState)([]);
824
- const [lastMove, setLastMove] = (0, import_react6.useState)(null);
825
- const [winningCells, setWinningCells] = (0, import_react6.useState)(null);
826
- const [gameOver, setGameOver] = (0, import_react6.useState)(false);
827
- const [gameResult, setGameResult] = (0, import_react6.useState)(null);
828
- const [afkWarning, setAfkWarning] = (0, import_react6.useState)(null);
829
- if (session.game !== "connect4") return null;
830
- const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
831
- const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
832
- const opponentColor = myColor === "red" ? "yellow" : "red";
833
- (0, import_react6.useEffect)(() => {
834
- const s = (0, import_socket4.io)(`${serverUrl}/connect4`, {
835
- auth: { token },
836
- path: "/socket.io",
837
- transports: ["websocket", "polling"]
838
- });
839
- socketRef.current = s;
840
- const join = () => s.emit("matchmaking:join", {
841
- username: myPlayer.displayName,
842
- playerId: myPlayer.id,
843
- wantsBot: false,
844
- sessionId: session.sessionId
845
- });
846
- if (s.connected) join();
847
- else s.once("connect", join);
848
- s.on("game:started", (d) => {
879
+ const socketRef = (0, import_react7.useRef)(null);
880
+ const roomIdRef = (0, import_react7.useRef)(null);
881
+ const myPlayerIdRef = (0, import_react7.useRef)(myPlayer?.id ?? "");
882
+ const afkTimerRef = (0, import_react7.useRef)(null);
883
+ const [board, setBoard] = (0, import_react7.useState)(Array(ROWS * COLS).fill(null));
884
+ const [myColor, setMyColor] = (0, import_react7.useState)("red");
885
+ const [currentTurn, setCurrentTurn] = (0, import_react7.useState)(0);
886
+ const [players, setPlayers] = (0, import_react7.useState)([]);
887
+ const [lastMove, setLastMove] = (0, import_react7.useState)(null);
888
+ const [winningCells, setWinningCells] = (0, import_react7.useState)(null);
889
+ const [gameOver, setGameOver] = (0, import_react7.useState)(false);
890
+ const [gameResult, setGameResult] = (0, import_react7.useState)(null);
891
+ const [afkWarning, setAfkWarning] = (0, import_react7.useState)(null);
892
+ (0, import_react7.useEffect)(() => {
893
+ const s4 = (0, import_socket5.io)(`${serverUrl}/connect4`, { auth: { token }, path: "/socket.io", transports: ["websocket", "polling"] });
894
+ socketRef.current = s4;
895
+ const join = () => s4.emit("matchmaking:join", { username: myPlayer.displayName, playerId: myPlayer.id, wantsBot: session.matchType === "bot", sessionId: session.sessionId });
896
+ if (s4.connected) join();
897
+ else s4.once("connect", join);
898
+ s4.on("game:started", (d) => {
849
899
  roomIdRef.current = d.roomId;
850
900
  myPlayerIdRef.current = d.playerId;
851
901
  setMyColor(d.color ?? "red");
@@ -853,7 +903,7 @@ function Connect4Board({ style, showAfkWarning = true, onLeave }) {
853
903
  setCurrentTurn(d.currentTurn ?? 0);
854
904
  setPlayers(d.players ?? []);
855
905
  });
856
- s.on("game:move:made", (d) => {
906
+ s4.on("game:move:made", (d) => {
857
907
  setBoard(d.board);
858
908
  setCurrentTurn(d.currentTurn ?? 0);
859
909
  if (d.lastMove != null) setLastMove(d.lastMove);
@@ -864,7 +914,7 @@ function Connect4Board({ style, showAfkWarning = true, onLeave }) {
864
914
  }
865
915
  setAfkWarning(null);
866
916
  });
867
- s.on("connect4:afk_warning", (d) => {
917
+ s4.on("connect4:afk_warning", (d) => {
868
918
  setAfkWarning(d);
869
919
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
870
920
  afkTimerRef.current = setInterval(() => {
@@ -872,129 +922,138 @@ function Connect4Board({ style, showAfkWarning = true, onLeave }) {
872
922
  if (!prev || prev.secondsRemaining <= 1) {
873
923
  clearInterval(afkTimerRef.current);
874
924
  afkTimerRef.current = null;
875
- if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
925
+ if (roomIdRef.current) s4.emit("afk:check", { roomId: roomIdRef.current });
876
926
  return prev;
877
927
  }
878
928
  return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
879
929
  });
880
930
  }, 1e3);
881
931
  });
882
- s.on("connect4:afk_warning_cleared", () => {
932
+ s4.on("connect4:afk_warning_cleared", () => {
883
933
  if (afkTimerRef.current) {
884
934
  clearInterval(afkTimerRef.current);
885
935
  afkTimerRef.current = null;
886
936
  }
887
937
  setAfkWarning(null);
888
938
  });
889
- s.on("afk:status", (status) => {
939
+ s4.on("afk:status", (status) => {
890
940
  if (!status) {
891
941
  if (afkTimerRef.current) {
892
942
  clearInterval(afkTimerRef.current);
893
943
  afkTimerRef.current = null;
894
944
  }
895
945
  setAfkWarning(null);
896
- } else {
897
- setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
898
- }
946
+ } else setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
899
947
  });
900
- s.on("game:over", (d) => {
948
+ s4.on("game:over", (d) => {
901
949
  if (d.winningCells) setWinningCells(d.winningCells);
902
950
  setGameResult({ winner: d.winner ?? null, reason: d.reason });
903
951
  setTimeout(() => setGameOver(true), 400);
904
952
  });
905
953
  return () => {
906
954
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
907
- s.disconnect();
955
+ s4.disconnect();
908
956
  socketRef.current = null;
909
957
  };
910
958
  }, [token, serverUrl]);
959
+ const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
911
960
  const handleColumnPress = (col) => {
912
- if (!isMyTurn || gameOver) return;
913
- if (board[col] !== null) return;
961
+ if (!isMyTurn || gameOver || board[col] !== null) return;
914
962
  socketRef.current?.emit("game:move", { roomId: roomIdRef.current, column: col, playerId: myPlayerIdRef.current });
915
963
  };
964
+ return { socket: socketRef.current, roomId: roomIdRef.current, myPlayerId: myPlayerIdRef.current, myColor, board, currentTurn, players, lastMove, winningCells, gameOver, gameResult, afkWarning, isMyTurn, handleColumnPress };
965
+ }
966
+ function Connect4Board({ style, layout = "board-only", showAfkWarning = true, onLeave }) {
967
+ const { session } = useBetaGamer();
968
+ if (session.game !== "connect4") return null;
969
+ const game = useConnect4Game();
970
+ const { myColor, board, lastMove, winningCells, gameOver, gameResult, afkWarning, isMyTurn, handleColumnPress } = game;
971
+ const myPlayer = session.players[0];
916
972
  const screenWidth = import_react_native7.Dimensions.get("window").width;
917
973
  const boardWidth = screenWidth - 32;
918
974
  const cellSize = boardWidth / COLS;
919
975
  const ColorDot = ({ color, size = 12 }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: color === "red" ? "#ef4444" : "#eab308", marginRight: 6 } });
920
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: [styles3.container, style], children: [
921
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles3.playerRow, children: [
922
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles3.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.avatarText, children: opponentName[0]?.toUpperCase() }) }),
976
+ const boardEl = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: { width: boardWidth, backgroundColor: "#2563eb", borderRadius: 12, padding: 6 }, children: [
977
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { flexDirection: "row" }, children: Array.from({ length: COLS }, (_, col) => {
978
+ const canDrop = isMyTurn && !gameOver && board[col] === null;
979
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
980
+ import_react_native7.TouchableOpacity,
981
+ {
982
+ onPress: () => handleColumnPress(col),
983
+ activeOpacity: 0.7,
984
+ style: { width: cellSize, height: 28, alignItems: "center", justifyContent: "center" },
985
+ children: canDrop && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: { color: "#fff", fontSize: 14 }, children: "\u25BC" })
986
+ },
987
+ col
988
+ );
989
+ }) }),
990
+ Array.from({ length: ROWS }, (_, row) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { flexDirection: "row" }, children: Array.from({ length: COLS }, (_2, col) => {
991
+ const pos = row * COLS + col;
992
+ const piece = board[pos];
993
+ const isWinning = winningCells?.includes(pos);
994
+ const isLast = lastMove === pos;
995
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { width: cellSize, height: cellSize, padding: 3 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: [s2.cell, isWinning && s2.cellWin], children: piece && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: [s2.piece, piece === "red" ? s2.pieceRed : s2.pieceYellow, isWinning && s2.pieceWin, isLast && !isWinning && s2.pieceLast] }) }) }, col);
996
+ }) }, row))
997
+ ] });
998
+ const gameOverModal = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: s2.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: s2.gameOverBox, children: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === game.myPlayerId ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
1000
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.gameOverTitle, children: gameResult?.winner === game.myPlayerId ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.gameOverReason, children: gameResult?.reason }),
1002
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.TouchableOpacity, { style: s2.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.btnPlayAgainText, children: "Play again" }) })
1003
+ ] }) }) });
1004
+ if (layout === "board-only") return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style, children: [
1005
+ boardEl,
1006
+ gameOverModal
1007
+ ] });
1008
+ const opponentName = game.players.find((p) => p.id !== game.myPlayerId)?.username ?? "Waiting\u2026";
1009
+ const opponentColor = myColor === "red" ? "yellow" : "red";
1010
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: [s2.container, style], children: [
1011
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: s2.playerRow, children: [
1012
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: s2.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.avatarText, children: opponentName[0]?.toUpperCase() }) }),
923
1013
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: { flex: 1, marginLeft: 10, flexDirection: "row", alignItems: "center" }, children: [
924
1014
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ColorDot, { color: opponentColor }),
925
1015
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { children: [
926
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.playerName, children: opponentName }),
927
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.playerColor, children: opponentColor })
1016
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.playerName, children: opponentName }),
1017
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.playerColor, children: opponentColor })
928
1018
  ] })
929
1019
  ] }),
930
- !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.turnIndicator, children: "\u25CF thinking" })
1020
+ !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.turnIndicator, children: "\u25CF thinking" })
931
1021
  ] }),
932
- showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: [styles3.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles3.afkSelf : styles3.afkOpponent], children: [
933
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F Drop a piece!" : "\u23F3 Opponent is AFK" }),
934
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.Text, { style: styles3.afkText, children: [
1022
+ showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: [s2.afkBanner, afkWarning.playerId === game.myPlayerId ? s2.afkSelf : s2.afkOpponent], children: [
1023
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.afkText, children: afkWarning.playerId === game.myPlayerId ? "\u26A0\uFE0F Drop a piece!" : "\u23F3 Opponent is AFK" }),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.Text, { style: s2.afkText, children: [
935
1025
  afkWarning.secondsRemaining,
936
1026
  "s"
937
1027
  ] })
938
1028
  ] }),
939
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: { width: boardWidth, backgroundColor: "#2563eb", borderRadius: 12, padding: 6 }, children: [
940
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { flexDirection: "row" }, children: Array.from({ length: COLS }, (_, col) => {
941
- const canDrop = isMyTurn && !gameOver && board[col] === null;
942
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
943
- import_react_native7.TouchableOpacity,
944
- {
945
- onPress: () => handleColumnPress(col),
946
- activeOpacity: 0.7,
947
- style: { width: cellSize, height: 28, alignItems: "center", justifyContent: "center" },
948
- children: canDrop && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: { color: "#fff", fontSize: 14 }, children: "\u25BC" })
949
- },
950
- col
951
- );
952
- }) }),
953
- Array.from({ length: ROWS }, (_, row) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { flexDirection: "row" }, children: Array.from({ length: COLS }, (_2, col) => {
954
- const pos = row * COLS + col;
955
- const piece = board[pos];
956
- const isWinning = winningCells?.includes(pos);
957
- const isLast = lastMove === pos;
958
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: { width: cellSize, height: cellSize, padding: 3 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: [styles3.cell, isWinning && styles3.cellWin], children: piece && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: [
959
- styles3.piece,
960
- piece === "red" ? styles3.pieceRed : styles3.pieceYellow,
961
- isWinning && styles3.pieceWin,
962
- isLast && !isWinning && styles3.pieceLast
963
- ] }) }) }, col);
964
- }) }, row))
965
- ] }),
966
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles3.playerRow, children: [
967
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: [styles3.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
1029
+ boardEl,
1030
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: s2.playerRow, children: [
1031
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: [s2.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
968
1032
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: { flex: 1, marginLeft: 10, flexDirection: "row", alignItems: "center" }, children: [
969
1033
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ColorDot, { color: myColor }),
970
1034
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { children: [
971
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.playerName, children: myPlayer.displayName }),
972
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.playerColor, children: myColor })
1035
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.playerName, children: myPlayer.displayName }),
1036
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.playerColor, children: myColor })
973
1037
  ] })
974
1038
  ] }),
975
- isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.turnIndicator, children: "\u25CF your turn" })
1039
+ isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.turnIndicator, children: "\u25CF your turn" })
976
1040
  ] }),
977
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles3.actions, children: [
1041
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: s2.actions, children: [
978
1042
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
979
1043
  import_react_native7.TouchableOpacity,
980
1044
  {
981
- style: styles3.btnResign,
1045
+ style: s2.btnResign,
982
1046
  disabled: gameOver,
983
- onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
984
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.btnResignText, children: "Resign" })
1047
+ onPress: () => game.socket?.emit("game:resign", { roomId: game.roomId, playerId: game.myPlayerId }),
1048
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.btnResignText, children: "Resign" })
985
1049
  }
986
1050
  ),
987
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.TouchableOpacity, { style: styles3.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.btnLeaveText, children: "Leave" }) })
1051
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.TouchableOpacity, { style: s2.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: s2.btnLeaveText, children: "Leave" }) })
988
1052
  ] }),
989
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles3.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles3.gameOverBox, children: [
990
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
991
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
992
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.gameOverReason, children: gameResult?.reason }),
993
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.TouchableOpacity, { style: styles3.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.btnPlayAgainText, children: "Play again" }) })
994
- ] }) }) })
1053
+ gameOverModal
995
1054
  ] });
996
1055
  }
997
- var styles3 = import_react_native7.StyleSheet.create({
1056
+ var s2 = import_react_native7.StyleSheet.create({
998
1057
  container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
999
1058
  playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
1000
1059
  avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
@@ -1005,8 +1064,8 @@ var styles3 = import_react_native7.StyleSheet.create({
1005
1064
  cell: { flex: 1, borderRadius: 999, backgroundColor: "#1e3a5f", alignItems: "center", justifyContent: "center" },
1006
1065
  cellWin: { backgroundColor: "#15803d" },
1007
1066
  piece: { width: "85%", height: "85%", borderRadius: 999 },
1008
- pieceRed: { backgroundColor: "#ef4444", shadowColor: "#ef4444", shadowOpacity: 0.5, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } },
1009
- pieceYellow: { backgroundColor: "#eab308", shadowColor: "#eab308", shadowOpacity: 0.5, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } },
1067
+ pieceRed: { backgroundColor: "#ef4444" },
1068
+ pieceYellow: { backgroundColor: "#eab308" },
1010
1069
  pieceWin: { transform: [{ scale: 1.1 }] },
1011
1070
  pieceLast: { borderWidth: 2, borderColor: "#22d3ee" },
1012
1071
  afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
@@ -1027,44 +1086,32 @@ var styles3 = import_react_native7.StyleSheet.create({
1027
1086
  });
1028
1087
 
1029
1088
  // src/components/tictactoe/index.tsx
1030
- var import_react7 = require("react");
1089
+ var import_react8 = require("react");
1031
1090
  var import_react_native8 = require("react-native");
1032
- var import_socket5 = require("socket.io-client");
1091
+ var import_socket6 = require("socket.io-client");
1033
1092
  var import_jsx_runtime8 = require("react/jsx-runtime");
1034
- function TictactoeBoard({ style, showAfkWarning = true, onLeave }) {
1093
+ function useTictactoeGame() {
1035
1094
  const { token, serverUrl, session } = useBetaGamer();
1036
1095
  const myPlayer = session.players[0];
1037
- const socketRef = (0, import_react7.useRef)(null);
1038
- const roomIdRef = (0, import_react7.useRef)(null);
1039
- const myPlayerIdRef = (0, import_react7.useRef)(myPlayer?.id ?? "");
1040
- const afkTimerRef = (0, import_react7.useRef)(null);
1041
- const [board, setBoard] = (0, import_react7.useState)(Array(9).fill(null));
1042
- const [myMark, setMyMark] = (0, import_react7.useState)("X");
1043
- const [currentTurn, setCurrentTurn] = (0, import_react7.useState)(0);
1044
- const [players, setPlayers] = (0, import_react7.useState)([]);
1045
- const [winningLine, setWinningLine] = (0, import_react7.useState)(null);
1046
- const [gameOver, setGameOver] = (0, import_react7.useState)(false);
1047
- const [gameResult, setGameResult] = (0, import_react7.useState)(null);
1048
- const [afkWarning, setAfkWarning] = (0, import_react7.useState)(null);
1049
- if (session.game !== "tictactoe") return null;
1050
- const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
1051
- const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
1052
- (0, import_react7.useEffect)(() => {
1053
- const s = (0, import_socket5.io)(`${serverUrl}/tictactoe`, {
1054
- auth: { token },
1055
- path: "/socket.io",
1056
- transports: ["websocket", "polling"]
1057
- });
1058
- socketRef.current = s;
1059
- const join = () => s.emit("matchmaking:join", {
1060
- username: myPlayer.displayName,
1061
- playerId: myPlayer.id,
1062
- wantsBot: false,
1063
- sessionId: session.sessionId
1064
- });
1065
- if (s.connected) join();
1066
- else s.once("connect", join);
1067
- s.on("game:started", (d) => {
1096
+ const socketRef = (0, import_react8.useRef)(null);
1097
+ const roomIdRef = (0, import_react8.useRef)(null);
1098
+ const myPlayerIdRef = (0, import_react8.useRef)(myPlayer?.id ?? "");
1099
+ const afkTimerRef = (0, import_react8.useRef)(null);
1100
+ const [board, setBoard] = (0, import_react8.useState)(Array(9).fill(null));
1101
+ const [myMark, setMyMark] = (0, import_react8.useState)("X");
1102
+ const [currentTurn, setCurrentTurn] = (0, import_react8.useState)(0);
1103
+ const [players, setPlayers] = (0, import_react8.useState)([]);
1104
+ const [winningLine, setWinningLine] = (0, import_react8.useState)(null);
1105
+ const [gameOver, setGameOver] = (0, import_react8.useState)(false);
1106
+ const [gameResult, setGameResult] = (0, import_react8.useState)(null);
1107
+ const [afkWarning, setAfkWarning] = (0, import_react8.useState)(null);
1108
+ (0, import_react8.useEffect)(() => {
1109
+ const s4 = (0, import_socket6.io)(`${serverUrl}/tictactoe`, { auth: { token }, path: "/socket.io", transports: ["websocket", "polling"] });
1110
+ socketRef.current = s4;
1111
+ const join = () => s4.emit("matchmaking:join", { username: myPlayer.displayName, playerId: myPlayer.id, wantsBot: session.matchType === "bot", sessionId: session.sessionId });
1112
+ if (s4.connected) join();
1113
+ else s4.once("connect", join);
1114
+ s4.on("game:started", (d) => {
1068
1115
  roomIdRef.current = d.roomId;
1069
1116
  myPlayerIdRef.current = d.playerId;
1070
1117
  setMyMark(d.mark);
@@ -1072,7 +1119,7 @@ function TictactoeBoard({ style, showAfkWarning = true, onLeave }) {
1072
1119
  setCurrentTurn(d.currentTurn ?? 0);
1073
1120
  setPlayers(d.players ?? []);
1074
1121
  });
1075
- s.on("game:move:made", (d) => {
1122
+ s4.on("game:move:made", (d) => {
1076
1123
  setBoard(d.board);
1077
1124
  setCurrentTurn(d.currentTurn ?? 0);
1078
1125
  if (d.winningLine) setWinningLine(d.winningLine);
@@ -1082,7 +1129,7 @@ function TictactoeBoard({ style, showAfkWarning = true, onLeave }) {
1082
1129
  }
1083
1130
  setAfkWarning(null);
1084
1131
  });
1085
- s.on("tictactoe:afk_warning", (d) => {
1132
+ s4.on("tictactoe:afk_warning", (d) => {
1086
1133
  setAfkWarning(d);
1087
1134
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
1088
1135
  afkTimerRef.current = setInterval(() => {
@@ -1090,117 +1137,124 @@ function TictactoeBoard({ style, showAfkWarning = true, onLeave }) {
1090
1137
  if (!prev || prev.secondsRemaining <= 1) {
1091
1138
  clearInterval(afkTimerRef.current);
1092
1139
  afkTimerRef.current = null;
1093
- if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
1140
+ if (roomIdRef.current) s4.emit("afk:check", { roomId: roomIdRef.current });
1094
1141
  return prev;
1095
1142
  }
1096
1143
  return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
1097
1144
  });
1098
1145
  }, 1e3);
1099
1146
  });
1100
- s.on("tictactoe:afk_warning_cleared", () => {
1147
+ s4.on("tictactoe:afk_warning_cleared", () => {
1101
1148
  if (afkTimerRef.current) {
1102
1149
  clearInterval(afkTimerRef.current);
1103
1150
  afkTimerRef.current = null;
1104
1151
  }
1105
1152
  setAfkWarning(null);
1106
1153
  });
1107
- s.on("afk:status", (status) => {
1154
+ s4.on("afk:status", (status) => {
1108
1155
  if (!status) {
1109
1156
  if (afkTimerRef.current) {
1110
1157
  clearInterval(afkTimerRef.current);
1111
1158
  afkTimerRef.current = null;
1112
1159
  }
1113
1160
  setAfkWarning(null);
1114
- } else {
1115
- setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
1116
- }
1161
+ } else setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
1117
1162
  });
1118
- s.on("game:over", (d) => {
1163
+ s4.on("game:over", (d) => {
1119
1164
  setGameResult({ winner: d.winner ?? null, reason: d.reason });
1120
1165
  setTimeout(() => setGameOver(true), 400);
1121
1166
  });
1122
1167
  return () => {
1123
1168
  if (afkTimerRef.current) clearInterval(afkTimerRef.current);
1124
- s.disconnect();
1169
+ s4.disconnect();
1125
1170
  socketRef.current = null;
1126
1171
  };
1127
1172
  }, [token, serverUrl]);
1173
+ const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
1128
1174
  const handleCellPress = (idx) => {
1129
1175
  if (!isMyTurn || gameOver || board[idx]) return;
1130
1176
  socketRef.current?.emit("game:move", { roomId: roomIdRef.current, position: idx, playerId: myPlayerIdRef.current });
1131
1177
  };
1178
+ return { socket: socketRef.current, roomId: roomIdRef.current, myPlayerId: myPlayerIdRef.current, myMark, board, currentTurn, players, winningLine, gameOver, gameResult, afkWarning, isMyTurn, handleCellPress };
1179
+ }
1180
+ function TictactoeBoard({ style, layout = "board-only", showAfkWarning = true, onLeave }) {
1181
+ const { session } = useBetaGamer();
1182
+ if (session.game !== "tictactoe") return null;
1183
+ const game = useTictactoeGame();
1184
+ const { myMark, board, winningLine, gameOver, gameResult, afkWarning, isMyTurn, handleCellPress } = game;
1185
+ const myPlayer = session.players[0];
1132
1186
  const screenWidth = import_react_native8.Dimensions.get("window").width;
1133
1187
  const boardSize = screenWidth - 32;
1134
1188
  const cellSize = boardSize / 3;
1135
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: [styles4.container, style], children: [
1136
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles4.playerRow, children: [
1137
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: styles4.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.avatarText, children: opponentName[0]?.toUpperCase() }) }),
1189
+ const boardEl = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: { width: boardSize, height: boardSize, flexDirection: "row", flexWrap: "wrap" }, children: board.map((cell, idx) => {
1190
+ const isWinning = winningLine?.includes(idx);
1191
+ const isEmpty = cell === null;
1192
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1193
+ import_react_native8.TouchableOpacity,
1194
+ {
1195
+ onPress: () => handleCellPress(idx),
1196
+ activeOpacity: 0.8,
1197
+ style: [
1198
+ { width: cellSize, height: cellSize, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: "#1e1b4b" },
1199
+ isWinning ? s3.cellWin : isEmpty && isMyTurn && !gameOver ? s3.cellActive : s3.cellIdle
1200
+ ],
1201
+ children: cell ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: [s3.mark, cell === "X" ? s3.markX : s3.markO, isWinning && s3.markWin], children: cell === "X" ? "\u2715" : "\u25CB" }) : isMyTurn && !gameOver ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: [s3.mark, myMark === "X" ? s3.markX : s3.markO, { opacity: 0.2 }], children: myMark === "X" ? "\u2715" : "\u25CB" }) : null
1202
+ },
1203
+ idx
1204
+ );
1205
+ }) });
1206
+ const gameOverModal = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: s3.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: s3.gameOverBox, children: [
1207
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === game.myPlayerId ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
1208
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.gameOverTitle, children: gameResult?.winner === game.myPlayerId ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.gameOverReason, children: gameResult?.reason }),
1210
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.TouchableOpacity, { style: s3.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.btnPlayAgainText, children: "Play again" }) })
1211
+ ] }) }) });
1212
+ if (layout === "board-only") return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style, children: [
1213
+ boardEl,
1214
+ gameOverModal
1215
+ ] });
1216
+ const opponentName = game.players.find((p) => p.id !== game.myPlayerId)?.username ?? "Waiting\u2026";
1217
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: [s3.container, style], children: [
1218
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: s3.playerRow, children: [
1219
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: s3.avatar, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.avatarText, children: opponentName[0]?.toUpperCase() }) }),
1138
1220
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: { flex: 1, marginLeft: 10 }, children: [
1139
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.playerName, children: opponentName }),
1140
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.playerColor, children: myMark === "X" ? "O" : "X" })
1221
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.playerName, children: opponentName }),
1222
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.playerColor, children: myMark === "X" ? "O" : "X" })
1141
1223
  ] }),
1142
- !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.turnIndicator, children: "\u25CF thinking" })
1224
+ !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.turnIndicator, children: "\u25CF thinking" })
1143
1225
  ] }),
1144
- showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: [styles4.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles4.afkSelf : styles4.afkOpponent], children: [
1145
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F Make a move!" : "\u23F3 Opponent is AFK" }),
1146
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Text, { style: styles4.afkText, children: [
1226
+ showAfkWarning && afkWarning && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: [s3.afkBanner, afkWarning.playerId === game.myPlayerId ? s3.afkSelf : s3.afkOpponent], children: [
1227
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.afkText, children: afkWarning.playerId === game.myPlayerId ? "\u26A0\uFE0F Make a move!" : "\u23F3 Opponent is AFK" }),
1228
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Text, { style: s3.afkText, children: [
1147
1229
  afkWarning.secondsRemaining,
1148
1230
  "s"
1149
1231
  ] })
1150
1232
  ] }),
1151
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: { width: boardSize, height: boardSize, flexDirection: "row", flexWrap: "wrap" }, children: board.map((cell, idx) => {
1152
- const isWinning = winningLine?.includes(idx);
1153
- const isEmpty = cell === null;
1154
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1155
- import_react_native8.TouchableOpacity,
1156
- {
1157
- onPress: () => handleCellPress(idx),
1158
- activeOpacity: 0.8,
1159
- style: [
1160
- {
1161
- width: cellSize,
1162
- height: cellSize,
1163
- alignItems: "center",
1164
- justifyContent: "center",
1165
- borderWidth: 2,
1166
- borderColor: "#1e1b4b"
1167
- },
1168
- isWinning ? styles4.cellWin : isEmpty && isMyTurn && !gameOver ? styles4.cellActive : styles4.cellIdle
1169
- ],
1170
- children: cell ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: [styles4.mark, cell === "X" ? styles4.markX : styles4.markO, isWinning && styles4.markWin], children: cell === "X" ? "\u2715" : "\u25CB" }) : isMyTurn && !gameOver ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: [styles4.mark, myMark === "X" ? styles4.markX : styles4.markO, { opacity: 0.2 }], children: myMark === "X" ? "\u2715" : "\u25CB" }) : null
1171
- },
1172
- idx
1173
- );
1174
- }) }),
1175
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles4.playerRow, children: [
1176
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: [styles4.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
1233
+ boardEl,
1234
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: s3.playerRow, children: [
1235
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: [s3.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
1177
1236
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: { flex: 1, marginLeft: 10 }, children: [
1178
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.playerName, children: myPlayer.displayName }),
1179
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.playerColor, children: myMark })
1237
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.playerName, children: myPlayer.displayName }),
1238
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.playerColor, children: myMark })
1180
1239
  ] }),
1181
- isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.turnIndicator, children: "\u25CF your turn" })
1240
+ isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.turnIndicator, children: "\u25CF your turn" })
1182
1241
  ] }),
1183
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles4.actions, children: [
1242
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: s3.actions, children: [
1184
1243
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1185
1244
  import_react_native8.TouchableOpacity,
1186
1245
  {
1187
- style: styles4.btnResign,
1246
+ style: s3.btnResign,
1188
1247
  disabled: gameOver,
1189
- onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
1190
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.btnResignText, children: "Resign" })
1248
+ onPress: () => game.socket?.emit("game:resign", { roomId: game.roomId, playerId: game.myPlayerId }),
1249
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.btnResignText, children: "Resign" })
1191
1250
  }
1192
1251
  ),
1193
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.TouchableOpacity, { style: styles4.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.btnLeaveText, children: "Leave" }) })
1252
+ onLeave && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.TouchableOpacity, { style: s3.btnLeave, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: s3.btnLeaveText, children: "Leave" }) })
1194
1253
  ] }),
1195
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: styles4.modalOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles4.gameOverBox, children: [
1196
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
1197
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
1198
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.gameOverReason, children: gameResult?.reason }),
1199
- onLeave && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.TouchableOpacity, { style: styles4.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.btnPlayAgainText, children: "Play again" }) })
1200
- ] }) }) })
1254
+ gameOverModal
1201
1255
  ] });
1202
1256
  }
1203
- var styles4 = import_react_native8.StyleSheet.create({
1257
+ var s3 = import_react_native8.StyleSheet.create({
1204
1258
  container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
1205
1259
  playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
1206
1260
  avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
@@ -1233,16 +1287,16 @@ var styles4 = import_react_native8.StyleSheet.create({
1233
1287
  });
1234
1288
 
1235
1289
  // src/components/subway-runner/index.tsx
1236
- var import_react9 = require("react");
1290
+ var import_react10 = require("react");
1237
1291
  var import_react_native9 = require("react-native");
1238
1292
 
1239
1293
  // src/components/GameWebView.tsx
1240
- var import_react8 = require("react");
1294
+ var import_react9 = require("react");
1241
1295
  var import_react_native_webview = require("react-native-webview");
1242
1296
  var import_jsx_runtime9 = require("react/jsx-runtime");
1243
1297
  function GameWebView({ game, style, showAfkWarning = true }) {
1244
1298
  const { token, serverUrl, session } = useBetaGamer();
1245
- const webViewRef = (0, import_react8.useRef)(null);
1299
+ const webViewRef = (0, import_react9.useRef)(null);
1246
1300
  if (session.game !== game) return null;
1247
1301
  const uri = showAfkWarning ? `${serverUrl}/embed/${game}` : `${serverUrl}/embed/${game}?afkWarning=0`;
1248
1302
  const initScript = `
@@ -1270,10 +1324,10 @@ function SubwayRunnerGame({ style }) {
1270
1324
  }
1271
1325
  function SubwayRunnerScore({ style, textStyle }) {
1272
1326
  const socket = useSocket();
1273
- const [score, setScore] = (0, import_react9.useState)(0);
1274
- (0, import_react9.useEffect)(() => {
1327
+ const [score, setScore] = (0, import_react10.useState)(0);
1328
+ (0, import_react10.useEffect)(() => {
1275
1329
  if (!socket) return;
1276
- socket.on("runner:score", (s) => setScore(s));
1330
+ socket.on("runner:score", (s4) => setScore(s4));
1277
1331
  return () => {
1278
1332
  socket.off("runner:score");
1279
1333
  };
@@ -1282,8 +1336,8 @@ function SubwayRunnerScore({ style, textStyle }) {
1282
1336
  }
1283
1337
  function SubwayRunnerLives({ style, lifeStyle }) {
1284
1338
  const socket = useSocket();
1285
- const [lives, setLives] = (0, import_react9.useState)(3);
1286
- (0, import_react9.useEffect)(() => {
1339
+ const [lives, setLives] = (0, import_react10.useState)(3);
1340
+ (0, import_react10.useEffect)(() => {
1287
1341
  if (!socket) return;
1288
1342
  socket.on("runner:lives", (l) => setLives(l));
1289
1343
  return () => {
@@ -1306,8 +1360,13 @@ function SubwayRunnerLives({ style, lifeStyle }) {
1306
1360
  TictactoeBoard,
1307
1361
  Timer,
1308
1362
  useBetaGamer,
1363
+ useCheckersGame,
1364
+ useChessGame,
1365
+ useConnect,
1366
+ useConnect4Game,
1309
1367
  useGameState,
1310
1368
  useSession,
1311
1369
  useSocket,
1312
- useTheme
1370
+ useTheme,
1371
+ useTictactoeGame
1313
1372
  });