# WebSocket Migration Plan

## Difficulty Assessment: **Medium** ⚙️

**Time estimate:** 3-5 days for full implementation
**Complexity:** Moderate - FastAPI has excellent WebSocket support

---

## Architecture Changes

### Current (Polling)
```
Client → HTTP GET every 5s → Server → Query DB → Return JSON
       ← HTTP Response ←
```

### Future (WebSocket)
```
Client ←→ WebSocket (persistent) ←→ Server
                                    ↓
                                  DB Query only on changes
                                    ↓
                                  Push to client immediately
```

---

## Implementation Steps

### Phase 1: Backend WebSocket Setup (1-2 days)

#### 1. Add WebSocket endpoint
**File:** `api/main.py`

```python
from fastapi import WebSocket, WebSocketDisconnect
from typing import Dict
import asyncio
import json

# Connection manager to track active WebSocket connections
class ConnectionManager:
    def __init__(self):
        self.active_connections: Dict[int, WebSocket] = {}  # player_id -> websocket
    
    async def connect(self, websocket: WebSocket, player_id: int):
        await websocket.accept()
        self.active_connections[player_id] = websocket
        print(f"Player {player_id} connected via WebSocket")
    
    def disconnect(self, player_id: int):
        if player_id in self.active_connections:
            del self.active_connections[player_id]
            print(f"Player {player_id} disconnected")
    
    async def send_personal_message(self, player_id: int, message: dict):
        """Send message to specific player"""
        if player_id in self.active_connections:
            try:
                await self.active_connections[player_id].send_json(message)
            except Exception as e:
                print(f"Error sending to player {player_id}: {e}")
                self.disconnect(player_id)
    
    async def broadcast(self, message: dict, exclude: int = None):
        """Send message to all connected players"""
        disconnected = []
        for player_id, connection in self.active_connections.items():
            if player_id == exclude:
                continue
            try:
                await connection.send_json(message)
            except Exception as e:
                print(f"Error broadcasting to player {player_id}: {e}")
                disconnected.append(player_id)
        
        # Clean up disconnected players
        for player_id in disconnected:
            self.disconnect(player_id)
    
    async def send_to_location(self, location_id: str, message: dict, exclude: int = None):
        """Send message to all players in a specific location"""
        # Get players in location from database
        players_in_location = await db.get_players_in_location(location_id)
        for player in players_in_location:
            if player['id'] != exclude and player['id'] in self.active_connections:
                await self.send_personal_message(player['id'], message)

manager = ConnectionManager()


@app.websocket("/ws/game/{token}")
async def websocket_endpoint(websocket: WebSocket, token: str):
    """
    Main WebSocket endpoint for real-time game updates
    Client connects with auth token, receives push updates
    """
    # Verify token and get player
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        player_id = payload.get("user_id")
        if not player_id:
            await websocket.close(code=1008, reason="Invalid token")
            return
    except JWTError:
        await websocket.close(code=1008, reason="Invalid token")
        return
    
    player = await db.get_player_by_id(player_id)
    if not player:
        await websocket.close(code=1008, reason="Player not found")
        return
    
    # Connect player
    await manager.connect(websocket, player_id)
    
    # Send initial state
    initial_state = await get_full_game_state(player_id)
    await websocket.send_json({
        "type": "initial_state",
        "data": initial_state
    })
    
    try:
        # Keep connection alive and listen for messages
        while True:
            # Receive messages from client (heartbeat, actions, etc.)
            data = await websocket.receive_json()
            
            # Handle different message types
            if data.get("type") == "heartbeat":
                await websocket.send_json({"type": "pong"})
            
            elif data.get("type") == "action":
                # Client performed action - process and broadcast updates
                await handle_websocket_action(player_id, data, websocket)
    
    except WebSocketDisconnect:
        manager.disconnect(player_id)
        print(f"Player {player_id} disconnected")


async def handle_websocket_action(player_id: int, data: dict, websocket: WebSocket):
    """Handle actions received via WebSocket"""
    action = data.get("action")
    
    if action == "move":
        # Process movement
        result = await process_move(player_id, data.get("direction"))
        
        # Send update to player
        await manager.send_personal_message(player_id, {
            "type": "state_update",
            "data": result
        })
        
        # Notify players in new location
        if result.get("success"):
            await manager.send_to_location(
                result["new_location_id"],
                {
                    "type": "player_entered",
                    "player": {
                        "id": player_id,
                        "username": result["username"]
                    }
                },
                exclude=player_id
            )
    
    # Add more actions as needed...
```

