Files
echoes-of-the-ash/old/OPTIMIZATION_STRATEGY.md
2025-11-27 16:27:01 +01:00

12 KiB

Game Optimization Strategy

Current Performance Analysis

Polling Overhead (Every 5 seconds)

Current polling fetches 5 endpoints simultaneously:

  1. /api/game/state - ~1.3KB+ (inventory, equipment, dropped items, player data)
  2. /api/game/location - Location data
  3. /api/game/profile - Player profile
  4. /api/game/combat - Combat status
  5. /api/game/pvp/status - PvP combat status

Problems:

  • Inventory sent every poll (~1.3KB) even though it rarely changes
  • Equipment sent every poll even though it rarely changes
  • Dropped items fetched every poll even though location doesn't change often
  • Duplicate data across endpoints (player data in multiple responses)

Optimization Strategy

Phase 1: Smart Polling - Only Fetch What Changes

A. Lightweight Polling Endpoint (Every 5s)

Create /api/game/state/minimal that returns ONLY data that changes frequently:

{
  "player": {
    "hp": 100,
    "stamina": 95,
    "location_id": "ruins_entrance",
    "movement_cooldown": 2
  },
  "in_combat": false,
  "in_pvp_combat": false,
  "location_changed": false,  // Flag if location changed
  "inventory_changed": false, // Flag if inventory changed
  "equipment_changed": false, // Flag if equipment changed
  "last_update_timestamp": 1699380123.456
}

Size estimate: ~200 bytes vs current 1.5KB+ = 87% reduction

B. On-Demand Fetching

Only fetch full data when flags indicate changes or after specific actions:

Inventory - Fetch only when:

  • inventory_changed flag is true
  • After pickup/drop/craft/use actions
  • After equipment changes
  • On initial load

Equipment - Fetch only when:

  • equipment_changed flag is true
  • After equip/unequip actions
  • On initial load

Location Details - Fetch only when:

  • location_changed flag is true
  • After movement
  • On initial load

Dropped Items - Fetch only when:

  • Location changes
  • After dropping items
  • After picking up items

C. Change Tracking in Database

Add columns to players table:

ALTER TABLE players ADD COLUMN inventory_version INTEGER DEFAULT 0;
ALTER TABLE players ADD COLUMN equipment_version INTEGER DEFAULT 0;
ALTER TABLE players ADD COLUMN last_state_change FLOAT DEFAULT 0;

Increment versions when:

  • Inventory changes: inventory_version++
  • Equipment changes: equipment_version++

Frontend caches versions and only re-fetches if version changed.


Phase 2: Delta Updates (Advanced)

Pros:

  • Even smaller payload (only changes)
  • Extremely efficient for large inventories

Cons:

  • More complex implementation
  • Need to handle edge cases (out-of-sync states)
  • Need full refresh mechanism as fallback

Verdict: Not recommended initially

  • Current optimization (Phase 1) will give 85%+ bandwidth reduction
  • Delta updates add complexity for diminishing returns
  • Only consider if player base grows significantly (10k+ concurrent users)

Phase 3: WebSocket for Real-Time Updates (Future)

Instead of polling, use WebSocket for:

  • Push notifications when state changes
  • Real-time PvP combat updates
  • Instant location updates from other players
  • Server broadcasts (events, announcements)

Benefits:

  • Zero polling overhead
  • True real-time experience
  • Server can push updates only when needed

Implementation complexity: Medium-High


Immediate Action Plan

1. Create Minimal State Endpoint (1-2 hours)

File: api/main.py

@app.get("/api/game/state/minimal")
async def get_minimal_state(current_user: dict = Depends(get_current_user)):
    """Lightweight endpoint for polling - returns only frequently changing data"""
    player = await db.get_player_by_id(current_user['id'])
    
    # Calculate movement cooldown
    import time
    current_time = time.time()
    last_movement = player.get('last_movement_time', 0)
    movement_cooldown = max(0, 5 - (current_time - last_movement))
    
    # Check if data changed since last poll
    # Client sends last known versions, server returns flags
    inventory_version = player.get('inventory_version', 0)
    equipment_version = player.get('equipment_version', 0)
    
    return {
        "player": {
            "hp": player['hp'],
            "max_hp": player['max_hp'],
            "stamina": player['stamina'],
            "max_stamina": player['max_stamina'],
            "location_id": player['location_id'],
            "movement_cooldown": int(movement_cooldown),
            "is_dead": player.get('is_dead', False)
        },
        "versions": {
            "inventory": inventory_version,
            "equipment": equipment_version
        },
        "timestamp": current_time
    }

2. Update Frontend Polling Logic (2-3 hours)

File: pwa/src/components/Game.tsx

// Cache for full data
const [cachedInventory, setCachedInventory] = useState(null)
const [cachedEquipment, setCachedEquipment] = useState(null)
const [lastVersions, setLastVersions] = useState({ inventory: 0, equipment: 0 })

const fetchMinimalState = async () => {
  const res = await api.get('/api/game/state/minimal')
  
  // Update HP/Stamina/Location immediately
  setPlayerState(prev => ({
    ...prev,
    health: res.data.player.hp,
    stamina: res.data.player.stamina,
    location_id: res.data.player.location_id
  }))
  
  // Check if inventory changed
  if (res.data.versions.inventory !== lastVersions.inventory) {
    await fetchFullInventory()
    setLastVersions(prev => ({ ...prev, inventory: res.data.versions.inventory }))
  }
  
  // Check if equipment changed
  if (res.data.versions.equipment !== lastVersions.equipment) {
    await fetchFullEquipment()
    setLastVersions(prev => ({ ...prev, equipment: res.data.versions.equipment }))
  }
}

3. Add Version Tracking to Database Functions (1 hour)

