571 lines
13 KiB
Markdown
571 lines
13 KiB
Markdown
|
|
# 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](#game-state-json-structure)
|
||
|
|
2. [Card Format Codes](#card-format-codes)
|
||
|
|
3. [API Endpoints](#api-endpoints)
|
||
|
|
4. [Integration Flow](#integration-flow)
|
||
|
|
5. [Example Bot Decision Logic](#example-bot-decision-logic)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Game State JSON Structure
|
||
|
|
|
||
|
|
The game exports a comprehensive state object with the following structure:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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:**
|
||
|
|
```bash
|
||
|
|
curl http://localhost:5000/api/game/ABC123/state
|
||
|
|
```
|
||
|
|
|
||
|
|
**Response:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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):**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "play_card",
|
||
|
|
"cardCode": "5R",
|
||
|
|
"chosenColor": null
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Request Body (Play Wild Card):**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "play_card",
|
||
|
|
"cardCode": "W",
|
||
|
|
"chosenColor": "R"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Request Body (Draw Card):**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "draw_card"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example:**
|
||
|
|
```bash
|
||
|
|
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:**
|
||
|
|
```javascript
|
||
|
|
const socket = io.connect('http://localhost:5000')
|
||
|
|
socket.emit('join', {room: 'ABC123'}, (error) => {
|
||
|
|
if (error) console.error(error)
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Listen for game state updates:**
|
||
|
|
```javascript
|
||
|
|
socket.on('updateGameState', (gameState) => {
|
||
|
|
// Process game state and make decision
|
||
|
|
console.log('Turn:', gameState.turn)
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Request game state:**
|
||
|
|
```javascript
|
||
|
|
socket.emit('requestGameState', (response) => {
|
||
|
|
console.log(response)
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Submit action:**
|
||
|
|
```javascript
|
||
|
|
socket.emit('botAction', {
|
||
|
|
action: 'play_card',
|
||
|
|
cardCode: '5R'
|
||
|
|
}, (response) => {
|
||
|
|
console.log(response)
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### For HTTP API Integration (Recommended for Miku Bot)
|
||
|
|
|
||
|
|
1. **Get Room Code:** User shares the game room code
|
||
|
|
|
||
|
|
2. **Poll Game State:**
|
||
|
|
```python
|
||
|
|
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:**
|
||
|
|
```python
|
||
|
|
if game_state['game']['currentTurn'] == 'Player 2':
|
||
|
|
# Bot's turn - make decision
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Analyze playable cards:**
|
||
|
|
```python
|
||
|
|
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:**
|
||
|
|
```python
|
||
|
|
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:
|
||
|
|
|
||
|
|
```python
|
||
|
|
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:
|
||
|
|
|
||
|
|
```python
|
||
|
|
# 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
|
||
|
|
```bash
|
||
|
|
cd /home/koko210Serve/docker/uno-online
|
||
|
|
npm install
|
||
|
|
npm start
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Start the Client (in another terminal)
|
||
|
|
```bash
|
||
|
|
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
|
||
|
|
```bash
|
||
|
|
# 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.
|