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.
This commit is contained in:
570
uno-online/BOT_INTEGRATION_GUIDE.md
Normal file
570
uno-online/BOT_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,570 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user