game-state

Game State Management Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "game-state" with this command: npx skills add fil512/upship/fil512-upship-game-state

Game State Management Skill

Overview

This skill provides expertise for managing complex game state in digital board games. It covers state structure design, action validation, phase management, and patterns for implementing intricate game rules reliably.

Core Principles

Immutable State

Always treat game state as immutable. Create new state objects rather than mutating existing ones:

// BAD: Mutating state function addMoney(state, playerId, amount) { state.players[playerId].money += amount; return state; }

// GOOD: Creating new state function addMoney(state, playerId, amount) { return { ...state, players: { ...state.players, [playerId]: { ...state.players[playerId], money: state.players[playerId].money + amount } } }; }

Single Source of Truth

All game state should live in one canonical object:

const gameState = { // Metadata id: 'game-123', version: 42, phase: 'worker-placement', currentPlayer: 'player-1', turnNumber: 5, age: 2,

// Player states players: { 'player-1': { /* player state / }, 'player-2': { / player state */ } },

// Shared state board: { /* board state / }, market: { / market state / }, decks: { / deck state */ } };

State Structure Design

Normalize Nested Data

Avoid deeply nested structures. Use ID references instead:

// BAD: Deeply nested const state = { players: [{ ships: [{ upgrades: [{ type: 'engine', stats: {...} }] }] }] };

// GOOD: Normalized with references const state = { players: { 'p1': { id: 'p1', shipIds: ['ship-1', 'ship-2'] } }, ships: { 'ship-1': { id: 'ship-1', ownerId: 'p1', upgradeIds: ['upg-1'] } }, upgrades: { 'upg-1': { id: 'upg-1', type: 'engine', stats: {...} } } };

Separate Derived State

Don't store computed values in state:

// BAD: Storing computed values const playerState = { money: 100, income: 15, totalMoney: 115 // Don't store this! };

// GOOD: Compute when needed function getTotalMoney(player) { return player.money + player.income; }

// Or use selectors const selectors = { totalLift: (state, playerId) => { const player = state.players[playerId]; return player.gasCubes * 5; }, canLaunch: (state, playerId) => { const lift = selectors.totalLift(state, playerId); const weight = selectors.totalWeight(state, playerId); return lift >= weight; } };

Action/Reducer Pattern

Action Structure

// Actions describe what happened const action = { type: 'PLACE_WORKER', playerId: 'player-1', payload: { workerId: 'agent-1', locationId: 'construction-hall' } };

// Action creators for consistency const actions = { placeWorker: (playerId, workerId, locationId) => ({ type: 'PLACE_WORKER', playerId, payload: { workerId, locationId } }),

acquireTechnology: (playerId, techId, cost) => ({ type: 'ACQUIRE_TECHNOLOGY', playerId, payload: { techId, cost } }) };

Reducer Implementation

function gameReducer(state, action) { switch (action.type) { case 'PLACE_WORKER': return placeWorkerReducer(state, action);

case 'ACQUIRE_TECHNOLOGY':
  return acquireTechnologyReducer(state, action);

case 'LAUNCH_SHIP':
  return launchShipReducer(state, action);

default:
  return state;

} }

function placeWorkerReducer(state, action) { const { playerId, payload: { workerId, locationId } } = action;

return { ...state, workers: { ...state.workers, [workerId]: { ...state.workers[workerId], locationId, available: false } }, locations: { ...state.locations, [locationId]: { ...state.locations[locationId], workerIds: [...state.locations[locationId].workerIds, workerId] } } }; }

Action Validation

Validation Layer

Always validate before applying actions:

function validateAction(state, action) { const validator = validators[action.type]; if (!validator) { return { valid: false, reason: 'Unknown action type' }; } return validator(state, action); }

const validators = { PLACE_WORKER: (state, action) => { const { playerId, payload: { workerId, locationId } } = action;

// Check it's player's turn
if (state.currentPlayer !== playerId) {
  return { valid: false, reason: 'Not your turn' };
}

// Check worker belongs to player and is available
const worker = state.workers[workerId];
if (!worker || worker.ownerId !== playerId) {
  return { valid: false, reason: 'Invalid worker' };
}
if (!worker.available) {
  return { valid: false, reason: 'Worker already placed' };
}

// Check location exists and has space
const location = state.locations[locationId];
if (!location) {
  return { valid: false, reason: 'Invalid location' };
}
if (location.workerIds.length >= location.capacity) {
  return { valid: false, reason: 'Location is full' };
}

return { valid: true };

} };

Process Action Flow

async function processAction(gameId, playerId, action) { // 1. Load current state const state = await loadGameState(gameId);

// 2. Validate const validation = validateAction(state, action); if (!validation.valid) { return { success: false, error: validation.reason }; }

// 3. Apply action let newState = gameReducer(state, action);

// 4. Check for triggered effects newState = processTriggers(newState, action);

// 5. Check for phase transitions newState = checkPhaseTransition(newState);

// 6. Increment version newState = { ...newState, version: newState.version + 1 };

// 7. Persist await saveGameState(gameId, newState);

return { success: true, newState }; }

Phase & Turn Management

State Machine for Phases