#### 2. Push updates on state changes

**Modify existing endpoints to push updates:**

```python
@app.post("/api/game/pickup")
async def pickup(req, current_user):
    # ... existing pickup logic ...
    
    # After successful pickup, push update via WebSocket
    await manager.send_personal_message(current_user['id'], {
        "type": "inventory_update",
        "data": {
            "inventory": new_inventory,
            "dropped_items": new_dropped_items
        }
    })
    
    # Also notify other players in location
    await manager.send_to_location(player['location_id'], {
        "type": "dropped_items_update",
        "data": {"dropped_items": new_dropped_items}
    }, exclude=current_user['id'])
    
    return result
```

#### 3. Add database helper for location players

**File:** `api/database.py`

```python
async def get_players_in_location(location_id: str) -> List[Dict]:
    """Get all players currently in a location"""
    async with DatabaseSession() as session:
        stmt = select(players).where(players.c.location_id == location_id)
        result = await session.execute(stmt)
        return [dict(row) for row in result.fetchall()]
```

---

### Phase 2: Frontend WebSocket Setup (1-2 days)

#### 1. Create WebSocket hook

**File:** `pwa/src/hooks/useGameWebSocket.ts`

```typescript
import { useEffect, useRef, useState } from 'react'

interface WebSocketMessage {
  type: string
  data: any
}

export const useGameWebSocket = (token: string, onMessage: (msg: WebSocketMessage) => void) => {
  const ws = useRef<WebSocket | null>(null)
  const [isConnected, setIsConnected] = useState(false)
  const reconnectTimeout = useRef<number | null>(null)

  const connect = () => {
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
    const host = window.location.host
    const wsUrl = `${protocol}//${host}/ws/game/${token}`

    ws.current = new WebSocket(wsUrl)

    ws.current.onopen = () => {
      console.log('WebSocket connected')
      setIsConnected(true)
      
      // Start heartbeat
      const heartbeat = setInterval(() => {
        if (ws.current?.readyState === WebSocket.OPEN) {
          ws.current.send(JSON.stringify({ type: 'heartbeat' }))
        }
      }, 30000) // Every 30 seconds

      // Store interval ID for cleanup
      ws.current.addEventListener('close', () => clearInterval(heartbeat))
    }

    ws.current.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data)
        onMessage(message)
      } catch (error) {
        console.error('Error parsing WebSocket message:', error)
      }
    }

    ws.current.onerror = (error) => {
      console.error('WebSocket error:', error)
    }

    ws.current.onclose = () => {
      console.log('WebSocket disconnected')
      setIsConnected(false)
      
      // Attempt to reconnect after 3 seconds
      reconnectTimeout.current = window.setTimeout(() => {
        console.log('Attempting to reconnect...')
        connect()
      }, 3000)
    }
  }

  useEffect(() => {
    connect()

    return () => {
      // Cleanup on unmount
      if (reconnectTimeout.current) {
        clearTimeout(reconnectTimeout.current)
      }
      if (ws.current) {
        ws.current.close()
      }
    }
  }, [token])

  const sendMessage = (message: any) => {
    if (ws.current?.readyState === WebSocket.OPEN) {
      ws.current.send(JSON.stringify(message))
    }
  }

  return { isConnected, sendMessage }
}
```

#### 2. Update Game component

**File:** `pwa/src/components/Game.tsx`

```typescript
import { useGameWebSocket } from '../hooks/useGameWebSocket'

