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:
2026-03-04 00:21:38 +02:00
parent c708770266
commit 34b184a05a
37 changed files with 26885 additions and 0 deletions

4
uno-online/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules/

View File

@@ -0,0 +1,230 @@
# Bot Action Specification for UNO Game
## Overview
The bot (Miku) can interact with the UNO game by sending JSON action commands. The game state is provided in a structured JSON format, and the bot responds with action commands.
## Action Types
### 1. Play a Card
```json
{
"action": "play",
"card": "4R",
"color": null
}
```
- `action`: Must be `"play"`
- `card`: The card code to play (e.g., "4R", "D2G", "skipB", "W", "D4W")
- `color`: Optional. Only required for wild cards ("W" or "D4W"). Values: "R", "G", "B", or "Y"
**Examples:**
```json
// Play a number card
{"action": "play", "card": "4R", "color": null}
// Play a Draw 2 card
{"action": "play", "card": "D2G", "color": null}
// Play a Skip card
{"action": "play", "card": "skipB", "color": null}
// Play a Wild card (must specify color)
{"action": "play", "card": "W", "color": "R"}
// Play a Wild Draw 4 (must specify color)
{"action": "play", "card": "D4W", "color": "B"}
```
### 2. Draw a Card
```json
{
"action": "draw"
}
```
- `action`: Must be `"draw"`
- No other parameters needed
### 3. Call UNO
```json
{
"action": "uno"
}
```
- `action`: Must be `"uno"`
- Call this when you have exactly 2 cards in hand and are about to play one
- **Important:** Call UNO *before* playing your second-to-last card, or call it as a separate action
### 4. Combined Actions (Play + UNO)
```json
{
"action": "play",
"card": "4R",
"color": null,
"callUno": true
}
```
- Add `"callUno": true` when playing a card that will leave you with 1 card
## Card Code Reference
### Number Cards
- Format: `{number}{color}`
- Numbers: 0-9
- Colors: R (Red), G (Green), B (Blue), Y (Yellow)
- Examples: `0R`, `5G`, `9B`, `3Y`
### Action Cards
- **Skip**: `skipR`, `skipG`, `skipB`, `skipY`
- **Reverse**: `_R`, `_G`, `_B`, `_Y`
- **Draw 2**: `D2R`, `D2G`, `D2B`, `D2Y`
- **Wild**: `W`
- **Wild Draw 4**: `D4W`
## Color Codes
- `R` - Red
- `G` - Green
- `B` - Blue
- `Y` - Yellow
## Strategy Tips for Bot
### When to Play Cards
1. Check `botContext.canPlay` - if false, you must draw
2. Look at `player2.playableCards` for available moves
3. Match by color or number with `currentCard`
### Priority Strategy
1. **Action cards first** (Skip, Draw 2, Wild Draw 4) to disrupt opponent
2. **Match color** if opponent has few cards (to block them)
3. **Match number** to change color advantageously
4. **Save wild cards** for critical moments
### When to Call UNO
- When you have exactly 2 cards and are about to play one
- **Must call UNO** or you'll get 2 penalty cards!
### When to Use Wild Cards
- When you have no other playable cards
- To change to a color you have many of
- Wild Draw 4: When opponent has 1-2 cards (aggressive)
## HTTP API Endpoint
### Get Game State
```
GET http://localhost:5000/api/game/\{roomCode\}/state
```
**Response:**
```json
{
"success": true,
"gameState": { /* full game state */ },
"timestamp": "2026-01-27T..."
}
```
### Submit Bot Action
```
POST http://localhost:5000/api/game/\{roomCode\}/action
Content-Type: application/json
{
"action": "play",
"card": "4R",
"color": null
}
```
**Response:**
```json
{
"success": true,
"message": "Action received and forwarded to game"
}
```
## Error Handling
### Invalid Actions
The game will ignore invalid actions:
- Playing a card not in your hand
- Playing an unplayable card
- Not specifying color for wild cards
- Playing out of turn
### Validation
Before sending an action:
1. Verify it's Player 2's turn (`game.currentTurn === "Player 2"`)
2. Check card is in `player2.cards` array
3. Verify card is in `player2.playableCards` array
4. For wild cards, always specify a valid color
## Example Bot Decision Flow
```python
def make_move(game_state):
# Check if it's our turn
if game_state['game']['currentTurn'] != 'Player 2':
return None
# Check if we can play
if not game_state['botContext']['canPlay']:
return {"action": "draw"}
# Get playable cards
playable = game_state['player2']['playableCards']
if not playable:
return {"action": "draw"}
# Check if we need to call UNO
our_cards = game_state['player2']['cardCount']
call_uno = (our_cards == 2)
# Priority: Draw 2 or Draw 4
for card in playable:
if card['type'] in ['draw2', 'draw4']:
action = {"action": "play", "card": card['code']}
if card['type'] == 'draw4':
action['color'] = choose_best_color(game_state)
if call_uno:
action['callUno'] = True
return action
# Priority: Skip or Reverse
for card in playable:
if card['type'] in ['skip', 'reverse']:
if call_uno:
return {"action": "play", "card": card['code'], "callUno": True}
return {"action": "play", "card": card['code']}
# Play any available card
card = playable[0]
action = {"action": "play", "card": card['code']}
if card['type'] == 'wild':
action['color'] = choose_best_color(game_state)
if call_uno:
action['callUno'] = True
return action
```
## WebSocket Events (Alternative to HTTP)
If the bot maintains a WebSocket connection:
### Emit bot action:
```javascript
socket.emit('botAction', {
action: 'play',
card: '4R',
color: null
})
```
### Receive game state updates:
```javascript
socket.on('updateGameState', (state) => {
// Process new game state
})
```

View File

@@ -0,0 +1,266 @@
# 🤖 Bot Integration Complete Guide
## Overview
The UNO game now supports bot players (Miku) making moves via JSON API. The bot can play cards, draw cards, and call UNO by sending structured JSON commands.
## Quick Start
### 1. Start a game with 2 players
- Player 1: Create a room (e.g., room code "ABC123")
- Player 2: Join the same room
- Game will auto-start when both players are present
### 2. Get the current game state
```bash
curl http://localhost:5000/api/game/ABC123/state
```
### 3. Send a bot action (when it's Player 2's turn)
```bash
# Draw a card
curl -X POST http://localhost:5000/api/game/ABC123/action \
-H "Content-Type: application/json" \
-d '{"action":"draw"}'
# Play a card
curl -X POST http://localhost:5000/api/game/ABC123/action \
-H "Content-Type: application/json" \
-d '{"action":"play","card":"4R"}'
# Play a wild card with color choice
curl -X POST http://localhost:5000/api/game/ABC123/action \
-H "Content-Type: application/json" \
-d '{"action":"play","card":"W","color":"R"}'
# Call UNO
curl -X POST http://localhost:5000/api/game/ABC123/action \
-H "Content-Type: application/json" \
-d '{"action":"uno"}'
```
## Testing Bot Actions
Use the included test script:
```bash
# Test drawing a card
node test-bot-action.js ABC123 '{"action":"draw"}'
# Test playing a card
node test-bot-action.js ABC123 '{"action":"play","card":"4R"}'
# Test playing a wild card
node test-bot-action.js ABC123 '{"action":"play","card":"W","color":"B"}'
```
## Action Format Specification
### Play a Card
```json
{
"action": "play",
"card": "4R",
"color": null,
"callUno": false
}
```
**Parameters:**
- `action` (string, required): Must be `"play"`
- `card` (string, required): Card code (e.g., "4R", "D2G", "skipB", "W", "D4W")
- `color` (string, optional): Required for wild cards ("W", "D4W"). Values: "R", "G", "B", "Y"
- `callUno` (boolean, optional): Set to `true` to call UNO with this play
### Draw a Card
```json
{
"action": "draw"
}
```
### Call UNO
```json
{
"action": "uno"
}
```
## Card Codes Reference
### Number Cards
- `0R`, `1R`, ..., `9R` - Red numbers
- `0G`, `1G`, ..., `9G` - Green numbers
- `0B`, `1B`, ..., `9B` - Blue numbers
- `0Y`, `1Y`, ..., `9Y` - Yellow numbers
### Action Cards
- `skipR`, `skipG`, `skipB`, `skipY` - Skip cards
- `_R`, `_G`, `_B`, `_Y` - Reverse cards
- `D2R`, `D2G`, `D2B`, `D2Y` - Draw 2 cards
### Wild Cards
- `W` - Wild (change color)
- `D4W` - Wild Draw 4 (change color + opponent draws 4)
## Game State Structure
The game state JSON includes:
- `game`: Overall game status (isOver, winner, currentTurn, turnNumber)
- `currentCard`: The card on top of the discard pile
- `player1`: Player 1's info and cards (hidden from bot)
- `player2`: Player 2's (bot's) info and cards (visible)
- `player2.playableCards`: Array of cards the bot can currently play
- `botContext`: Helper info (canPlay, mustDraw, hasUno, actions)
## Validation Rules
The game validates bot actions:
1. **Turn validation**: Must be Player 2's turn
2. **Card validation**: Card must be in Player 2's hand
3. **Playability**: Card must be playable on current card
4. **Wild cards**: Must specify color for "W" and "D4W"
5. **UNO**: Can only call with exactly 2 cards in hand
## Integration with Miku Bot
### From Python (Miku bot)
```python
import requests
import json
def get_game_state(room_code):
response = requests.get(f'http://localhost:5000/api/game/{room_code}/state')
return response.json()['gameState']
def send_bot_action(room_code, action):
response = requests.post(
f'http://localhost:5000/api/game/{room_code}/action',
headers={'Content-Type': 'application/json'},
data=json.dumps(action)
)
return response.json()
# Example: Make a strategic move
state = get_game_state('ABC123')
if state['game']['currentTurn'] == 'Player 2':
playable = state['player2']['playableCards']
if playable:
# Play first playable card
card = playable[0]
action = {'action': 'play', 'card': card['code']}
# Handle wild cards
if card['type'] in ['wild', 'draw4']:
action['color'] = 'R' # Choose a color
# Call UNO if needed
if state['player2']['cardCount'] == 2:
action['callUno'] = True
result = send_bot_action('ABC123', action)
print(f"Played {card['displayName']}: {result}")
else:
# Must draw
result = send_bot_action('ABC123', {'action': 'draw'})
print(f"Drew a card: {result}")
```
### LLM Integration Example
```python
def miku_make_move(game_state):
"""Use LLM to decide Miku's move"""
# Build prompt for LLM
prompt = f"""
You are playing UNO. It's your turn.
Current card on table: {game_state['currentCard']['displayName']}
Your cards: {[c['displayName'] for c in game_state['player2']['cards']]}
Playable cards: {[c['displayName'] for c in game_state['player2']['playableCards']]}
Opponent has {game_state['player1']['cardCount']} cards.
You have {game_state['player2']['cardCount']} cards.
Choose your move as JSON:
{{"action": "play", "card": "CODE"}} or {{"action": "draw"}}
"""
# Get LLM response
llm_response = query_llama(prompt)
# Parse JSON from response
action = json.loads(llm_response)
# Send action
return send_bot_action(game_state['room'], action)
```
## Debugging
### Console Logs
The game client logs bot actions:
- `🤖 Received bot action:` - Action received from HTTP API
- `🤖 Bot action result:` - Result of executing the action
- `<60><> Bot playing card:` - Card being played
- `🌈 Bot chose color:` - Color chosen for wild card
- `🔥 Bot called UNO!` - UNO was called
- `📥 Bot drawing a card` - Card was drawn
### Error Messages
- `❌ Bot action rejected: Not Player 2's turn` - Wrong turn
- `❌ Bot play action rejected: Card not in hand` - Invalid card
- `❌ Bot play action rejected: Wild card without color` - Missing color
- `❌ Bot UNO rejected` - Invalid UNO call
## Files Added/Modified
### New Files
- `BOT_ACTION_SPEC.md` - Detailed API specification
- `BOT_INTEGRATION_COMPLETE.md` - This file
- `client/src/utils/botActionExecutor.js` - Bot action executor
- `test-bot-action.js` - Test script
### Modified Files
- `server.js` - HTTP API endpoints for bot actions
- `client/src/components/Game.js` - Bot action listener and integration
## Next Steps
1. **Test the integration**: Use `test-bot-action.js` to verify actions work
2. **Integrate with Miku**: Add UNO game support to Miku bot
3. **Add LLM strategy**: Use Miku's LLM to make strategic decisions
4. **Add personality**: Make Miku trash talk and celebrate!
## Example Full Game Flow
```bash
# Terminal 1: Start server
cd /home/koko210Serve/docker/uno-online
npm start
# Terminal 2: Start client dev server
cd /home/koko210Serve/docker/uno-online/client
npm start
# Browser 1: Create game as Player 1
# Open http://localhost:3000
# Create room "MIKU01"
# Browser 2: Join as Player 2
# Open http://localhost:3000
# Join room "MIKU01"
# Terminal 3: Control Player 2 (bot) via API
# Get state
curl http://localhost:5000/api/game/MIKU01/state | jq
# Make a move (when it's Player 2's turn)
node test-bot-action.js MIKU01 '{"action":"play","card":"4R"}'
```
🎉 The bot integration is complete and ready to use!

