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:
2025-12-04 19:16:20 +05:30
parent 650d7b7ed6
commit 06bdc92190
8 changed files with 95 additions and 80 deletions

View File

@@ -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 () => {
@@ -96,9 +104,12 @@ export default function App() {
function handleCellClick(row: number, col: number) { function handleCellClick(row: number, col: number) {
if (!matchId) return; if (!matchId) return;
sendMatchData(matchId, 1, {data: {row, col}}); sendMatchData(matchId, 1, { data: { row, col } });
} }
// ---------------------------------------------------
// UI LAYOUT
// ---------------------------------------------------
return ( return (
<div <div
style={{ style={{

View File

@@ -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,

View File

@@ -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>;
} }

View File

@@ -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,

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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>;
}