const Game = () => {
  // ... existing state ...

  const handleWebSocketMessage = (message: WebSocketMessage) => {
    switch (message.type) {
      case 'initial_state':
        // Set full game state on connect
        setPlayerState(message.data.player)
        setLocation(message.data.location)
        setInventory(message.data.inventory)
        setEquipment(message.data.equipment)
        break

      case 'state_update':
        // Partial update to player state
        setPlayerState(prev => ({ ...prev, ...message.data.player }))
        break

      case 'inventory_update':
        // Update inventory
        setInventory(message.data.inventory)
        if (message.data.dropped_items) {
          setDroppedItems(message.data.dropped_items)
        }
        break

      case 'pvp_combat_start':
        // PvP combat initiated
        setCombatState(message.data.combat)
        break

      case 'pvp_combat_action':
        // Opponent performed action in PvP
        setCombatLog(prev => [...prev, message.data.action])
        setCombatState(prev => ({ ...prev, ...message.data.combat }))
        break

      case 'player_entered':
        // Another player entered your location
        setMessage(`${message.data.player.username} entered the area`)
        break

      // Add more message types...
    }
  }

  const { isConnected, sendMessage } = useGameWebSocket(
    localStorage.getItem('token') || '',
    handleWebSocketMessage
  )

  // Remove old polling useEffect, replace with WebSocket
  // Keep fallback polling for when WebSocket disconnects
  useEffect(() => {
    if (!isConnected) {
      // Fallback to polling if WebSocket disconnected
      const interval = setInterval(() => {
        fetchGameData(true)
      }, 10000) // Poll every 10s as fallback

      return () => clearInterval(interval)
    }
  }, [isConnected])

  // ... rest of component
}
```

---

### Phase 3: Hybrid Approach (Recommended - Best of Both Worlds)

**Use WebSockets for real-time updates + Polling as fallback**

```typescript
const Game = () => {
  const [connectionMode, setConnectionMode] = useState<'websocket' | 'polling'>('websocket')

  const { isConnected, sendMessage } = useGameWebSocket(token, handleWebSocketMessage)

  // Monitor connection and switch to polling if WebSocket fails
  useEffect(() => {
    if (!isConnected) {
      console.warn('WebSocket unavailable, using polling fallback')
      setConnectionMode('polling')
    } else {
      setConnectionMode('websocket')
    }
  }, [isConnected])

  // Fallback polling when WebSocket not available
  useEffect(() => {
    if (connectionMode === 'polling') {
      const interval = setInterval(fetchGameData, 5000)
      return () => clearInterval(interval)
    }
  }, [connectionMode])
}
```

**Benefits:**
- ✅ Best UX when WebSocket works (99% of time)
- ✅ Graceful fallback for problematic networks
- ✅ Works behind corporate firewalls
- ✅ No downtime during deployments

---

## Scaling Considerations

### Single Server (Current - Simple)
```
Client ←→ WebSocket ←→ FastAPI Server ←→ Database
```
**Works for:** Up to 1,000 concurrent connections

### Multi-Server (Future - When growing)
```
Client ←→ WebSocket ←→ FastAPI Server 1 ←→ Redis Pub/Sub
                     ↓                      ↓
                  Database ←→ FastAPI Server 2
```

**Use Redis for message broadcasting between servers:**

```python
import redis.asyncio as redis

redis_client = redis.from_url("redis://localhost")

class ConnectionManager:
    async def broadcast(self, message: dict):
        # Publish to Redis channel
        await redis_client.publish('game_events', json.dumps(message))
    
    async def listen_to_broadcasts(self):
        # Subscribe to Redis channel
        pubsub = redis_client.pubsub()
        await pubsub.subscribe('game_events')
        
        async for message in pubsub.listen():
            if message['type'] == 'message':
                data = json.loads(message['data'])
                # Forward to connected clients
                await self._send_to_local_connections(data)