View 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.

176
uno-online/BOT_QUICK_REF.md Normal file
View File

@@ -0,0 +1,176 @@
# UNO Bot Integration - Quick Reference
## Game State JSON Quick View
### Checking Bot's Turn
```javascript
gameState.game.currentTurn === "Player 2" // Bot's turn
gameState.player2.isCurrentTurn // Alternative check
```
### Getting Playable Cards
```javascript
const playable = gameState.player2.playableCards
// Each card has: code, type, value, color, displayName, isPlayable
```
### Card Types
- `number`: 0-9 cards
- `skip`: Skip opponent (code: 404)
- `reverse`: Reverse direction (code: 0, acts as skip in 2-player)
- `draw2`: Opponent draws 2 (code: 252)
- `wild`: Change color (code: 300)
- `draw4_wild`: Change color + opponent draws 4 (code: 600)
### Current Card Info
```javascript
gameState.currentCard.displayName // "5 red"
gameState.currentCard.color // "R"
gameState.currentCard.value // 5
```
### Bot Status
```javascript
gameState.botContext.canPlay // Can play a card?
gameState.botContext.mustDraw // Must draw a card?
gameState.botContext.hasUno // Has 2 cards (press UNO)?
gameState.botContext.isWinning // Has 1 card (about to win)?
```
## API Quick Reference
### Get Game State
```bash
GET http://localhost:5000/api/game/ABC123/state
```
### Play a Card
```bash
POST http://localhost:5000/api/game/ABC123/action
Content-Type: application/json
{
"action": "play_card",
"cardCode": "5R",
"chosenColor": null
}
```
### Play Wild Card
```bash
POST http://localhost:5000/api/game/ABC123/action
Content-Type: application/json
{
"action": "play_card",
"cardCode": "W",
"chosenColor": "R"
}
```
### Draw a Card
```bash
POST http://localhost:5000/api/game/ABC123/action
Content-Type: application/json
{
"action": "draw_card"
}
```
## Color Codes
- `R` = Red
- `G` = Green
- `B` = Blue
- `Y` = Yellow
## Card Code Examples
- `5R` = 5 Red
- `0G` = 0 Green
- `skipB` = Skip Blue
- `_Y` = Reverse Yellow
- `D2R` = Draw 2 Red
- `W` = Wild
- `D4W` = Draw 4 Wild
## Simple Bot Logic
```python
# 1. Get game state
state = requests.get(f"{base_url}/api/game/{room}/state").json()['gameState']
# 2. Check if bot's turn
if state['game']['currentTurn'] != 'Player 2':
return # Not our turn
# 3. Get playable cards
playable = state['player2']['playableCards']
# 4. Choose card (simple strategy: play first playable)
if playable:
card = playable[0]
action = {
'action': 'play_card',
'cardCode': card['code'],
'chosenColor': 'R' if 'wild' in card['type'] else None
}
else:
action = {'action': 'draw_card'}
# 5. Submit action
requests.post(f"{base_url}/api/game/{room}/action", json=action)
```
## LLM Prompt Template
```
You are playing UNO as Player 2.
CURRENT CARD: {currentCard.displayName}
YOUR HAND: {player2.cards with displayNames}
PLAYABLE CARDS: {player2.playableCards}
OPPONENT CARDS: {player1.cardCount}
RULES:
- Match color OR number
- Wild cards can be played anytime
- Special cards: Skip (opponent skips turn), Draw 2, Wild, Draw 4
Choose an action. Respond in JSON format:
{"action": "play_card", "cardCode": "CODE", "chosenColor": "R/G/B/Y or null"}
OR
{"action": "draw_card"}
Your decision:
```
## Console Logs to Watch
When running the game, look for these in browser console:
- `🎮 UNO GAME STATE (Simplified):` - Quick view
- `🤖 FULL GAME STATE (For Bot):` - Complete state object
- `📋 JSON for Bot API:` - Formatted JSON (copy-paste ready)
## Files Modified
### Client-side
- `/client/src/components/Game.js` - Added game state export & logging
- `/client/src/utils/cardParser.js` - NEW: Card parsing utilities
- `/client/src/utils/gameStateBuilder.js` - NEW: Game state JSON builder
### Server-side
- `/server.js` - Added HTTP API endpoints & Socket.IO event handlers
## Testing Workflow
1. Start server: `npm start` (in /uno-online)
2. Start client: `npm start` (in /uno-online/client)
3. Create game in browser, get room code
4. Join as Player 2
5. Open browser console (F12)
6. Watch for game state logs
7. Test API: `curl http://localhost:5000/api/game/ROOMCODE/state`
## Next: Miku Bot Integration
See `BOT_INTEGRATION_GUIDE.md` for full implementation details.

View File