Update these functions to increment versions:

  • add_item_to_inventory() - increment inventory_version
  • remove_inventory_item() - increment inventory_version
  • equip_item() - increment equipment_version
  • unequip_item() - increment equipment_version

4. Keep Full State Fetch for Actions (Already done!)

After actions (pickup, craft, equip, etc.), fetch full data:

await handlePickup(itemId)
await fetchFullInventory() // Refresh immediately after action

Expected Performance Gains

Bandwidth Reduction

  • Current: 1.5KB every 5s = 18KB/minute per player
  • Optimized: 200 bytes every 5s = 2.4KB/minute per player
  • Savings: 87% reduction in polling bandwidth

Server Load Reduction

  • Database queries per poll:
    • Current: 8-12 queries (inventory items, equipment, dropped items, etc.)
    • Optimized: 1 query (player state only)
  • CPU usage: ~80% reduction per poll
  • Memory: Significant reduction from not loading/serializing inventory every poll

Scalability

  • Current: ~100 concurrent users max
  • Optimized: ~800+ concurrent users with same resources

Steam Integration & Monetization

Can you release on Steam?

YES! Your game architecture is perfect for Steam:

  • Progressive Web App can be packaged as desktop app
  • Electron or Tauri wrapper around your PWA
  • Steamworks SDK integration is straightforward

Integration Steps

1. Package as Desktop App (Choose one)

Option A: Electron (Most common)

{
  "name": "echoes-of-the-ashes",
  "main": "main.js",
  "dependencies": {
    "electron": "^27.0.0"
  }
}

Option B: Tauri (More efficient, Rust-based)

  • Smaller bundle size
  • Better performance
  • Rust backend integration

2. Integrate Steamworks

// Steam initialization
const steamworks = require('steamworks.js')

// Initialize Steam client
if (steamworks.init()) {
  const steamId = steamworks.localplayer.getSteamId()
  const username = steamworks.localplayer.getName()
  
  // Use Steam ID for authentication
  await api.post('/api/auth/steam', { 
    steamId, 
    username,
    ticket: steamworks.auth.getSessionTicket()
  })
}

3. Two-Version Strategy (Free vs Premium)

Database Schema:

ALTER TABLE players ADD COLUMN account_type VARCHAR(20) DEFAULT 'free';
-- Values: 'free', 'steam_premium', 'web_premium'

ALTER TABLE players ADD COLUMN steam_id VARCHAR(50) UNIQUE;
ALTER TABLE players ADD COLUMN premium_expires_at FLOAT;

Backend Restrictions:

@app.post("/api/game/move")
async def move(req, current_user):
    player = await db.get_player_by_id(current_user['id'])
    
    # Check premium restrictions
    if player['account_type'] == 'free':
        if player['level'] >= 3:
            raise HTTPException(
                status_code=402,
                detail="Level 3 is the maximum for free accounts. Upgrade to premium to continue!"
            )
        
        # Check location restrictions
        if location_id in PREMIUM_LOCATIONS:
            raise HTTPException(
                status_code=402,
                detail="This area is only accessible to premium accounts."
            )
    
    # Continue with move logic...

Monetization Options:

  1. Steam Purchase (One-time)

    • Buy on Steam = Permanent premium
    • Price: $9.99 - $19.99
    • Steam handles payment, you get 70%
  2. Web Subscription

    • Stripe/PayPal integration
    • $4.99/month or $39.99/year
    • Separate from Steam version
  3. Hybrid Model

    def is_premium(player):
        # Steam premium (permanent)
        if player['account_type'] == 'steam_premium':
            return True
    
        # Web premium (subscription)
        if player['account_type'] == 'web_premium':
            if player['premium_expires_at'] > time.time():
                return True
    
        return False
    

Steam Features You Can Use

  1. Achievements

    steamworks.achievement.activate('FIRST_COMBAT_WIN')
    
  2. Cloud Saves

    • Sync player state across devices
    • Automatic backups
  3. Leaderboards

    steamworks.leaderboards.uploadScore('PLAYER_LEVEL', player.level)
    
  4. Friends/Multiplayer

    • See Steam friends in-game
    • Invite to PvP combat
    • Group features
  5. Workshop

    • User-created content
    • Custom locations/items
    • Community maps
  1. Steam Agreement

    • Need business entity (or individual) to sign Steamworks agreement
    • Need Tax ID (EIN for business, SSN for individual)
  2. Separate Versions

    • ALLOWED: Selling Steam version + separate web subscription
    • NOT ALLOWED: Forcing Steam users to pay again on web
    • ALLOWED: Different pricing models
    • ALLOWED: Web has features Steam doesn't (and vice versa)
  3. Revenue Sharing

    • Steam: 30% cut (you get 70%)
    • Web direct: 100% yours (minus payment processor ~3%)

Phase 1: Optimize & Test (Current)

  • Implement polling optimizations
  • Test with small player base
  • Gather feedback

Phase 2: Steam Preparation (2-4 weeks)

  • Package as Electron app
  • Integrate Steamworks SDK
  • Implement premium restrictions
  • Add achievements/leaderboards

Phase 3: Soft Launch (Steam Early Access)

  • Launch as Early Access ($14.99)
  • Gather Steam community feedback
  • Regular updates

Phase 4: Dual Platform (After Steam is stable)

  • Keep Steam version
  • Launch web version with subscription
  • Cross-platform support (shared servers)

Priority Order

  1. Immediate: Polling optimization (biggest impact, low effort)
  2. Short-term: Premium restrictions system (prep for monetization)
  3. Medium-term: Steam integration (packaging + Steamworks)
  4. 🔮 Long-term: WebSocket real-time updates (if needed)

Would you like me to start implementing the polling optimization now?