stability
This commit is contained in:
@@ -67,10 +67,14 @@ export default function PlayPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "disconnected" && shouldRedirectToConnect) {
|
if (status === "disconnected" && shouldRedirectToConnect) {
|
||||||
clearRedirectFlag();
|
clearRedirectFlag();
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === "idle") {
|
||||||
|
router.replace("/");
|
||||||
|
}
|
||||||
|
|
||||||
if (role !== "player" && status !== "idle") {
|
if (role !== "player" && status !== "idle") {
|
||||||
router.replace("/spectate");
|
router.replace("/spectate");
|
||||||
@@ -80,8 +84,6 @@ export default function PlayPage() {
|
|||||||
if (status === "connected" && gamePhase === "idle") {
|
if (status === "connected" && gamePhase === "idle") {
|
||||||
setGamePhase("connected");
|
setGamePhase("connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}, [
|
}, [
|
||||||
role,
|
role,
|
||||||
status,
|
status,
|
||||||
@@ -236,6 +238,8 @@ export default function PlayPage() {
|
|||||||
myColor === 1 ? "🔴 Red" : myColor === 2 ? "🟡 Yellow" : null;
|
myColor === 1 ? "🔴 Red" : myColor === 2 ? "🟡 Yellow" : null;
|
||||||
const opponentColor: 1 | 2 | null =
|
const opponentColor: 1 | 2 | null =
|
||||||
myColor === 1 ? 2 : myColor === 2 ? 1 : null;
|
myColor === 1 ? 2 : myColor === 2 ? 1 : null;
|
||||||
|
const redPlayerName = myColor === 1 ? username : "Opponent";
|
||||||
|
const yellowPlayerName = myColor === 2 ? username : "Opponent";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@@ -418,8 +422,8 @@ export default function PlayPage() {
|
|||||||
<Board
|
<Board
|
||||||
board={board}
|
board={board}
|
||||||
lastMove={lastMove}
|
lastMove={lastMove}
|
||||||
player1={username}
|
player1={redPlayerName}
|
||||||
player2="Opponent"
|
player2={yellowPlayerName}
|
||||||
currentTurnColor={
|
currentTurnColor={
|
||||||
gamePhase === "playing" && myColor
|
gamePhase === "playing" && myColor
|
||||||
? isMyTurn
|
? isMyTurn
|
||||||
|
|||||||
@@ -82,12 +82,14 @@ export default function SpectatePage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "disconnected" && shouldRedirectToConnect) {
|
if (status === "disconnected" && shouldRedirectToConnect) {
|
||||||
clearRedirectFlag();
|
clearRedirectFlag();
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
} else if (status === "idle") {
|
}
|
||||||
|
|
||||||
|
if (status === "idle") {
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role !== "observer" && status !== "idle") {
|
if (role !== "observer" && status !== "idle") {
|
||||||
router.replace("/play");
|
router.replace("/play");
|
||||||
@@ -147,37 +149,36 @@ export default function SpectatePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "GAME_MOVE": {
|
case "GAME_MOVE": {
|
||||||
const gamesSnapshot = liveGamesRef.current;
|
if (typeof msg.matchId !== "number") {
|
||||||
for (const [id, game] of gamesSnapshot) {
|
addLog("Protocol error: GAME_MOVE missing matchId");
|
||||||
if (
|
break;
|
||||||
game.player1 === msg.username ||
|
|
||||||
game.player2 === msg.username
|
|
||||||
) {
|
|
||||||
const color: 1 | 2 = msg.username === game.player1 ? 1 : 2;
|
|
||||||
const { board: next, row } = placeToken(
|
|
||||||
game.board,
|
|
||||||
color,
|
|
||||||
msg.column,
|
|
||||||
);
|
|
||||||
updateGame(id, {
|
|
||||||
board: next,
|
|
||||||
lastMove: { column: msg.column, row },
|
|
||||||
currentTurnColor: (color === 1 ? 2 : 1) as 1 | 2,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const game = liveGamesRef.current.get(msg.matchId);
|
||||||
|
if (!game) break;
|
||||||
|
const color: 1 | 2 = msg.username === game.player1 ? 1 : 2;
|
||||||
|
const { board: next, row } = placeToken(
|
||||||
|
game.board,
|
||||||
|
color,
|
||||||
|
msg.column,
|
||||||
|
);
|
||||||
|
updateGame(msg.matchId, {
|
||||||
|
board: next,
|
||||||
|
lastMove: { column: msg.column, row },
|
||||||
|
currentTurnColor: (color === 1 ? 2 : 1) as 1 | 2,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "GAME_WIN": {
|
case "GAME_WIN": {
|
||||||
const gamesSnapshot = liveGamesRef.current;
|
if (typeof msg.matchId !== "number") {
|
||||||
for (const [id, game] of gamesSnapshot) {
|
addLog("Protocol error: GAME_WIN missing matchId");
|
||||||
if (game.player1 === msg.winner || game.player2 === msg.winner) {
|
break;
|
||||||
updateGame(id, { result: { kind: "win", winner: msg.winner } });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateGame(msg.matchId, {
|
||||||
|
result: { kind: "win", winner: msg.winner },
|
||||||
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
send(cmd.gameList());
|
send(cmd.gameList());
|
||||||
send(cmd.playerList());
|
send(cmd.playerList());
|
||||||
@@ -186,15 +187,19 @@ export default function SpectatePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "GAME_DRAW":
|
case "GAME_DRAW":
|
||||||
if (selectedGame !== null) {
|
if (typeof msg.matchId !== "number") {
|
||||||
updateGame(selectedGame, { result: { kind: "draw" } });
|
addLog("Protocol error: GAME_DRAW missing matchId");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
updateGame(msg.matchId, { result: { kind: "draw" } });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "GAME_TERMINATED":
|
case "GAME_TERMINATED":
|
||||||
if (selectedGame !== null) {
|
if (typeof msg.matchId !== "number") {
|
||||||
updateGame(selectedGame, { result: { kind: "terminated" } });
|
addLog("Protocol error: GAME_TERMINATED missing matchId");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
updateGame(msg.matchId, { result: { kind: "terminated" } });
|
||||||
send(cmd.gameList());
|
send(cmd.gameList());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function ConnectionProvider({
|
|||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
if (session.role === "observer") {
|
if (session.role === "observer") {
|
||||||
setStatus("connected");
|
socket.send(cmd.observe());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,24 +160,31 @@ export function ConnectionProvider({
|
|||||||
|
|
||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
const raw = event.data as string;
|
const raw = event.data as string;
|
||||||
|
console.log(raw);
|
||||||
const parsed = parseMessage(raw);
|
const parsed = parseMessage(raw);
|
||||||
|
|
||||||
if (parsed.type === "CONNECT_ACK") {
|
if (parsed.type === "OBSERVE_ACK") {
|
||||||
setRole("player");
|
setRole("observer");
|
||||||
|
setShouldRedirectToConnect(false);
|
||||||
|
setStatus("connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.type === "RECONNECT_ACK") {
|
if (parsed.type === "CONNECT_ACK") {
|
||||||
|
setRole("player");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.type === "RECONNECT_ACK") {
|
||||||
clearReconnectState();
|
clearReconnectState();
|
||||||
setShouldRedirectToConnect(false);
|
setShouldRedirectToConnect(false);
|
||||||
setStatus("connected");
|
setStatus("connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.type === "DISCONNECT_ACK") {
|
if (parsed.type === "DISCONNECT_ACK") {
|
||||||
setRole("observer");
|
setRole("observer");
|
||||||
setUsername("");
|
setUsername("");
|
||||||
isInMatchRef.current = false;
|
isInMatchRef.current = false;
|
||||||
setIsInMatch(false);
|
setIsInMatch(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.type === "GAME_START") {
|
if (parsed.type === "GAME_START") {
|
||||||
isInMatchRef.current = true;
|
isInMatchRef.current = true;
|
||||||
@@ -296,7 +303,6 @@ export function ConnectionProvider({
|
|||||||
[clearReconnectState, openSocket],
|
[clearReconnectState, openSocket],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const disconnect = useCallback(() => {
|
const disconnect = useCallback(() => {
|
||||||
clearReconnectState();
|
clearReconnectState();
|
||||||
manualCloseRef.current = true;
|
manualCloseRef.current = true;
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ export interface MoveEntry {
|
|||||||
column: number;
|
column: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_WS_URL = process.env.NODE_ENV === "development"
|
export const DEFAULT_WS_URL =
|
||||||
? "ws://localhost:8080"
|
process.env.NODE_ENV === "development"
|
||||||
: "wss://connect4.abunchofknowitalls.com";
|
? "ws://localhost:8080"
|
||||||
|
: "wss://connect4.abunchofknowitalls.com";
|
||||||
export const RECONNECT_INTERVAL_MS = 5000;
|
export const RECONNECT_INTERVAL_MS = 5000;
|
||||||
export const RECONNECT_TIMEOUT_MS = 60000;
|
export const RECONNECT_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
@@ -34,12 +35,13 @@ export type ParsedMessage =
|
|||||||
| { type: "CONNECT_ACK" }
|
| { type: "CONNECT_ACK" }
|
||||||
| { type: "RECONNECT_ACK" }
|
| { type: "RECONNECT_ACK" }
|
||||||
| { type: "DISCONNECT_ACK" }
|
| { type: "DISCONNECT_ACK" }
|
||||||
|
| { type: "OBSERVE_ACK"; enabled: boolean }
|
||||||
| { type: "READY_ACK" }
|
| { type: "READY_ACK" }
|
||||||
| { type: "GAME_START"; goesFirst: boolean }
|
| { type: "GAME_START"; goesFirst: boolean }
|
||||||
| { type: "GAME_WINS" }
|
| { type: "GAME_WINS" }
|
||||||
| { type: "GAME_LOSS" }
|
| { type: "GAME_LOSS" }
|
||||||
| { type: "GAME_DRAW" }
|
| { type: "GAME_DRAW"; matchId?: number }
|
||||||
| { type: "GAME_TERMINATED" }
|
| { type: "GAME_TERMINATED"; matchId?: number }
|
||||||
| { type: "OPPONENT_MOVE"; column: number }
|
| { type: "OPPONENT_MOVE"; column: number }
|
||||||
| { type: "GAME_LIST"; games: GameEntry[] }
|
| { type: "GAME_LIST"; games: GameEntry[] }
|
||||||
| {
|
| {
|
||||||
@@ -49,8 +51,8 @@ export type ParsedMessage =
|
|||||||
player2: string;
|
player2: string;
|
||||||
moves: MoveEntry[];
|
moves: MoveEntry[];
|
||||||
}
|
}
|
||||||
| { type: "GAME_MOVE"; username: string; column: number }
|
| { type: "GAME_MOVE"; matchId?: number; username: string; column: number }
|
||||||
| { type: "GAME_WIN"; winner: string }
|
| { type: "GAME_WIN"; matchId?: number; winner: string }
|
||||||
| { type: "PLAYER_LIST"; players: PlayerEntry[] }
|
| { type: "PLAYER_LIST"; players: PlayerEntry[] }
|
||||||
| { type: "TOURNAMENT_START"; tournamentType: string }
|
| { type: "TOURNAMENT_START"; tournamentType: string }
|
||||||
| { type: "TOURNAMENT_CANCEL" }
|
| { type: "TOURNAMENT_CANCEL" }
|
||||||
@@ -77,11 +79,39 @@ export function parseMessage(raw: string): ParsedMessage {
|
|||||||
case "DISCONNECT":
|
case "DISCONNECT":
|
||||||
if (parts[1] === "ACK") return { type: "DISCONNECT_ACK" };
|
if (parts[1] === "ACK") return { type: "DISCONNECT_ACK" };
|
||||||
break;
|
break;
|
||||||
|
case "OBSERVE":
|
||||||
|
if (parts[1] === "ACK") {
|
||||||
|
return { type: "OBSERVE_ACK", enabled: parts[2] === "1" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "READY":
|
case "READY":
|
||||||
if (parts[1] === "ACK") return { type: "READY_ACK" };
|
if (parts[1] === "ACK") return { type: "READY_ACK" };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "GAME": {
|
case "GAME": {
|
||||||
|
const scopedMatchId = parseInt(parts[1], 10);
|
||||||
|
if (!Number.isNaN(scopedMatchId)) {
|
||||||
|
switch (parts[2]) {
|
||||||
|
case "MOVE":
|
||||||
|
return {
|
||||||
|
type: "GAME_MOVE",
|
||||||
|
matchId: scopedMatchId,
|
||||||
|
username: parts[3],
|
||||||
|
column: parseInt(parts[4], 10),
|
||||||
|
};
|
||||||
|
case "WIN":
|
||||||
|
return {
|
||||||
|
type: "GAME_WIN",
|
||||||
|
matchId: scopedMatchId,
|
||||||
|
winner: parts[3],
|
||||||
|
};
|
||||||
|
case "DRAW":
|
||||||
|
return { type: "GAME_DRAW", matchId: scopedMatchId };
|
||||||
|
case "TERMINATED":
|
||||||
|
return { type: "GAME_TERMINATED", matchId: scopedMatchId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (parts[1]) {
|
switch (parts[1]) {
|
||||||
case "START":
|
case "START":
|
||||||
return { type: "GAME_START", goesFirst: parts[2] === "1" };
|
return { type: "GAME_START", goesFirst: parts[2] === "1" };
|
||||||
@@ -130,23 +160,15 @@ export function parseMessage(raw: string): ParsedMessage {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "MOVE":
|
|
||||||
// GAME:MOVE:<username>:<column>
|
|
||||||
return {
|
|
||||||
type: "GAME_MOVE",
|
|
||||||
username: parts[2],
|
|
||||||
column: parseInt(parts[3]),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "WIN":
|
|
||||||
return { type: "GAME_WIN", winner: parts[2] };
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "OPPONENT":
|
case "OPPONENT":
|
||||||
return { type: "OPPONENT_MOVE", column: parseInt(parts[1]) };
|
return {
|
||||||
|
type: "OPPONENT_MOVE",
|
||||||
|
column: parseInt(parts[parts.length - 1], 10),
|
||||||
|
};
|
||||||
|
|
||||||
case "PLAYER": {
|
case "PLAYER": {
|
||||||
if (parts[1] === "LIST") {
|
if (parts[1] === "LIST") {
|
||||||
@@ -214,6 +236,7 @@ export const cmd = {
|
|||||||
connect: (username: string) => `CONNECT:${username}`,
|
connect: (username: string) => `CONNECT:${username}`,
|
||||||
reconnect: (username: string) => `RECONNECT:${username}`,
|
reconnect: (username: string) => `RECONNECT:${username}`,
|
||||||
disconnect: () => "DISCONNECT",
|
disconnect: () => "DISCONNECT",
|
||||||
|
observe: () => "OBSERVE",
|
||||||
ready: () => "READY",
|
ready: () => "READY",
|
||||||
play: (column: number) => `PLAY:${column}`,
|
play: (column: number) => `PLAY:${column}`,
|
||||||
playerList: () => "PLAYER:LIST",
|
playerList: () => "PLAYER:LIST",
|
||||||
|
|||||||
Reference in New Issue
Block a user