Files
miku-discord/uno-online/BOT_INTEGRATION_GUIDE.md
koko210Serve 34b184a05a add: absorb uno-online as regular subdirectory
UNO card game web app (Node.js/React) with Miku bot integration.
Previously an independent git repo (fork of mizanxali/uno-online).
Removed .git/ and absorbed into main repo for unified tracking.

Includes bot integration code: botActionExecutor, cardParser,
gameStateBuilder, and server-side bot action support.
37 files, node_modules excluded via local .gitignore.
2026-03-04 00:21:38 +02:00

13 KiB

UNO Game - Bot Integration Guide

Overview

This UNO online game has been enhanced with a comprehensive bot integration system that exports the game state in JSON format at every turn. This allows external AI systems (like Miku Discord Bot) to play as Player 2.

Table of Contents

  1. Game State JSON Structure
  2. Card Format Codes
  3. API Endpoints
  4. Integration Flow
  5. Example Bot Decision Logic

Game State JSON Structure

The game exports a comprehensive state object with the following structure:

{
  "game": {
    "isOver": false,
    "winner": null,
    "currentTurn": "Player 2",
    "turnNumber": 15
  },
  "currentCard": {
    "code": "5R",
    "type": "number",
    "value": 5,
    "color": "R",
    "colorName": "red",
    "displayName": "5 red",
    "currentColor": "R",
    "currentNumber": 5
  },
  "recentlyPlayed": [
    {
      "code": "5R",
      "type": "number",
      "value": 5,
      "color": "R",
      "colorName": "red",
      "displayName": "5 red",
      "position": 1
    }
  ],
  "player1": {
    "name": "Player 1",
    "cardCount": 7,
    "isCurrentTurn": false,
    "cards": []
  },
  "player2": {
    "name": "Player 2",
    "cardCount": 5,
    "isCurrentTurn": true,
    "cards": [
      {
        "code": "3R",
        "type": "number",
        "value": 3,
        "color": "R",
        "colorName": "red",
        "displayName": "3 red",
        "isPlayable": true
      },
      {
        "code": "7B",
        "type": "number",
        "value": 7,
        "color": "B",
        "colorName": "blue",
        "displayName": "7 blue",
        "isPlayable": false
      }
    ],
    "playableCards": [
      {
        "code": "3R",
        "type": "number",
        "value": 3,
        "color": "R",
        "colorName": "red",
        "displayName": "3 red",
        "isPlayable": true
      }
    ]
  },
  "deck": {
    "drawPileCount": 78,
    "playedPileCount": 15
  },
  "botContext": {
    "canPlay": true,
    "mustDraw": false,
    "hasUno": false,
    "isWinning": false,
    "actions": [
      {
        "action": "play_card",
        "card": {
          "code": "3R",
          "type": "number",
          "value": 3,
          "color": "R",
          "displayName": "3 red"
        },
        "requiresColorChoice": false
      }
    ]
  }
}

Key Fields Explanation

game object

  • isOver: Boolean indicating if the game has ended
  • winner: Player name who won, or null if game is ongoing
  • currentTurn: "Player 1" or "Player 2" - whose turn it is
  • turnNumber: Approximate turn count (based on played cards)

currentCard object

  • Complete information about the card currently on top of the pile
  • currentColor and currentNumber are the active game rules

player2.cards array

  • All cards in the bot's hand with full details
  • Each card has isPlayable flag based on current game rules

player2.playableCards array

  • Filtered list of only cards that can be played right now

botContext object

  • canPlay: Can the bot play a card this turn?
  • mustDraw: Must the bot draw a card?
  • hasUno: Should the bot press the UNO button? (2 cards remaining)
  • isWinning: Does the bot have only 1 card left?
  • actions: Array of valid actions the bot can take

Card Format Codes

Cards are represented as 2-4 character codes:

Number Cards (0-9)

  • Format: [number][color]
  • Examples: 0R, 5G, 9B, 3Y
  • Colors: R (Red), G (Green), B (Blue), Y (Yellow)

Special Cards

Skip

  • Format: skip[color]
  • Examples: skipR, skipG, skipB, skipY
  • Value code: 404

Reverse

  • Format: _[color]
  • Examples: _R, _G, _B, _Y
  • Value code: 0

Draw 2

  • Format: D2[color]
  • Examples: D2R, D2G, D2B, D2Y
  • Value code: 252