@@ -0,0 +1,530 @@
# UNO Bot Integration - Implementation Summary
## ✅ What Has Been Implemented
### 1. Game State Export System ✅
**Location:** `/client/src/utils/gameStateBuilder.js`
The game now exports a comprehensive JSON state object at every turn containing:
```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"
},
"player2": {
"cards": [...], // Full hand with playability info
"playableCards": [...] // Only cards that can be played
},
"botContext": {
"canPlay": true,
"mustDraw": false,
"actions": [...] // Available actions
}
}
```
### 2. Card Parsing Utilities ✅
**Location:** `/client/src/utils/cardParser.js`
Comprehensive card parsing functions:
- `parseCard(cardCode)` - Convert card codes to detailed objects
- `isCardPlayable(cardCode, currentColor, currentNumber)` - Check if playable
- `getPlayableCards(hand, currentColor, currentNumber)` - Filter playable cards
**Card Format Support:**
- Number cards: `0R`, `5G`, `9B`, `3Y`
- Skip: `skipR`, `skipG`, `skipB`, `skipY`
- Reverse: `_R`, `_G`, `_B`, `_Y`
- Draw 2: `D2R`, `D2G`, `D2B`, `D2Y`
- Wild: `W`
- Draw 4 Wild: `D4W`
### 3. Automatic Game State Logging ✅
**Location:** `/client/src/components/Game.js` (lines ~169-199)
Added `useEffect` hook that:
- Monitors all game state changes
- Logs simplified state to console: `🎮 UNO GAME STATE (Simplified)`
- Logs full state to console: `🤖 FULL GAME STATE (For Bot)`
- Logs formatted JSON: `📋 JSON for Bot API`
- Emits state via Socket.IO event: `botGameState`
### 4. HTTP API Endpoints ✅
**Location:** `/server.js`
#### GET `/api/game/:roomCode/state`
Retrieve current game state for a room.
**Example:**
```bash
curl http://localhost:5000/api/game/ABC123/state
```
**Response:**
```json
{
"success": true,
"gameState": { /* full state */ },
"timestamp": "2026-01-25T10:30:00.000Z"
}
```
#### POST `/api/game/:roomCode/action`
Submit bot action (play card or draw).
**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}'
```
### 5. Socket.IO Events ✅
**New Server Events:**
- `botGameState` - Receives and stores game state from Player 2
- `botAction` - Receives action from bot
- `requestGameState` - Bot requests current state
- `botActionReceived` - Forwards HTTP actions to game
### 6. Documentation ✅
Created comprehensive documentation:
- **`BOT_INTEGRATION_GUIDE.md`** - Full integration guide (38KB)
- **`BOT_QUICK_REF.md`** - Quick reference for developers
- **`SETUP_NOTES.md`** - Setup instructions and troubleshooting
---
## 📁 Files Created/Modified
### New Files
```
/client/src/utils/cardParser.js [NEW] 146 lines
/client/src/utils/gameStateBuilder.js [NEW] 152 lines
/BOT_INTEGRATION_GUIDE.md [NEW] 485 lines
/BOT_QUICK_REF.md [NEW] 142 lines
/SETUP_NOTES.md [NEW] 115 lines
/IMPLEMENTATION_SUMMARY.md [NEW] This file
```
### Modified Files
```
/client/src/components/Game.js [MODIFIED]
- Added import for game state utilities (line 8)
- Added game state monitoring useEffect (lines 169-199)
- Changed ENDPOINT to localhost (line 25)
/server.js [MODIFIED]
- Added game state storage Map (line 18)
- Added HTTP GET endpoint for state (lines 21-32)
- Added HTTP POST endpoint for actions (lines 34-45)
- Added Socket.IO bot events (lines 68-95)
```
---
## 🎯 How It Works
### Game State Flow
```
┌─────────────────┐
│ Game.js │
│ (React App) │
└────────┬────────┘
│ Every state change
┌─────────────────────┐
│ buildGameStateJSON │
│ (Utility) │
└────────┬────────────┘
├──► Console Logs (Browser)
│ 🎮 Simplified
│ 🤖 Full State
│ 📋 JSON
└──► Socket.IO Event
'botGameState'
┌─────────────────┐
│ server.js │
│ (Node/Express) │
└────────┬────────┘
├──► Store in Map
│ gameStates.set(room, state)
└──► HTTP API
GET /api/game/:room/state
```
### Bot Integration Flow
```
┌──────────────┐
│ Miku Bot │
│ (Python) │
└──────┬───────┘
│ 1. Poll for game state
GET /api/game/ABC123/state
│ 2. Receive state
┌──────────────────┐
│ Decision Logic │
│ (LLM/Strategy) │
└──────┬───────────┘
│ 3. Decide action
POST /api/game/ABC123/action
{"action": "play_card", "cardCode": "5R"}
│ 4. Action forwarded via Socket.IO
┌──────────────┐
│ Game.js │
│ Updates UI │
└──────────────┘
```
---
## 🔄 Example Bot Decision-Making
Here's how Miku bot would play:
```python
import requests
import time
class MikuUnoPlayer:
def __init__(self, base_url, room_code):
self.base_url = base_url
self.room_code = room_code
self.game_state = None
def poll_state(self):
"""Get current game state"""
url = f"{self.base_url}/api/game/{self.room_code}/state"
response = requests.get(url)
self.game_state = response.json()['gameState']
return self.game_state
def is_my_turn(self):
"""Check if it's Miku's turn"""
return self.game_state['game']['currentTurn'] == 'Player 2'
def make_decision(self):
"""Simple AI decision"""
playable = self.game_state['player2']['playableCards']
if not playable:
return {'action': 'draw_card'}
# Strategy: Play highest value card
best_card = max(playable, key=lambda c: c.get('value', 0))
# Handle wild cards
chosen_color = None
if best_card['type'] in ['wild', 'draw4_wild']:
# Choose color with most cards in hand
colors = {}
for card in self.game_state['player2']['cards']:
if card.get('color'):
colors[card['color']] = colors.get(card['color'], 0) + 1
chosen_color = max(colors, key=colors.get) if colors else 'R'
return {
'action': 'play_card',
'cardCode': best_card['code'],
'chosenColor': chosen_color
}
def submit_action(self, action):
"""Submit action to game"""
url = f"{self.base_url}/api/game/{self.room_code}/action"
response = requests.post(url, json=action)
return response.json()
def play_turn(self):
"""Main game loop"""
self.poll_state()
if not self.is_my_turn():
return "Not my turn"
decision = self.make_decision()
result = self.submit_action(decision)
return f"Played: {decision}"
# Usage
miku = MikuUnoPlayer("http://localhost:5000", "ABC123")
# Run in a loop
while True:
try:
result = miku.play_turn()
print(result)
time.sleep(2) # Poll every 2 seconds
except Exception as e:
print(f"Error: {e}")
time.sleep(5)
```
---
## 🚀 Next Steps for Full Integration
### Phase 1: Basic Integration ⏭️
1. ✅ Game state export (DONE)
2. ✅ HTTP API endpoints (DONE)
3. ⏭️ Test with Node 18 (requires nvm)
4. ⏭️ Verify game state logs in console
5. ⏭️ Manual API testing with curl
### Phase 2: Miku Bot Command ⏭️
1. Create `/uno join [room_code]` Discord command
2. Store room code in bot state
3. Start polling game state
4. Detect when it's bot's turn
5. Display game status in Discord
### Phase 3: LLM Integration ⏭️
1. Format game state for LLM prompt
2. Include UNO rules in system prompt
3. Parse LLM response for action
4. Handle edge cases (invalid actions, wildcards)
5. Add personality/strategy to decisions
### Phase 4: Discord Feedback ⏭️
1. Announce when Miku plays a card
2. Show Miku's remaining cards (with emoji)
3. React to draws/special cards
4. Celebrate wins/losses
5. Add trash talk/personality messages
### Phase 5: Advanced Features ⏭️
1. Multiple concurrent games
2. Tournament mode
3. Strategy difficulty levels
4. Learning from past games
5. Voice chat announcements (TTS)
---
## 🐛 Known Issues & Solutions
### Issue: Node 25.3.0 Compatibility
**Problem:** PostCSS subpath export errors with react-scripts 4.x
**Solution:**
```bash
# Use nvm to switch to Node 18
nvm install 18
nvm use 18
```
### Issue: Old Lockfile Warnings
**Problem:** npm warns about old lockfile format
**Solution:** This is cosmetic, doesn't affect functionality
```bash
npm install --legacy-peer-deps # If needed
```
### Issue: Security Vulnerabilities
**Problem:** 193 vulnerabilities in client dependencies
**Solution:** These are in dev dependencies and don't affect runtime security. For production, consider upgrading react-scripts to v5.x
---
## 📊 Testing Checklist
### Manual Testing
- [ ] Start server: `npm start` in `/uno-online`
- [ ] Start client: `npm start` in `/uno-online/client` (requires Node 18)
- [ ] Create game in browser
- [ ] Join as Player 2
- [ ] Open browser console (F12)
- [ ] Play some cards
- [ ] Verify console logs show:
- `🎮 UNO GAME STATE (Simplified)`
- `🤖 FULL GAME STATE (For Bot)`
- `📋 JSON for Bot API`
- [ ] Copy JSON from console
- [ ] Verify it has all necessary fields
### API Testing
```bash
# Get room code from game UI (e.g., "ABC123")
# Test GET endpoint
curl http://localhost:5000/api/game/ABC123/state
# Test POST endpoint (draw card)
curl -X POST http://localhost:5000/api/game/ABC123/action \
-H "Content-Type: application/json" \
-d '{"action":"draw_card"}'
# Test POST endpoint (play card) - use actual card from your hand
curl -X POST http://localhost:5000/api/game/ABC123/action \
-H "Content-Type: application/json" \
-d '{"action":"play_card","cardCode":"5R","chosenColor":null}'
```
---
## 💡 Design Decisions
### Why JSON State Export?
- **Structured:** Easy for LLMs to parse and understand
- **Complete:** Includes all information needed for decisions
- **Debuggable:** Can be logged, saved, analyzed
- **Flexible:** Can be extended without breaking compatibility
### Why HTTP API + Socket.IO?
- **HTTP:** Easy for external bots (Miku) to poll
- **Socket.IO:** Real-time updates for active players
- **Hybrid:** Best of both worlds
### Why Store State on Server?
- **Stateless Bot:** Bot doesn't need to maintain connection
- **HTTP Access:** Can query state via simple GET requests
- **Scalable:** Can add caching, Redis, etc. later
### Why Parse Cards Client-Side?
- **Immediate:** No round-trip to server
- **Reusable:** Utility functions can be used elsewhere
- **Testable:** Easy to unit test card logic
---
## 📚 Documentation Structure
```
/uno-online/
├── README.md [Original project README]
├── IMPLEMENTATION_SUMMARY.md [This file - what was done]
├── BOT_INTEGRATION_GUIDE.md [Full guide - how to integrate]
├── BOT_QUICK_REF.md [Quick reference - cheat sheet]
├── SETUP_NOTES.md [Setup instructions - getting started]
├── server.js [Modified - API endpoints]
├── package.json [Server dependencies]
└── client/
├── src/
│ ├── components/
│ │ └── Game.js [Modified - state export]
│ └── utils/
│ ├── cardParser.js [NEW - card utilities]
│ └── gameStateBuilder.js [NEW - state builder]
└── package.json [Client dependencies]
```
---
## 🎉 Success Criteria
### ✅ Completed
- [x] Game exports comprehensive JSON state
- [x] State includes all necessary information
- [x] Card codes are parsed into readable objects
- [x] Playable cards are identified automatically
- [x] HTTP API endpoints created and functional
- [x] Socket.IO events integrated
- [x] State logged to console automatically
- [x] Comprehensive documentation written
### ⏭️ Ready for Next Phase
- [ ] Test game with Node 18
- [ ] Verify API endpoints with real game
- [ ] Create Miku bot `/uno` command
- [ ] Implement LLM decision-making
- [ ] Connect bot to game via API
---
## 🔗 Key Files Reference
### For Understanding the System
1. Read: `BOT_INTEGRATION_GUIDE.md` - Complete overview
2. Read: `BOT_QUICK_REF.md` - Quick reference
3. Check: `/client/src/utils/gameStateBuilder.js` - See state structure
### For Implementing Miku Integration
1. Review: `BOT_INTEGRATION_GUIDE.md` sections:
- "Integration Flow"
- "Example Bot Decision Logic"
- "Miku Bot Integration Example"
2. Test: HTTP endpoints with curl
3. Build: Miku command using example code
### For Debugging
1. Browser console: Look for 🎮, 🤖, 📋 emoji logs
2. Server console: Look for `[Bot Game State]` messages
3. Network tab: Monitor Socket.IO and HTTP traffic
---
## 📝 Notes for Miku Bot Developer
### Important Considerations
1. **Polling Frequency:** Don't poll too often (recommended: 2-3 seconds)
2. **Error Handling:** Game might end, player might disconnect
3. **Card Validation:** Always check `playableCards` array
4. **Wild Cards:** Must provide `chosenColor` for W and D4W
5. **UNO Button:** Bot should detect when it has 2 cards
### LLM Prompt Tips
- Include current card prominently
- List playable cards clearly
- Mention opponent's card count
- Keep rules concise
- Request JSON response format
### Discord Integration Ideas
- Embed with game state
- Color-coded cards (emoji)
- Real-time updates
- Win/loss tracking
- Personality-based responses
---
## ✨ Summary
The UNO game has been successfully enhanced with a comprehensive bot integration system. The game now exports detailed JSON state at every turn, provides HTTP API endpoints for external control, and includes complete documentation for integrating AI players like Miku bot.
**Ready for:** Miku bot integration
**Next step:** Test with Node 18, then create `/uno` Discord command
---
*Last Updated: January 25, 2026*
*Implementation by: GitHub Copilot*
*For: Miku Discord Bot Integration*

21
uno-online/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Mizan Ali
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,108 @@
# 🎴 Quick Start: Bot Control via JSON
## TL;DR
Miku can now play UNO by sending JSON commands! The game outputs state as JSON, and Miku responds with action JSON.
## JSON Action Format
```json
// Play a number/action card
{"action": "play", "card": "4R"}
// Play a wild card (must specify color)
{"action": "play", "card": "W", "color": "R"}
// Draw a card
{"action": "draw"}
// Call UNO (when you have 2 cards)
{"action": "uno"}
// Play card + call UNO in one action
{"action": "play", "card": "4R", "callUno": true}
```
## Card Codes Cheat Sheet
- Numbers: `0R`-`9R` (red), `0G`-`9G` (green), `0B`-`9B` (blue), `0Y`-`9Y` (yellow)
- Skip: `skipR`, `skipG`, `skipB`, `skipY`
- Reverse: `_R`, `_G`, `_B`, `_Y`
- Draw 2: `D2R`, `D2G`, `D2B`, `D2Y`
- Wild: `W`
- Wild Draw 4: `D4W`
## Colors
- `R` = Red
- `G` = Green
- `B` = Blue
- `Y` = Yellow
## HTTP API
```bash
# Get game state
GET http://localhost:5000/api/game/\{roomCode\}/state
# Send bot action
POST http://localhost:5000/api/game/\{roomCode\}/action
Content-Type: application/json
{"action": "play", "card": "4R"}
```
## Test It
```bash
# Create a game in browser with room code "TEST01"
# Join with 2nd player
# When it's Player 2's turn:
node test-bot-action.js TEST01 '{"action":"draw"}'
node test-bot-action.js TEST01 '{"action":"play","card":"4R"}'
node test-bot-action.js TEST01 '{"action":"play","card":"W","color":"B"}'
```
## For Miku Bot Integration
```python
import requests
# Get state
state = requests.get('http://localhost:5000/api/game/TEST01/state').json()
# Make move
action = {"action": "play", "card": "4R"}
response = requests.post(
'http://localhost:5000/api/game/TEST01/action',
json=action
).json()
```
## Game State JSON Structure
```json
{
"game": {"currentTurn": "Player 2", "isOver": false},
"currentCard": {"code": "4G", "color": "G", "value": 4},
"player2": {
"cards": [...], // Your cards
"playableCards": [...], // Cards you can play right now
"cardCount": 7
},
"player1": {
"cardCount": 7 // Opponent's card count (cards hidden)
},
"botContext": {
"canPlay": true, // Can you play a card?
"mustDraw": false, // Must you draw?
"hasUno": false // Do you have 1 card?
}
}
```
## Strategy Tips
1. Check `player2.playableCards` for valid moves
2. Call UNO when you have 2 cards and are about to play one
3. For wild cards, choose color you have most of
4. Play Draw 2/Draw 4 when opponent has few cards
📖 See `BOT_ACTION_SPEC.md` for full documentation

104
uno-online/README.md Normal file
View File

@@ -0,0 +1,104 @@
> Update: I have run out of Heroku credits and can no longer continue to keep the game deployed. Feel free to clone the repo and run it locally on your machine. Thanks!
<h1 align="center">UNO ONLINE</h1>
<h2 align="center">Two player online game of UNO</h2>
<h3 align="center">Was previously live at https://uno-online-multiplayer.herokuapp.com</h3>
<img src="client/src/assets/logo.png" alt="UNO Logo" width="33%" align="center" />
## 🤔 What is UNO?
UNO is the classic and beloved card game thats easy to pick up and impossible to put down! Players take turns matching a card in their hand with the current card shown on top of the deck either by color or number. Special action cards deliver game-changing moments as they each perform a function to help you defeat your opponents. These include skips, reverses, draw twos, color-changing wild and draw four wild cards.
## ⚠️ Game Rules
Read the complete rules of UNO [here](https://www.unorules.com/).
## ❓ About the Game
This two-player online version of the game was built with [React](https://reactjs.org/), [Socket.IO](https://socket.io/), [Express](https://expressjs.com/) and [Node](https://nodejs.org/en/). It currently supports two-players in each game. It also has text chat functionality to communicate with your opponent!
[How I Made This Game](https://www.youtube.com/watch?v=FBAJdbpFnjs)
## 🧐 How to Play?
1. Once you're on the homepage of the game, you can either host a new game or join a friend's game.
2. To host a new game, click on CREATE GAME. A game code will be generated which you can share with your friend.
3. To join a friend's game, enter the game code given by them, and click on JOIN GAME.
4. That's it! Enjoy the game and remember, no toxicity!
## 🎮 Screenshots
<img src="screenshots/Screenshot-1.png" alt="Screenshot 1" width="75%" align="center" />
<img src="screenshots/Screenshot-2.png" alt="Screenshot 2" width="75%" align="center" />
<img src="screenshots/Screenshot-3.png" alt="Screenshot 3" width="75%" align="center" />
## 🏁 Getting Started (to run game locally)
Follow the steps below, after cloning the repository:
### 🖐 Requirements
**For Installing:**
- Node
**For Running:**
- Change socket.IO endpoint on client side. To do this, go to `client/src/components/Game.js` and change line #26 from `const ENDPOINT = 'https://uno-online-multiplayer.herokuapp.com/'` to `const ENDPOINT = 'http://localhost:5000'`
### ⏳ Installation
- At the root of the project directory, use npm to install the server-side dependencies
```bash
npm install
```
This command installs all the server-side dependencies needed for the game to run locally.
- Use npm to run server
```bash
npm start
```
This command gets the server running on localhost port 5000.
- In a separate terminal, navigate into the client folder and use npm to install the client-side dependencies
```bash
cd client
npm install
```
This command installs all the client-side dependencies needed for the game to run locally.
- Finally, use npm to run client
```bash
npm start
```
This command gets the client running on localhost port 3000.
Head over to http://localhost:3000/ and enjoy the game! 🎉
## 🤝 Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. The **Issues** tab is a good place to begin!
1. Fork the project repo
2. Clone the forked repo on your machine
3. Create your feature branch (`git checkout -b feature/AmazingFeature`)
4. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
5. Push to the branch on your forked repo (`git push origin feature/AmazingFeature`)
6. Open a pull request
## ❤️ Acknowledgements
* [Chirantan P](https://www.linkedin.com/in/chirantan-pradhan-76673019b/) for the background images
* [AlexDer](https://alexder.itch.io/) for the UNO cards assets
* [3mil1](https://codepen.io/3mil1) for the button designs
* [Divyank](https://codepen.io/Pahlaz) for the chat box design

152
uno-online/SETUP_NOTES.md Normal file
View File

@@ -0,0 +1,152 @@
# UNO Bot Integration - Setup Notes
## Node.js Version Compatibility
This project was originally developed with Node.js 14-16.
**IMPORTANT:** Due to PostCSS compatibility issues, this project requires Node.js 16 or 18.
### Current System Issue
- ❌ Node.js v25.3.0 - PostCSS subpath export errors
- ✅ Node.js 18.x - Recommended
- ✅ Node.js 16.x - Also works
### Solution: Use nvm to switch Node version
```bash
# Install nvm if not already installed
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Install Node 18
nvm install 18
# Use Node 18
nvm use 18
# Verify version
node --version # Should show v18.x.x
```
After switching to Node 18, proceed with the setup below.
## Quick Start
### 1. Install Dependencies
**Server:**
```bash
cd /home/koko210Serve/docker/uno-online
npm install
```
**Client:**
```bash
cd /home/koko210Serve/docker/uno-online/client
npm install
```
### 2. Start Server (Terminal 1)
```bash
cd /home/koko210Serve/docker/uno-online
npm start
```
Server runs on: `http://localhost:5000`
### 3. Start Client (Terminal 2)
```bash
cd /home/koko210Serve/docker/uno-online/client
npm start
```
Client opens at: `http://localhost:3000`
### 4. Test Bot Integration
**Create a game:**
- Open `http://localhost:3000`
- Click "CREATE GAME"
- Note the room code (e.g., "ABCDEF")
**Open browser console (F12)** to see game state logs
**Test API:**
```bash
# Get game state (replace ABCDEF with your room code)
curl http://localhost:5000/api/game/ABCDEF/state
# Play a card (after joining as Player 2)
curl -X POST http://localhost:5000/api/game/ABCDEF/action \
-H "Content-Type: application/json" \
-d '{"action":"draw_card"}'
```
## What to Look For
### Browser Console Output
When you play as Player 2, you'll see:
```
🎮 UNO GAME STATE (Simplified): { turn: "Player 2", currentCard: "5 red (5R)", ... }
🤖 FULL GAME STATE (For Bot): { game: {...}, currentCard: {...}, ... }
📋 JSON for Bot API:
{
"game": { ... }
...
}
```
### Server Console Output
```
[Bot Game State] Room: ABCDEF
{
"game": { ... }
}
```
## Files Modified for Bot Integration
### New Files
- `/client/src/utils/cardParser.js` - Parse card codes to JSON
- `/client/src/utils/gameStateBuilder.js` - Build comprehensive game state
- `/BOT_INTEGRATION_GUIDE.md` - Full integration documentation
- `/BOT_QUICK_REF.md` - Quick reference guide
- `/SETUP_NOTES.md` - This file
### Modified Files
- `/client/src/components/Game.js` - Added state export and logging
- `/server.js` - Added HTTP API endpoints and Socket.IO handlers
## Common Issues
### Port Already in Use
If port 5000 or 3000 is busy:
```bash
# Kill process on port 5000
lsof -ti:5000 | xargs kill -9
# Kill process on port 3000
lsof -ti:3000 | xargs kill -9
```
### npm Install Failures
If you get peer dependency warnings, use:
```bash
npm install --legacy-peer-deps
```
### Module Not Found Errors
Clear cache and reinstall:
```bash
rm -rf node_modules package-lock.json
npm install
```
## Next Steps
1. ✅ Game state exports to console
2. ✅ HTTP API endpoints created
3. ⏭️ Test manual API calls
4. ⏭️ Integrate with Miku bot
5. ⏭️ Create Discord command `/uno join [room_code]`
6. ⏭️ Implement LLM decision-making
See `BOT_INTEGRATION_GUIDE.md` for Miku bot integration details.

