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:
10
src/App.tsx
10
src/App.tsx
@@ -35,7 +35,6 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
const ticTacToeProps: TicTacToeGameProps = {
|
const ticTacToeProps: TicTacToeGameProps = {
|
||||||
...commonProps,
|
...commonProps,
|
||||||
onCellClick: handleCellClick,
|
|
||||||
};
|
};
|
||||||
const battleshipProps: BattleshipGameProps = {
|
const battleshipProps: BattleshipGameProps = {
|
||||||
...commonProps,
|
...commonProps,
|
||||||
@@ -104,15 +103,6 @@ export default function App() {
|
|||||||
onMatchData(onMatchDataCallback);
|
onMatchData(onMatchDataCallback);
|
||||||
}, [onMatchData]);
|
}, [onMatchData]);
|
||||||
|
|
||||||
// ------------------------------------------
|
|
||||||
// SEND A MOVE
|
|
||||||
// ------------------------------------------
|
|
||||||
function handleCellClick(row: number, col: number) {
|
|
||||||
if (!matchId) return;
|
|
||||||
|
|
||||||
sendMatchData(matchId, 1, { data: { row, col } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
// UI LAYOUT
|
// UI LAYOUT
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { useNakama } from "../../providers/NakamaProvider";
|
|||||||
import PlacementGrid from "./placement/PlacementGrid";
|
import PlacementGrid from "./placement/PlacementGrid";
|
||||||
import ShotGrid from "./battle/ShotGrid";
|
import ShotGrid from "./battle/ShotGrid";
|
||||||
import { BattleshipGameProps } from "./props";
|
import { BattleshipGameProps } from "./props";
|
||||||
|
import { BattleshipPayload } from "./models";
|
||||||
|
import {
|
||||||
|
placePayload,
|
||||||
|
shootPayload,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
const Fleet: Record<string, number> = {
|
const Fleet: Record<string, number> = {
|
||||||
carrier: 5,
|
carrier: 5,
|
||||||
@@ -40,28 +45,10 @@ export default function BattleshipGame({
|
|||||||
const nextShip = FLEET_ORDER[placed] || null;
|
const nextShip = FLEET_ORDER[placed] || null;
|
||||||
const nextShipSize = nextShip ? Fleet[nextShip] : null;
|
const nextShipSize = nextShip ? Fleet[nextShip] : null;
|
||||||
|
|
||||||
// ------------------- PLACE SHIP -------------------
|
function handleMove(matchPayload: BattleshipPayload) {
|
||||||
function handlePlace(ship: string, r: number, c: number, dir: "h" | "v") {
|
if (!matchId) return;
|
||||||
sendMatchData(matchId!, 1, {
|
|
||||||
action: "place",
|
|
||||||
data: {
|
|
||||||
ship: ship,
|
|
||||||
row: r,
|
|
||||||
col: c,
|
|
||||||
dir,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------- SHOOT -------------------
|
sendMatchData(matchId!, 1, matchPayload);
|
||||||
function handleShoot(r: number, c: number) {
|
|
||||||
sendMatchData(matchId!, 1, {
|
|
||||||
action: "shoot",
|
|
||||||
data: {
|
|
||||||
row: r,
|
|
||||||
col: c,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------- STATUS LABEL -------------------
|
// ------------------- STATUS LABEL -------------------
|
||||||
@@ -90,7 +77,11 @@ export default function BattleshipGame({
|
|||||||
shipBoard={myShips}
|
shipBoard={myShips}
|
||||||
shipName={nextShip}
|
shipName={nextShip}
|
||||||
shipSize={nextShipSize}
|
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}
|
grid={myShots}
|
||||||
isMyTurn={isMyTurn}
|
isMyTurn={isMyTurn}
|
||||||
gameOver={!!gameOver}
|
gameOver={!!gameOver}
|
||||||
onShoot={handleShoot}
|
onShoot={(
|
||||||
|
r,c
|
||||||
|
) => handleMove(
|
||||||
|
shootPayload(r,c)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 style={{ marginTop: "18px" }}>Your Ships</h3>
|
<h3 style={{ marginTop: "18px" }}>Your Ships</h3>
|
||||||
|
|||||||
15
src/games/battleship/models.ts
Normal file
15
src/games/battleship/models.ts
Normal 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>;
|
||||||
22
src/games/battleship/utils.ts
Normal file
22
src/games/battleship/utils.ts
Normal 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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { useNakama } from "../../providers/NakamaProvider";
|
|||||||
import getHaiku from "../../utils/haikus";
|
import getHaiku from "../../utils/haikus";
|
||||||
|
|
||||||
import { TicTacToeGameProps } from "./props";
|
import { TicTacToeGameProps } from "./props";
|
||||||
|
import { TicTacToePayload } from "./models";
|
||||||
|
import { movePayload } from "./utils";
|
||||||
|
|
||||||
export default function TicTacToeGame({
|
export default function TicTacToeGame({
|
||||||
boards,
|
boards,
|
||||||
@@ -12,13 +14,11 @@ export default function TicTacToeGame({
|
|||||||
gameOver,
|
gameOver,
|
||||||
players,
|
players,
|
||||||
myUserId,
|
myUserId,
|
||||||
onCellClick,
|
|
||||||
}: TicTacToeGameProps) {
|
}: TicTacToeGameProps) {
|
||||||
|
const { sendMatchData, matchId } = useNakama();
|
||||||
|
|
||||||
const myIndex = players.findIndex(p => p.user_id === myUserId);
|
const myIndex = players.findIndex(p => p.user_id === myUserId);
|
||||||
const gameReady = players.length === 2;
|
const gameReady = players.length === 2;
|
||||||
const {
|
|
||||||
matchId
|
|
||||||
} = useNakama();
|
|
||||||
|
|
||||||
const mySymbol =
|
const mySymbol =
|
||||||
myIndex !== null && players[myIndex]
|
myIndex !== null && players[myIndex]
|
||||||
@@ -67,6 +67,12 @@ export default function TicTacToeGame({
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [haikuIndex]);
|
}, [haikuIndex]);
|
||||||
|
|
||||||
|
function handleMove(matchPayload: TicTacToePayload) {
|
||||||
|
if (!matchId) return;
|
||||||
|
|
||||||
|
sendMatchData(matchId!, 1, matchPayload);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{matchId && (
|
{matchId && (
|
||||||
@@ -132,7 +138,9 @@ export default function TicTacToeGame({
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
whileTap={!disabled ? { scale: 0.85 } : {}}
|
whileTap={!disabled ? { scale: 0.85 } : {}}
|
||||||
onClick={() => !disabled && onCellClick(rIdx, cIdx)}
|
onClick={() => !disabled && handleMove(
|
||||||
|
movePayload(rIdx, cIdx)
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: "80px",
|
width: "80px",
|
||||||
height: "80px",
|
height: "80px",
|
||||||
|
|||||||
11
src/games/tictactoe/models.ts
Normal file
11
src/games/tictactoe/models.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
MatchDataModel,
|
||||||
|
} from '../../interfaces/models'
|
||||||
|
|
||||||
|
export interface TicTacToePayload {
|
||||||
|
data: {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type TicTacToeMatchDataModel = MatchDataModel<TicTacToePayload>;
|
||||||
@@ -4,5 +4,5 @@ import {
|
|||||||
|
|
||||||
|
|
||||||
export interface TicTacToeGameProps extends GameProps {
|
export interface TicTacToeGameProps extends GameProps {
|
||||||
onCellClick: (row: number, col: number) => void;
|
// metadata: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/games/tictactoe/utils.ts
Normal file
12
src/games/tictactoe/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
TicTacToePayload
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
export function movePayload(
|
||||||
|
row: number,
|
||||||
|
col: number,
|
||||||
|
): TicTacToePayload {
|
||||||
|
return {
|
||||||
|
data: { row, col }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -19,9 +19,3 @@ export interface GameMetadataModel {
|
|||||||
game: string;
|
game: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchDataModel<T = any> {
|
|
||||||
opCode: number;
|
|
||||||
data: T;
|
|
||||||
userId: string | null;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user