📨 Command System
The command system in Shoehive provides a structured way to send messages between clients and the server, enabling bidirectional communication for game actions and state updates.
It is not to be confused with the Event System which is used to manage events between components in the game server.
Introduction
The command system consists of:
- Message Router: Routes incoming client messages to appropriate handlers
- Command Handlers: Functions that process specific types of commands
- Message Types: Standard formats for outbound messages from server to client
Message Flow
- Client to Server: Clients send commands as JSON messages with an
action
property - Server Processing: The MessageRouter processes these commands using registered handlers
- Server to Client: The server sends structured responses using standard message types
Sending Commands (Client)
Clients send commands to the server as JSON messages with this structure:
{
"action": "domain:action",
"tableId": "table-123", // Optional, depends on the action
// Other action-specific properties
}
Example of sending a command from the client:
// Example client-side code
const sendCommand = (action, data = {}) => {
const message = JSON.stringify({
action,
...data
});
socket.send(message);
};
// Join a table
sendCommand('table:join', { tableId: 'table-123' });
// Make a game move
sendCommand('game:poker:bet', { tableId: 'table-123', amount: 50 });
Command Naming Convention
Commands follow the same namespaced pattern as events:
domain:action
or
domain:subject:action
For example:
table:join
game:choice:make
player:ready
Handling Commands (Server)
On the server side, the MessageRouter processes incoming commands:
import { MessageRouter } from 'shoehive';
import { Player, Lobby } from 'shoehive';
// Create a message router connected to the event bus
const messageRouter = new MessageRouter(eventBus);
// Register a command handler for table creation
messageRouter.registerCommandHandler(
'lobby:table:create',
(player: Player, data: any) => {
const { gameId, options } = data;
const table = gameServer.lobby.createTable(gameId, options);
if (table) {
// Add the player to the new table
table.addPlayer(player);
// Notify the player
player.sendMessage({
type: 'table:created',
tableId: table.id
});
// Send current table state
table.sendState(player);
} else {
player.sendMessage({
type: 'error',
message: 'Failed to create table'
});
}
}
);
// Register a game-specific command handler
messageRouter.registerCommandHandler(
'game:choice:make',
(player: Player, data: any) => {
const { tableId, choice } = data;
// Process the player's choice
console.log(`Player ${player.id} chose ${choice} at table ${tableId}`);
// Update game state based on choice
// ...
// Respond to the player
player.sendMessage({
type: 'game:choice:result',
success: true,
result: 'Your choice was processed'
});
}
);
Outbound Messages (Server to Client)
The server sends structured messages to clients. There are several standard message types:
Standard Message Types
Category | Message Type | Description |
---|---|---|
Lobby | lobby:state | Provides current lobby state with available games and tables |
Table | table:state | Provides the current state of a specific table |
Player | player:state | Provides the current state of a player |
Error | error | Indicates an error occurred processing a command |
Example Outbound Messages
Player State Message:
{
"type": "player:state",
"id": "player-123",
"name": "PlayerName",
"tableId": "table-456",
"connected": true,
"attributes": {
"chips": 1000,
"avatar": "avatar1"
}
}
Error Message:
{
"type": "error",
"message": "Invalid message format: missing or invalid action"
}
Extending with Custom Commands
You can extend the command system with your own game-specific commands:
// Register a custom command for a poker game
messageRouter.registerCommandHandler(
'poker:bet',
(player: Player, data: any) => {
const { tableId, amount } = data;
const table = player.getTable();
if (!table || table.id !== tableId) {
player.sendMessage({
type: 'error',
message: 'You are not at this table'
});
return;
}
// Process the bet
// ...
// Broadcast the action to all players at the table
table.broadcastToAll({
type: 'poker:action',
playerId: player.id,
action: 'bet',
amount: amount
});
}
);
Best Practices
- Consistent Naming: Follow the
domain:action
pattern for all your commands - Validation: Always validate incoming command data before processing
- Error Handling: Send clear error messages when commands fail
- Security: Never trust client input; validate permissions before processing commands
- Idempotency: Design commands to be idempotent when possible (can be safely retried)
- Command Documentation: Document all available commands and their expected parameters
- Response Consistency: Maintain consistent response formats across all commands
Debugging Commands
For debugging purposes, you can log incoming commands:
// Simple command logging middleware
messageRouter.registerCommandHandler('*', (player, data) => {
console.log(`[Command] ${player.id} -> ${data.action}`, data);
// Note: This doesn't handle the command, just logs it
return false; // Continue processing with other handlers
});
Complete Example
import { createGameServer } from 'shoehive';
import * as http from 'http';
// Create HTTP server
const server = http.createServer();
// Create game server
const gameServer = createGameServer(server);
const { messageRouter, lobby } = gameServer;
// Register command handler for table creation
messageRouter.registerCommandHandler('lobby:table:create', (player, data) => {
const { gameId, options } = data;
// Create the table using the lobby
const table = lobby.createTable(gameId, options);
if (table) {
// Notify player of success
player.sendMessage({
type: 'table:created',
tableId: table.id
});
} else {
player.sendMessage({
type: 'error',
message: 'Failed to create table'
});
}
});
// Register command handlers for table joining
messageRouter.registerCommandHandler('table:join', (player, data) => {
const { tableId } = data;
const table = gameServer.tableManager.getTable(tableId);
if (!table) {
player.sendMessage({
type: 'error',
message: 'Table not found'
});
return;
}
// Add player to table
const success = table.addPlayer(player);
if (success) {
// Notify player of success
player.sendMessage({
type: 'table:joined',
tableId: table.id
});
// Send the current table state
table.sendState(player);
} else {
player.sendMessage({
type: 'error',
message: 'Failed to join table'
});
}
});
// Start the server
server.listen(3000, () => {
console.log('Game server running on port 3000');
});