View File

20304
uno-online/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "uno-online",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.7.0",
"query-string": "^6.14.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.2",
"socket.io-client": "^3.1.1",
"use-sound": "^2.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Two player online game of UNO."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>UNO Online</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="text/javascript">
window.onbeforeunload = function() {
return true
}
</script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,387 @@
.App {
text-align: center;
}
a {
color: #ffffff;
}
/* Homepage */
.Homepage {
background-image: url('./assets/Landing-Page.gif');
background-size: cover;
margin: 0;
height: 100vh;
}
.homepage-menu {
position: relative;
top: 120px;
}
.homepage-form {
display: flex;
justify-content: center;
margin-top: 50px;
}
.homepage-form>h1 {
margin: 0 30px;
}
.homepage-join {
display: flex;
flex-direction: column;
}
.homepage-join>input {
font-size: 15px;
width: 150px;
line-height: 1.5em;
}
/* Game.js parent div */
.Game {
background-size: cover;
margin: 0;
height: 100vh;
}
/* Game.js Background */
.backgroundColorR {
background-image: url('./assets/backgrounds/bgR.png');
}
.backgroundColorG {
background-image: url('./assets/backgrounds/bgG.png');
}
.backgroundColorB {
background-image: url('./assets/backgrounds/bgB.png');
}
.backgroundColorY {
background-image: url('./assets/backgrounds/bgY.png');
}
/* UNO Cards */
.Card {
width: 6rem;
margin: 2px;
cursor: pointer;
}
/* Game.js Top Row */
.topInfo {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 60px;
height: 100px;
}
.topInfo>img {
width: 7%;
margin: 0;
padding: 0;
}
.topInfo>h1 {
margin-top: 10px;
font-size: 1.8rem;
padding-top: 2%;
}
.topInfoText {
font-size: 2rem;
margin: 0;
}
/* Player Decks */
.player1Deck {
display: flex;
flex-direction: row;
align-items: center;
}
.player1Deck>img {
transition: transform 350ms;
}
.player1Deck>img:hover {
transform: scale(1.08);
opacity: 1;
}
.player2Deck {
display: flex;
flex-direction: row-reverse;
align-items: center;
}
.player2Deck>img {
transition: transform 350ms;
}
.player2Deck>img:hover {
transform: scale(1.08);
opacity: 1;
}
.playerDeckText {
font-size: 2rem;
margin: 0 20px;
}
/* Game.js Middle Row */
.middleInfo {
display: flex;
flex-direction: row;
justify-content: space-around;
}
.middleInfo>button {
align-self: center;
}
/* Game Buttons */
.game-button {
position: relative;
top: 0;
cursor: pointer;
text-decoration: none !important;
outline: none !important;
font-family: 'Carter One', sans-serif;
font-size: 15px;
line-height: 1.5em;
letter-spacing: .1em;
text-shadow: 2px 2px 1px #0066a2, -2px 2px 1px #0066a2, 2px -2px 1px #0066a2, -2px -2px 1px #0066a2, 0px 2px 1px #0066a2, 0px -2px 1px #0066a2, 0px 4px 1px #004a87, 2px 4px 1px #004a87, -2px 4px 1px #004a87;
border: none;
margin: 15px 15px 30px;
background: repeating-linear-gradient( 45deg, #3ebbf7, #3ebbf7 5px, #45b1f4 5px, #45b1f4 10px);
border-bottom: 3px solid rgba(16, 91, 146, 0.5);
border-top: 3px solid rgba(255,255,255,.3);
color: #fff !important;
border-radius: 8px;
padding: 8px 15px 10px;
box-shadow: 0 6px 0 #266b91, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #12517d, 0 12px 0 5px #1a6b9a, 0 15px 0 5px #0c405e, 0 15px 1px 6px rgba(0,0,0,.3);
}
.game-button:hover {
top:2px;
box-shadow: 0 4px 0 #266b91, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #12517d, 0 10px 0 5px #1a6b9a, 0 13px 0 5px #0c405e, 0 13px 1px 6px rgba(0,0,0,.3);
}
.game-button::before {
content: '';
height: 10%;
position: absolute;
width: 40%;
background: #fff;
right: 13%;
top: -3%;
border-radius: 99px;
}
.game-button::after {
content: '';
height: 10%;
position: absolute;
width: 5%;
background: #fff;
right: 5%;
top: -3%;
border-radius: 99px;
}
.game-button.orange {
background: repeating-linear-gradient( 45deg, #ffc800, #ffc800 5px, #ffc200 5px, #ffc200 10px);
box-shadow: 0 6px 0 #b76113, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #75421f, 0 12px 0 5px #8a542b, 0 15px 0 5px #593116, 0 15px 1px 6px rgba(0,0,0,.3);
border-bottom: 3px solid rgba(205, 102, 0, 0.5);
text-shadow: 2px 2px 1px #e78700, -2px 2px 1px #e78700, 2px -2px 1px #e78700, -2px -2px 1px #e78700, 0px 2px 1px #e78700, 0px -2px 1px #e78700, 0px 4px 1px #c96100, 2px 4px 1px #c96100, -2px 4px 1px #c96100;
}
.game-button.orange:hover {
top:2px;
box-shadow: 0 4px 0 #b76113, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #75421f, 0 10px 0 5px #8a542b, 0 13px 0 5px #593116, 0 13px 1px 6px rgba(0,0,0,.3);
}
.game-button.red {
background: repeating-linear-gradient( 45deg, #ff4f4c, #ff4f4c 5px, #ff4643 5px, #ff4643 10px);
box-shadow: 0 6px 0 #ae2725, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #831614, 0 12px 0 5px #a33634, 0 15px 0 5px #631716, 0 15px 1px 6px rgba(0,0,0,.3);
border-bottom: 3px solid rgba(160, 25, 23, 0.5);
text-shadow: 2px 2px 1px #d72d21, -2px 2px 1px #d72d21, 2px -2px 1px #d72d21, -2px -2px 1px #d72d21, 0px 2px 1px #d72d21, 0px -2px 1px #d72d21, 0px 4px 1px #930704, 2px 4px 1px #930704, -2px 4px 1px #930704;
}
.game-button.red:hover {
top:2px;
box-shadow: 0 4px 0 #ae2725, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #831614, 0 10px 0 5px #a33634, 0 13px 0 5px #631716, 0 13px 1px 6px rgba(0,0,0,.3);
}
.game-button.green {
background: repeating-linear-gradient( 45deg, #54d440, #54d440 5px, #52cc3f 5px, #52cc3f 10px);
box-shadow: 0 6px 0 #348628, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #2a6d20, 0 12px 0 5px #39822e, 0 15px 0 5px #1d4c16, 0 15px 1px 6px rgba(0,0,0,.3);
border-bottom: 3px solid rgba(40, 117, 29, 0.5);
text-shadow: 2px 2px 1px #348628, -2px 2px 1px #348628, 2px -2px 1px #348628, -2px -2px 1px #348628, 0px 2px 1px #348628, 0px -2px 1px #348628, 0px 4px 1px #1d4c16, 2px 4px 1px #1d4c16, -2px 4px 1px #1d4c16;
}
.game-button.green:hover {
top:2px;
box-shadow: 0 4px 0 #348628, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #2a6d20, 0 10px 0 5px #39822e, 0 13px 0 5px #1d4c16, 0 13px 1px 6px rgba(0,0,0,.3);
}
/* Spinner */
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
width: 2.5em;
height: 2.5em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: load7 1.8s infinite ease-in-out;
animation: load7 1.8s infinite ease-in-out;
}
.loader {
color: #ffffff;
font-size: 5px;
margin: 0 50px 0 50px;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 0;
}
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 3.5em;
}
@-webkit-keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
/* Chat Box */
.chat-box{
position: absolute;
bottom: 0px;
background: white;
width: 355px;
border-radius: 5px 5px 0px 0px;
z-index: 100;
}
.chat-box-player1{
right: 20px;
}
.chat-box-player2{
left: 20px;
}
.chat-head{
width: inherit;
height: 45px;
background: #2c3e50;
border-radius: 5px 5px 0px 0px;
}
.chat-head h2{
color: white;
padding-top: 5px;
display: inline-block;
}
.chat-head span{
cursor: pointer;
float: right;
width: 25px;
margin: 10px;
}
.chat-body{
display: none;
height: 205px;
width: inherit;
overflow: hidden auto;
margin-bottom: 45px;
}
.chat-text{
position: fixed;
bottom: 0px;
height: 45px;
width: inherit;
}
.chat-text input{
width: inherit;
height: inherit;
box-sizing: border-box;
border: 1px solid #bdc3c7;
padding: 10px;
resize: none;
outline: none;
}
.chat-text input:active, .chat-text input:focus, .chat-text input:hover{
border-color: royalblue;
}
.msg-send{
background: #406a4b;
}
.msg-receive{
background: #595080;
}
.msg-send, .msg-receive{
width: 285px;
height: 35px;
padding: 5px 5px 5px 10px;
margin: 5px auto;
border-radius: 3px;
line-height: 30px;
position: relative;
color: white;
}
.msg-receive:before{
content: '';
width: 0px;
height: 0px;
position: absolute;
border: 15px solid;
border-color: transparent #595080 transparent transparent;
left: -29px;
top: 7px;
}
.msg-send:after{
content: '';
width: 0px;
height: 0px;
position: absolute;
border: 15px solid;
border-color: transparent transparent transparent #406a4b;
right: -29px;
top: 7px;
}
.msg-receive:hover, .msg-send:hover{
opacity: .9;
}

View File

@@ -0,0 +1,17 @@
import './App.css'
import { Routes, Route } from 'react-router-dom'
import Homepage from './components/Homepage'
import Game from './components/Game'
const App = () => {
return (
<div className="App">
<Routes>
<Route path='/' element={<Homepage />} />
<Route path='/play' element={<Game />} />
</Routes>
</div>
)
}
export default App

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import randomCodeGenerator from '../utils/randomCodeGenerator'
const Homepage = () => {
const [roomCode, setRoomCode] = useState('')
return (
<div className='Homepage'>
<div className='homepage-menu'>
<img src={require('../assets/logo.png').default} width='200px' />
<div className='homepage-form'>
<div className='homepage-join'>
<input type='text' placeholder='Game Code' onChange={(event) => setRoomCode(event.target.value)} />
<Link to={`/play?roomCode=${roomCode}`}><button className="game-button green">JOIN GAME</button></Link>
</div>
<h1>OR</h1>
<div className='homepage-create'>
<Link to={`/play?roomCode=${randomCodeGenerator(5)}`}><button className="game-button orange">CREATE GAME</button></Link>
</div>
</div>
</div>
</div>
)
}
export default Homepage

View File

@@ -0,0 +1,9 @@
import React from 'react'
const Spinner = () => {
return (
<div className="loader">Loading...</div>
)
}
export default Spinner

View File

@@ -0,0 +1,14 @@
@import url('https://fonts.googleapis.com/css?family=Carter+One');
* {
margin: 0;
padding: 0;
}
body {
margin: 0;
font-family: 'Carter One', sans-serif;
color: white;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1,14 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
)

View File

@@ -0,0 +1,231 @@
/**
* Bot Action Executor
* Processes bot actions and executes them in the game
*/
import { isCardPlayable } from './cardParser';
/**
* Execute a bot action in the game
* @param {Object} action - The bot action to execute
* @param {Object} gameContext - Current game context
* @returns {Object} Result of the action execution
*/
export const executeBotAction = (action, gameContext) => {
const {
turn,
currentUser,
currentColor,
currentNumber,
player2Deck,
onCardPlayedHandler,
onCardDrawnHandler,
setUnoButtonPressed
} = gameContext
// Validate it's Player 2's turn and the current user is Player 2
if (turn !== 'Player 2') {
console.warn('❌ Bot action rejected: Not Player 2\'s turn')
return {
success: false,
error: 'Not your turn',
message: 'It\'s not Player 2\'s turn'
}
}
if (currentUser !== 'Player 2') {
console.warn('❌ Bot action rejected: Current user is not Player 2')
return {
success: false,
error: 'Not Player 2',
message: 'Current user is not Player 2'
}
}
// Process the action based on type
switch (action.action) {
case 'play':
return executePlayAction(action, gameContext)
case 'draw':
return executeDrawAction(gameContext)
case 'uno':
return executeUnoAction(gameContext)
default:
console.warn('❌ Unknown bot action:', action.action)
return {
success: false,
error: 'Unknown action',
message: `Unknown action type: ${action.action}`
}
}
}
/**
* Execute a play card action
*/
const executePlayAction = (action, gameContext) => {
const { card, color, callUno } = action
const { player2Deck, onCardPlayedHandler, setUnoButtonPressed } = gameContext
// Validate card parameter
if (!card) {
console.warn('❌ Bot play action rejected: No card specified')
return {
success: false,
error: 'No card specified',
message: 'Play action requires a card parameter'
}
}
// Check if card is in Player 2's hand
if (!player2Deck.includes(card)) {
console.warn('❌ Bot play action rejected: Card not in hand:', card)
return {
success: false,
error: 'Card not in hand',
message: `Card ${card} is not in Player 2's hand`
}
}
// Validate wild card has color specified
if ((card === 'W' || card === 'D4W') && !color) {
console.warn('❌ Bot play action rejected: Wild card without color')
return {
success: false,
error: 'No color specified',
message: 'Wild cards require a color parameter (R/G/B/Y)'
}
}
// Handle UNO call if specified
if (callUno) {
console.log('🔥 Bot called UNO!')
setUnoButtonPressed(true)
}
// Store color for wild cards in a way the game can access it
if (color && (card === 'W' || card === 'D4W')) {
// Set a global or context variable for the color choice
// The onCardPlayedHandler will check this
window.botChosenColor = color
console.log(`🌈 Bot chose color: ${color}`)
}
// Execute the play
console.log(`🎴 Bot playing card: ${card}`)
console.log(`🎴 Current game state:`, {
turn: gameContext.turn,
currentUser: gameContext.currentUser,
player2DeckLength: gameContext.player2Deck?.length,
hasCard: gameContext.player2Deck?.includes(card)
})
// Track deck size before play to verify card was actually played
const deckSizeBefore = gameContext.player2Deck.length
onCardPlayedHandler(card)
// Wait a moment for state to update
// Note: This is a hack because onCardPlayedHandler doesn't return success/failure
// In a real implementation, we'd need to refactor the game logic
return new Promise((resolve) => {
setTimeout(() => {
const deckSizeAfter = gameContext.player2Deck.length
const cardWasPlayed = deckSizeAfter < deckSizeBefore
if (cardWasPlayed) {
resolve({
success: true,
message: `Played card ${card}${color ? ` (chose ${color})` : ''}${callUno ? ' and called UNO' : ''}`
})
} else {
console.warn('❌ Card was not actually played (deck size unchanged)')
resolve({
success: false,
error: 'Invalid play',
message: `Card ${card} could not be played (invalid move)`
})
}
}, 100) // Wait 100ms for state update
})
}
/**
* Execute a draw card action
*/
const executeDrawAction = (gameContext) => {
const { onCardDrawnHandler } = gameContext
console.log('📥 Bot drawing a card')
onCardDrawnHandler()
return {
success: true,
message: 'Drew a card'
}
}
/**
* Execute a UNO call action
*/
const executeUnoAction = (gameContext) => {
const { setUnoButtonPressed, player2Deck } = gameContext
if (player2Deck.length !== 2) {
console.warn('❌ Bot UNO rejected: Player 2 doesn\'t have exactly 2 cards')
return {
success: false,
error: 'Invalid UNO call',
message: `Can only call UNO with 2 cards, currently have ${player2Deck.length}`
}
}
console.log('🔥 Bot called UNO!')
setUnoButtonPressed(true)
return {
success: true,
message: 'Called UNO'
}
}
/**
* Validate a bot action before execution
*/
export const validateBotAction = (action, gameState) => {
if (!action || typeof action !== 'object') {
return { valid: false, error: 'Invalid action format' }
}
if (!action.action) {
return { valid: false, error: 'Missing action type' }
}
// Validate specific action types
switch (action.action) {
case 'play':
if (!action.card) {
return { valid: false, error: 'Play action requires card parameter' }
}
if ((action.card === 'W' || action.card === 'D4W') && !action.color) {
return { valid: false, error: 'Wild cards require color parameter' }
}
if (action.color && !['R', 'G', 'B', 'Y'].includes(action.color)) {
return { valid: false, error: 'Invalid color. Must be R, G, B, or Y' }
}
break
case 'draw':
case 'uno':
// No additional validation needed
break
default:
return { valid: false, error: `Unknown action type: ${action.action}` }
}
return { valid: true }
}

View File

@@ -0,0 +1,26 @@
// Build a map of all card front images using webpack's require.context
import CARD_BACK from '../assets/card-back.png'
// require all png files in cards-front directory
const req = require.context('../assets/cards-front', false, /\.png$/)
const cardMap = {}
req.keys().forEach((key) => {
// key is like './5R.png' -> strip './' and '.png'
const code = key.replace('./', '').replace('.png', '')
try {
const resolved = req(key)
cardMap[code] = resolved && resolved.default ? resolved.default : resolved
} catch (e) {
cardMap[code] = CARD_BACK
}
})
export const getCardImage = (code) => {
if (!code) return CARD_BACK
return cardMap[code] || CARD_BACK
}
export default {
getCardImage,
}

View File

@@ -0,0 +1,150 @@
/**
* Utility functions for parsing UNO card codes into readable JSON objects
* Card codes format: '5R', 'D2G', 'skipB', 'W', 'D4W', '_Y' (reverse)
*/
/**
* Parse a card code into a detailed card object
* @param {string} cardCode - The card code (e.g., '5R', 'D2G', 'W')
* @returns {object} Card object with type, value, color, and display name
*/
export const parseCard = (cardCode) => {
if (!cardCode) return null;
const card = {
code: cardCode,
type: 'unknown',
value: null,
color: null,
colorName: null,
displayName: ''
};
// Extract color (last character for most cards)
const lastChar = cardCode.charAt(cardCode.length - 1).toUpperCase();
const colorMap = {
'R': 'red',
'G': 'green',
'B': 'blue',
'Y': 'yellow'
};
// Wild cards (no color)
if (cardCode === 'W') {
card.type = 'wild';
card.value = 300;
card.displayName = 'Wild';
return card;
}
if (cardCode === 'D4W') {
card.type = 'draw4_wild';
card.value = 600;
card.displayName = 'Draw 4 Wild';
return card;
}
// Cards with color
card.color = lastChar;
card.colorName = colorMap[lastChar] || 'unknown';
// Number cards (0-9)
const firstChar = cardCode.charAt(0);
if (firstChar >= '0' && firstChar <= '9') {
card.type = 'number';
card.value = parseInt(firstChar, 10);
card.displayName = `${firstChar} ${card.colorName}`;
return card;
}
// Skip cards
if (cardCode.startsWith('skip')) {
card.type = 'skip';
card.value = 404;
card.displayName = `Skip ${card.colorName}`;
return card;
}
// Draw 2 cards
if (cardCode.startsWith('D2')) {
card.type = 'draw2';
card.value = 252;
card.displayName = `Draw 2 ${card.colorName}`;
return card;
}
// Reverse cards
if (cardCode === '_' + lastChar) {
card.type = 'reverse';
card.value = 0;
card.displayName = `Reverse ${card.colorName}`;
return card;
}
return card;
};
/**
* Check if a card can be played on the current card
* @param {string} cardCode - The card to check
* @param {string} currentColor - Current color in play
* @param {string|number} currentNumber - Current number/value in play
* @returns {boolean} Whether the card can be played
*/
export const isCardPlayable = (cardCode, currentColor, currentNumber) => {
const card = parseCard(cardCode);
// Wild cards can always be played
if (card.type === 'wild' || card.type === 'draw4_wild') {
return true;
}
// Normalize currentColor: accept 'R' or 'red' (case-insensitive)
let normColor = null;
if (typeof currentColor === 'string') {
const c = currentColor.trim().toUpperCase();
const nameToLetter = { RED: 'R', GREEN: 'G', BLUE: 'B', YELLOW: 'Y' };
normColor = nameToLetter[c] || c.charAt(0);
}
// Check color match
if (card.color && normColor && card.color === normColor) {
return true;
}
// Check number/value match (coerce currentNumber to number)
const normNumber = (currentNumber === null || currentNumber === undefined) ? null : Number(currentNumber);
if (card.value !== null && normNumber !== null && card.value === normNumber) {
return true;
}
// Special case: reverse cards
if (card.type === 'reverse' && normNumber === 0) {
return true;
}
return false;
};
/**
* Parse multiple cards into detailed objects
* @param {string[]} cardCodes - Array of card codes
* @returns {object[]} Array of parsed card objects
*/
export const parseCards = (cardCodes) => {
return cardCodes.map(parseCard);
};
/**
* Get playable cards from a hand
* @param {string[]} hand - Player's card codes
* @param {string} currentColor - Current color in play
* @param {string|number} currentNumber - Current number/value in play
* @returns {object[]} Array of playable cards with their details
*/
export const getPlayableCards = (hand, currentColor, currentNumber) => {
return hand
.filter(cardCode => isCardPlayable(cardCode, currentColor, currentNumber))
.map(cardCode => ({
...parseCard(cardCode),
isPlayable: true
}));
};

View File

@@ -0,0 +1,167 @@
/**
* Utility for building comprehensive game state JSON for bot integration
*/
import { parseCard, parseCards, getPlayableCards, isCardPlayable } from './cardParser';
/**
* Build a complete game state object for external consumption (e.g., bot/AI)
* @param {object} gameState - Current game state from React component
* @param {string} currentUser - The current user's player name (Player 1 or Player 2)
* @returns {object} Comprehensive game state in JSON format
*/
export const buildGameStateJSON = (gameState, currentUser) => {
const {
gameOver,
winner,
turn,
player1Deck,
player2Deck,
currentColor,
currentNumber,
playedCardsPile,
drawCardPile
} = gameState;
// Get last 5 played cards (or all if less than 5)
const recentCards = playedCardsPile.slice(-5);
const currentCard = playedCardsPile[playedCardsPile.length - 1];
// Determine which player is the bot (Player 2)
const botDeck = player2Deck;
const opponentDeck = player1Deck;
const isBotTurn = turn === 'Player 2';
// Parse bot's cards with playability info
// Parse bot's cards and mark playability based on game rules (independent of whose turn)
const botParsedCards = botDeck.map(cardCode => {
const card = parseCard(cardCode);
const playable = isCardPlayable(cardCode, currentColor, currentNumber);
return {
...card,
isPlayable: playable
};
});
// Build the comprehensive state object
return {
// Game meta info
game: {
isOver: gameOver,
winner: winner || null,
currentTurn: turn,
turnNumber: playedCardsPile.length, // Approximate turn count
},
// Current card on the pile
currentCard: {
code: currentCard,
...parseCard(currentCard),
currentColor: currentColor,
currentNumber: currentNumber
},
// Recently played cards (last 5)
recentlyPlayed: recentCards.map((cardCode, index) => ({
code: cardCode,
...parseCard(cardCode),
position: recentCards.length - index // 1 = most recent
})),
// Player 1 info (opponent to bot)
player1: {
name: 'Player 1',
cardCount: player1Deck.length,
isCurrentTurn: turn === 'Player 1',
cards: [] // Hidden from bot
},
// Player 2 info (bot)
player2: {
name: 'Player 2',
cardCount: player2Deck.length,
isCurrentTurn: turn === 'Player 2',
cards: botParsedCards, // Visible to bot
playableCards: botParsedCards.filter(c => c.isPlayable)
},
// Deck info
deck: {
drawPileCount: drawCardPile.length,
playedPileCount: playedCardsPile.length
},
// Bot decision context
botContext: {
canPlay: isBotTurn && botParsedCards.some(c => c.isPlayable),
mustDraw: isBotTurn && !botParsedCards.some(c => c.isPlayable),
hasUno: player2Deck.length === 2, // Should press UNO button next turn
isWinning: player2Deck.length === 1,
actions: isBotTurn ? getAvailableActions(botParsedCards, currentColor, currentNumber) : []
}
};
};
/**
* Get available actions for the bot
* @param {object[]} parsedCards - Bot's parsed cards
* @param {string} currentColor - Current color
* @param {number|string} currentNumber - Current number
* @returns {object[]} Array of available action objects
*/
const getAvailableActions = (parsedCards, currentColor, currentNumber) => {
const actions = [];
// Check each card for playability
parsedCards.forEach(card => {
if (card.isPlayable) {
actions.push({
action: 'play_card',
card: {
code: card.code,
type: card.type,
value: card.value,
color: card.color,
displayName: card.displayName
},
// For wild cards, need to choose a color
requiresColorChoice: card.type === 'wild' || card.type === 'draw4_wild'
});
}
});
// If no playable cards, must draw
if (actions.length === 0) {
actions.push({
action: 'draw_card',
card: null
});
}
return actions;
};
/**
* Format game state for console logging
* @param {object} gameStateJSON - The game state object
* @returns {string} Formatted JSON string
*/
export const formatGameStateForLog = (gameStateJSON) => {
return JSON.stringify(gameStateJSON, null, 2);
};
/**
* Create a simplified game state for quick display
* @param {object} gameStateJSON - The full game state object
* @returns {object} Simplified state
*/
export const simplifyGameState = (gameStateJSON) => {
return {
turn: gameStateJSON.game.currentTurn,
currentCard: `${gameStateJSON.currentCard.displayName} (${gameStateJSON.currentCard.code})`,
player1Cards: gameStateJSON.player1.cardCount,
player2Cards: gameStateJSON.player2.cardCount,
botCanPlay: gameStateJSON.botContext.canPlay,
playableCards: gameStateJSON.player2.playableCards.length
};
};

View File

@@ -0,0 +1,8 @@
//pack of 108 cards (_ = reverse)
export default [
'0R', '1R', '1R', '2R', '2R', '3R', '3R', '4R', '4R', '5R', '5R', '6R', '6R', '7R', '7R', '8R', '8R', '9R', '9R', 'skipR', 'skipR', '_R', '_R', 'D2R', 'D2R',
'0G', '1G', '1G', '2G', '2G', '3G', '3G', '4G', '4G', '5G', '5G', '6G', '6G', '7G', '7G', '8G', '8G', '9G', '9G', 'skipG', 'skipG', '_G', '_G', 'D2G', 'D2G',
'0B', '1B', '1B', '2B', '2B', '3B', '3B', '4B', '4B', '5B', '5B', '6B', '6B', '7B', '7B', '8B', '8B', '9B', '9B', 'skipB', 'skipB', '_B', '_B', 'D2B', 'D2B',
'0Y', '1Y', '1Y', '2Y', '2Y', '3Y', '3Y', '4Y', '4Y', '5Y', '5Y', '6Y', '6Y', '7Y', '7Y', '8Y', '8Y', '9Y', '9Y', 'skipY', 'skipY', '_Y', '_Y', 'D2Y', 'D2Y',
'W', 'W', 'W', 'W', 'D4W', 'D4W', 'D4W', 'D4W'
]

View File

@@ -0,0 +1,9 @@
export default function makeid(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View File

@@ -0,0 +1,9 @@
export default function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1))
var temp = array[i]
array[i] = array[j]
array[j] = temp;
}
return array
}

