refactor(game): unify move handling using typed payloads and remove UI-driven handlers

- Removed onCellClick from TicTacToeGameProps and migrated move sending inside TicTacToeGame
- Updated TicTacToeGame to:
  - import TicTacToePayload
  - use movePayload() builder
  - send moves using handleMove() with matchId + sendMatchData
  - remove old matchId destructuring duplication

- Updated BattleshipGame to:
  - import BattleshipPayload
  - use placePayload() and shootPayload() helpers
  - collapse place and shoot handlers into a single handleMove()
  - send typed payloads instead of raw objects

- Updated App.tsx:
  - Removed handleCellClick and no longer pass onCellClick down
  - Created typed ticTacToeProps and battleshipProps without UI callbacks
  - Cleaned unused state and simplified board rendering
  - Use {...commonProps} to propagate shared game state

- Updated props:
  - Removed TicTacToeGameProps.onCellClick
  - BattleshipGameProps continues to extend GameProps

- Removed duplicate MatchDataModel definition from interfaces/models
- Fixed imports to use revised models and payload types

This refactor completes the transition from UI-triggered handlers to
typed action payloads per game, significantly improving type safety,
consistency, and separation of concerns.
This commit is contained in:
2025-12-04 19:56:46 +05:30
parent 135fdd332d
commit 8436cdbcdd
9 changed files with 92 additions and 45 deletions

View File

@@ -35,7 +35,6 @@ export default function App() {
};
const ticTacToeProps: TicTacToeGameProps = {
...commonProps,
onCellClick: handleCellClick,
};
const battleshipProps: BattleshipGameProps = {
...commonProps,
@@ -104,15 +103,6 @@ export default function App() {
onMatchData(onMatchDataCallback);
}, [onMatchData]);
// ------------------------------------------
// SEND A MOVE
// ------------------------------------------
function handleCellClick(row: number, col: number) {
if (!matchId) return;
sendMatchData(matchId, 1, { data: { row, col } });
}
// ---------------------------------------------------
// UI LAYOUT
// ---------------------------------------------------

View File

@@ -5,6 +5,11 @@ import { useNakama } from "../../providers/NakamaProvider";
import PlacementGrid from "./placement/PlacementGrid";
import ShotGrid from "./battle/ShotGrid";
import { BattleshipGameProps } from "./props";
import { BattleshipPayload } from "./models";
import {
placePayload,
shootPayload,
} from "./utils";
const Fleet: Record<string, number> = {
carrier: 5,
@@ -40,28 +45,10 @@ export default function BattleshipGame({
const nextShip = FLEET_ORDER[placed] || null;
const nextShipSize = nextShip ? Fleet[nextShip] : null;
// ------------------- PLACE SHIP -------------------
function handlePlace(ship: string, r: number, c: number, dir: "h" | "v") {
sendMatchData(matchId!, 1, {
action: "place",
data: {
ship: ship,
row: r,
col: c,
dir,
}
});
}
function handleMove(matchPayload: BattleshipPayload) {
if (!matchId) return;
// ------------------- SHOOT -------------------
function handleShoot(r: number, c: number) {
sendMatchData(matchId!, 1, {
action: "shoot",
data: {
row: r,
col: c,
}
});
sendMatchData(matchId!, 1, matchPayload);
}
// ------------------- STATUS LABEL -------------------
@@ -90,7 +77,11 @@ export default function BattleshipGame({
shipBoard={myShips}
shipName={nextShip}
shipSize={nextShipSize}
onPlace={handlePlace}
onPlace={(
s,r,c,d
) => handleMove(
placePayload(s,r,c,d)
)}
/>
)}
@@ -103,7 +94,11 @@ export default function BattleshipGame({
grid={myShots}
isMyTurn={isMyTurn}
gameOver={!!gameOver}
onShoot={handleShoot}
onShoot={(
r,c
) => handleMove(
shootPayload(r,c)
)}
/>
<h3 style={{ marginTop: "18px" }}>Your Ships</h3>

View File

@@ -0,0 +1,15 @@
import {
MatchDataModel,
} from '../../interfaces/models'
export interface BattleshipPayload {
action: "place" | "shoot"; // extend as needed
data: {
ship?: string; // only for placement
row: number;
col: number;
dir?: "h" | "v";
};
}
export type BattleshipMatchDataModel = MatchDataModel<BattleshipPayload>;

View File

@@ -0,0 +1,22 @@
import {
BattleshipPayload
} from "./models";
export function placePayload(
ship: string,
row: number,
col: number,
dir: "h" | "v"
): BattleshipPayload {
return {
action: "place",
data: { ship, row, col, dir }
};
}
export function shootPayload(row: number, col: number): BattleshipPayload {
return {
action: "shoot",
data: { row, col }
};
}

View File

@@ -4,6 +4,8 @@ import { useNakama } from "../../providers/NakamaProvider";
import getHaiku from "../../utils/haikus";
import { TicTacToeGameProps } from "./props";
import { TicTacToePayload } from "./models";
import { movePayload } from "./utils";
export default function TicTacToeGame({
boards,
@@ -12,13 +14,11 @@ export default function TicTacToeGame({
gameOver,
players,
myUserId,
onCellClick,
}: TicTacToeGameProps) {
const { sendMatchData, matchId } = useNakama();
const myIndex = players.findIndex(p => p.user_id === myUserId);
const gameReady = players.length === 2;
const {
matchId
} = useNakama();
const mySymbol =
myIndex !== null && players[myIndex]
@@ -67,6 +67,12 @@ export default function TicTacToeGame({
return () => clearTimeout(timer);
}, [haikuIndex]);
function handleMove(matchPayload: TicTacToePayload) {
if (!matchId) return;
sendMatchData(matchId!, 1, matchPayload);
}
return (
<>
{matchId && (
@@ -132,7 +138,9 @@ export default function TicTacToeGame({
: {}
}
whileTap={!disabled ? { scale: 0.85 } : {}}
onClick={() => !disabled && onCellClick(rIdx, cIdx)}
onClick={() => !disabled && handleMove(
movePayload(rIdx, cIdx)
)}
style={{
width: "80px",
height: "80px",

View File

@@ -0,0 +1,11 @@
import {
MatchDataModel,
} from '../../interfaces/models'
export interface TicTacToePayload {
data: {
row: number;
col: number;
};
}
export type TicTacToeMatchDataModel = MatchDataModel<TicTacToePayload>;

View File

@@ -4,5 +4,5 @@ import {
export interface TicTacToeGameProps extends GameProps {
onCellClick: (row: number, col: number) => void;
// metadata: Record<string, any>;
}

View File

@@ -0,0 +1,12 @@
import {
TicTacToePayload
} from "./models";
export function movePayload(
row: number,
col: number,
): TicTacToePayload {
return {
data: { row, col }
};
}

View File

@@ -19,9 +19,3 @@ export interface GameMetadataModel {
game: string;
mode: string;
}
export interface MatchDataModel<T = any> {
opCode: number;
data: T;
userId: string | null;
}