refactor(game): unify GameState, standardize board props, and rename game components
- Replaced multiple App-level state fields with unified GameState
- Added INITIAL_GAME_STATE and migrated App.tsx to use single game state
- Introduced GameProps as shared base props for all turn-based board games
- Created TicTacToeGameProps and BattleshipGameProps extending GameProps
- Updated TicTacToe and Battleship components to use new props
- Replaced verbose prop passing with spread {...commonProps}
- Updated renderGameBoard to use game.metadata consistently
- Renamed TicTacToeBoard -> TicTacToeGame for clarity
- Renamed BattleShipBoard -> BattleShipGame for naming consistency
- Updated all import paths to reflect new component names
- Replaced MatchDataMessage with MatchDataModel
- Moved GameState definition from models.ts to interfaces/states.ts
- Removed old board-specific prop structures and per-field state management
- Increased type safety and reduced duplication across the codebase
This commit consolidates game state flow, introduces a clean component props
architecture, and standardizes naming convention
This commit is contained in:
101
src/App.tsx
101
src/App.tsx
@@ -1,57 +1,67 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useNakama } from "./providers/NakamaProvider";
|
import { useNakama } from "./providers/NakamaProvider";
|
||||||
import Player from "./Player";
|
|
||||||
import { PlayerModel } from "./interfaces/models";
|
|
||||||
|
|
||||||
import TicTacToeBoard from "./games/tictactoe/TicTacToeBoard";
|
import Player from "./Player";
|
||||||
import BattleShipBoard from "./games/battleship/BattleShipBoard";
|
import TicTacToeGame from "./games/tictactoe/TicTacToeGame";
|
||||||
|
import { TicTacToeGameProps } from "./games/tictactoe/props";
|
||||||
|
import BattleShipGame from "./games/battleship/BattleShipGame";
|
||||||
|
import { BattleshipGameProps } from "./games/battleship/props";
|
||||||
|
|
||||||
|
import { GameState } from "./interfaces/states";
|
||||||
|
import { GameProps } from "./interfaces/props";
|
||||||
|
|
||||||
|
const INITIAL_GAME_STATE: GameState = {
|
||||||
|
boards: {},
|
||||||
|
turn: 0,
|
||||||
|
winner: null,
|
||||||
|
gameOver: false,
|
||||||
|
players: [],
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
// setting up a 2D game boards
|
// unified game state
|
||||||
const [boards, setBoards] = useState<Record<string, { grid: string[][] }>>({});
|
const [game, setGame] = useState<GameState>(INITIAL_GAME_STATE);
|
||||||
const [turn, setTurn] = useState<number>(0);
|
|
||||||
const [winner, setWinner] = useState<string | null>(null);
|
|
||||||
const [gameOver, setGameOver] = useState<boolean | null>(null);
|
|
||||||
const [players, setPlayers] = useState<PlayerModel[]>([]);
|
|
||||||
const [metadata, setMetadata] = useState<Record<string, any>>({});
|
|
||||||
|
|
||||||
const { sendMatchData, onMatchData, matchId, session } = useNakama();
|
const { sendMatchData, onMatchData, matchId, session } = useNakama();
|
||||||
|
|
||||||
function renderGameBoard() {
|
const commonProps: GameProps = {
|
||||||
if (!matchId || !metadata?.game) return null;
|
boards: game.boards,
|
||||||
|
turn: game.turn,
|
||||||
|
winner: game.winner,
|
||||||
|
gameOver: game.gameOver,
|
||||||
|
players: game.players,
|
||||||
|
myUserId: session?.user_id ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
switch (metadata.game) {
|
// ---------------------------------------------------
|
||||||
|
// RENDER GAME BOARD
|
||||||
|
// ---------------------------------------------------
|
||||||
|
function renderGameBoard() {
|
||||||
|
if (!matchId || !game.metadata?.game) return null;
|
||||||
|
|
||||||
|
switch (game.metadata.game) {
|
||||||
case "tictactoe":
|
case "tictactoe":
|
||||||
return (
|
return (
|
||||||
<TicTacToeBoard
|
<TicTacToeGame
|
||||||
boards={boards}
|
{...commonProps}
|
||||||
turn={turn}
|
|
||||||
winner={winner}
|
|
||||||
gameOver={gameOver}
|
|
||||||
players={players}
|
|
||||||
myUserId={session?.user_id ?? null}
|
|
||||||
onCellClick={handleCellClick}
|
onCellClick={handleCellClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "battleship":
|
case "battleship":
|
||||||
return (
|
return (
|
||||||
<BattleShipBoard
|
<BattleShipGame
|
||||||
boards={boards}
|
{...commonProps}
|
||||||
turn={turn}
|
metadata={game.metadata}
|
||||||
winner={winner}
|
|
||||||
gameOver={gameOver}
|
|
||||||
players={players}
|
|
||||||
myUserId={session?.user_id ?? null}
|
|
||||||
metadata={metadata}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <div>Unknown game: {metadata.game}</div>;
|
return <div>Unknown game: {game.metadata.game}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
// MATCH DATA CALLBACK (from Player component)
|
// MATCH DATA CALLBACK (from Player component)
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
@@ -62,23 +72,21 @@ export default function App() {
|
|||||||
const state = msg.data;
|
const state = msg.data;
|
||||||
console.log("Match state:", state);
|
console.log("Match state:", state);
|
||||||
|
|
||||||
setBoards(state.boards);
|
setGame({
|
||||||
setTurn(state.turn);
|
boards: state.boards,
|
||||||
setGameOver(state.game_over);
|
turn: state.turn,
|
||||||
if (state.winner >= 0) {
|
gameOver: state.game_over,
|
||||||
setWinner(state.players[state.winner].username);
|
winner:
|
||||||
// } else if (state.game_over) {
|
state.winner >= 0 ? state.players[state.winner].username : null,
|
||||||
// // Game ended but winner = -1 → draw
|
players: state.players ?? [],
|
||||||
// setWinner("draw");
|
metadata: state.metadata ?? {},
|
||||||
} else {
|
});
|
||||||
// Ongoing game, no winner
|
|
||||||
setWinner(null);
|
|
||||||
}
|
|
||||||
setPlayers(state.players || []);
|
|
||||||
setMetadata(state.metadata || {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------
|
||||||
|
// EFFECTS
|
||||||
|
// ---------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
return () => {
|
return () => {
|
||||||
@@ -99,6 +107,9 @@ export default function App() {
|
|||||||
sendMatchData(matchId, 1, { data: { row, col } });
|
sendMatchData(matchId, 1, { data: { row, col } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------
|
||||||
|
// UI LAYOUT
|
||||||
|
// ---------------------------------------------------
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Fleet: Record<string, number> = {
|
|||||||
};
|
};
|
||||||
const FLEET_ORDER = ["carrier", "battleship", "cruiser", "submarine", "destroyer"];
|
const FLEET_ORDER = ["carrier", "battleship", "cruiser", "submarine", "destroyer"];
|
||||||
|
|
||||||
export default function BattleShipBoard({
|
export default function BattleShipGame({
|
||||||
boards,
|
boards,
|
||||||
players,
|
players,
|
||||||
myUserId,
|
myUserId,
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Board,
|
GameProps,
|
||||||
PlayerModel,
|
} from '../../interfaces/props'
|
||||||
} from '../../interfaces/models'
|
|
||||||
|
|
||||||
|
|
||||||
export interface BattleShipBoardProps {
|
export interface BattleshipGameProps extends GameProps {
|
||||||
boards: Record<string, Board>;
|
|
||||||
turn: number;
|
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean | null;
|
|
||||||
players: PlayerModel[];
|
|
||||||
myUserId: string | null;
|
|
||||||
metadata: Record<string, any>;
|
metadata: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface BoardProps {
|
|||||||
onCellClick: (row: number, col: number) => void;
|
onCellClick: (row: number, col: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TicTacToeBoard({
|
export default function TicTacToeGame({
|
||||||
boards,
|
boards,
|
||||||
turn,
|
turn,
|
||||||
winner,
|
winner,
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Board,
|
GameProps,
|
||||||
PlayerModel,
|
} from '../../interfaces/props'
|
||||||
} from '../../interfaces/models'
|
|
||||||
|
|
||||||
|
|
||||||
export interface TicTacToeBoardProps {
|
export interface TicTacToeGameProps extends GameProps {
|
||||||
boards: Record<string, Board>;
|
|
||||||
turn: number;
|
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean | null;
|
|
||||||
players: PlayerModel[];
|
|
||||||
myUserId: string | null;
|
|
||||||
onCellClick: (row: number, col: number) => void;
|
onCellClick: (row: number, col: number) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface PlayerModel {
|
|||||||
metadata: Record<string, string>; // e.g. { symbol: "X" }
|
metadata: Record<string, string>; // e.g. { symbol: "X" }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchDataMessage<T = any> {
|
export interface MatchDataModel<T = any> {
|
||||||
opCode: number;
|
opCode: number;
|
||||||
data: T;
|
data: T;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
@@ -15,15 +15,6 @@ export interface Board {
|
|||||||
grid: string[][];
|
grid: string[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameState {
|
|
||||||
boards: Record<string, Board>;
|
|
||||||
turn: number;
|
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean;
|
|
||||||
players: PlayerModel[];
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameMetadata {
|
export interface GameMetadata {
|
||||||
game: string;
|
game: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
MatchDataMessage,
|
MatchDataModel,
|
||||||
} from './models'
|
} from './models'
|
||||||
|
|
||||||
|
import {
|
||||||
|
GameState
|
||||||
|
} from "./states";
|
||||||
|
|
||||||
export interface PlayerProps {
|
export interface PlayerProps {
|
||||||
onMatchDataCallback: (msg:MatchDataMessage) => void;
|
onMatchDataCallback: (msg:MatchDataModel) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameProps
|
||||||
|
extends Pick<
|
||||||
|
GameState,
|
||||||
|
"boards" | "turn" | "winner" | "gameOver" | "players"
|
||||||
|
> {
|
||||||
|
myUserId: string | null;
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,24 @@ import {
|
|||||||
Socket
|
Socket
|
||||||
} from "@heroiclabs/nakama-js";
|
} from "@heroiclabs/nakama-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Board,
|
||||||
|
PlayerModel,
|
||||||
|
} from "./models"
|
||||||
|
|
||||||
|
|
||||||
export interface NakamaProviderState {
|
export interface NakamaProviderState {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
matchId: string | null;
|
matchId: string | null;
|
||||||
matchmakerTicket: string | null;
|
matchmakerTicket: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
boards: Record<string, Board>;
|
||||||
|
turn: number;
|
||||||
|
winner: string | null;
|
||||||
|
gameOver: boolean;
|
||||||
|
players: PlayerModel[];
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user