1298
uno-online/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
uno-online/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "uno-online",
"version": "1.0.0",
"description": "Online multiplayer card game",
"main": "server.js",
"scripts": {
"start": "node server",
"client": "npm start --prefix client",
"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mizanxali/uno-online.git"
},
"author": "Mizan Ali",
"license": "ISC",
"bugs": {
"url": "https://github.com/mizanxali/uno-online/issues"
},
"homepage": "https://github.com/mizanxali/uno-online#readme",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"socket.io": "^3.1.1"
}
}

158
uno-online/server.js Normal file
View File

@@ -0,0 +1,158 @@
const express = require('express')
const socketio = require('socket.io')
const http = require('http')
const cors = require('cors')
const { addUser, removeUser, getUser, getUsersInRoom } = require('./users')
const path = require('path')
const PORT = process.env.PORT || 5000
const app = express()
const server = http.createServer(app)
const io = socketio(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
})
app.use(cors())
// Store latest game states by room code for bot API access
const gameStates = new Map()
// HTTP endpoint for bot to get game state
app.get('/api/game/:roomCode/state', (req, res) => {
const roomCode = req.params.roomCode
const state = gameStates.get(roomCode)
if (state) {
res.json({
success: true,
gameState: state,
timestamp: new Date().toISOString()
})
} else {
res.status(404).json({
success: false,
error: 'Game room not found or no state available'
})
}
})
// HTTP endpoint for bot to submit action
app.post('/api/game/:roomCode/action', express.json(), (req, res) => {
const roomCode = req.params.roomCode
const action = req.body
// minimal log for incoming bot action
console.log(`[Bot HTTP Action] Room: ${roomCode}`)
// Emit action to the game room via socket
io.to(roomCode).emit('botActionReceived', action)
res.json({
success: true,
message: 'Action received and forwarded to game'
})
})
io.on('connection', socket => {
socket.on('join', (payload, callback) => {
let numberOfUsersInRoom = getUsersInRoom(payload.room).length
const { error, newUser} = addUser({
id: socket.id,
name: numberOfUsersInRoom===0 ? 'Player 1' : 'Player 2',
room: payload.room
})
if(error)
return callback(error)
socket.join(newUser.room)
io.to(newUser.room).emit('roomData', {room: newUser.room, users: getUsersInRoom(newUser.room)})
socket.emit('currentUserData', {name: newUser.name})
callback()
})
socket.on('initGameState', gameState => {
const user = getUser(socket.id)
if(user)
io.to(user.room).emit('initGameState', gameState)
})
socket.on('updateGameState', gameState => {
const user = getUser(socket.id)
if(user) {
io.to(user.room).emit('updateGameState', gameState)
// Also update stored game state for bot REST API access
const currentState = gameStates.get(user.room) || {}
gameStates.set(user.room, {
...currentState,
...gameState,
room: user.room,
lastUpdate: new Date().toISOString()
})
}
})
socket.on('sendMessage', (payload, callback) => {
const user = getUser(socket.id)
io.to(user.room).emit('message', {user: user.name, text: payload.message})
callback()
})
// Bot integration: receive game state from Player 2 (bot)
socket.on('botGameState', gameState => {
const user = getUser(socket.id)
if(user && user.name === 'Player 2') {
// Store latest game state for bot access (do not log full JSON to avoid noise)
gameStates.set(user.room, {
...gameState,
room: user.room,
lastUpdate: new Date().toISOString()
})
}
})
// Bot integration: receive bot action (play card or draw)
socket.on('botAction', (action, callback) => {
const user = getUser(socket.id)
if(user && user.name === 'Player 2') {
// Forward action to game logic. Client will handle via regular game state updates
callback && callback({ success: true })
}
})
// Bot integration: request current game state
socket.on('requestGameState', (callback) => {
const user = getUser(socket.id)
if(user) {
// Request game state from room
socket.to(user.room).emit('gameStateRequested')
callback && callback({ success: true })
}
})
socket.on('disconnect', () => {
const user = removeUser(socket.id)
if(user)
io.to(user.room).emit('roomData', {room: user.room, users: getUsersInRoom(user.room)})
})
})
//serve static assets in production
if(process.env.NODE_ENV === 'production') {
//set static folder
app.use(express.static('client/build'))
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})

