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:
4
uno-online/.gitignore
vendored
Normal file
4
uno-online/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
230
uno-online/BOT_ACTION_SPEC.md
Normal file
230
uno-online/BOT_ACTION_SPEC.md
Normal 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
|
||||
})
|
||||
```
|
||||
266
uno-online/BOT_INTEGRATION_COMPLETE.md
Normal file
266
uno-online/BOT_INTEGRATION_COMPLETE.md
Normal 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!
|
||||
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.
|
||||
176
uno-online/BOT_QUICK_REF.md
Normal file
176
uno-online/BOT_QUICK_REF.md
Normal 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.
|
||||
530
uno-online/IMPLEMENTATION_SUMMARY.md
Normal file
530
uno-online/IMPLEMENTATION_SUMMARY.md
Normal 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
21
uno-online/LICENSE
Normal 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.
|
||||
108
uno-online/QUICK_START_BOT.md
Normal file
108
uno-online/QUICK_START_BOT.md
Normal 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
104
uno-online/README.md
Normal 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 that’s 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
152
uno-online/SETUP_NOTES.md
Normal 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.
|
||||
0
uno-online/TESTING_GUIDE.md
Normal file
0
uno-online/TESTING_GUIDE.md
Normal file
20304
uno-online/client/package-lock.json
generated
Normal file
20304
uno-online/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
uno-online/client/package.json
Normal file
41
uno-online/client/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
uno-online/client/public/favicon.ico
Normal file
BIN
uno-online/client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
50
uno-online/client/public/index.html
Normal file
50
uno-online/client/public/index.html
Normal 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>
|
||||
25
uno-online/client/public/manifest.json
Normal file
25
uno-online/client/public/manifest.json
Normal 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"
|
||||
}
|
||||
3
uno-online/client/public/robots.txt
Normal file
3
uno-online/client/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
387
uno-online/client/src/App.css
Normal file
387
uno-online/client/src/App.css
Normal 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;
|
||||
}
|
||||
17
uno-online/client/src/App.js
Normal file
17
uno-online/client/src/App.js
Normal 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
|
||||
1595
uno-online/client/src/components/Game.js
Normal file
1595
uno-online/client/src/components/Game.js
Normal file
File diff suppressed because it is too large
Load Diff
27
uno-online/client/src/components/Homepage.js
Normal file
27
uno-online/client/src/components/Homepage.js
Normal 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
|
||||
9
uno-online/client/src/components/Spinner.js
Normal file
9
uno-online/client/src/components/Spinner.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const Spinner = () => {
|
||||
return (
|
||||
<div className="loader">Loading...</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
14
uno-online/client/src/index.css
Normal file
14
uno-online/client/src/index.css
Normal 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;
|
||||
}
|
||||
14
uno-online/client/src/index.js
Normal file
14
uno-online/client/src/index.js
Normal 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')
|
||||
)
|
||||
231
uno-online/client/src/utils/botActionExecutor.js
Normal file
231
uno-online/client/src/utils/botActionExecutor.js
Normal 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 }
|
||||
}
|
||||
26
uno-online/client/src/utils/cardAssets.js
Normal file
26
uno-online/client/src/utils/cardAssets.js
Normal 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,
|
||||
}
|
||||
150
uno-online/client/src/utils/cardParser.js
Normal file
150
uno-online/client/src/utils/cardParser.js
Normal 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
|
||||
}));
|
||||
};
|
||||
167
uno-online/client/src/utils/gameStateBuilder.js
Normal file
167
uno-online/client/src/utils/gameStateBuilder.js
Normal 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
|
||||
};
|
||||
};
|
||||
8
uno-online/client/src/utils/packOfCards.js
Normal file
8
uno-online/client/src/utils/packOfCards.js
Normal 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'
|
||||
]
|
||||
9
uno-online/client/src/utils/randomCodeGenerator.js
Normal file
9
uno-online/client/src/utils/randomCodeGenerator.js
Normal 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;
|
||||
}
|
||||
9
uno-online/client/src/utils/shuffleArray.js
Normal file
9
uno-online/client/src/utils/shuffleArray.js
Normal 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
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
26
uno-online/package.json
Normal 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
158
uno-online/server.js
Normal 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
76
uno-online/test-bot-action.js
Executable 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();
|
||||
52
uno-online/test-integration.js
Normal file
52
uno-online/test-integration.js
Normal 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
28
uno-online/users.js
Normal 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 }
|
||||
Reference in New Issue
Block a user