Designing Game Events
In this section, weâll define the event system for our Tic-Tac-Toe game. Events form the foundation of our event-driven architecture, allowing different components to communicate without tight coupling.
Event Types
For our Tic-Tac-Toe game, weâll create event constants using a consistent naming convention. Each event follows the pattern domain:action
to make them clear and organized.
Letâs create our src/events.ts
file:
// src/events.ts
/**
* Tic-Tac-Toe specific event constants
* Following the naming convention: "domain:action"
*/
export const TIC_TAC_TOE_EVENTS = {
// Game lifecycle events
GAME_CREATED: "tictactoe:game:created",
GAME_STARTED: "tictactoe:game:started",
GAME_ENDED: "tictactoe:game:ended",
GAME_RESET: "tictactoe:game:reset",
// Player actions
MOVE_MADE: "tictactoe:move:made",
PLAYER_JOINED: "tictactoe:player:joined",
PLAYER_FORFEITED: "tictactoe:player:forfeited",
// Game state changes
TURN_CHANGED: "tictactoe:turn:changed",
BOARD_UPDATED: "tictactoe:board:updated",
WINNER_DETERMINED: "tictactoe:winner:determined",
DRAW_DETERMINED: "tictactoe:draw:determined"
} as const;
// Create a type for our custom events
export type TicTacToeEventType = typeof TIC_TAC_TOE_EVENTS[keyof typeof TIC_TAC_TOE_EVENTS];
By using the as const
assertion, we ensure that TypeScript treats these string literals as exact values, giving us better type checking.
Event Payloads
Next, weâll define TypeScript interfaces for our event payloads. These interfaces describe the data that will be sent along with each event:
/**
* Type definitions for event payloads
*/
export interface MovePayload {
row: number;
col: number;
symbol: 'X' | 'O';
playerId: string;
}
export interface GameEndedPayload {
winnerId: string | null;
reason: 'win' | 'draw' | 'forfeit';
winningCells?: [number, number][];
}
Game States
Finally, weâll define an enum for our game states. This will help us maintain and transition between the different states of our game:
export enum GameState {
WAITING_FOR_PLAYERS = 'waitingForPlayers',
READY_TO_START = 'readyToStart',
IN_PROGRESS = 'inProgress',
GAME_OVER = 'gameOver'
}
Benefits of This Approach
Using TypeScript with these constants and interfaces provides several benefits:
- Type Safety: TypeScript will catch errors if we use event names or payloads incorrectly
- Autocompletion: IDEs can suggest event names and payload properties
- Refactoring Support: If we rename an event, TypeScript will find all places itâs used
- Documentation: The code itself documents the event structure
Final events.ts File
The complete src/events.ts
file looks like this:
// src/events.ts
/**
* Tic-Tac-Toe specific event constants
* Following the naming convention: "domain:action"
*/
export const TIC_TAC_TOE_EVENTS = {
// Game lifecycle events
GAME_CREATED: "tictactoe:game:created",
GAME_STARTED: "tictactoe:game:started",
GAME_ENDED: "tictactoe:game:ended",
GAME_RESET: "tictactoe:game:reset",
// Player actions
MOVE_MADE: "tictactoe:move:made",
PLAYER_JOINED: "tictactoe:player:joined",
PLAYER_FORFEITED: "tictactoe:player:forfeited",
// Game state changes
TURN_CHANGED: "tictactoe:turn:changed",
BOARD_UPDATED: "tictactoe:board:updated",
WINNER_DETERMINED: "tictactoe:winner:determined",
DRAW_DETERMINED: "tictactoe:draw:determined"
} as const;
// Create a type for our custom events
export type TicTacToeEventType = typeof TIC_TAC_TOE_EVENTS[keyof typeof TIC_TAC_TOE_EVENTS];
/**
* Type definitions for event payloads
*/
export interface MovePayload {
row: number;
col: number;
symbol: 'X' | 'O';
playerId: string;
}
export interface GameEndedPayload {
winnerId: string | null;
reason: 'win' | 'draw' | 'forfeit';
winningCells?: [number, number][];
}
export enum GameState {
WAITING_FOR_PLAYERS = 'waitingForPlayers',
READY_TO_START = 'readyToStart',
IN_PROGRESS = 'inProgress',
GAME_OVER = 'gameOver'
}
Next Steps
With our event system in place, weâre ready to implement the core game logic that will handle these events and maintain the game state.