const phases = { SETUP: 'setup', WORKER_PLACEMENT: 'worker-placement', REVEAL: 'reveal', LAUNCH: 'launch', INCOME: 'income', CLEANUP: 'cleanup', AGE_TRANSITION: 'age-transition', GAME_END: 'game-end' };

const phaseTransitions = { [phases.SETUP]: { next: phases.WORKER_PLACEMENT, canTransition: (state) => allPlayersReady(state) }, [phases.WORKER_PLACEMENT]: { next: phases.REVEAL, canTransition: (state) => allWorkersPlaced(state) }, [phases.REVEAL]: { next: phases.LAUNCH, canTransition: (state) => allCardsRevealed(state) }, // ... etc };

function checkPhaseTransition(state) { const currentPhase = phaseTransitions[state.phase]; if (currentPhase && currentPhase.canTransition(state)) { return transitionToPhase(state, currentPhase.next); } return state; }

Turn Order Management

function getNextPlayer(state) { const playerOrder = state.playerOrder; const currentIndex = playerOrder.indexOf(state.currentPlayer); const nextIndex = (currentIndex + 1) % playerOrder.length; return playerOrder[nextIndex]; }

function advanceTurn(state) { const nextPlayer = getNextPlayer(state); const isNewRound = nextPlayer === state.playerOrder[0];

return { ...state, currentPlayer: nextPlayer, turnNumber: isNewRound ? state.turnNumber + 1 : state.turnNumber }; }

Complex Game Logic

Calculating Ship Stats

function calculateShipStats(state, playerId) { const player = state.players[playerId]; const blueprint = state.blueprints[player.blueprintId];

// Start with baseline stats let stats = { ...blueprint.baselineStats };

// Add stats from installed upgrades for (const slotId of blueprint.slotIds) { const slot = state.slots[slotId]; if (slot.upgradeId) { const upgrade = state.upgrades[slot.upgradeId]; stats = mergeStats(stats, upgrade.stats); } }

// Calculate lift from gas cubes const gasCubes = countGasCubes(state, playerId); stats.lift = gasCubes * 5;

return stats; }

function canLaunch(state, playerId) { const stats = calculateShipStats(state, playerId);

// Physics check: Lift >= Weight if (stats.lift < stats.weight) { return { can: false, reason: 'Insufficient lift' }; }

// Must have pilot const player = state.players[playerId]; if (player.pilots < 1) { return { can: false, reason: 'No pilot available' }; }

// Must have ship in hangar if (player.launchHangar.length === 0) { return { can: false, reason: 'No ships in hangar' }; }

return { can: true }; }

Hazard Check Resolution

function resolveHazardCheck(state, playerId, hazardCard) { const shipStats = calculateShipStats(state, playerId); const results = [];

for (const check of hazardCard.checks) { const playerValue = shipStats[check.stat]; const passed = playerValue >= check.difficulty;

results.push({
  stat: check.stat,
  required: check.difficulty,
  actual: playerValue,
  passed
});

}

const allPassed = results.every(r => r.passed);

return { hazardCard, results, outcome: allPassed ? 'success' : hazardCard.failureEffect }; }

Undo/Redo Support

Action History

const gameWithHistory = { ...gameState, history: { past: [], // Previous states future: [] // States after undo (for redo) } };

function applyActionWithHistory(state, action) { // Save current state to history const newPast = [...state.history.past, state];

// Apply action const newState = gameReducer(state, action);

return { ...newState, history: { past: newPast, future: [] // Clear redo stack on new action } }; }

function undo(state) { if (state.history.past.length === 0) return state;

const previous = state.history.past[state.history.past.length - 1]; const newPast = state.history.past.slice(0, -1);

return { ...previous, history: { past: newPast, future: [state, ...state.history.future] } }; }

Testing Game Logic

Unit Testing Reducers

describe('placeWorkerReducer', () => { it('should place worker at location', () => { const state = createTestState(); const action = actions.placeWorker('p1', 'worker-1', 'location-a');

const newState = gameReducer(state, action);

expect(newState.workers['worker-1'].locationId).toBe('location-a');
expect(newState.locations['location-a'].workerIds).toContain('worker-1');

});

it('should not mutate original state', () => { const state = createTestState(); const original = JSON.stringify(state); const action = actions.placeWorker('p1', 'worker-1', 'location-a');

gameReducer(state, action);

expect(JSON.stringify(state)).toBe(original);

}); });

Integration Testing Game Flow

describe('full game round', () => { it('should complete worker placement phase', () => { let state = createGameState({ playerCount: 2 });

// Each player places 3 workers
for (let i = 0; i &#x3C; 6; i++) {
  const playerId = state.currentPlayer;
  const worker = getAvailableWorker(state, playerId);
  const location = getValidLocation(state);

  state = processAction(state, actions.placeWorker(
    playerId, worker.id, location.id
  ));
}

expect(state.phase).toBe('reveal');

}); });

When This Skill Activates

Use this skill when:

  • Designing game state structure

  • Implementing action/reducer logic

  • Building validation for game rules

  • Managing phase and turn transitions

  • Calculating derived game values

  • Implementing undo/redo

  • Testing game logic

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

ui-design-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

realtime-multiplayer

No summary provided by upstream source.

Repository SourceNeeds Review
General

boardgame-design

No summary provided by upstream source.

Repository SourceNeeds Review