Wild

  • Format: W
  • Value code: 300
  • No color until played

Draw 4 Wild

  • Format: D4W
  • Value code: 600
  • No color until played

Card Types

Each parsed card has a type field:

  • number: Regular number cards (0-9)
  • skip: Skip next player's turn
  • reverse: Reverse play direction (in 2-player, acts as skip)
  • draw2: Next player draws 2 cards
  • wild: Change color
  • draw4_wild: Change color and next player draws 4

API Endpoints

1. Get Game State (HTTP)

Endpoint: GET /api/game/:roomCode/state

Description: Retrieve the current game state for a specific room.

Example:

curl http://localhost:5000/api/game/ABC123/state

Response:

{
  "success": true,
  "gameState": { /* full game state object */ },
  "timestamp": "2026-01-25T10:30:00.000Z"
}

2. Submit Bot Action (HTTP)

Endpoint: POST /api/game/:roomCode/action

Description: Submit a bot's action (play card or draw card).

Request Body (Play Card):

{
  "action": "play_card",
  "cardCode": "5R",
  "chosenColor": null
}

Request Body (Play Wild Card):

{
  "action": "play_card",
  "cardCode": "W",
  "chosenColor": "R"
}

Request Body (Draw Card):

{
  "action": "draw_card"
}

Example:

curl -X POST http://localhost:5000/api/game/ABC123/action \
  -H "Content-Type: application/json" \
  -d '{"action":"play_card","cardCode":"5R","chosenColor":null}'

3. Socket.IO Events

The game also supports real-time Socket.IO events:

Client → Server Events

  • botGameState: Emitted by Player 2, contains full game state
  • botAction: Bot submits an action
  • requestGameState: Bot requests current state

Server → Client Events

  • botActionReceived: Action received from HTTP API, forwarded to game
  • gameStateRequested: Server requests updated state from game

Integration Flow

For Socket.IO Integration

  1. Connect as Player 2:

    const socket = io.connect('http://localhost:5000')
    socket.emit('join', {room: 'ABC123'}, (error) => {
      if (error) console.error(error)
    })
    
  2. Listen for game state updates:

    socket.on('updateGameState', (gameState) => {
      // Process game state and make decision
      console.log('Turn:', gameState.turn)
    })
    
  3. Request game state:

    socket.emit('requestGameState', (response) => {
      console.log(response)
    })
    
  4. Submit action:

    socket.emit('botAction', {
      action: 'play_card',
      cardCode: '5R'
    }, (response) => {
      console.log(response)
    })
    
  1. Get Room Code: User shares the game room code

  2. Poll Game State:

    import requests
    
    room_code = "ABC123"
    response = requests.get(f"http://localhost:5000/api/game/{room_code}/state")
    game_state = response.json()['gameState']
    
  3. Check if it's bot's turn:

    if game_state['game']['currentTurn'] == 'Player 2':
        # Bot's turn - make decision
    
  4. Analyze playable cards:

    playable = game_state['player2']['playableCards']
    if len(playable) > 0:
        # Choose a card to play
        chosen_card = playable[0]['code']
    else:
        # Must draw
        action = {'action': 'draw_card'}
    
  5. Submit action:

    action = {
        'action': 'play_card',
        'cardCode': chosen_card,
        'chosenColor': 'R' if 'wild' in playable[0]['type'] else None
    }
    
    response = requests.post(
        f"http://localhost:5000/api/game/{room_code}/action",
        json=action
    )
    

Example Bot Decision Logic

Here's a simple decision-making algorithm for a bot:

