@beta-gamer/react-native 0.1.19 → 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.mjs CHANGED
@@ -47,7 +47,7 @@ function decodeToken(token) {
47
47
  return JSON.parse(base64Decode(parts[1]));
48
48
  }
49
49
  var BetaGamerContext = createContext(null);
50
- function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", socketPath = "/socket.io", children }) {
50
+ function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", socketPath = "/socket.io", connectSocket = true, children }) {
51
51
  const session = decodeToken(token);
52
52
  const [socket, setSocket] = useState(null);
53
53
  const [gameState, setGameState] = useState({
@@ -55,6 +55,7 @@ function BetaGamerProvider({ token, serverUrl = "https://api.beta-gamer.com", so
55
55
  players: session.players
56
56
  });
57
57
  useEffect(() => {
58
+ if (!connectSocket) return;
58
59
  const s = io(`${serverUrl}/${session.game}`, {
59
60
  auth: { token },
60
61
  path: socketPath,
@@ -152,47 +153,339 @@ function Timer({ player, initialSeconds = 600, style, textStyle }) {
152
153
  ] }) });
153
154
  }
154
155
 
155
- // src/components/GameWebView.tsx
156
- import { useRef } from "react";
157
- import { WebView } from "react-native-webview";
158
- import { jsx as jsx4 } from "react/jsx-runtime";
159
- function GameWebView({ game, style, showAfkWarning = true }) {
156
+ // src/components/chess/ChessBoard.tsx
157
+ import { useEffect as useEffect3, useRef, useState as useState3 } from "react";
158
+ import {
159
+ View as View3,
160
+ Text as Text3,
161
+ TouchableOpacity,
162
+ Modal,
163
+ StyleSheet,
164
+ Dimensions
165
+ } from "react-native";
166
+ import { Chess } from "chess.js";
167
+ import { io as io2 } from "socket.io-client";
168
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
169
+ var PIECES = {
170
+ p: { white: "\u2659", black: "\u265F" },
171
+ n: { white: "\u2658", black: "\u265E" },
172
+ b: { white: "\u2657", black: "\u265D" },
173
+ r: { white: "\u2656", black: "\u265C" },
174
+ q: { white: "\u2655", black: "\u265B" },
175
+ k: { white: "\u2654", black: "\u265A" }
176
+ };
177
+ function ChessBoard({ style, showAfkWarning = true, onLeave }) {
160
178
  const { token, serverUrl, session } = useBetaGamer();
161
- const webViewRef = useRef(null);
162
- if (session.game !== game) return null;
163
- const uri = showAfkWarning ? `${serverUrl}/embed/${game}` : `${serverUrl}/embed/${game}?afkWarning=0`;
164
- const initScript = `
165
- window.__BG_TOKEN__ = ${JSON.stringify(token)};
166
- window.postMessage(JSON.stringify({ type: 'bg:init', token: ${JSON.stringify(token)} }), '*');
167
- true;
168
- `;
169
- return /* @__PURE__ */ jsx4(
170
- WebView,
171
- {
172
- ref: webViewRef,
173
- source: { uri },
174
- injectedJavaScriptBeforeContentLoaded: initScript,
175
- style: [{ flex: 1 }, style],
176
- allowsInlineMediaPlayback: true,
177
- mediaPlaybackRequiresUserAction: false
179
+ const myPlayer = session.players[0];
180
+ const socketRef = useRef(null);
181
+ const roomIdRef = useRef(null);
182
+ const myPlayerIdRef = useRef(myPlayer?.id ?? "");
183
+ const afkTimerRef = useRef(null);
184
+ const chessRef = useRef(new Chess());
185
+ const [fen, setFen] = useState3("start");
186
+ const [myColor, setMyColor] = useState3("white");
187
+ const [currentTurn, setCurrentTurn] = useState3(0);
188
+ const [players, setPlayers] = useState3([]);
189
+ const [clocks, setClocks] = useState3({ self: 600, opponent: 600 });
190
+ const [selected, setSelected] = useState3(null);
191
+ const [legalMoves, setLegalMoves] = useState3([]);
192
+ const [lastMove, setLastMove] = useState3(null);
193
+ const [gameOver, setGameOver] = useState3(false);
194
+ const [gameResult, setGameResult] = useState3(null);
195
+ const [afkWarning, setAfkWarning] = useState3(null);
196
+ const [promotionMove, setPromotionMove] = useState3(null);
197
+ const [moveHistory, setMoveHistory] = useState3([]);
198
+ if (session.game !== "chess") return null;
199
+ const isMyTurn = players.length > 0 ? myColor === "white" ? currentTurn === 0 : currentTurn === 1 : false;
200
+ useEffect3(() => {
201
+ const s = io2(`${serverUrl}/chess`, {
202
+ auth: { token },
203
+ path: "/socket.io",
204
+ transports: ["websocket", "polling"]
205
+ });
206
+ socketRef.current = s;
207
+ const join = () => s.emit("matchmaking:join", {
208
+ username: myPlayer.displayName,
209
+ playerId: myPlayer.id,
210
+ wantsBot: false,
211
+ sessionId: session.sessionId
212
+ });
213
+ if (s.connected) join();
214
+ else s.once("connect", join);
215
+ s.on("game:started", (d) => {
216
+ roomIdRef.current = d.roomId;
217
+ myPlayerIdRef.current = d.playerId;
218
+ setMyColor(d.color);
219
+ chessRef.current.load(d.fen);
220
+ setFen(d.fen);
221
+ setCurrentTurn(d.currentTurn ?? 0);
222
+ setPlayers(d.players ?? []);
223
+ if (d.playerTimers) {
224
+ const vals = Object.values(d.playerTimers);
225
+ setClocks({ self: Math.floor((vals[0] ?? 6e5) / 1e3), opponent: Math.floor((vals[1] ?? 6e5) / 1e3) });
226
+ }
227
+ });
228
+ s.on("game:move:made", (d) => {
229
+ chessRef.current.load(d.fen);
230
+ setFen(d.fen);
231
+ setCurrentTurn(d.currentTurn ?? 0);
232
+ if (d.move) {
233
+ setLastMove({ from: d.move.from, to: d.move.to });
234
+ if (d.move.san) setMoveHistory((prev) => [...prev, { san: d.move.san }]);
235
+ }
236
+ setSelected(null);
237
+ setLegalMoves([]);
238
+ if (afkTimerRef.current) {
239
+ clearInterval(afkTimerRef.current);
240
+ afkTimerRef.current = null;
241
+ }
242
+ setAfkWarning(null);
243
+ });
244
+ s.on("timer:update", (d) => {
245
+ if (!d.playerTimers) return;
246
+ Object.entries(d.playerTimers).forEach(([id, ms]) => {
247
+ if (id === myPlayerIdRef.current) setClocks((c) => ({ ...c, self: Math.floor(ms / 1e3) }));
248
+ else setClocks((c) => ({ ...c, opponent: Math.floor(ms / 1e3) }));
249
+ });
250
+ });
251
+ s.on("chess:afk_warning", (d) => {
252
+ setAfkWarning(d);
253
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
254
+ afkTimerRef.current = setInterval(() => {
255
+ setAfkWarning((prev) => {
256
+ if (!prev || prev.secondsRemaining <= 1) {
257
+ clearInterval(afkTimerRef.current);
258
+ afkTimerRef.current = null;
259
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
260
+ return prev;
261
+ }
262
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
263
+ });
264
+ }, 1e3);
265
+ });
266
+ s.on("chess:afk_warning_cleared", () => {
267
+ if (afkTimerRef.current) {
268
+ clearInterval(afkTimerRef.current);
269
+ afkTimerRef.current = null;
270
+ }
271
+ setAfkWarning(null);
272
+ });
273
+ s.on("afk:status", (status) => {
274
+ if (!status) {
275
+ if (afkTimerRef.current) {
276
+ clearInterval(afkTimerRef.current);
277
+ afkTimerRef.current = null;
278
+ }
279
+ setAfkWarning(null);
280
+ } else {
281
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
282
+ }
283
+ });
284
+ s.on("game:over", (d) => {
285
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
286
+ setTimeout(() => setGameOver(true), 400);
287
+ });
288
+ return () => {
289
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
290
+ s.disconnect();
291
+ socketRef.current = null;
292
+ };
293
+ }, [token, serverUrl]);
294
+ const handleSquarePress = (square) => {
295
+ if (gameOver || !isMyTurn) return;
296
+ const chess2 = chessRef.current;
297
+ if (selected) {
298
+ if (legalMoves.includes(square)) {
299
+ const piece = chess2.get(selected);
300
+ const toRank = square[1];
301
+ if (piece?.type === "p" && (piece.color === "w" && toRank === "8" || piece.color === "b" && toRank === "1")) {
302
+ setPromotionMove({ from: selected, to: square });
303
+ setSelected(null);
304
+ setLegalMoves([]);
305
+ return;
306
+ }
307
+ emitMove(selected, square);
308
+ setSelected(null);
309
+ setLegalMoves([]);
310
+ } else {
311
+ const piece = chess2.get(square);
312
+ const myColorChar = myColor === "white" ? "w" : "b";
313
+ if (piece && piece.color === myColorChar) {
314
+ const moves = chess2.moves({ square, verbose: true });
315
+ setSelected(square);
316
+ setLegalMoves(moves.map((m) => m.to));
317
+ } else {
318
+ setSelected(null);
319
+ setLegalMoves([]);
320
+ }
321
+ }
322
+ } else {
323
+ const piece = chess2.get(square);
324
+ const myColorChar = myColor === "white" ? "w" : "b";
325
+ if (piece && piece.color === myColorChar) {
326
+ const moves = chess2.moves({ square, verbose: true });
327
+ setSelected(square);
328
+ setLegalMoves(moves.map((m) => m.to));
329
+ }
178
330
  }
179
- );
180
- }
181
-
182
- // src/components/chess/ChessBoard.tsx
183
- import { jsx as jsx5 } from "react/jsx-runtime";
184
- function ChessBoard({ style, showAfkWarning }) {
185
- return /* @__PURE__ */ jsx5(GameWebView, { game: "chess", style, showAfkWarning });
331
+ };
332
+ const emitMove = (from, to, promotion) => {
333
+ if (!socketRef.current || !roomIdRef.current) return;
334
+ socketRef.current.emit("game:move", {
335
+ roomId: roomIdRef.current,
336
+ playerId: myPlayerIdRef.current,
337
+ move: promotion ? { from, to, promotion } : { from, to }
338
+ });
339
+ };
340
+ const formatClock = (s) => `${Math.floor(s / 60).toString().padStart(2, "0")}:${(s % 60).toString().padStart(2, "0")}`;
341
+ const boardSize = Math.min(Dimensions.get("window").width, Dimensions.get("window").height) - 32;
342
+ const squareSize = boardSize / 8;
343
+ const files = ["a", "b", "c", "d", "e", "f", "g", "h"];
344
+ const ranks = ["8", "7", "6", "5", "4", "3", "2", "1"];
345
+ if (myColor === "black") {
346
+ files.reverse();
347
+ ranks.reverse();
348
+ }
349
+ const chess = chessRef.current;
350
+ const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
351
+ return /* @__PURE__ */ jsxs3(View3, { style: [styles.container, style], children: [
352
+ /* @__PURE__ */ jsxs3(View3, { style: styles.playerRow, children: [
353
+ /* @__PURE__ */ jsx4(View3, { style: styles.avatar, children: /* @__PURE__ */ jsx4(Text3, { style: styles.avatarText, children: opponentName[0]?.toUpperCase() }) }),
354
+ /* @__PURE__ */ jsxs3(View3, { style: { flex: 1, marginLeft: 10 }, children: [
355
+ /* @__PURE__ */ jsx4(Text3, { style: styles.playerName, children: opponentName }),
356
+ /* @__PURE__ */ jsx4(Text3, { style: styles.playerColor, children: myColor === "white" ? "Black" : "White" })
357
+ ] }),
358
+ /* @__PURE__ */ jsx4(Text3, { style: [styles.clock, clocks.opponent < 30 && styles.clockDanger], children: formatClock(clocks.opponent) })
359
+ ] }),
360
+ showAfkWarning && afkWarning && /* @__PURE__ */ jsxs3(View3, { style: [styles.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles.afkBannerSelf : styles.afkBannerOpponent], children: [
361
+ /* @__PURE__ */ jsx4(Text3, { style: styles.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F You are AFK \u2014 make a move!" : "\u23F3 Opponent is AFK" }),
362
+ /* @__PURE__ */ jsxs3(Text3, { style: styles.afkText, children: [
363
+ afkWarning.secondsRemaining,
364
+ "s"
365
+ ] })
366
+ ] }),
367
+ /* @__PURE__ */ jsx4(View3, { style: { width: boardSize, height: boardSize }, children: ranks.map((rank, rowIdx) => /* @__PURE__ */ jsx4(View3, { style: { flexDirection: "row" }, children: files.map((file, colIdx) => {
368
+ const square = `${file}${rank}`;
369
+ const piece = chess.get(square);
370
+ const isLight = (rowIdx + colIdx) % 2 === 0;
371
+ const isSelected = selected === square;
372
+ const isLegal = legalMoves.includes(square);
373
+ const isLast = lastMove && (lastMove.from === square || lastMove.to === square);
374
+ let bg = isLight ? "#f0d9b5" : "#b58863";
375
+ if (isSelected) bg = "#7fc97f";
376
+ else if (isLast) bg = isLight ? "#cdd16f" : "#aaa23a";
377
+ return /* @__PURE__ */ jsxs3(
378
+ TouchableOpacity,
379
+ {
380
+ onPress: () => handleSquarePress(square),
381
+ activeOpacity: 0.85,
382
+ style: [{ width: squareSize, height: squareSize, backgroundColor: bg, alignItems: "center", justifyContent: "center" }],
383
+ children: [
384
+ piece && /* @__PURE__ */ jsx4(Text3, { style: [styles.piece, { fontSize: squareSize * 0.72 }, piece.color === "w" ? styles.whitePiece : styles.blackPiece], children: PIECES[piece.type]?.[piece.color === "w" ? "white" : "black"] }),
385
+ isLegal && /* @__PURE__ */ jsx4(View3, { style: [
386
+ styles.legalDot,
387
+ piece ? { ...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)" }
388
+ ] })
389
+ ]
390
+ },
391
+ square
392
+ );
393
+ }) }, rank)) }),
394
+ /* @__PURE__ */ jsxs3(View3, { style: styles.playerRow, children: [
395
+ /* @__PURE__ */ jsx4(View3, { style: [styles.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ jsx4(Text3, { style: styles.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
396
+ /* @__PURE__ */ jsxs3(View3, { style: { flex: 1, marginLeft: 10 }, children: [
397
+ /* @__PURE__ */ jsx4(Text3, { style: styles.playerName, children: myPlayer.displayName }),
398
+ /* @__PURE__ */ jsx4(Text3, { style: styles.playerColor, children: myColor })
399
+ ] }),
400
+ /* @__PURE__ */ jsx4(Text3, { style: [styles.clock, clocks.self < 30 && styles.clockDanger], children: formatClock(clocks.self) })
401
+ ] }),
402
+ /* @__PURE__ */ jsxs3(View3, { style: styles.actions, children: [
403
+ /* @__PURE__ */ jsx4(
404
+ TouchableOpacity,
405
+ {
406
+ style: styles.btnResign,
407
+ onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
408
+ disabled: gameOver,
409
+ children: /* @__PURE__ */ jsx4(Text3, { style: styles.btnResignText, children: "Resign" })
410
+ }
411
+ ),
412
+ /* @__PURE__ */ jsx4(
413
+ TouchableOpacity,
414
+ {
415
+ style: styles.btnDraw,
416
+ onPress: () => socketRef.current?.emit("game:draw:offer", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
417
+ disabled: gameOver,
418
+ children: /* @__PURE__ */ jsx4(Text3, { style: styles.btnDrawText, children: "Offer draw" })
419
+ }
420
+ ),
421
+ onLeave && /* @__PURE__ */ jsx4(TouchableOpacity, { style: styles.btnLeave, onPress: onLeave, children: /* @__PURE__ */ jsx4(Text3, { style: styles.btnLeaveText, children: "Leave" }) })
422
+ ] }),
423
+ /* @__PURE__ */ jsx4(Modal, { visible: !!promotionMove, transparent: true, animationType: "fade", children: /* @__PURE__ */ jsx4(View3, { style: styles.modalOverlay, children: /* @__PURE__ */ jsxs3(View3, { style: styles.promotionBox, children: [
424
+ /* @__PURE__ */ jsx4(Text3, { style: styles.promotionTitle, children: "Promote pawn" }),
425
+ /* @__PURE__ */ jsx4(View3, { style: { flexDirection: "row", gap: 12 }, children: ["q", "r", "b", "n"].map((p) => /* @__PURE__ */ jsx4(
426
+ TouchableOpacity,
427
+ {
428
+ style: styles.promotionPiece,
429
+ onPress: () => {
430
+ if (promotionMove) emitMove(promotionMove.from, promotionMove.to, p);
431
+ setPromotionMove(null);
432
+ },
433
+ children: /* @__PURE__ */ jsx4(Text3, { style: { fontSize: 36 }, children: PIECES[p]?.[myColor === "white" ? "white" : "black"] })
434
+ },
435
+ p
436
+ )) })
437
+ ] }) }) }),
438
+ /* @__PURE__ */ jsx4(Modal, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ jsx4(View3, { style: styles.modalOverlay, children: /* @__PURE__ */ jsxs3(View3, { style: styles.gameOverBox, children: [
439
+ /* @__PURE__ */ jsx4(Text3, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
440
+ /* @__PURE__ */ jsx4(Text3, { style: styles.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
441
+ /* @__PURE__ */ jsx4(Text3, { style: styles.gameOverReason, children: gameResult?.reason }),
442
+ onLeave && /* @__PURE__ */ jsx4(TouchableOpacity, { style: styles.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ jsx4(Text3, { style: styles.btnPlayAgainText, children: "Play again" }) })
443
+ ] }) }) })
444
+ ] });
186
445
  }
446
+ var styles = StyleSheet.create({
447
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
448
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
449
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
450
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
451
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
452
+ playerColor: { color: "#64748b", fontSize: 12, textTransform: "capitalize" },
453
+ clock: { color: "#fff", fontFamily: "monospace", fontSize: 18, fontWeight: "bold" },
454
+ clockDanger: { color: "#f87171" },
455
+ piece: { lineHeight: void 0, includeFontPadding: false },
456
+ whitePiece: { color: "#fff", textShadowColor: "rgba(0,0,0,0.8)", textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 3 },
457
+ blackPiece: { color: "#1a1a1a", textShadowColor: "rgba(255,255,255,0.3)", textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2 },
458
+ legalDot: { position: "absolute" },
459
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
460
+ afkBannerSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
461
+ afkBannerOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
462
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
463
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
464
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
465
+ btnResignText: { color: "#f87171", fontSize: 13 },
466
+ btnDraw: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
467
+ btnDrawText: { color: "#94a3b8", fontSize: 13 },
468
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
469
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
470
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
471
+ promotionBox: { backgroundColor: "#1e293b", borderRadius: 16, padding: 24, alignItems: "center", gap: 16 },
472
+ promotionTitle: { color: "#fff", fontWeight: "bold", fontSize: 16 },
473
+ promotionPiece: { width: 60, height: 60, backgroundColor: "#f0d9b5", borderRadius: 8, alignItems: "center", justifyContent: "center" },
474
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
475
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
476
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
477
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
478
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
479
+ });
187
480
 
188
481
  // src/components/chess/ChessMoveHistory.tsx
189
- import { useEffect as useEffect3, useState as useState3 } from "react";
190
- import { View as View3, Text as Text3, ScrollView } from "react-native";
191
- import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
482
+ import { useEffect as useEffect4, useState as useState4 } from "react";
483
+ import { View as View4, Text as Text4, ScrollView } from "react-native";
484
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
192
485
  function ChessMoveHistory({ style, rowStyle, textStyle }) {
193
486
  const socket = useSocket();
194
- const [moves, setMoves] = useState3([]);
195
- useEffect3(() => {
487
+ const [moves, setMoves] = useState4([]);
488
+ useEffect4(() => {
196
489
  if (!socket) return;
197
490
  const handler = ({ san, moveIndex }) => {
198
491
  const moveNumber = Math.floor(moveIndex / 2) + 1;
@@ -213,7 +506,7 @@ function ChessMoveHistory({ style, rowStyle, textStyle }) {
213
506
  socket.off("chess:move", handler);
214
507
  };
215
508
  }, [socket]);
216
- return /* @__PURE__ */ jsx6(ScrollView, { style, children: moves.map(({ moveNumber, white, black }) => /* @__PURE__ */ jsx6(View3, { style: rowStyle, children: /* @__PURE__ */ jsxs3(Text3, { style: textStyle, children: [
509
+ return /* @__PURE__ */ jsx5(ScrollView, { style, children: moves.map(({ moveNumber, white, black }) => /* @__PURE__ */ jsx5(View4, { style: rowStyle, children: /* @__PURE__ */ jsxs4(Text4, { style: textStyle, children: [
217
510
  moveNumber,
218
511
  ". ",
219
512
  white,
@@ -222,53 +515,769 @@ function ChessMoveHistory({ style, rowStyle, textStyle }) {
222
515
  }
223
516
 
224
517
  // src/components/checkers/index.tsx
225
- import { jsx as jsx7 } from "react/jsx-runtime";
226
- function CheckersBoard({ style, showAfkWarning }) {
227
- return /* @__PURE__ */ jsx7(GameWebView, { game: "checkers", style, showAfkWarning });
518
+ import { useEffect as useEffect5, useRef as useRef2, useState as useState5 } from "react";
519
+ import {
520
+ View as View5,
521
+ Text as Text5,
522
+ TouchableOpacity as TouchableOpacity2,
523
+ Modal as Modal2,
524
+ StyleSheet as StyleSheet2,
525
+ Dimensions as Dimensions2
526
+ } from "react-native";
527
+ import { io as io3 } from "socket.io-client";
528
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
529
+ function CheckersBoard({ style, showAfkWarning = true, onLeave }) {
530
+ const { token, serverUrl, session } = useBetaGamer();
531
+ const myPlayer = session.players[0];
532
+ const socketRef = useRef2(null);
533
+ const roomIdRef = useRef2(null);
534
+ const myPlayerIdRef = useRef2(myPlayer?.id ?? "");
535
+ const afkTimerRef = useRef2(null);
536
+ const [board, setBoard] = useState5(Array(64).fill(null));
537
+ const [myColor, setMyColor] = useState5("red");
538
+ const [currentTurn, setCurrentTurn] = useState5(0);
539
+ const [players, setPlayers] = useState5([]);
540
+ const [selectedPiece, setSelectedPiece] = useState5(null);
541
+ const [validMoves, setValidMoves] = useState5([]);
542
+ const [gameOver, setGameOver] = useState5(false);
543
+ const [gameResult, setGameResult] = useState5(null);
544
+ const [afkWarning, setAfkWarning] = useState5(null);
545
+ if (session.game !== "checkers") return null;
546
+ const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
547
+ const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
548
+ const opponentColor = myColor === "red" ? "black" : "red";
549
+ useEffect5(() => {
550
+ const s = io3(`${serverUrl}/checkers`, {
551
+ auth: { token },
552
+ path: "/socket.io",
553
+ transports: ["websocket", "polling"]
554
+ });
555
+ socketRef.current = s;
556
+ const join = () => s.emit("matchmaking:join", {
557
+ username: myPlayer.displayName,
558
+ playerId: myPlayer.id,
559
+ wantsBot: false,
560
+ sessionId: session.sessionId
561
+ });
562
+ if (s.connected) join();
563
+ else s.once("connect", join);
564
+ s.on("game:started", (d) => {
565
+ roomIdRef.current = d.roomId;
566
+ myPlayerIdRef.current = d.playerId;
567
+ setMyColor(d.color ?? "red");
568
+ setBoard(d.board ?? Array(64).fill(null));
569
+ setCurrentTurn(d.currentTurn ?? 0);
570
+ setPlayers(d.players ?? []);
571
+ });
572
+ s.on("game:move:made", (d) => {
573
+ setBoard(d.board);
574
+ setCurrentTurn(d.currentTurn ?? 0);
575
+ setSelectedPiece(null);
576
+ setValidMoves([]);
577
+ if (afkTimerRef.current) {
578
+ clearInterval(afkTimerRef.current);
579
+ afkTimerRef.current = null;
580
+ }
581
+ setAfkWarning(null);
582
+ });
583
+ s.on("game:valid_moves", (d) => {
584
+ setSelectedPiece(d.position);
585
+ setValidMoves(d.moves);
586
+ });
587
+ s.on("checkers:afk_warning", (d) => {
588
+ setAfkWarning(d);
589
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
590
+ afkTimerRef.current = setInterval(() => {
591
+ setAfkWarning((prev) => {
592
+ if (!prev || prev.secondsRemaining <= 1) {
593
+ clearInterval(afkTimerRef.current);
594
+ afkTimerRef.current = null;
595
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
596
+ return prev;
597
+ }
598
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
599
+ });
600
+ }, 1e3);
601
+ });
602
+ s.on("checkers:afk_warning_cleared", () => {
603
+ if (afkTimerRef.current) {
604
+ clearInterval(afkTimerRef.current);
605
+ afkTimerRef.current = null;
606
+ }
607
+ setAfkWarning(null);
608
+ });
609
+ s.on("afk:status", (status) => {
610
+ if (!status) {
611
+ if (afkTimerRef.current) {
612
+ clearInterval(afkTimerRef.current);
613
+ afkTimerRef.current = null;
614
+ }
615
+ setAfkWarning(null);
616
+ } else {
617
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
618
+ }
619
+ });
620
+ s.on("game:over", (d) => {
621
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
622
+ setTimeout(() => setGameOver(true), 400);
623
+ });
624
+ return () => {
625
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
626
+ s.disconnect();
627
+ socketRef.current = null;
628
+ };
629
+ }, [token, serverUrl]);
630
+ const handleCellPress = (pos) => {
631
+ if (gameOver || !isMyTurn) return;
632
+ const piece = board[pos];
633
+ const moveTarget = validMoves.find((m) => m.to === pos);
634
+ if (moveTarget && selectedPiece !== null) {
635
+ socketRef.current?.emit("game:move", {
636
+ roomId: roomIdRef.current,
637
+ playerId: myPlayerIdRef.current,
638
+ move: { from: selectedPiece, to: pos, captures: moveTarget.captures }
639
+ });
640
+ setSelectedPiece(null);
641
+ setValidMoves([]);
642
+ return;
643
+ }
644
+ if (piece && piece.player === myColor) {
645
+ socketRef.current?.emit("game:get_moves", {
646
+ roomId: roomIdRef.current,
647
+ position: pos,
648
+ playerId: myPlayerIdRef.current
649
+ });
650
+ } else {
651
+ setSelectedPiece(null);
652
+ setValidMoves([]);
653
+ }
654
+ };
655
+ const screenWidth = Dimensions2.get("window").width;
656
+ const boardSize = screenWidth - 32;
657
+ const cellSize = boardSize / 8;
658
+ const rows = Array.from({ length: 8 }, (_, r) => r);
659
+ const cols = Array.from({ length: 8 }, (_, c) => c);
660
+ if (myColor === "black") {
661
+ rows.reverse();
662
+ cols.reverse();
663
+ }
664
+ return /* @__PURE__ */ jsxs5(View5, { style: [styles2.container, style], children: [
665
+ /* @__PURE__ */ jsxs5(View5, { style: styles2.playerRow, children: [
666
+ /* @__PURE__ */ jsx6(View5, { style: styles2.avatar, children: /* @__PURE__ */ jsx6(Text5, { style: styles2.avatarText, children: opponentName[0]?.toUpperCase() }) }),
667
+ /* @__PURE__ */ jsxs5(View5, { style: { flex: 1, marginLeft: 10 }, children: [
668
+ /* @__PURE__ */ jsx6(Text5, { style: styles2.playerName, children: opponentName }),
669
+ /* @__PURE__ */ jsx6(Text5, { style: [styles2.playerColor, { color: opponentColor === "red" ? "#f87171" : "#94a3b8" }], children: opponentColor })
670
+ ] }),
671
+ !isMyTurn && !gameOver && /* @__PURE__ */ jsx6(Text5, { style: styles2.turnIndicator, children: "\u25CF thinking" })
672
+ ] }),
673
+ showAfkWarning && afkWarning && /* @__PURE__ */ jsxs5(View5, { style: [styles2.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles2.afkSelf : styles2.afkOpponent], children: [
674
+ /* @__PURE__ */ jsx6(Text5, { style: styles2.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F Make a move!" : "\u23F3 Opponent is AFK" }),
675
+ /* @__PURE__ */ jsxs5(Text5, { style: styles2.afkText, children: [
676
+ afkWarning.secondsRemaining,
677
+ "s"
678
+ ] })
679
+ ] }),
680
+ /* @__PURE__ */ jsx6(View5, { style: { width: boardSize, height: boardSize, borderWidth: 3, borderColor: "#92400e", borderRadius: 4 }, children: rows.map((row) => /* @__PURE__ */ jsx6(View5, { style: { flexDirection: "row" }, children: cols.map((col) => {
681
+ const pos = row * 8 + col;
682
+ const piece = board[pos];
683
+ const isLight = (row + col) % 2 === 0;
684
+ const isSelected = selectedPiece === pos;
685
+ const moveTarget = validMoves.find((m) => m.to === pos);
686
+ const isCapture = (moveTarget?.captures?.length ?? 0) > 0;
687
+ return /* @__PURE__ */ jsxs5(
688
+ TouchableOpacity2,
689
+ {
690
+ onPress: () => handleCellPress(pos),
691
+ activeOpacity: isLight ? 1 : 0.85,
692
+ style: [
693
+ { width: cellSize, height: cellSize, alignItems: "center", justifyContent: "center" },
694
+ { backgroundColor: isLight ? "#f0d9b5" : "#b58863" },
695
+ isSelected && styles2.cellSelected,
696
+ moveTarget && styles2.cellValidMove
697
+ ],
698
+ children: [
699
+ piece && /* @__PURE__ */ jsx6(View5, { style: [
700
+ styles2.piece,
701
+ { width: cellSize * 0.78, height: cellSize * 0.78 },
702
+ piece.player === "red" ? styles2.pieceRed : styles2.pieceBlack,
703
+ isSelected && styles2.pieceSelected
704
+ ], children: piece.type === "king" && /* @__PURE__ */ jsx6(Text5, { style: { fontSize: cellSize * 0.35 }, children: "\u{1F451}" }) }),
705
+ moveTarget && !piece && /* @__PURE__ */ jsx6(View5, { style: [
706
+ styles2.moveDot,
707
+ { width: cellSize * 0.3, height: cellSize * 0.3, borderRadius: cellSize * 0.15 },
708
+ isCapture ? styles2.moveDotCapture : styles2.moveDotNormal
709
+ ] }),
710
+ moveTarget && piece && /* @__PURE__ */ jsx6(View5, { style: [StyleSheet2.absoluteFill, styles2.captureRing, { borderRadius: cellSize / 2 }] })
711
+ ]
712
+ },
713
+ col
714
+ );
715
+ }) }, row)) }),
716
+ /* @__PURE__ */ jsxs5(View5, { style: styles2.playerRow, children: [
717
+ /* @__PURE__ */ jsx6(View5, { style: [styles2.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ jsx6(Text5, { style: styles2.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
718
+ /* @__PURE__ */ jsxs5(View5, { style: { flex: 1, marginLeft: 10 }, children: [
719
+ /* @__PURE__ */ jsx6(Text5, { style: styles2.playerName, children: myPlayer.displayName }),
720
+ /* @__PURE__ */ jsx6(Text5, { style: [styles2.playerColor, { color: myColor === "red" ? "#f87171" : "#94a3b8" }], children: myColor })
721
+ ] }),
722
+ isMyTurn && !gameOver && /* @__PURE__ */ jsx6(Text5, { style: styles2.turnIndicator, children: "\u25CF your turn" })
723
+ ] }),
724
+ /* @__PURE__ */ jsxs5(View5, { style: styles2.actions, children: [
725
+ /* @__PURE__ */ jsx6(
726
+ TouchableOpacity2,
727
+ {
728
+ style: styles2.btnResign,
729
+ disabled: gameOver,
730
+ onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
731
+ children: /* @__PURE__ */ jsx6(Text5, { style: styles2.btnResignText, children: "Resign" })
732
+ }
733
+ ),
734
+ onLeave && /* @__PURE__ */ jsx6(TouchableOpacity2, { style: styles2.btnLeave, onPress: onLeave, children: /* @__PURE__ */ jsx6(Text5, { style: styles2.btnLeaveText, children: "Leave" }) })
735
+ ] }),
736
+ /* @__PURE__ */ jsx6(Modal2, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ jsx6(View5, { style: styles2.modalOverlay, children: /* @__PURE__ */ jsxs5(View5, { style: styles2.gameOverBox, children: [
737
+ /* @__PURE__ */ jsx6(Text5, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
738
+ /* @__PURE__ */ jsx6(Text5, { style: styles2.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
739
+ /* @__PURE__ */ jsx6(Text5, { style: styles2.gameOverReason, children: gameResult?.reason }),
740
+ onLeave && /* @__PURE__ */ jsx6(TouchableOpacity2, { style: styles2.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ jsx6(Text5, { style: styles2.btnPlayAgainText, children: "Play again" }) })
741
+ ] }) }) })
742
+ ] });
228
743
  }
744
+ var styles2 = StyleSheet2.create({
745
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
746
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
747
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
748
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
749
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
750
+ playerColor: { fontSize: 12, textTransform: "capitalize" },
751
+ turnIndicator: { color: "#a78bfa", fontSize: 12 },
752
+ cellSelected: { borderWidth: 3, borderColor: "#facc15" },
753
+ cellValidMove: { borderWidth: 3, borderColor: "#4ade80" },
754
+ piece: { borderRadius: 999, alignItems: "center", justifyContent: "center" },
755
+ pieceRed: { backgroundColor: "#ef4444", borderWidth: 2, borderColor: "#991b1b" },
756
+ pieceBlack: { backgroundColor: "#374151", borderWidth: 2, borderColor: "#111827" },
757
+ pieceSelected: { transform: [{ scale: 1.1 }] },
758
+ moveDot: { position: "absolute" },
759
+ moveDotNormal: { backgroundColor: "rgba(74,222,128,0.6)" },
760
+ moveDotCapture: { backgroundColor: "rgba(251,146,60,0.7)" },
761
+ captureRing: { borderWidth: 3, borderColor: "rgba(251,146,60,0.7)" },
762
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
763
+ afkSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
764
+ afkOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
765
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
766
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
767
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
768
+ btnResignText: { color: "#f87171", fontSize: 13 },
769
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
770
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
771
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
772
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
773
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
774
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
775
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
776
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
777
+ });
229
778
 
230
779
  // src/components/connect4/index.tsx
231
- import { jsx as jsx8 } from "react/jsx-runtime";
232
- function Connect4Board({ style, showAfkWarning }) {
233
- return /* @__PURE__ */ jsx8(GameWebView, { game: "connect4", style, showAfkWarning });
780
+ import { useEffect as useEffect6, useRef as useRef3, useState as useState6 } from "react";
781
+ import {
782
+ View as View6,
783
+ Text as Text6,
784
+ TouchableOpacity as TouchableOpacity3,
785
+ Modal as Modal3,
786
+ StyleSheet as StyleSheet3,
787
+ Dimensions as Dimensions3
788
+ } from "react-native";
789
+ import { io as io4 } from "socket.io-client";
790
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
791
+ function Connect4Board({ style, showAfkWarning = true, onLeave }) {
792
+ const { token, serverUrl, session } = useBetaGamer();
793
+ const myPlayer = session.players[0];
794
+ const socketRef = useRef3(null);
795
+ const roomIdRef = useRef3(null);
796
+ const myPlayerIdRef = useRef3(myPlayer?.id ?? "");
797
+ const afkTimerRef = useRef3(null);
798
+ const ROWS = 6;
799
+ const COLS = 7;
800
+ const [board, setBoard] = useState6(Array(ROWS * COLS).fill(null));
801
+ const [myColor, setMyColor] = useState6("red");
802
+ const [currentTurn, setCurrentTurn] = useState6(0);
803
+ const [players, setPlayers] = useState6([]);
804
+ const [lastMove, setLastMove] = useState6(null);
805
+ const [winningCells, setWinningCells] = useState6(null);
806
+ const [gameOver, setGameOver] = useState6(false);
807
+ const [gameResult, setGameResult] = useState6(null);
808
+ const [afkWarning, setAfkWarning] = useState6(null);
809
+ if (session.game !== "connect4") return null;
810
+ const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
811
+ const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
812
+ const opponentColor = myColor === "red" ? "yellow" : "red";
813
+ useEffect6(() => {
814
+ const s = io4(`${serverUrl}/connect4`, {
815
+ auth: { token },
816
+ path: "/socket.io",
817
+ transports: ["websocket", "polling"]
818
+ });
819
+ socketRef.current = s;
820
+ const join = () => s.emit("matchmaking:join", {
821
+ username: myPlayer.displayName,
822
+ playerId: myPlayer.id,
823
+ wantsBot: false,
824
+ sessionId: session.sessionId
825
+ });
826
+ if (s.connected) join();
827
+ else s.once("connect", join);
828
+ s.on("game:started", (d) => {
829
+ roomIdRef.current = d.roomId;
830
+ myPlayerIdRef.current = d.playerId;
831
+ setMyColor(d.color ?? "red");
832
+ setBoard(d.board ?? Array(ROWS * COLS).fill(null));
833
+ setCurrentTurn(d.currentTurn ?? 0);
834
+ setPlayers(d.players ?? []);
835
+ });
836
+ s.on("game:move:made", (d) => {
837
+ setBoard(d.board);
838
+ setCurrentTurn(d.currentTurn ?? 0);
839
+ if (d.lastMove != null) setLastMove(d.lastMove);
840
+ if (d.winningCells) setWinningCells(d.winningCells);
841
+ if (afkTimerRef.current) {
842
+ clearInterval(afkTimerRef.current);
843
+ afkTimerRef.current = null;
844
+ }
845
+ setAfkWarning(null);
846
+ });
847
+ s.on("connect4:afk_warning", (d) => {
848
+ setAfkWarning(d);
849
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
850
+ afkTimerRef.current = setInterval(() => {
851
+ setAfkWarning((prev) => {
852
+ if (!prev || prev.secondsRemaining <= 1) {
853
+ clearInterval(afkTimerRef.current);
854
+ afkTimerRef.current = null;
855
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
856
+ return prev;
857
+ }
858
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
859
+ });
860
+ }, 1e3);
861
+ });
862
+ s.on("connect4:afk_warning_cleared", () => {
863
+ if (afkTimerRef.current) {
864
+ clearInterval(afkTimerRef.current);
865
+ afkTimerRef.current = null;
866
+ }
867
+ setAfkWarning(null);
868
+ });
869
+ s.on("afk:status", (status) => {
870
+ if (!status) {
871
+ if (afkTimerRef.current) {
872
+ clearInterval(afkTimerRef.current);
873
+ afkTimerRef.current = null;
874
+ }
875
+ setAfkWarning(null);
876
+ } else {
877
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
878
+ }
879
+ });
880
+ s.on("game:over", (d) => {
881
+ if (d.winningCells) setWinningCells(d.winningCells);
882
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
883
+ setTimeout(() => setGameOver(true), 400);
884
+ });
885
+ return () => {
886
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
887
+ s.disconnect();
888
+ socketRef.current = null;
889
+ };
890
+ }, [token, serverUrl]);
891
+ const handleColumnPress = (col) => {
892
+ if (!isMyTurn || gameOver) return;
893
+ if (board[col] !== null) return;
894
+ socketRef.current?.emit("game:move", { roomId: roomIdRef.current, column: col, playerId: myPlayerIdRef.current });
895
+ };
896
+ const screenWidth = Dimensions3.get("window").width;
897
+ const boardWidth = screenWidth - 32;
898
+ const cellSize = boardWidth / COLS;
899
+ const ColorDot = ({ color, size = 12 }) => /* @__PURE__ */ jsx7(View6, { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: color === "red" ? "#ef4444" : "#eab308", marginRight: 6 } });
900
+ return /* @__PURE__ */ jsxs6(View6, { style: [styles3.container, style], children: [
901
+ /* @__PURE__ */ jsxs6(View6, { style: styles3.playerRow, children: [
902
+ /* @__PURE__ */ jsx7(View6, { style: styles3.avatar, children: /* @__PURE__ */ jsx7(Text6, { style: styles3.avatarText, children: opponentName[0]?.toUpperCase() }) }),
903
+ /* @__PURE__ */ jsxs6(View6, { style: { flex: 1, marginLeft: 10, flexDirection: "row", alignItems: "center" }, children: [
904
+ /* @__PURE__ */ jsx7(ColorDot, { color: opponentColor }),
905
+ /* @__PURE__ */ jsxs6(View6, { children: [
906
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.playerName, children: opponentName }),
907
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.playerColor, children: opponentColor })
908
+ ] })
909
+ ] }),
910
+ !isMyTurn && !gameOver && /* @__PURE__ */ jsx7(Text6, { style: styles3.turnIndicator, children: "\u25CF thinking" })
911
+ ] }),
912
+ showAfkWarning && afkWarning && /* @__PURE__ */ jsxs6(View6, { style: [styles3.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles3.afkSelf : styles3.afkOpponent], children: [
913
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F Drop a piece!" : "\u23F3 Opponent is AFK" }),
914
+ /* @__PURE__ */ jsxs6(Text6, { style: styles3.afkText, children: [
915
+ afkWarning.secondsRemaining,
916
+ "s"
917
+ ] })
918
+ ] }),
919
+ /* @__PURE__ */ jsxs6(View6, { style: { width: boardWidth, backgroundColor: "#2563eb", borderRadius: 12, padding: 6 }, children: [
920
+ /* @__PURE__ */ jsx7(View6, { style: { flexDirection: "row" }, children: Array.from({ length: COLS }, (_, col) => {
921
+ const canDrop = isMyTurn && !gameOver && board[col] === null;
922
+ return /* @__PURE__ */ jsx7(
923
+ TouchableOpacity3,
924
+ {
925
+ onPress: () => handleColumnPress(col),
926
+ activeOpacity: 0.7,
927
+ style: { width: cellSize, height: 28, alignItems: "center", justifyContent: "center" },
928
+ children: canDrop && /* @__PURE__ */ jsx7(Text6, { style: { color: "#fff", fontSize: 14 }, children: "\u25BC" })
929
+ },
930
+ col
931
+ );
932
+ }) }),
933
+ Array.from({ length: ROWS }, (_, row) => /* @__PURE__ */ jsx7(View6, { style: { flexDirection: "row" }, children: Array.from({ length: COLS }, (_2, col) => {
934
+ const pos = row * COLS + col;
935
+ const piece = board[pos];
936
+ const isWinning = winningCells?.includes(pos);
937
+ const isLast = lastMove === pos;
938
+ return /* @__PURE__ */ jsx7(View6, { style: { width: cellSize, height: cellSize, padding: 3 }, children: /* @__PURE__ */ jsx7(View6, { style: [styles3.cell, isWinning && styles3.cellWin], children: piece && /* @__PURE__ */ jsx7(View6, { style: [
939
+ styles3.piece,
940
+ piece === "red" ? styles3.pieceRed : styles3.pieceYellow,
941
+ isWinning && styles3.pieceWin,
942
+ isLast && !isWinning && styles3.pieceLast
943
+ ] }) }) }, col);
944
+ }) }, row))
945
+ ] }),
946
+ /* @__PURE__ */ jsxs6(View6, { style: styles3.playerRow, children: [
947
+ /* @__PURE__ */ jsx7(View6, { style: [styles3.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ jsx7(Text6, { style: styles3.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
948
+ /* @__PURE__ */ jsxs6(View6, { style: { flex: 1, marginLeft: 10, flexDirection: "row", alignItems: "center" }, children: [
949
+ /* @__PURE__ */ jsx7(ColorDot, { color: myColor }),
950
+ /* @__PURE__ */ jsxs6(View6, { children: [
951
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.playerName, children: myPlayer.displayName }),
952
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.playerColor, children: myColor })
953
+ ] })
954
+ ] }),
955
+ isMyTurn && !gameOver && /* @__PURE__ */ jsx7(Text6, { style: styles3.turnIndicator, children: "\u25CF your turn" })
956
+ ] }),
957
+ /* @__PURE__ */ jsxs6(View6, { style: styles3.actions, children: [
958
+ /* @__PURE__ */ jsx7(
959
+ TouchableOpacity3,
960
+ {
961
+ style: styles3.btnResign,
962
+ disabled: gameOver,
963
+ onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
964
+ children: /* @__PURE__ */ jsx7(Text6, { style: styles3.btnResignText, children: "Resign" })
965
+ }
966
+ ),
967
+ onLeave && /* @__PURE__ */ jsx7(TouchableOpacity3, { style: styles3.btnLeave, onPress: onLeave, children: /* @__PURE__ */ jsx7(Text6, { style: styles3.btnLeaveText, children: "Leave" }) })
968
+ ] }),
969
+ /* @__PURE__ */ jsx7(Modal3, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ jsx7(View6, { style: styles3.modalOverlay, children: /* @__PURE__ */ jsxs6(View6, { style: styles3.gameOverBox, children: [
970
+ /* @__PURE__ */ jsx7(Text6, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
971
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
972
+ /* @__PURE__ */ jsx7(Text6, { style: styles3.gameOverReason, children: gameResult?.reason }),
973
+ onLeave && /* @__PURE__ */ jsx7(TouchableOpacity3, { style: styles3.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ jsx7(Text6, { style: styles3.btnPlayAgainText, children: "Play again" }) })
974
+ ] }) }) })
975
+ ] });
234
976
  }
977
+ var styles3 = StyleSheet3.create({
978
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
979
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
980
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
981
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
982
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
983
+ playerColor: { color: "#64748b", fontSize: 12, textTransform: "capitalize" },
984
+ turnIndicator: { color: "#a78bfa", fontSize: 12 },
985
+ cell: { flex: 1, borderRadius: 999, backgroundColor: "#1e3a5f", alignItems: "center", justifyContent: "center" },
986
+ cellWin: { backgroundColor: "#15803d" },
987
+ piece: { width: "85%", height: "85%", borderRadius: 999 },
988
+ pieceRed: { backgroundColor: "#ef4444", shadowColor: "#ef4444", shadowOpacity: 0.5, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } },
989
+ pieceYellow: { backgroundColor: "#eab308", shadowColor: "#eab308", shadowOpacity: 0.5, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } },
990
+ pieceWin: { transform: [{ scale: 1.1 }] },
991
+ pieceLast: { borderWidth: 2, borderColor: "#22d3ee" },
992
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
993
+ afkSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
994
+ afkOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
995
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
996
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
997
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
998
+ btnResignText: { color: "#f87171", fontSize: 13 },
999
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1000
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
1001
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
1002
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
1003
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
1004
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
1005
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
1006
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
1007
+ });
235
1008
 
236
1009
  // src/components/tictactoe/index.tsx
1010
+ import { useEffect as useEffect7, useRef as useRef4, useState as useState7 } from "react";
1011
+ import {
1012
+ View as View7,
1013
+ Text as Text7,
1014
+ TouchableOpacity as TouchableOpacity4,
1015
+ Modal as Modal4,
1016
+ StyleSheet as StyleSheet4,
1017
+ Dimensions as Dimensions4
1018
+ } from "react-native";
1019
+ import { io as io5 } from "socket.io-client";
1020
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1021
+ function TictactoeBoard({ style, showAfkWarning = true, onLeave }) {
1022
+ const { token, serverUrl, session } = useBetaGamer();
1023
+ const myPlayer = session.players[0];
1024
+ const socketRef = useRef4(null);
1025
+ const roomIdRef = useRef4(null);
1026
+ const myPlayerIdRef = useRef4(myPlayer?.id ?? "");
1027
+ const afkTimerRef = useRef4(null);
1028
+ const [board, setBoard] = useState7(Array(9).fill(null));
1029
+ const [myMark, setMyMark] = useState7("X");
1030
+ const [currentTurn, setCurrentTurn] = useState7(0);
1031
+ const [players, setPlayers] = useState7([]);
1032
+ const [winningLine, setWinningLine] = useState7(null);
1033
+ const [gameOver, setGameOver] = useState7(false);
1034
+ const [gameResult, setGameResult] = useState7(null);
1035
+ const [afkWarning, setAfkWarning] = useState7(null);
1036
+ if (session.game !== "tictactoe") return null;
1037
+ const isMyTurn = players.length > 0 ? players[currentTurn]?.id === myPlayerIdRef.current : false;
1038
+ const opponentName = players.find((p) => p.id !== myPlayerIdRef.current)?.username ?? "Waiting\u2026";
1039
+ useEffect7(() => {
1040
+ const s = io5(`${serverUrl}/tictactoe`, {
1041
+ auth: { token },
1042
+ path: "/socket.io",
1043
+ transports: ["websocket", "polling"]
1044
+ });
1045
+ socketRef.current = s;
1046
+ const join = () => s.emit("matchmaking:join", {
1047
+ username: myPlayer.displayName,
1048
+ playerId: myPlayer.id,
1049
+ wantsBot: false,
1050
+ sessionId: session.sessionId
1051
+ });
1052
+ if (s.connected) join();
1053
+ else s.once("connect", join);
1054
+ s.on("game:started", (d) => {
1055
+ roomIdRef.current = d.roomId;
1056
+ myPlayerIdRef.current = d.playerId;
1057
+ setMyMark(d.mark);
1058
+ setBoard(d.board ?? Array(9).fill(null));
1059
+ setCurrentTurn(d.currentTurn ?? 0);
1060
+ setPlayers(d.players ?? []);
1061
+ });
1062
+ s.on("game:move:made", (d) => {
1063
+ setBoard(d.board);
1064
+ setCurrentTurn(d.currentTurn ?? 0);
1065
+ if (d.winningLine) setWinningLine(d.winningLine);
1066
+ if (afkTimerRef.current) {
1067
+ clearInterval(afkTimerRef.current);
1068
+ afkTimerRef.current = null;
1069
+ }
1070
+ setAfkWarning(null);
1071
+ });
1072
+ s.on("tictactoe:afk_warning", (d) => {
1073
+ setAfkWarning(d);
1074
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
1075
+ afkTimerRef.current = setInterval(() => {
1076
+ setAfkWarning((prev) => {
1077
+ if (!prev || prev.secondsRemaining <= 1) {
1078
+ clearInterval(afkTimerRef.current);
1079
+ afkTimerRef.current = null;
1080
+ if (roomIdRef.current) s.emit("afk:check", { roomId: roomIdRef.current });
1081
+ return prev;
1082
+ }
1083
+ return { ...prev, secondsRemaining: prev.secondsRemaining - 1 };
1084
+ });
1085
+ }, 1e3);
1086
+ });
1087
+ s.on("tictactoe:afk_warning_cleared", () => {
1088
+ if (afkTimerRef.current) {
1089
+ clearInterval(afkTimerRef.current);
1090
+ afkTimerRef.current = null;
1091
+ }
1092
+ setAfkWarning(null);
1093
+ });
1094
+ s.on("afk:status", (status) => {
1095
+ if (!status) {
1096
+ if (afkTimerRef.current) {
1097
+ clearInterval(afkTimerRef.current);
1098
+ afkTimerRef.current = null;
1099
+ }
1100
+ setAfkWarning(null);
1101
+ } else {
1102
+ setAfkWarning({ playerId: status.playerId, secondsRemaining: Math.max(1, Math.round((status.expiresAt - Date.now()) / 1e3)) });
1103
+ }
1104
+ });
1105
+ s.on("game:over", (d) => {
1106
+ setGameResult({ winner: d.winner ?? null, reason: d.reason });
1107
+ setTimeout(() => setGameOver(true), 400);
1108
+ });
1109
+ return () => {
1110
+ if (afkTimerRef.current) clearInterval(afkTimerRef.current);
1111
+ s.disconnect();
1112
+ socketRef.current = null;
1113
+ };
1114
+ }, [token, serverUrl]);
1115
+ const handleCellPress = (idx) => {
1116
+ if (!isMyTurn || gameOver || board[idx]) return;
1117
+ socketRef.current?.emit("game:move", { roomId: roomIdRef.current, position: idx, playerId: myPlayerIdRef.current });
1118
+ };
1119
+ const screenWidth = Dimensions4.get("window").width;
1120
+ const boardSize = screenWidth - 32;
1121
+ const cellSize = boardSize / 3;
1122
+ return /* @__PURE__ */ jsxs7(View7, { style: [styles4.container, style], children: [
1123
+ /* @__PURE__ */ jsxs7(View7, { style: styles4.playerRow, children: [
1124
+ /* @__PURE__ */ jsx8(View7, { style: styles4.avatar, children: /* @__PURE__ */ jsx8(Text7, { style: styles4.avatarText, children: opponentName[0]?.toUpperCase() }) }),
1125
+ /* @__PURE__ */ jsxs7(View7, { style: { flex: 1, marginLeft: 10 }, children: [
1126
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.playerName, children: opponentName }),
1127
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.playerColor, children: myMark === "X" ? "O" : "X" })
1128
+ ] }),
1129
+ !isMyTurn && !gameOver && /* @__PURE__ */ jsx8(Text7, { style: styles4.turnIndicator, children: "\u25CF thinking" })
1130
+ ] }),
1131
+ showAfkWarning && afkWarning && /* @__PURE__ */ jsxs7(View7, { style: [styles4.afkBanner, afkWarning.playerId === myPlayerIdRef.current ? styles4.afkSelf : styles4.afkOpponent], children: [
1132
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.afkText, children: afkWarning.playerId === myPlayerIdRef.current ? "\u26A0\uFE0F Make a move!" : "\u23F3 Opponent is AFK" }),
1133
+ /* @__PURE__ */ jsxs7(Text7, { style: styles4.afkText, children: [
1134
+ afkWarning.secondsRemaining,
1135
+ "s"
1136
+ ] })
1137
+ ] }),
1138
+ /* @__PURE__ */ jsx8(View7, { style: { width: boardSize, height: boardSize, flexDirection: "row", flexWrap: "wrap" }, children: board.map((cell, idx) => {
1139
+ const isWinning = winningLine?.includes(idx);
1140
+ const isEmpty = cell === null;
1141
+ return /* @__PURE__ */ jsx8(
1142
+ TouchableOpacity4,
1143
+ {
1144
+ onPress: () => handleCellPress(idx),
1145
+ activeOpacity: 0.8,
1146
+ style: [
1147
+ {
1148
+ width: cellSize,
1149
+ height: cellSize,
1150
+ alignItems: "center",
1151
+ justifyContent: "center",
1152
+ borderWidth: 2,
1153
+ borderColor: "#1e1b4b"
1154
+ },
1155
+ isWinning ? styles4.cellWin : isEmpty && isMyTurn && !gameOver ? styles4.cellActive : styles4.cellIdle
1156
+ ],
1157
+ children: cell ? /* @__PURE__ */ jsx8(Text7, { style: [styles4.mark, cell === "X" ? styles4.markX : styles4.markO, isWinning && styles4.markWin], children: cell === "X" ? "\u2715" : "\u25CB" }) : isMyTurn && !gameOver ? /* @__PURE__ */ jsx8(Text7, { style: [styles4.mark, myMark === "X" ? styles4.markX : styles4.markO, { opacity: 0.2 }], children: myMark === "X" ? "\u2715" : "\u25CB" }) : null
1158
+ },
1159
+ idx
1160
+ );
1161
+ }) }),
1162
+ /* @__PURE__ */ jsxs7(View7, { style: styles4.playerRow, children: [
1163
+ /* @__PURE__ */ jsx8(View7, { style: [styles4.avatar, { backgroundColor: "#92400e" }], children: /* @__PURE__ */ jsx8(Text7, { style: styles4.avatarText, children: myPlayer.displayName[0]?.toUpperCase() }) }),
1164
+ /* @__PURE__ */ jsxs7(View7, { style: { flex: 1, marginLeft: 10 }, children: [
1165
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.playerName, children: myPlayer.displayName }),
1166
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.playerColor, children: myMark })
1167
+ ] }),
1168
+ isMyTurn && !gameOver && /* @__PURE__ */ jsx8(Text7, { style: styles4.turnIndicator, children: "\u25CF your turn" })
1169
+ ] }),
1170
+ /* @__PURE__ */ jsxs7(View7, { style: styles4.actions, children: [
1171
+ /* @__PURE__ */ jsx8(
1172
+ TouchableOpacity4,
1173
+ {
1174
+ style: styles4.btnResign,
1175
+ disabled: gameOver,
1176
+ onPress: () => socketRef.current?.emit("game:resign", { roomId: roomIdRef.current, playerId: myPlayerIdRef.current }),
1177
+ children: /* @__PURE__ */ jsx8(Text7, { style: styles4.btnResignText, children: "Resign" })
1178
+ }
1179
+ ),
1180
+ onLeave && /* @__PURE__ */ jsx8(TouchableOpacity4, { style: styles4.btnLeave, onPress: onLeave, children: /* @__PURE__ */ jsx8(Text7, { style: styles4.btnLeaveText, children: "Leave" }) })
1181
+ ] }),
1182
+ /* @__PURE__ */ jsx8(Modal4, { visible: gameOver && !!gameResult, transparent: true, animationType: "fade", children: /* @__PURE__ */ jsx8(View7, { style: styles4.modalOverlay, children: /* @__PURE__ */ jsxs7(View7, { style: styles4.gameOverBox, children: [
1183
+ /* @__PURE__ */ jsx8(Text7, { style: { fontSize: 48, marginBottom: 8 }, children: gameResult?.winner === myPlayerIdRef.current ? "\u{1F3C6}" : gameResult?.winner ? "\u{1F614}" : "\u{1F91D}" }),
1184
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.gameOverTitle, children: gameResult?.winner === myPlayerIdRef.current ? "You won!" : gameResult?.winner ? "You lost" : "Draw" }),
1185
+ /* @__PURE__ */ jsx8(Text7, { style: styles4.gameOverReason, children: gameResult?.reason }),
1186
+ onLeave && /* @__PURE__ */ jsx8(TouchableOpacity4, { style: styles4.btnPlayAgain, onPress: onLeave, children: /* @__PURE__ */ jsx8(Text7, { style: styles4.btnPlayAgainText, children: "Play again" }) })
1187
+ ] }) }) })
1188
+ ] });
1189
+ }
1190
+ var styles4 = StyleSheet4.create({
1191
+ container: { flex: 1, backgroundColor: "#0a0a0f", alignItems: "center", padding: 16, gap: 10 },
1192
+ playerRow: { flexDirection: "row", alignItems: "center", backgroundColor: "#0f172a", borderRadius: 12, padding: 12, width: "100%" },
1193
+ avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#334155", alignItems: "center", justifyContent: "center" },
1194
+ avatarText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
1195
+ playerName: { color: "#fff", fontWeight: "600", fontSize: 14 },
1196
+ playerColor: { color: "#64748b", fontSize: 12 },
1197
+ turnIndicator: { color: "#a78bfa", fontSize: 12 },
1198
+ cellActive: { backgroundColor: "#4c1d95" },
1199
+ cellIdle: { backgroundColor: "#1e1b4b" },
1200
+ cellWin: { backgroundColor: "#15803d" },
1201
+ mark: { fontSize: 48, fontWeight: "bold", lineHeight: 56 },
1202
+ markX: { color: "#60a5fa" },
1203
+ markO: { color: "#f472b6" },
1204
+ markWin: { color: "#fff" },
1205
+ afkBanner: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, width: "100%" },
1206
+ afkSelf: { backgroundColor: "rgba(127,29,29,0.6)", borderWidth: 1, borderColor: "#dc2626" },
1207
+ afkOpponent: { backgroundColor: "rgba(78,63,0,0.4)", borderWidth: 1, borderColor: "#a16207" },
1208
+ afkText: { color: "#fecaca", fontWeight: "600", fontSize: 13 },
1209
+ actions: { flexDirection: "row", gap: 8, width: "100%" },
1210
+ btnResign: { flex: 1, borderWidth: 1, borderColor: "#7f1d1d", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1211
+ btnResignText: { color: "#f87171", fontSize: 13 },
1212
+ btnLeave: { flex: 1, borderWidth: 1, borderColor: "#334155", borderRadius: 8, paddingVertical: 8, alignItems: "center" },
1213
+ btnLeaveText: { color: "#94a3b8", fontSize: 13 },
1214
+ modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", alignItems: "center", justifyContent: "center" },
1215
+ gameOverBox: { backgroundColor: "#1e293b", borderRadius: 20, padding: 32, alignItems: "center", width: 280 },
1216
+ gameOverTitle: { color: "#fff", fontWeight: "bold", fontSize: 22, marginBottom: 4 },
1217
+ gameOverReason: { color: "#94a3b8", fontSize: 14, marginBottom: 20, textTransform: "capitalize" },
1218
+ btnPlayAgain: { backgroundColor: "#d97706", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 32 },
1219
+ btnPlayAgainText: { color: "#fff", fontWeight: "bold", fontSize: 15 }
1220
+ });
1221
+
1222
+ // src/components/subway-runner/index.tsx
1223
+ import { useEffect as useEffect8, useState as useState8 } from "react";
1224
+ import { View as View8, Text as Text8 } from "react-native";
1225
+
1226
+ // src/components/GameWebView.tsx
1227
+ import { useRef as useRef5 } from "react";
1228
+ import { WebView } from "react-native-webview";
237
1229
  import { jsx as jsx9 } from "react/jsx-runtime";
238
- function TictactoeBoard({ style, showAfkWarning }) {
239
- return /* @__PURE__ */ jsx9(GameWebView, { game: "tictactoe", style, showAfkWarning });
1230
+ function GameWebView({ game, style, showAfkWarning = true }) {
1231
+ const { token, serverUrl, session } = useBetaGamer();
1232
+ const webViewRef = useRef5(null);
1233
+ if (session.game !== game) return null;
1234
+ const uri = showAfkWarning ? `${serverUrl}/embed/${game}` : `${serverUrl}/embed/${game}?afkWarning=0`;
1235
+ const initScript = `
1236
+ window.__BG_TOKEN__ = ${JSON.stringify(token)};
1237
+ window.postMessage(JSON.stringify({ type: 'bg:init', token: ${JSON.stringify(token)} }), '*');
1238
+ true;
1239
+ `;
1240
+ return /* @__PURE__ */ jsx9(
1241
+ WebView,
1242
+ {
1243
+ ref: webViewRef,
1244
+ source: { uri },
1245
+ injectedJavaScriptBeforeContentLoaded: initScript,
1246
+ style: [{ flex: 1 }, style],
1247
+ allowsInlineMediaPlayback: true,
1248
+ mediaPlaybackRequiresUserAction: false
1249
+ }
1250
+ );
240
1251
  }
241
1252
 
242
1253
  // src/components/subway-runner/index.tsx
243
- import { useEffect as useEffect4, useState as useState4 } from "react";
244
- import { View as View4, Text as Text4 } from "react-native";
245
1254
  import { jsx as jsx10 } from "react/jsx-runtime";
246
1255
  function SubwayRunnerGame({ style }) {
247
1256
  return /* @__PURE__ */ jsx10(GameWebView, { game: "subway-runner", style });
248
1257
  }
249
1258
  function SubwayRunnerScore({ style, textStyle }) {
250
1259
  const socket = useSocket();
251
- const [score, setScore] = useState4(0);
252
- useEffect4(() => {
1260
+ const [score, setScore] = useState8(0);
1261
+ useEffect8(() => {
253
1262
  if (!socket) return;
254
1263
  socket.on("runner:score", (s) => setScore(s));
255
1264
  return () => {
256
1265
  socket.off("runner:score");
257
1266
  };
258
1267
  }, [socket]);
259
- return /* @__PURE__ */ jsx10(View4, { style, children: /* @__PURE__ */ jsx10(Text4, { style: textStyle, children: score }) });
1268
+ return /* @__PURE__ */ jsx10(View8, { style, children: /* @__PURE__ */ jsx10(Text8, { style: textStyle, children: score }) });
260
1269
  }
261
1270
  function SubwayRunnerLives({ style, lifeStyle }) {
262
1271
  const socket = useSocket();
263
- const [lives, setLives] = useState4(3);
264
- useEffect4(() => {
1272
+ const [lives, setLives] = useState8(3);
1273
+ useEffect8(() => {
265
1274
  if (!socket) return;
266
1275
  socket.on("runner:lives", (l) => setLives(l));
267
1276
  return () => {
268
1277
  socket.off("runner:lives");
269
1278
  };
270
1279
  }, [socket]);
271
- return /* @__PURE__ */ jsx10(View4, { style: [{ flexDirection: "row" }, style], children: Array.from({ length: lives }).map((_, i) => /* @__PURE__ */ jsx10(View4, { style: lifeStyle, accessibilityLabel: "life" }, i)) });
1280
+ return /* @__PURE__ */ jsx10(View8, { style: [{ flexDirection: "row" }, style], children: Array.from({ length: lives }).map((_, i) => /* @__PURE__ */ jsx10(View8, { style: lifeStyle, accessibilityLabel: "life" }, i)) });
272
1281
  }
273
1282
  export {
274
1283
  BetaGamerProvider,