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 { motion } from "framer-motion";
import { useNakama } from "./providers/NakamaProvider";
import Player from "./Player";
import { PlayerModel } from "./interfaces/models";
import TicTacToeBoard from "./games/tictactoe/TicTacToeBoard";
import BattleShipBoard from "./games/battleship/BattleShipBoard";
import Player from "./Player";
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() {
// setting up a 2D game boards
const [boards, setBoards] = useState<Record<string, { grid: string[][] }>>({});
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>>({});
// unified game state
const [game, setGame] = useState<GameState>(INITIAL_GAME_STATE);
const { sendMatchData, onMatchData, matchId, session } = useNakama();
function renderGameBoard() {
if (!matchId || !metadata?.game) return null;
const commonProps: GameProps = {
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":
return (
<TicTacToeBoard
boards={boards}
turn={turn}
winner={winner}
gameOver={gameOver}
players={players}
myUserId={session?.user_id ?? null}
<TicTacToeGame
{...commonProps}
onCellClick={handleCellClick}
/>
);
case "battleship":
return (
<BattleShipBoard
boards={boards}
turn={turn}
winner={winner}
gameOver={gameOver}
players={players}
myUserId={session?.user_id ?? null}
metadata={metadata}
<BattleShipGame
{...commonProps}
metadata={game.metadata}
/>
);
default:
return <div>Unknown game: {metadata.game}</div>;
return <div>Unknown game: {game.metadata.game}</div>;
}
}
// ------------------------------------------
// MATCH DATA CALLBACK (from Player component)
// ------------------------------------------
@@ -62,23 +72,21 @@ export default function App() {
const state = msg.data;
console.log("Match state:", state);
setBoards(state.boards);
setTurn(state.turn);
setGameOver(state.game_over);
if (state.winner >= 0) {
setWinner(state.players[state.winner].username);
// } else if (state.game_over) {
// // Game ended but winner = -1 → draw
// setWinner("draw");
} else {
// Ongoing game, no winner
setWinner(null);
}
setPlayers(state.players || []);
setMetadata(state.metadata || {});
setGame({
boards: state.boards,
turn: state.turn,
gameOver: state.game_over,
winner:
state.winner >= 0 ? state.players[state.winner].username : null,
players: state.players ?? [],
metadata: state.metadata ?? {},
});
}
}
// ---------------------------------------------------
// EFFECTS
// ---------------------------------------------------
useEffect(() => {
document.body.style.overflow = "hidden";
return () => {
@@ -96,9 +104,12 @@ export default function App() {
function handleCellClick(row: number, col: number) {
if (!matchId) return;
sendMatchData(matchId, 1, {data: {row, col}});
sendMatchData(matchId, 1, { data: { row, col } });
}
// ---------------------------------------------------
// UI LAYOUT
// ---------------------------------------------------
return (
<div
style={{

View File

@@ -25,7 +25,7 @@ const Fleet: Record<string, number> = {
};
const FLEET_ORDER = ["carrier", "battleship", "cruiser", "submarine", "destroyer"];
export default function BattleShipBoard({
export default function BattleShipGame({
boards,
players,
myUserId,

View File

@@ -1,15 +1,8 @@
import {
Board,
PlayerModel,
} from '../../interfaces/models'
GameProps,
} from '../../interfaces/props'
export interface BattleShipBoardProps {
boards: Record<string, Board>;
turn: number;
winner: string | null;
gameOver: boolean | null;
players: PlayerModel[];
myUserId: string | null;
export interface BattleshipGameProps extends GameProps {
metadata: Record<string, any>;
}

View File

@@ -14,7 +14,7 @@ interface BoardProps {
onCellClick: (row: number, col: number) => void;
}
export default function TicTacToeBoard({
export default function TicTacToeGame({
boards,
turn,
winner,

View File

@@ -1,15 +1,8 @@
import {
Board,
PlayerModel,
} from '../../interfaces/models'
GameProps,
} from '../../interfaces/props'
export interface TicTacToeBoardProps {
boards: Record<string, Board>;
turn: number;
winner: string | null;
gameOver: boolean | null;
players: PlayerModel[];
myUserId: string | null;
export interface TicTacToeGameProps extends GameProps {
onCellClick: (row: number, col: number) => void;
}

View File

@@ -5,7 +5,7 @@ export interface PlayerModel {
metadata: Record<string, string>; // e.g. { symbol: "X" }
}
export interface MatchDataMessage<T = any> {
export interface MatchDataModel<T = any> {
opCode: number;
data: T;
userId: string | null;
@@ -15,15 +15,6 @@ export interface Board {
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 {
game: string;
mode: string;

View File

@@ -1,7 +1,19 @@
import {
MatchDataMessage,
MatchDataModel,
} from './models'
import {
GameState
} from "./states";
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
} from "@heroiclabs/nakama-js";
import {
Board,
PlayerModel,
} from "./models"
export interface NakamaProviderState {
session: Session | null;
socket: Socket | null;
matchId: 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>;
}