Attributes System in Shoehive
Shoehive provides a flexible attribute system that allows you to store and retrieve custom data on various game components. This guide explains how to effectively use attributes across the framework.
Overview
Attributes are key-value pairs that can be attached to various game components such as Players, Tables, and Hands. They provide a flexible way to associate custom data with these objects without extending their base classes.
The following components support attributes:
Player
: Store player-specific dataTable
: Store game state as well as table metadataHand
: Store hand-specific data
Common Attribute Methods
All supported components implement the following methods:
// Setting an attribute
component.setAttribute(key: string, value: any): void;
// Getting an attribute
component.getAttribute(key: string): any;
// Checking if an attribute exists
component.hasAttribute(key: string): boolean;
// Removing an attribute
component.removeAttribute(key: string): void;
// Setting multiple attributes at once
component.setAttributes(attributes: Record<string, any>): void;
// Getting all attributes as a Record object
component.getAttributes(): Record<string, any>;
Configurable Player Attribute Relevance
When a playerâs attributes change, the game server needs to decide whether to broadcast these changes to other components. The Shoehive framework allows game developers to configure which player attributes are relevant for table state updates and which are relevant for lobby updates.
Configuring Relevant Attributes
When registering a game definition, you can specify which player attributes should trigger updates:
gameManager.registerGame({
id: 'your-game',
name: 'Your Game',
// ... other game properties ...
// Define which player attributes should trigger a table state update when changed
tableRelevantPlayerAttributes: ['name', 'chips', 'isReady', 'score'],
// Define which player attributes should trigger a lobby update when changed
lobbyRelevantPlayerAttributes: ['name', 'status', 'isOnline']
});
How It Works
-
Table Relevance: When a playerâs attribute changes and that player is seated at a table, the system checks if the attribute is in the
tableRelevantPlayerAttributes
list. If it is, the table state is broadcast to all players at the table. -
Lobby Relevance: When a playerâs attribute changes and that attribute is in the
lobbyRelevantPlayerAttributes
list, the Lobby class updates the lobby state and broadcasts it to all connected players.
Default Values
If you donât specify these attributes, the system uses sensible defaults:
- Default Table-Relevant Attributes:
["name", "avatar", "chips", "status", "isReady", "role", "team"]
- Default Lobby-Relevant Attributes:
["name", "avatar", "isReady", "status"]
Performance Benefits
This configuration allows you to optimize the serverâs performance by reducing unnecessary broadcasts. For example, if a playerâs internal game state changes but doesnât need to be displayed to other players, you can exclude those attributes from the relevant lists.
Practical Example
Consider a card game where players have the following attributes:
name
: Playerâs display nameavatar
: Playerâs profile imagecards
: Playerâs cards in their hand (private)points
: Playerâs score (visible to all)strategy
: Playerâs internal strategy tracking (private)isReady
: Playerâs ready status
Optimized Configuration:
gameManager.registerGame({
id: 'card-game',
name: 'Strategic Card Game',
// ... other properties ...
// Only broadcast attributes that affect the visual state of the table
tableRelevantPlayerAttributes: ['name', 'avatar', 'points', 'isReady'],
// Only show essential player info in the lobby
lobbyRelevantPlayerAttributes: ['name', 'isReady']
});
// Later in your game logic:
// This will trigger a table state update because 'points' is table-relevant
player.setAttribute('points', 100);
// This will NOT trigger a table state update because 'strategy' is not table-relevant
player.setAttribute('strategy', 'defensive');
// This will NOT trigger a table state update because 'secretNumber' is not table-relevant
player.setAttribute('secretNumber', 42);
// This will trigger both table and lobby updates because 'isReady' is in both lists
player.setAttribute('isReady', true);
This configuration ensures that:
- Private information like cards and strategy never leak to other players
- Network traffic is minimized by only broadcasting necessary changes
- The server avoids unnecessary state calculations and broadcasts
Basic Usage
Setting and Getting Attributes
// Players
player.setAttribute('score', 100);
const score = player.getAttribute('score'); // 100
// Tables
table.setAttribute('gameMode', 'tournament');
const mode = table.getAttribute('gameMode'); // 'tournament'
// Hands
const hand = table.getHandAtSeat(0);
hand?.setAttribute('bet', 50);
const bet = hand?.getAttribute('bet'); // 50
Common Use Cases
Player Attributes
Player attributes are useful for storing user profiles, game progress, and session data:
// Player profile
player.setAttribute('profile', {
username: 'GamerX',
avatar: 'https://example.com/avatars/gamerx.png',
level: 42,
created: Date.now()
});
// Session tracking
player.setAttribute('session', {
connectedAt: Date.now(),
lastActive: Date.now(),
deviceInfo: request.headers['user-agent']
});
// Game progress
player.setAttribute('progress', {
level: 5,
score: 3200,
unlockedItems: ['sword', 'shield', 'potion']
});
Table Attributes
Table attributes are excellent for managing game state and configuration:
// Game configuration
table.setAttribute('gameConfig', {
rounds: 10,
timeLimit: 60,
difficulty: 'hard'
});
// Round state
table.setAttribute('roundState', {
currentRound: 3,
timeRemaining: 45,
activePlayerIndex: 2
});
// Table metadata
table.setAttribute('metadata', {
name: 'High Rollers Table',
creator: 'admin',
createdAt: Date.now(),
isPrivate: true
});
Hand Attributes
Hand attributes are useful for card game specific data:
const hand = table.getHandAtSeat(0);
if (hand) {
// Track bet information
hand.setAttribute('bet', 100);
hand.setAttribute('sideBets', { insurance: 50 });
// Track hand status
hand.setAttribute('status', 'standing');
// Track hand evaluation
hand.setAttribute('evaluation', {
score: 21,
isBlackjack: true
});
}
Advanced Patterns
Type Safety with TypeScript
For type safety, define interfaces for your attributes:
// Define interfaces for your attributes
interface PlayerProfile {
username: string;
avatar: string;
level: number;
created: number;
}
interface TableConfig {
rounds: number;
timeLimit: number;
difficulty: string;
}
// Create type-safe getter and setter functions
function getProfile(player: Player): PlayerProfile | null {
return player.getAttribute('profile') as PlayerProfile;
}
function setProfile(player: Player, profile: PlayerProfile): void {
player.setAttribute('profile', profile);
}
function getTableConfig(table: Table): TableConfig | null {
return table.getAttribute('gameConfig') as TableConfig;
}
function setTableConfig(table: Table, config: TableConfig): void {
table.setAttribute('gameConfig', config);
}
// Usage
const profile = getProfile(player);
if (profile) {
console.log(`${profile.username} is level ${profile.level}`);
}
setTableConfig(table, { rounds: 5, timeLimit: 30, difficulty: 'easy' });
Namespaced Attributes
For complex games, use namespacing to organize attributes:
// Set namespaced attributes
player.setAttribute('poker:hand', ['â A', 'âĽK', 'âŚQ', 'âŁJ', 'â 10']);
player.setAttribute('poker:chips', 1000);
player.setAttribute('poker:position', 3);
table.setAttribute('poker:pot', 500);
table.setAttribute('poker:dealer', 2);
table.setAttribute('poker:round', 'flop');
// Helper function to work with namespaced attributes
function getNamespacedAttribute(component: Player | Table | Hand, namespace: string, key: string): any {
return component.getAttribute(`${namespace}:${key}`);
}
function setNamespacedAttribute(component: Player | Table | Hand, namespace: string, key: string, value: any): void {
component.setAttribute(`${namespace}:${key}`, value);
}
// Usage
const chips = getNamespacedAttribute(player, 'poker', 'chips');
setNamespacedAttribute(table, 'poker', 'pot', 750);
Ephemeral vs. Persistent Attributes
Differentiate between ephemeral (session-only) and persistent attributes:
// Ephemeral (in-memory only) attributes
player.setAttribute('currentGame', tableId);
player.setAttribute('isReady', true);
player.setAttribute('latency', 45); // ms
// Persistent attributes (should be saved to database)
player.setAttribute('persistent:profile', {
username: 'GamerX',
avatar: 'avatar1.png',
level: 42
});
player.setAttribute('persistent:stats', {
gamesPlayed: 157,
gamesWon: 83,
totalScore: 12450
});
// Helper function to save persistent attributes to database
async function savePersistentAttributes(player: Player) {
const persistentAttrs = {};
// Find all attributes with 'persistent:' prefix
for (const key of Object.keys(player.getAttributes())) {
if (key.startsWith('persistent:')) {
const actualKey = key.substring('persistent:'.length);
persistentAttrs[actualKey] = player.getAttribute(key);
}
}
// Save to database
await database.savePlayerAttributes(player.id, persistentAttrs);
}
Integration with Shoehive Events
Using Attributes with Events
Attributes can be combined with Shoehiveâs event system for powerful functionality:
// Listen for player connection and load attributes
gameServer.eventBus.on('player:connected', async (player) => {
// Load player data from database
const userData = await database.getPlayerData(player.id);
if (userData) {
// Set player attributes
player.setAttribute('profile', userData.profile);
player.setAttribute('stats', userData.stats);
player.setAttribute('preferences', userData.preferences);
}
// Track connection info
player.setAttribute('session', {
connectedAt: Date.now(),
ipAddress: getUserIp(player),
lastActive: Date.now()
});
});
// Update last active timestamp
gameServer.eventBus.on('player:action', (player) => {
const session = player.getAttribute('session') || {};
player.setAttribute('session', {
...session,
lastActive: Date.now()
});
});
// Store attributes on disconnect
gameServer.eventBus.on('player:disconnected', (player) => {
const session = player.getAttribute('session');
if (session) {
// Calculate session duration
const sessionDuration = Date.now() - session.connectedAt;
// Store in database for analytics
database.recordPlayerSession(player.id, {
duration: sessionDuration,
connectedAt: session.connectedAt,
disconnectedAt: Date.now()
});
}
// Save persistent attributes
savePersistentAttributes(player);
});
Best Practices
- Be consistent: Use a consistent naming convention for attributes
- Keep it clean: Donât store unnecessary data in attributes
- Type safety: Use TypeScript interfaces to define attribute structure
- Namespaces: Use namespaced keys for complex games
- Validation: Validate attribute values before setting them
- Keep serializable: Ensure attributes can be serialized to JSON
- Avoid circular references: Donât create circular references in attributes
- Documentation: Document your attribute schema for team members
- Privacy: Donât store sensitive information in attributes
- Performance: Be mindful of attribute size for frequently accessed data
Example: Complete Attribute Schema for a Card Game
Hereâs an example of a complete attribute schema for a poker game:
// Player Attributes
interface PokerPlayerAttributes {
// Profile information
'profile': {
username: string;
avatar: string;
displayName: string;
created: number; // timestamp
lastLogin: number; // timestamp
};
// Game statistics
'stats': {
handsPlayed: number;
handsWon: number;
biggestPot: number;
totalWinnings: number;
};
// Current game state
'poker:state': {
chips: number;
currentBet: number;
hasFolded: boolean;
position: number; // seat position
isDealer: boolean;
lastAction?: 'fold' | 'check' | 'call' | 'raise' | 'all-in';
};
// Settings
'settings': {
autoFold: boolean;
autoBuyIn: boolean;
notifications: boolean;
theme: 'light' | 'dark' | 'classic';
};
// Session information
'session': {
connectionTime: number;
lastActive: number;
deviceInfo: string;
ipAddress: string;
};
}
// Table Attributes
interface PokerTableAttributes {
// Game configuration
'config': {
blinds: [number, number]; // [small, big]
buyIn: [number, number]; // [min, max]
timeBank: number; // seconds
isPrivate: boolean;
};
// Current game state
'poker:state': {
pot: number;
sidePots: Array<{amount: number, eligiblePlayers: string[]}>;
dealer: number; // seat index
currentPlayer: number; // seat index
round: 'preflop' | 'flop' | 'turn' | 'river' | 'showdown';
communityCards: string[];
lastRaise: number;
};
// Table metadata
'metadata': {
name: string;
creator: string;
createdAt: number;
gameCount: number;
};
}
// Hand Attributes
interface PokerHandAttributes {
// Bet information
'bet': number;
// Hand evaluation
'evaluation': {
handType: 'high-card' | 'pair' | 'two-pair' | 'three-of-a-kind' | 'straight' | 'flush' | 'full-house' | 'four-of-a-kind' | 'straight-flush' | 'royal-flush';
score: number;
description: string;
};
}