76
uno-online/test-bot-action.js Executable file
View File

@@ -0,0 +1,76 @@
/**
* Test script for bot actions
* Usage: node test-bot-action.js <roomCode> <action>
*
* Examples:
* node test-bot-action.js ABC123 '{"action":"draw"}'
* node test-bot-action.js ABC123 '{"action":"play","card":"4R"}'
* node test-bot-action.js ABC123 '{"action":"play","card":"W","color":"R"}'
* node test-bot-action.js ABC123 '{"action":"uno"}'
*/
const http = require('http');
const roomCode = process.argv[2];
const actionJson = process.argv[3];
if (!roomCode || !actionJson) {
console.error('❌ Usage: node test-bot-action.js <roomCode> <actionJson>');
console.error('Example: node test-bot-action.js ABC123 \'{"action":"draw"}\'');
process.exit(1);
}
let action;
try {
action = JSON.parse(actionJson);
} catch (e) {
console.error('❌ Invalid JSON:', e.message);
process.exit(1);
}
console.log(`🤖 Sending bot action to room ${roomCode}:`);
console.log(JSON.stringify(action, null, 2));
const postData = JSON.stringify(action);
const options = {
hostname: 'localhost',
port: 5000,
path: `/api/game/${roomCode}/action`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log(`\n📡 Response (${res.statusCode}):`);
try {
const response = JSON.parse(data);
console.log(JSON.stringify(response, null, 2));
if (response.success) {
console.log('✅ Action sent successfully!');
} else {
console.log('❌ Action failed');
}
} catch (e) {
console.log(data);
}
});
});
req.on('error', (e) => {
console.error('❌ Request failed:', e.message);
console.error('Make sure the server is running on port 5000');
});
req.write(postData);
req.end();

