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:
103
src/App.tsx
103
src/App.tsx
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface BoardProps {
|
||||
onCellClick: (row: number, col: number) => void;
|
||||
}
|
||||
|
||||
export default function TicTacToeBoard({
|
||||
export default function TicTacToeGame({
|
||||
boards,
|
||||
turn,
|
||||
winner,
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user