def make_uno_decision(game_state):
    """
    Simple bot logic for playing UNO
    """
    bot_context = game_state['botContext']
    
    # Not our turn
    if not game_state['player2']['isCurrentTurn']:
        return None
    
    # Check if we must draw
    if bot_context['mustDraw']:
        return {
            'action': 'draw_card'
        }
    
    # Get playable cards
    playable = game_state['player2']['playableCards']
    
    if len(playable) == 0:
        return {
            'action': 'draw_card'
        }
    
    # Strategy: prioritize special cards, then high numbers
    chosen_card = None
    
    # 1. Try to play Draw 4 or Draw 2
    for card in playable:
        if card['type'] in ['draw4_wild', 'draw2']:
            chosen_card = card
            break
    
    # 2. Try to play Skip
    if not chosen_card:
        for card in playable:
            if card['type'] == 'skip':
                chosen_card = card
                break
    
    # 3. Play highest number card
    if not chosen_card:
        number_cards = [c for c in playable if c['type'] == 'number']
        if number_cards:
            chosen_card = max(number_cards, key=lambda x: x['value'])
    
    # 4. Play wild card as last resort
    if not chosen_card:
        chosen_card = playable[0]
    
    # Determine color for wild cards
    chosen_color = None
    if chosen_card['type'] in ['wild', 'draw4_wild']:
        # Count colors in hand
        colors = {}
        for card in game_state['player2']['cards']:
            if card.get('color'):
                colors[card['color']] = colors.get(card['color'], 0) + 1
        # Choose most common color
        chosen_color = max(colors, key=colors.get) if colors else 'R'
    
    return {
        'action': 'play_card',
        'cardCode': chosen_card['code'],
        'chosenColor': chosen_color
    }

Miku Bot Integration Example

For integrating with the Miku Discord bot:

# In Miku bot's commands folder, create uno_player.py

import requests
import time

class MikuUnoPlayer:
    def __init__(self, game_server_url, room_code):
        self.base_url = game_server_url
        self.room_code = room_code
    
    def get_game_state(self):
        """Fetch current game state"""
        url = f"{self.base_url}/api/game/{self.room_code}/state"
        response = requests.get(url)
        return response.json()['gameState']
    
    def play_turn(self):
        """Play one turn as Miku"""
        state = self.get_game_state()
        
        # Check if it's our turn
        if state['game']['currentTurn'] != 'Player 2':
            return "Not my turn yet!"
        
        # Use LLM to decide (pass game state to Miku's LLM)
        decision = self.make_llm_decision(state)
        
        # Submit action
        url = f"{self.base_url}/api/game/{self.room_code}/action"
        response = requests.post(url, json=decision)
        
        return response.json()
    
    def make_llm_decision(self, game_state):
        """
        Pass game state to Miku's LLM for decision-making
        """
        # Format prompt for LLM
        prompt = f"""
You are playing UNO. Here's the current game state:

Current card: {game_state['currentCard']['displayName']}
Your cards ({len(game_state['player2']['cards'])}):
{', '.join([c['displayName'] for c in game_state['player2']['cards']])}

Playable cards:
{', '.join([c['displayName'] for c in game_state['player2']['playableCards']])}

What should you do? Respond with JSON:
{{"action": "play_card", "cardCode": "5R"}}
or
{{"action": "draw_card"}}
"""
        
        # Query Miku's LLM
        from utils.llm import query_llama
        response = query_llama(prompt, model="llama3.1")
        
        # Parse LLM response to extract decision
        # ... parse JSON from response ...
        
        return decision

Testing the Integration

1. Start the Game Server

cd /home/koko210Serve/docker/uno-online
npm install
npm start

2. Start the Client (in another terminal)

cd /home/koko210Serve/docker/uno-online/client
npm install
npm start

3. Create a Game

  • Open browser to http://localhost:3000
  • Click "CREATE GAME"
  • Note the room code (e.g., "ABC123")

4. Test API Endpoints

# Get game state
curl http://localhost:5000/api/game/ABC123/state

# Submit action (after joining as Player 2)
curl -X POST http://localhost:5000/api/game/ABC123/action \
  -H "Content-Type: application/json" \
  -d '{"action":"draw_card"}'

5. Monitor Console Output

  • Open browser console (F12)
  • Look for 🎮 UNO GAME STATE logs
  • Copy JSON for testing with bot

Next Steps for Miku Integration

  1. Create UNO command in Miku bot (/uno join [room_code])
  2. Implement polling loop to check when it's Miku's turn
  3. Pass game state to LLM with structured prompt
  4. Parse LLM response to extract action decision
  5. Submit action via API
  6. Add chat integration to announce moves in Discord
  7. Handle edge cases (disconnects, game over, etc.)

Troubleshooting

Game State Not Appearing

  • Ensure you've joined as Player 2
  • Check browser console for logs
  • Verify Socket.IO connection

API Returns 404

  • Check that the room code is correct
  • Ensure the game has started (both players joined)
  • Verify game state has been emitted at least once

Actions Not Working

  • Ensure the action JSON format is correct
  • Check that it's Player 2's turn
  • Verify the card code exists in the bot's hand

License

This integration system is part of the UNO Online project. See main README for license information.