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