@beta-gamer/react-native 0.1.18 → 0.1.21

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
@@ -88,7 +88,7 @@ function decodeToken(token) {
88
88
  return JSON.parse(base64Decode(parts[1]));
89
89
  }
90
90
  var BetaGamerContext = (0, import_react.createContext)(null);
91
- function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", socketPath = "/socket.io", children }) {
91
+ function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", socketPath = "/socket.io", connectSocket = true, children }) {
92
92
  const session = decodeToken(token);
93
93
  const [socket, setSocket] = (0, import_react.useState)(null);
94
94
  const [gameState, setGameState] = (0, import_react.useState)({
@@ -96,6 +96,7 @@ function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", so
96
96
  players: session.players
97
97
  });
98
98
  (0, import_react.useEffect)(() => {
99
+ if (!connectSocket) return;
99
100
  const s = (0, import_socket.io)(`${serverUrl}/${session.game}`, {
100
101
  auth: { token },
101
102
  path: socketPath,
@@ -193,42 +194,328 @@ function Timer({ player, initialSeconds = 600, style, textStyle }) {
193
194
  ] }) });
194
195
  }
195
196
 
196
- // src/components/GameWebView.tsx
197
+ // src/components/chess/ChessBoard.tsx
197
198
  var import_react3 = require("react");
198
- var import_react_native_webview = require("react-native-webview");
199
+ var import_react_native4 = require("react-native");
200
+ var import_chess = require("chess.js");
201
+ var import_socket2 = require("socket.io-client");
199
202
  var import_jsx_runtime4 = require("react/jsx-runtime");