View File

@@ -0,0 +1,52 @@
// Simple test to verify the card parser and game state builder work
const { parseCard, isCardPlayable, getPlayableCards } = require('./client/src/utils/cardParser');
const { buildGameStateJSON } = require('./client/src/utils/gameStateBuilder');
console.log('Testing Card Parser...');
// Test 1: Parse a simple number card
const card1 = parseCard('5R');
console.log('✓ Parse 5R:', JSON.stringify(card1, null, 2));
// Test 2: Parse a wild card
const card2 = parseCard('W');
console.log('✓ Parse W:', JSON.stringify(card2, null, 2));
// Test 3: Check playability
const playable = isCardPlayable('5R', 'R', 3);
console.log('✓ Can play 5R on 3R?', playable);
// Test 4: Get playable cards
const hand = ['5R', '7B', 'W', '3G'];
const playableCards = getPlayableCards(hand, 'R', 5);
console.log('✓ Playable cards from hand:', playableCards.map(c => c.code));
console.log('\nTesting Game State Builder...');
// Test 5: Build game state
const mockGameState = {
gameOver: false,
winner: null,
turn: 'Player 2',
player1Deck: ['1R', '2B', '3G', '4Y', '5R'],
player2Deck: ['5R', '7B', 'W'],
currentColor: 'R',
currentNumber: 5,
playedCardsPile: ['1B', '2B', '3R', '4R', '5R'],
drawCardPile: new Array(80).fill('back')
};
try {
const gameStateJSON = buildGameStateJSON(mockGameState, 'Player 2');
console.log('✓ Game State JSON generated successfully!');
console.log(' Turn:', gameStateJSON.game.currentTurn);
console.log(' Current Card:', gameStateJSON.currentCard.displayName);
console.log(' Bot has', gameStateJSON.player2.cards.length, 'cards');
console.log(' Playable:', gameStateJSON.player2.playableCards.length);
console.log(' Actions:', gameStateJSON.botContext.actions.length);
} catch (error) {
console.error('✗ Error building game state:', error.message);
}
console.log('\n✅ All tests passed!');

28
uno-online/users.js Normal file
View File

@@ -0,0 +1,28 @@
const users = []
const addUser = ({id, name, room}) => {
const numberOfUsersInRoom = users.filter(user => user.room === room).length
if(numberOfUsersInRoom === 2)
return { error: 'Room full' }
const newUser = { id, name, room }
users.push(newUser)
return { newUser }
}
const removeUser = id => {
const removeIndex = users.findIndex(user => user.id === id)
if(removeIndex!==-1)
return users.splice(removeIndex, 1)[0]
}
const getUser = id => {
return users.find(user => user.id === id)
}
const getUsersInRoom = room => {
return users.filter(user => user.room === room)
}
module.exports = { addUser, removeUser, getUser, getUsersInRoom }