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