200
- function GameWebView({ game, style }) {
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 }) {
201
212
  const { token, serverUrl, session } = useBetaGamer();
202
- const webViewRef = (0, import_react3.useRef)(null);
203
- if (session.game !== game) return null;
204
- const initScript = `
205
- window.__BG_TOKEN__ = ${JSON.stringify(token)};
206
- window.postMessage(JSON.stringify({ type: 'bg:init', token: ${JSON.stringify(token)} }), '*');
207
- true;
208
- `;
209
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
210
- import_react_native_webview.WebView,
211
- {
212
- ref: webViewRef,
213
- source: { uri: `${serverUrl}/embed/${game}` },
214
- injectedJavaScriptBeforeContentLoaded: initScript,
215
- style: [{ flex: 1 }, style],
216
- allowsInlineMediaPlayback: true,
217
- mediaPlaybackRequiresUserAction: false
213
+ 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`, {
236
+ auth: { token },
237
+ path: "/socket.io",
238
+ transports: ["websocket", "polling"]
239
+ });
240
+ socketRef.current = s;
241
+ const join = () => s.emit("matchmaking:join", {
242
+ username: myPlayer.displayName,
243
+ playerId: myPlayer.id,
244
+ wantsBot: false,
245
+ sessionId: session.sessionId
246
+ });
247
+ if (s.connected) join();
248
+ else s.once("connect", join);
249
+ s.on("game:started", (d) => {
250
+ roomIdRef.current = d.roomId;
251
+ myPlayerIdRef.current = d.playerId;
252
+ setMyColor(d.color);
253
+ chessRef.current.load(d.fen);
254
+ setFen(d.fen);
255
+ setCurrentTurn(d.currentTurn ?? 0);
256
+ setPlayers(d.players ?? []);
257
+ if (d.playerTimers) {
258
+ const vals = Object.values(d.playerTimers);
259
+ setClocks({ self: Math.floor((vals[0] ?? 6e5) / 1e3), opponent: Math.floor((vals[1] ?? 6e5) / 1e3) });
260
+ }
261
+ });
262
+ s.on("game:move:made", (d) => {
263
+ chessRef.current.load(d.fen);
264
+ setFen(d.fen);
265
+ setCurrentTurn(d.currentTurn ?? 0);
266
+ if (d.move) {
267
+ setLastMove({ from: d.move.from, to: d.move.to });
268
+ if (d.move.san) setMoveHistory((prev) => [...prev, { san: d.move.san }]);
269
+ }
270
+ setSelected(null);
271
+ setLegalMoves([]);
272
+ if (afkTimerRef.current) {
273
+ clearInterval(afkTimerRef.current);
274
+ afkTimerRef.current = null;
275
+ }
276
+ setAfkWarning(null);
277
+ });
278
+ s.on("timer:update", (d) => {
279
+ if (!d.playerTimers) return;
280
+ Object.entries(d.playerTimers).forEach(([id, ms]) => {
281
+ if (id === myPlayerIdRef.current) setClocks((c) => ({ ...c, self: Math.floor(ms / 1e3) }));
282
+ else setClocks((c) => ({ ...c, opponent: Math.floor(ms / 1e3) }));
283
+ });
284
+ });
285
+ s.on("chess:afk_warning", (d) => {
286
+ setAfkWarning(d);
287
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
288
+ afkTimerRef.current = setInterval(() => {
289
+ setAfkWarning((prev) => {
290
+ if (!prev || prev.secondsRemaining <= 1) {
291
+ clearInterval(afkTimerRef.current);
292
+ afkTimerRef.current = null;
293
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
294
+ return prev;
295
+ }
296
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
297
+ });
298
+ }, 1e3);
299
+ });
300
+ s.on("chess:afk_warning_cleared", () => {
301
+ if (afkTimerRef.current) {
302
+ clearInterval(afkTimerRef.current);
303
+ afkTimerRef.current = null;
304
+ }
305
+ setAfkWarning(null);
306
+ });
307
+ s.on("afk:status", (status) => {
308
+ if (!status) {
309
+ if (afkTimerRef.current) {
310
+ clearInterval(afkTimerRef.current);
311
+ afkTimerRef.current = null;
312
+ }
313
+ setAfkWarning(null);
314
+ } else {
315
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
316
+ }
317
+ });
318
+ s.on("game:over", (d) => {
319
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
320
+ setTimeout(() => setGameOver(true), 400);
321
+ });
322
+ return () => {
323
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
324
+ s.disconnect();
325
+ socketRef.current = null;
326
+ };
327
+ }, [token, serverUrl]);
328
+ const handleSquarePress = (square) => {
329
+ if (gameOver || !isMyTurn) return;
330
+ const chess2 = chessRef.current;
331
+ if (selected) {
332
+ if (legalMoves.includes(square)) {
333
+ const piece = chess2.get(selected);
334
+ const toRank = square[1];
335
+ if (piece?.type === "p" && (piece.color === "w" && toRank === "8" || piece.color === "b" && toRank === "1")) {
336
+ setPromotionMove({ from: selected, to: square });
337
+ setSelected(null);
338
+ setLegalMoves([]);
339
+ return;
340
+ }
341
+ emitMove(selected, square);
342
+ setSelected(null);
343
+ setLegalMoves([]);
344
+ } else {
345
+ const piece = chess2.get(square);
346
+ const myColorChar = myColor === "white" ? "w" : "b";
347
+ if (piece && piece.color === myColorChar) {
348
+ const moves = chess2.moves({ square, verbose: true });
349
+ setSelected(square);
350
+ setLegalMoves(moves.map((m) => m.to));
351
+ } else {
352
+ setSelected(null);
353
+ setLegalMoves([]);
354
+ }
355
+ }
356
+ } else {
357
+ const piece = chess2.get(square);
358
+ const myColorChar = myColor === "white" ? "w" : "b";
359
+ if (piece && piece.color === myColorChar) {
360
+ const moves = chess2.moves({ square, verbose: true });
361
+ setSelected(square);
362
+ setLegalMoves(moves.map((m) => m.to));
363
+ }
218
364
  }
219
- );
220
- }
221
-
222
- // src/components/chess/ChessBoard.tsx
223
- var import_jsx_runtime5 = require("react/jsx-runtime");
224
- function ChessBoard({ style }) {
225
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GameWebView, { game: "chess", style });
365
+ };
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
+ });
373
+ };
374
+ const formatClock = (s) => `${Math.floor(s / 60).toString().padStart(2, "0")}:${(s % 60).toString().padStart(2, "0")}`;
375
+ const boardSize = Math.min(import_react_native4.Dimensions.get("window").width, import_react_native4.Dimensions.get("window").height) - 32;
376
+ const squareSize = boardSize / 8;
377
+ const files = ["a", "b", "c", "d", "e", "f", "g", "h"];
378
+ const ranks = ["8", "7", "6", "5", "4", "3", "2", "1"];
379
+ if (myColor === "black") {
380
+ files.reverse();
381
+ ranks.reverse();
382
+ }
383
+ const chess = chessRef.current;
384
+ const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
385
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles.container, style], children: [
386
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.playerRow, children: [
387
+ /* @__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() }) }),
388
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: { flex: 1, marginLeft: 10 }, children: [
389
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerName, children: opponentName }),
390
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerColor, children: myColor === "white" ? "Black" : "White" })
391
+ ] }),
392
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.clock, clocks.opponent < 30 && styles.clockDanger], children: formatClock(clocks.opponent) })
393
+ ] }),
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" }),
396
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.Text, { style: styles.afkText, children: [
397
+ afkWarning.secondsRemaining,
398
+ "s"
399
+ ] })
400
+ ] }),
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)) }),
428
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.playerRow, children: [
429
+ /* @__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
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: { flex: 1, marginLeft: 10 }, children: [
431
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerName, children: myPlayer.displayName }),
432
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.playerColor, children: myColor })
433
+ ] }),
434
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles.clock, clocks.self < 30 && styles.clockDanger], children: formatClock(clocks.self) })
435
+ ] }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.actions, children: [
437
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
438
+ import_react_native4.TouchableOpacity,
439
+ {
440
+ style: styles.btnResign,
441
+ onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
442
+ disabled: gameOver,
443
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnResignText, children: "Resign" })
444
+ }
445
+ ),
446
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
447
+ import_react_native4.TouchableOpacity,
448
+ {
449
+ style: styles.btnDraw,
450
+ onPress: () => socketRef.current?.emit("game:draw:offer", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
451
+ disabled: gameOver,
452
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.btnDrawText, children: "Offer draw" })
453
+ }
454
+ ),
455
+ 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
+ ] }),
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
+ ] }) }) })
478
+ ] });
226
479
  }
480
+ var styles = import_react_native4.StyleSheet.create({
481
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
482
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
483
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
484
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
485
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
486
+ playerColor: { color: "#64748b", fontSize: 12, textTransform: "capitalize" },
487
+ clock: { color: "#fff", fontFamily: "monospace", fontSize: 18, fontWeight: "bold" },
488
+ clockDanger: { color: "#f87171" },
489
+ piece: { lineHeight: void 0, includeFontPadding: false },
490
+ whitePiece: { color: "#fff", textShadowColor: "rgba(0,0,0,0.8)", textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 3 },
491
+ blackPiece: { color: "#1a1a1a", textShadowColor: "rgba(255,255,255,0.3)", textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2 },
492
+ legalDot: { position: "absolute" },
493
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
494
+ afkBannerSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
495
+ afkBannerOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
496
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
497
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
498
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
499
+ btnResignText: { color: "#f87171", fontSize: 13 },
500
+ btnDraw: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
501
+ btnDrawText: { color: "#94a3b8", fontSize: 13 },
502
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
503
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
504
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
505
+ promotionBox: { backgroundColor: "#1e293b", borderRadius: 16, padding: 24, alignItems: "center", gap: 16 },
506
+ promotionTitle: { color: "#fff", fontWeight: "bold", fontSize: 16 },
507
+ promotionPiece: { width: 60, height: 60, backgroundColor: "#f0d9b5", borderRadius: 8, alignItems: "center", justifyContent: "center" },
508
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
509
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
510
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
511
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
512
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
513
+ });
227
514
 
228
515
  // src/components/chess/ChessMoveHistory.tsx
229
516
  var import_react4 = require("react");
230
- var import_react_native4 = require("react-native");
231
- var import_jsx_runtime6 = require("react/jsx-runtime");
517
+ var import_react_native5 = require("react-native");
518
+ var import_jsx_runtime5 = require("react/jsx-runtime");
232
519
  function ChessMoveHistory({ style, rowStyle, textStyle }) {
233
520
  const socket = useSocket();
234
521
  const [moves, setMoves] = (0, import_react4.useState)([]);
@@ -253,7 +540,7 @@ function ChessMoveHistory({ style, rowStyle, textStyle }) {
253
540
  socket.off("chess:move", handler);
254
541
  };
255
542
  }, [socket]);
256
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.ScrollView, { style, children: moves.map(({ moveNumber, white, black }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.View, { style: rowStyle, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native4.Text, { style: textStyle, children: [
543
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ScrollView, { style, children: moves.map(({ moveNumber, white, black }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: rowStyle, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Text, { style: textStyle, children: [
257
544
  moveNumber,
258
545
  ". ",
259
546
  white,
@@ -262,53 +549,748 @@ function ChessMoveHistory({ style, rowStyle, textStyle }) {
262
549
  }
263
550
 
264
551
  // src/components/checkers/index.tsx
265
- var import_jsx_runtime7 = require("react/jsx-runtime");
266
- function CheckersBoard({ style }) {
267
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GameWebView, { game: "checkers", style });
552
+ var import_react5 = require("react");
553
+ var import_react_native6 = require("react-native");
554
+ var import_socket3 = require("socket.io-client");
555
+ var import_jsx_runtime6 = require("react/jsx-runtime");
556
+ function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
557
+ const { token, serverUrl, session } = useBetaGamer();
558
+ 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) => {
592
+ roomIdRef.current = d.roomId;
593
+ myPlayerIdRef.current = d.playerId;
594
+ setMyColor(d.color ?? "red");
595
+ setBoard(d.board ?? Array(64).fill(null));
596
+ setCurrentTurn(d.currentTurn ?? 0);
597
+ setPlayers(d.players ?? []);
598
+ });
599
+ s.on("game:move:made", (d) => {
600
+ setBoard(d.board);
601
+ setCurrentTurn(d.currentTurn ?? 0);
602
+ setSelectedPiece(null);
603
+ setValidMoves([]);
604
+ if (afkTimerRef.current) {
605
+ clearInterval(afkTimerRef.current);
606
+ afkTimerRef.current = null;
607
+ }
608
+ setAfkWarning(null);
609
+ });
610
+ s.on("game:valid_moves", (d) => {
611
+ setSelectedPiece(d.position);
612
+ setValidMoves(d.moves);
613
+ });
614
+ s.on("checkers:afk_warning", (d) => {
615
+ setAfkWarning(d);
616
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
617
+ afkTimerRef.current = setInterval(() => {
618
+ setAfkWarning((prev) => {
619
+ if (!prev || prev.secondsRemaining <= 1) {
620
+ clearInterval(afkTimerRef.current);
621
+ afkTimerRef.current = null;
622
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
623
+ return prev;
624
+ }
625
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
626
+ });
627
+ }, 1e3);
628
+ });
629
+ s.on("checkers:afk_warning_cleared", () => {
630
+ if (afkTimerRef.current) {
631
+ clearInterval(afkTimerRef.current);
632
+ afkTimerRef.current = null;
633
+ }
634
+ setAfkWarning(null);
635
+ });
636
+ s.on("afk:status", (status) => {
637
+ if (!status) {
638
+ if (afkTimerRef.current) {
639
+ clearInterval(afkTimerRef.current);
640
+ afkTimerRef.current = null;
641
+ }
642
+ setAfkWarning(null);
643
+ } else {
644
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
645
+ }
646
+ });
647
+ s.on("game:over", (d) => {
648
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
649
+ setTimeout(() => setGameOver(true), 400);
650
+ });
651
+ return () => {
652
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
653
+ s.disconnect();
654
+ socketRef.current = null;
655
+ };
656
+ }, [token, serverUrl]);
657
+ const handleCellPress = (pos) => {
658
+ if (gameOver || !isMyTurn) return;
659
+ const piece = board[pos];
660
+ const moveTarget = validMoves.find((m) => m.to === pos);
661
+ 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
+ });
667
+ setSelectedPiece(null);
668
+ 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
+ });
677
+ } else {
678
+ setSelectedPiece(null);
679
+ setValidMoves([]);
680
+ }
681
+ };
682
+ const screenWidth = import_react_native6.Dimensions.get("window").width;
683
+ const boardSize = screenWidth - 32;
684
+ const cellSize = boardSize / 8;
685
+ const rows = Array.from({ length: 8 }, (_, r) => r);
686
+ const cols = Array.from({ length: 8 }, (_, c) => c);
687
+ if (myColor === "black") {
688
+ rows.reverse();
689
+ cols.reverse();
690
+ }
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() }) }),
694
+ /* @__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 })
697
+ ] }),
698
+ !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.turnIndicator, children: "\u25CF thinking" })
699
+ ] }),
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: [
703
+ afkWarning.secondsRemaining,
704
+ "s"
705
+ ] })
706
+ ] }),
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() }) }),
745
+ /* @__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 })
748
+ ] }),
749
+ isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles2.turnIndicator, children: "\u25CF your turn" })
750
+ ] }),
751
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles2.actions, children: [
752
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
753
+ import_react_native6.TouchableOpacity,
754
+ {
755
+ style: styles2.btnResign,
756
+ 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" })
759
+ }
760
+ ),
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" }) })
762
+ ] }),
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
+ ] }) }) })
769
+ ] });
268
770
  }
771
+ var styles2 = import_react_native6.StyleSheet.create({
772
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
773
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
774
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
775
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
776
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
777
+ playerColor: { fontSize: 12, textTransform: "capitalize" },
778
+ turnIndicator: { color: "#a78bfa", fontSize: 12 },
779
+ cellSelected: { borderWidth: 3, borderColor: "#facc15" },
780
+ cellValidMove: { borderWidth: 3, borderColor: "#4ade80" },
781
+ piece: { borderRadius: 999, alignItems: "center", justifyContent: "center" },
782
+ pieceRed: { backgroundColor: "#ef4444", borderWidth: 2, borderColor: "#991b1b" },
783
+ pieceBlack: { backgroundColor: "#374151", borderWidth: 2, borderColor: "#111827" },
784
+ pieceSelected: { transform: [{ scale: 1.1 }] },
785
+ moveDot: { position: "absolute" },
786
+ moveDotNormal: { backgroundColor: "rgba(74,222,128,0.6)" },
787
+ moveDotCapture: { backgroundColor: "rgba(251,146,60,0.7)" },
788
+ captureRing: { borderWidth: 3, borderColor: "rgba(251,146,60,0.7)" },
789
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
790
+ afkSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
791
+ afkOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
792
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
793
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
794
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
795
+ btnResignText: { color: "#f87171", fontSize: 13 },
796
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
797
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
798
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
799
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
800
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
801
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
802
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
803
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
804
+ });
269
805
 
270
806
  // src/components/connect4/index.tsx
271
- var import_jsx_runtime8 = require("react/jsx-runtime");
272
- function Connect4Board({ style }) {
273
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GameWebView, { game: "connect4", style });
807
+ var import_react6 = require("react");
808
+ var import_react_native7 = require("react-native");
809
+ var import_socket4 = require("socket.io-client");
810
+ var import_jsx_runtime7 = require("react/jsx-runtime");
811
+ function Connect4Board({ style, showAfkWarning = true, onLeave }) {
812
+ const { token, serverUrl, session } = useBetaGamer();
813
+ 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) => {
849
+ roomIdRef.current = d.roomId;
850
+ myPlayerIdRef.current = d.playerId;
851
+ setMyColor(d.color ?? "red");
852
+ setBoard(d.board ?? Array(ROWS * COLS).fill(null));
853
+ setCurrentTurn(d.currentTurn ?? 0);
854
+ setPlayers(d.players ?? []);
855
+ });
856
+ s.on("game:move:made", (d) => {
857
+ setBoard(d.board);
858
+ setCurrentTurn(d.currentTurn ?? 0);
859
+ if (d.lastMove != null) setLastMove(d.lastMove);
860
+ if (d.winningCells) setWinningCells(d.winningCells);
861
+ if (afkTimerRef.current) {
862
+ clearInterval(afkTimerRef.current);
863
+ afkTimerRef.current = null;
864
+ }
865
+ setAfkWarning(null);
866
+ });
867
+ s.on("connect4:afk_warning", (d) => {
868
+ setAfkWarning(d);
869
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
870
+ afkTimerRef.current = setInterval(() => {
871
+ setAfkWarning((prev) => {
872
+ if (!prev || prev.secondsRemaining <= 1) {
873
+ clearInterval(afkTimerRef.current);
874
+ afkTimerRef.current = null;
875
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
876
+ return prev;
877
+ }
878
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
879
+ });
880
+ }, 1e3);
881
+ });
882
+ s.on("connect4:afk_warning_cleared", () => {
883
+ if (afkTimerRef.current) {
884
+ clearInterval(afkTimerRef.current);
885
+ afkTimerRef.current = null;
886
+ }
887
+ setAfkWarning(null);
888
+ });
889
+ s.on("afk:status", (status) => {
890
+ if (!status) {
891
+ if (afkTimerRef.current) {
892
+ clearInterval(afkTimerRef.current);
893
+ afkTimerRef.current = null;
894
+ }
895
+ setAfkWarning(null);
896
+ } else {
897
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
898
+ }
899
+ });
900
+ s.on("game:over", (d) => {
901
+ if (d.winningCells) setWinningCells(d.winningCells);
902
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
903
+ setTimeout(() => setGameOver(true), 400);
904
+ });
905
+ return () => {
906
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
907
+ s.disconnect();
908
+ socketRef.current = null;
909
+ };
910
+ }, [token, serverUrl]);
911
+ const handleColumnPress = (col) => {
912
+ if (!isMyTurn || gameOver) return;
913
+ if (board[col] !== null) return;
914
+ socketRef.current?.emit("game:move", { roomId: roomIdRef.current, column: col, playerId: myPlayerIdRef.current });
915
+ };
916
+ const screenWidth = import_react_native7.Dimensions.get("window").width;
917
+ const boardWidth = screenWidth - 32;
918
+ const cellSize = boardWidth / COLS;
919
+ 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() }) }),
923
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: { flex: 1, marginLeft: 10, flexDirection: "row", alignItems: "center" }, children: [
924
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ColorDot, { color: opponentColor }),
925
+ /* @__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 })
928
+ ] })
929
+ ] }),
930
+ !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.turnIndicator, children: "\u25CF thinking" })
931
+ ] }),
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: [
935
+ afkWarning.secondsRemaining,
936
+ "s"
937
+ ] })
938
+ ] }),
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() }) }),
968
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: { flex: 1, marginLeft: 10, flexDirection: "row", alignItems: "center" }, children: [
969
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ColorDot, { color: myColor }),
970
+ /* @__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 })
973
+ ] })
974
+ ] }),
975
+ isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { style: styles3.turnIndicator, children: "\u25CF your turn" })
976
+ ] }),
977
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles3.actions, children: [
978
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
979
+ import_react_native7.TouchableOpacity,
980
+ {
981
+ style: styles3.btnResign,
982
+ 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" })
985
+ }
986
+ ),
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" }) })
988
+ ] }),
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
+ ] }) }) })
995
+ ] });
274
996
  }
997
+ var styles3 = import_react_native7.StyleSheet.create({
998
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
999
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
1000
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
1001
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
1002
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
1003
+ playerColor: { color: "#64748b", fontSize: 12, textTransform: "capitalize" },
1004
+ turnIndicator: { color: "#a78bfa", fontSize: 12 },
1005
+ cell: { flex: 1, borderRadius: 999, backgroundColor: "#1e3a5f", alignItems: "center", justifyContent: "center" },
1006
+ cellWin: { backgroundColor: "#15803d" },
1007
+ 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 } },
1010
+ pieceWin: { transform: [{ scale: 1.1 }] },
1011
+ pieceLast: { borderWidth: 2, borderColor: "#22d3ee" },
1012
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
1013
+ afkSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
1014
+ afkOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
1015
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
1016
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
1017
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1018
+ btnResignText: { color: "#f87171", fontSize: 13 },
1019
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1020
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
1021
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
1022
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
1023
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
1024
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
1025
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
1026
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
1027
+ });
275
1028
 
276
1029
  // src/components/tictactoe/index.tsx
1030
+ var import_react7 = require("react");
1031
+ var import_react_native8 = require("react-native");
1032
+ var import_socket5 = require("socket.io-client");
1033
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1034
+ function TictactoeBoard({ style, showAfkWarning = true, onLeave }) {
1035
+ const { token, serverUrl, session } = useBetaGamer();
1036
+ 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) => {
1068
+ roomIdRef.current = d.roomId;
1069
+ myPlayerIdRef.current = d.playerId;
1070
+ setMyMark(d.mark);
1071
+ setBoard(d.board ?? Array(9).fill(null));
1072
+ setCurrentTurn(d.currentTurn ?? 0);
1073
+ setPlayers(d.players ?? []);
1074
+ });
1075
+ s.on("game:move:made", (d) => {
1076
+ setBoard(d.board);
1077
+ setCurrentTurn(d.currentTurn ?? 0);
1078
+ if (d.winningLine) setWinningLine(d.winningLine);
1079
+ if (afkTimerRef.current) {
1080
+ clearInterval(afkTimerRef.current);
1081
+ afkTimerRef.current = null;
1082
+ }
1083
+ setAfkWarning(null);
1084
+ });
1085
+ s.on("tictactoe:afk_warning", (d) => {
1086
+ setAfkWarning(d);
1087
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
1088
+ afkTimerRef.current = setInterval(() => {
1089
+ setAfkWarning((prev) => {
1090
+ if (!prev || prev.secondsRemaining <= 1) {
1091
+ clearInterval(afkTimerRef.current);
1092
+ afkTimerRef.current = null;
1093
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
1094
+ return prev;
1095
+ }
1096
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
1097
+ });
1098
+ }, 1e3);
1099
+ });
1100
+ s.on("tictactoe:afk_warning_cleared", () => {
1101
+ if (afkTimerRef.current) {
1102
+ clearInterval(afkTimerRef.current);
1103
+ afkTimerRef.current = null;
1104
+ }
1105
+ setAfkWarning(null);
1106
+ });
1107
+ s.on("afk:status", (status) => {
1108
+ if (!status) {
1109
+ if (afkTimerRef.current) {
1110
+ clearInterval(afkTimerRef.current);
1111
+ afkTimerRef.current = null;
1112
+ }
1113
+ setAfkWarning(null);
1114
+ } else {
1115
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
1116
+ }
1117
+ });
1118
+ s.on("game:over", (d) => {
1119
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
1120
+ setTimeout(() => setGameOver(true), 400);
1121
+ });
1122
+ return () => {
1123
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
1124
+ s.disconnect();
1125
+ socketRef.current = null;
1126
+ };
1127
+ }, [token, serverUrl]);
1128
+ const handleCellPress = (idx) => {
1129
+ if (!isMyTurn || gameOver || board[idx]) return;
1130
+ socketRef.current?.emit("game:move", { roomId: roomIdRef.current, position: idx, playerId: myPlayerIdRef.current });
1131
+ };
1132
+ const screenWidth = import_react_native8.Dimensions.get("window").width;
1133
+ const boardSize = screenWidth - 32;
1134
+ 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() }) }),
1138
+ /* @__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" })
1141
+ ] }),
1142
+ !isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.turnIndicator, children: "\u25CF thinking" })
1143
+ ] }),
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: [
1147
+ afkWarning.secondsRemaining,
1148
+ "s"
1149
+ ] })
1150
+ ] }),
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() }) }),
1177
+ /* @__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 })
1180
+ ] }),
1181
+ isMyTurn && !gameOver && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles4.turnIndicator, children: "\u25CF your turn" })
1182
+ ] }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles4.actions, children: [
1184
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1185
+ import_react_native8.TouchableOpacity,
1186
+ {
1187
+ style: styles4.btnResign,
1188
+ 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" })
1191
+ }
1192
+ ),
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" }) })
1194
+ ] }),
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
+ ] }) }) })
1201
+ ] });
1202
+ }
1203
+ var styles4 = import_react_native8.StyleSheet.create({
1204
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
1205
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
1206
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
1207
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
1208
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
1209
+ playerColor: { color: "#64748b", fontSize: 12 },
1210
+ turnIndicator: { color: "#a78bfa", fontSize: 12 },
1211
+ cellActive: { backgroundColor: "#4c1d95" },
1212
+ cellIdle: { backgroundColor: "#1e1b4b" },
1213
+ cellWin: { backgroundColor: "#15803d" },
1214
+ mark: { fontSize: 48, fontWeight: "bold", lineHeight: 56 },
1215
+ markX: { color: "#60a5fa" },
1216
+ markO: { color: "#f472b6" },
1217
+ markWin: { color: "#fff" },
1218
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
1219
+ afkSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
1220
+ afkOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
1221
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
1222
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
1223
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1224
+ btnResignText: { color: "#f87171", fontSize: 13 },
1225
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1226
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
1227
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
1228
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
1229
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
1230
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
1231
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
1232
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
1233
+ });
1234
+
1235
+ // src/components/subway-runner/index.tsx
1236
+ var import_react9 = require("react");
1237
+ var import_react_native9 = require("react-native");
1238
+
1239
+ // src/components/GameWebView.tsx
1240
+ var import_react8 = require("react");
1241
+ var import_react_native_webview = require("react-native-webview");
277
1242
  var import_jsx_runtime9 = require("react/jsx-runtime");
278
- function TictactoeBoard({ style }) {
279
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GameWebView, { game: "tictactoe", style });
1243
+ function GameWebView({ game, style, showAfkWarning = true }) {
1244
+ const { token, serverUrl, session } = useBetaGamer();
1245
+ const webViewRef = (0, import_react8.useRef)(null);
1246
+ if (session.game !== game) return null;
1247
+ const uri = showAfkWarning ? `${serverUrl}/embed/${game}` : `${serverUrl}/embed/${game}?afkWarning=0`;
1248
+ const initScript = `
1249
+ window.__BG_TOKEN__ = ${JSON.stringify(token)};
1250
+ window.postMessage(JSON.stringify({ type: 'bg:init', token: ${JSON.stringify(token)} }), '*');
1251
+ true;
1252
+ `;
1253
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1254
+ import_react_native_webview.WebView,
1255
+ {
1256
+ ref: webViewRef,
1257
+ source: { uri },
1258
+ injectedJavaScriptBeforeContentLoaded: initScript,
1259
+ style: [{ flex: 1 }, style],
1260
+ allowsInlineMediaPlayback: true,
1261
+ mediaPlaybackRequiresUserAction: false
1262
+ }
1263
+ );
280
1264
  }
281
1265
 
282
1266
  // src/components/subway-runner/index.tsx
283
- var import_react5 = require("react");
284
- var import_react_native5 = require("react-native");
285
1267
  var import_jsx_runtime10 = require("react/jsx-runtime");
286
1268
  function SubwayRunnerGame({ style }) {
287
1269
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(GameWebView, { game: "subway-runner", style });
288
1270
  }
289
1271
  function SubwayRunnerScore({ style, textStyle }) {
290
1272
  const socket = useSocket();
291
- const [score, setScore] = (0, import_react5.useState)(0);
292
- (0, import_react5.useEffect)(() => {
1273
+ const [score, setScore] = (0, import_react9.useState)(0);
1274
+ (0, import_react9.useEffect)(() => {
293
1275
  if (!socket) return;
294
1276
  socket.on("runner:score", (s) => setScore(s));
295
1277
  return () => {
296
1278
  socket.off("runner:score");
297
1279
  };
298
1280
  }, [socket]);
299
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native5.View, { style, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native5.Text, { style: textStyle, children: score }) });
1281
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.View, { style, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.Text, { style: textStyle, children: score }) });
300
1282
  }
301
1283
  function SubwayRunnerLives({ style, lifeStyle }) {
302
1284
  const socket = useSocket();
303
- const [lives, setLives] = (0, import_react5.useState)(3);
304
- (0, import_react5.useEffect)(() => {
1285
+ const [lives, setLives] = (0, import_react9.useState)(3);
1286
+ (0, import_react9.useEffect)(() => {
305
1287
  if (!socket) return;
306
1288
  socket.on("runner:lives", (l) => setLives(l));
307
1289
  return () => {
308
1290
  socket.off("runner:lives");
309
1291
  };
310
1292
  }, [socket]);
311
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native5.View, { style: [{ flexDirection: "row" }, style], children: Array.from({ length: lives }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native5.View, { style: lifeStyle, accessibilityLabel: "life" }, i)) });
1293
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.View, { style: [{ flexDirection: "row" }, style], children: Array.from({ length: lives }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.View, { style: lifeStyle, accessibilityLabel: "life" }, i)) });
312
1294
  }
313
1295
  // Annotate the CommonJS export names for ESM import in node:
314
1296
  0 && (module.exports = {