```

**Works for:** Unlimited connections (horizontal scaling)

---

## Migration Strategy: Gradual Rollout

### Option 1: Big Bang (3-5 days downtime)
- Implement everything at once
- Test thoroughly
- Deploy and switch

### Option 2: Gradual (Recommended - Zero downtime)

**Week 1:**
- ✅ Implement WebSocket endpoint
- ✅ Keep polling working
- ✅ Add feature flag: `USE_WEBSOCKET=false`

**Week 2:**
- ✅ Test WebSocket with beta users
- ✅ Fix any issues
- ✅ Enable for 10% of users

**Week 3:**
- ✅ Enable for 50% of users
- ✅ Monitor performance
- ✅ Fix edge cases

**Week 4:**
- ✅ Enable for 100% of users
- ✅ Keep polling as fallback
- ✅ Remove old polling code (optional)

---

## Expected Benefits After Migration

### Performance
- **Latency:** 5000ms → **<100ms** (50x faster)
- **Bandwidth:** 18KB/min → **~1KB/min** (95% reduction)
- **Server Load:** 12 queries/poll → **1 query/change** (90% reduction)

### User Experience
- ⚡ **Instant combat updates** (no 5s delay)
- 🗺️ **Live player locations** on map
- 💬 **Real-time chat** capability
- 🎮 **Better PvP** feel (see actions immediately)
- 📢 **Server announcements** (events, maintenance)

### New Features Enabled
- 👥 Party/group system
- 💬 In-game chat
- 🗺️ Live world map with player positions
- 📊 Real-time leaderboards
- 🎪 Live events/raids
- 🎁 Random spawns/drops broadcast

---

## My Recommendation

### For Your Game: **Implement WebSockets Now** 🚀

**Why:**
1. **You're already planning Steam release** - WebSocket quality expected
2. **PvP combat exists** - Real-time feel makes huge difference
3. **FastAPI has excellent WebSocket support** - Not that hard
4. **Your codebase is clean** - Easy to refactor
5. **Growing player base** - Better to do it now than later

**Timeline:**
- Day 1-2: Backend WebSocket setup
- Day 3-4: Frontend integration
- Day 5: Testing & polish
- Week 2: Gradual rollout

**Risk:** Low - Keep polling as fallback

---

## Code Complexity Comparison

### Polling (Current)
```typescript
// Client - 20 lines
useEffect(() => {
  const interval = setInterval(fetchGameData, 5000)
  return () => clearInterval(interval)
}, [])
```

```python
# Server - 5 lines
@app.get("/api/game/state")
async def get_state(user):
    return await db.get_player_state(user.id)
```

**Total:** ~25 lines, very simple

### WebSocket (After migration)
```typescript
// Client - 80 lines (hook + handler)
const useGameWebSocket = (token, onMessage) => {
  // Connection management
  // Reconnection logic
  // Message handling
  // Heartbeat
}
```

```python
# Server - 150 lines
class ConnectionManager:
    # Connection tracking
    # Broadcasting
    # Message routing

@app.websocket("/ws/game/{token}")
async def websocket_endpoint():
    # Auth
    # Connection handling
    # Message processing
```

**Total:** ~230 lines, moderate complexity

**Verdict:** About 10x more code, but FastAPI does heavy lifting. Complexity is **manageable**.

---

## My Assessment

**Difficulty:** 6/10 for me to implement
- I know the codebase well
- FastAPI WebSocket support is great
- Your architecture is clean

**Would take me:** 3-4 days to implement fully with testing

**Worth it?** **Absolutely YES** 💯
- Long-term better performance
- Better UX
- Industry standard for real-time games
- Enables future features
- Required for serious Steam release

Want me to start implementing it? I can do it in phases with zero downtime!
