shortdeck1.3:ui and fix

This commit is contained in:
2025-10-11 18:24:24 +08:00
parent 4763f9a630
commit 8f30e75e1a
69 changed files with 2753 additions and 97 deletions

856
client/index.html Normal file
View File

@@ -0,0 +1,856 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ShortDeck Poker - Professional Client</title>
<!-- React & TypeScript (via CDN for quick setup) -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script crossorigin src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Custom Styles -->
<style>
body {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
font-family: 'Inter', system-ui, sans-serif;
min-height: 100vh;
}
.poker-table {
background: radial-gradient(ellipse at center, #059669 0%, #047857 70%, #065f46 100%);
border: 8px solid #92400e;
box-shadow: 0 0 50px rgba(0, 0, 0, 0.7);
}
.card {
width: 60px;
height: 84px;
border-radius: 8px;
background: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.card.red {
color: #dc2626;
}
.card.back {
background: linear-gradient(45deg, #1e40af, #3730a3);
color: white;
}
.player-seat {
transition: all 0.3s ease;
}
.player-seat.active {
box-shadow: 0 0 20px rgba(34, 197, 94, 0.6);
border: 2px solid #10b981;
}
.action-button {
transition: all 0.2s ease;
}
.action-button:hover {
transform: translateY(-2px);
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useContext, createContext, useCallback } = React;
// API Configuration
const API_BASE_URL = 'http://127.0.0.1:8001';
// Game Context
const GameContext = createContext();
// TypeScript-like interfaces (comments for structure)
/*
interface GameState {
gameId: string;
playerId: number | null;
playerName: string;
players: string[];
dealerIndex: number;
currentTurn: number;
stage: string;
totalPot: number;
sidePots: Array<{amount: number, eligible_players: number[]}>;
boardCards: string[];
playerCards: string[];
stacks: number[];
playerStates: string[];
currentPot: number[];
actions: {
can_act: boolean;
can_fold?: boolean;
can_call?: boolean;
can_raise?: boolean;
can_check?: boolean;
can_bet?: boolean;
can_allin?: boolean;
call_amount?: number;
min_raise?: number;
max_raise?: number;
};
handStrength?: {
handType: string;
description: string;
strength: number;
cards: string[];
};
showdown?: any;
loading: boolean;
error: string | null;
}
*/
// Game Provider Component
const GameProvider = ({ children }) => {
const [gameState, setGameState] = useState({
gameId: '',
playerId: null,
playerName: '',
players: [],
dealerIndex: 0,
currentTurn: 0,
stage: 'waiting',
totalPot: 0,
sidePots: [],
boardCards: [],
playerCards: [],
stacks: [],
playerStates: [],
currentPot: [],
actions: { can_act: false },
handStrength: null,
showdown: null,
winnings: null,
loading: false,
error: null,
});
// API Functions
const joinGame = async (playerName) => {
setGameState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await axios.post(`${API_BASE_URL}/join`, { name: playerName });
setGameState(prev => ({
...prev,
playerId: response.data.player_id,
playerName: response.data.name,
loading: false
}));
} catch (error) {
setGameState(prev => ({
...prev,
error: error.response?.data?.detail || '加入游戏失败',
loading: false
}));
}
};
const applyAction = async (action, amount = null) => {
if (gameState.playerId === null) return;
setGameState(prev => ({ ...prev, loading: true }));
try {
const payload = {
player_id: gameState.playerId,
action,
amount
};
await axios.post(`${API_BASE_URL}/apply_action`, payload);
await fetchGameInfo();
} catch (error) {
setGameState(prev => ({
...prev,
error: error.response?.data?.detail || '行动失败',
loading: false
}));
}
};
const fetchGameInfo = async () => {
if (gameState.playerId === null) return;
try {
const response = await axios.get(`${API_BASE_URL}/info/${gameState.playerId}`);
const data = response.data;
console.log('Debug - player_cards from API:', data.player_cards);
console.log('Debug - player_cards type:', typeof data.player_cards);
console.log('Debug - is array:', Array.isArray(data.player_cards));
console.log('Frontend Debug - Raw API data.player_cards:', data.player_cards);
console.log('Frontend Debug - Player ID:', gameState.playerId);
setGameState(prev => ({
...prev,
gameId: data.game_id || '',
players: data.players || [],
dealerIndex: data.dealer_index || 0,
currentTurn: data.current_turn || 0,
stage: data.stage || 'waiting',
totalPot: data.total_pot || 0,
sidePots: data.side_pots || [],
boardCards: data.board_cards || [],
playerCards: data.player_cards || [],
stacks: data.stacks || [],
playerStates: data.player_states || [],
currentPot: data.current_pot || [],
actions: data.actions || { can_act: false },
showdown: data.showdown_hands || null,
winnings: data.winnings || null,
loading: false,
error: null
}));
} catch (error) {
console.error('Failed to fetch game info:', error);
}
};
const fetchHandStrength = async () => {
if (gameState.playerId === null || gameState.playerCards.length === 0) return;
try {
const response = await axios.get(`${API_BASE_URL}/hand_strength/${gameState.playerId}`);
if (!response.data.error) {
setGameState(prev => ({
...prev,
handStrength: response.data
}));
}
} catch (error) {
console.error('Failed to fetch hand strength:', error);
}
};
const resetGame = async (resetChips = true) => {
try {
if (resetChips) {
await axios.post(`${API_BASE_URL}/reset`, { keep_chips: false });
setGameState({
gameId: '',
playerId: null,
playerName: '',
players: [],
dealerIndex: 0,
currentTurn: 0,
stage: 'waiting',
totalPot: 0,
sidePots: [],
boardCards: [],
playerCards: [],
stacks: [],
playerStates: [],
currentPot: [],
actions: { can_act: false },
handStrength: null,
showdown: null,
loading: false,
error: null,
});
} else {
await axios.post(`${API_BASE_URL}/reset`, { keep_chips: true });
setGameState(prev => ({
...prev,
stage: 'waiting',
totalPot: 0,
sidePots: [],
boardCards: [],
playerCards: [],
currentPot: [],
actions: { can_act: false },
handStrength: null,
showdown: null,
loading: false,
error: null,
}));
setTimeout(() => {
fetchGameInfo();
}, 1000);
}
} catch (error) {
console.error('Failed to reset game:', error);
}
};
const leaveTable = async () => {
try {
await resetGame(true);
} catch (error) {
console.error('Failed to leave table:', error);
}
};
// Polling for game state updates
useEffect(() => {
if (gameState.playerId !== null) {
const interval = setInterval(fetchGameInfo, 1000);
return () => clearInterval(interval);
}
}, [gameState.playerId]);
// Fetch hand strength when cards change
useEffect(() => {
if (gameState.playerCards.length > 0) {
fetchHandStrength();
}
}, [gameState.playerCards.length, gameState.boardCards.length]);
return React.createElement(GameContext.Provider, {
value: {
gameState,
joinGame,
applyAction,
fetchGameInfo,
resetGame,
leaveTable
}
}, children);
};
const useGame = () => {
const context = useContext(GameContext);
if (!context) {
throw new Error('useGame must be used within a GameProvider');
}
return context;
};
// Card Component
const Card = ({ card, isBack = false }) => {
if (isBack || !card) {
return React.createElement('div', {
className: 'card back'
}, '🂠');
}
// Convert card notation to display format
const convertCard = (cardStr) => {
if (!cardStr || cardStr.length < 2) return cardStr;
const rank = cardStr.slice(0, -1);
const suit = cardStr.slice(-1).toLowerCase();
const suitSymbols = {
's': '♠',
'h': '♥',
'd': '♦',
'c': '♣'
};
return rank + (suitSymbols[suit] || suit);
};
const displayCard = convertCard(card);
const isRed = displayCard.includes('♥') || displayCard.includes('♦');
return React.createElement('div', {
className: `card ${isRed ? 'red' : ''}`
}, displayCard);
};
const PlayerSeat = ({ player, index, isCurrentPlayer, isDealer, isCurrentTurn, stack, currentBet, state, cards }) => {
const safeStack = typeof stack === 'number' ? stack : 0;
const safeCurrentBet = typeof currentBet === 'number' ? currentBet : 0;
const safeState = state || 'WAITING';
const safeCards = Array.isArray(cards) ? cards : [];
if (isCurrentPlayer) {
console.log('Debug - Current player cards:', cards);
console.log('Debug - Safe cards:', safeCards);
console.log('Debug - Cards length:', safeCards.length);
console.log('Debug - isCurrentPlayer:', isCurrentPlayer);
console.log('Debug - Will show cards:', isCurrentPlayer && safeCards.length > 0);
}
return React.createElement('div', {
className: `player-seat bg-gray-800 rounded-lg p-4 text-white relative ${isCurrentTurn ? 'active' : ''} ${safeState !== 'ACTIVE' && safeState !== 'active' ? 'opacity-50' : ''}`
},
// Dealer Button
isDealer && React.createElement('div', {
className: 'absolute -top-2 -right-2 bg-yellow-500 text-black rounded-full w-8 h-8 flex items-center justify-center text-xs font-bold'
}, 'D'),
// Player Name
React.createElement('div', {
className: 'font-bold mb-2 text-center'
}, `${player || '未知'}${isCurrentPlayer ? ' (你)' : ''}`),
// Cards
React.createElement('div', {
className: 'flex gap-1 justify-center mb-2'
},
isCurrentPlayer && safeCards.length > 0
? safeCards.map((card, idx) => React.createElement(Card, { key: idx, card }))
: [React.createElement(Card, { key: 0, isBack: true }), React.createElement(Card, { key: 1, isBack: true })]
),
// Stack and Bet Info
React.createElement('div', { className: 'text-sm text-center' },
React.createElement('div', null, `筹码: $${safeStack}`),
safeCurrentBet > 0 && React.createElement('div', { className: 'text-yellow-400' }, `下注: $${safeCurrentBet}`),
React.createElement('div', {
className: `text-xs mt-1 font-bold ${
safeState === 'allin' ? 'text-purple-400' :
safeState === 'folded' ? 'text-red-400' :
safeState === 'active' ? 'text-green-400' :
'text-gray-400'
}`
}, safeState === 'allin' ? '🎯 ALL-IN' :
safeState === 'folded' ? '❌ 已弃牌' :
safeState === 'active' ? '✅ 活跃' :
safeState.toUpperCase())
)
);
};
// Game Table Component
const GameTable = () => {
const { gameState } = useGame();
return React.createElement('div', { className: 'poker-table relative rounded-full w-96 h-64 mx-auto mb-8' },
// Center Area - Community Cards & Pot
React.createElement('div', { className: 'absolute inset-0 flex flex-col items-center justify-center' },
// Community Cards
React.createElement('div', { className: 'flex gap-2 mb-4' },
Array.from({ length: 5 }, (_, idx) =>
React.createElement(Card, {
key: idx,
card: gameState.boardCards[idx],
isBack: !gameState.boardCards[idx]
})
)
),
// Pot Display
React.createElement('div', { className: 'text-white text-center' },
React.createElement('div', { className: 'text-2xl font-bold' }, `$${gameState.totalPot}`),
React.createElement('div', { className: 'text-sm' }, `${gameState.stage} 阶段`),
gameState.sidePots.length > 0 && React.createElement('div', { className: 'text-xs mt-1' },
`边池: ${gameState.sidePots.map(pot => `$${pot.amount}`).join(', ')}`
)
)
)
);
};
// Hand Type Display Component
const HandStrengthDisplay = () => {
const { gameState } = useGame();
console.log('HandStrengthDisplay Debug:', {
hasHandStrength: !!gameState.handStrength,
playerCardsLength: gameState.playerCards.length,
handStrength: gameState.handStrength
});
if (!gameState.handStrength || gameState.playerCards.length === 0) {
return React.createElement('div', { className: 'bg-gray-800 rounded-lg p-4 mb-4' },
React.createElement('div', { className: 'text-gray-400 text-sm' }, '等待手牌数据...')
);
}
return React.createElement('div', { className: 'bg-gray-800 rounded-lg p-4 mb-4' },
React.createElement('h3', { className: 'text-white font-bold mb-2' }, '当前牌型'),
React.createElement('div', { className: 'text-yellow-400 text-lg font-semibold' }, gameState.handStrength.hand_type),
React.createElement('div', { className: 'text-gray-400 text-sm mt-1' }, gameState.handStrength.description)
);
};
// Game Result Component
const GameResult = () => {
const { gameState, resetGame, leaveTable } = useGame();
if (!gameState.showdown || Object.keys(gameState.showdown).length === 0) {
return null;
}
useEffect(() => {
const timer = setTimeout(() => {
handleContinueGame();
}, 3000);
return () => clearTimeout(timer);
}, [gameState.showdown]);
const handleContinueGame = () => {
resetGame(false);
};
const handleLeaveTable = () => {
if (confirm('确定要离开牌桌吗?你的筹码将被重置。')) {
leaveTable();
}
};
return React.createElement('div', { className: 'bg-gray-800 rounded-lg p-6 mb-4' },
React.createElement('h3', { className: 'text-white font-bold text-xl mb-4 text-center' }, '🎉 游戏结果 - 摊牌'),
React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
Object.entries(gameState.showdown).map(([playerId, handInfo]) => {
const playerName = gameState.players[parseInt(playerId)] || `玩家${playerId}`;
const isWinner = handInfo.is_winner || false;
const winAmount = gameState.winnings && gameState.winnings[playerId] ? gameState.winnings[playerId] : 0;
return React.createElement('div', {
key: playerId,
className: `p-4 rounded-lg ${isWinner ? 'bg-green-700 border-2 border-green-400' : 'bg-gray-700'}`
},
React.createElement('div', { className: 'text-center mb-2' },
React.createElement('div', { className: `font-bold ${isWinner ? 'text-green-200' : 'text-white'}` },
`${playerName} ${isWinner ? '🏆 获胜者' : ''}`),
React.createElement('div', { className: 'text-sm text-gray-300' }, handInfo.hand_type || '无牌型'),
isWinner && winAmount > 0 && React.createElement('div', { className: 'text-yellow-400 text-sm font-bold' },
`+$${winAmount}`)
),
React.createElement('div', { className: 'flex gap-1 justify-center mb-2' },
(handInfo.cards || []).map((card, idx) => React.createElement(Card, { key: idx, card }))
),
React.createElement('div', { className: 'text-xs text-center text-gray-400' },
handInfo.description || '无描述')
);
})
),
React.createElement('div', { className: 'mt-4 pt-4 border-t border-gray-600' },
React.createElement('h4', { className: 'text-white font-bold mb-3 text-center' }, '💰 本轮结果'),
React.createElement('div', { className: 'text-center mb-3' },
React.createElement('div', { className: 'text-yellow-400 font-bold' },
`总奖池: $${gameState.totalPot || 0}`)
),
gameState.winnings && Object.keys(gameState.winnings).length > 0 &&
React.createElement('div', { className: 'space-y-2' },
React.createElement('h5', { className: 'text-white font-semibold mb-2' }, '🏆 奖金分配:'),
Object.entries(gameState.winnings).map(([playerId, amount]) => {
const playerName = gameState.players[parseInt(playerId)] || `玩家${playerId}`;
const currentStack = gameState.stacks[parseInt(playerId)] || 0;
return React.createElement('div', {
key: playerId,
className: 'flex justify-between items-center bg-green-800 p-2 rounded'
},
React.createElement('span', { className: 'text-green-200 font-bold' }, playerName),
React.createElement('div', { className: 'text-right' },
React.createElement('div', { className: 'text-green-400 font-bold' }, `+$${amount}`),
React.createElement('div', { className: 'text-gray-300 text-xs' }, `总筹码: $${currentStack}`)
)
);
})
)
),
React.createElement('div', { className: 'mt-6 pt-4 border-t border-gray-600 flex gap-4 justify-center' },
React.createElement('button', {
onClick: handleContinueGame,
className: 'bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-bold flex items-center gap-2 transition-all'
},
'🎮 继续下一轮'),
React.createElement('button', {
onClick: handleLeaveTable,
className: 'bg-red-600 hover:bg-red-700 text-white px-6 py-3 rounded-lg font-bold flex items-center gap-2 transition-all'
},
'🚪 离开牌桌')
)
);
};
// Action Panel Component
const ActionPanel = () => {
const { gameState, applyAction } = useGame();
const [betAmount, setBetAmount] = useState(0);
if (gameState.stage === 'finished' && gameState.showdown && Object.keys(gameState.showdown).length > 0) {
return React.createElement(GameResult);
}
const isMyTurn = gameState.actions.can_act;
useEffect(() => {
if (gameState.actions.min_raise) {
setBetAmount(gameState.actions.min_raise);
}
}, [gameState.actions.min_raise]);
console.log('ActionPanel Debug:', {
playerId: gameState.playerId,
currentTurn: gameState.currentTurn,
canAct: gameState.actions.can_act,
isMyTurn: isMyTurn,
stage: gameState.stage,
actions: gameState.actions
});
if (!isMyTurn) {
const currentPlayerName = gameState.players[gameState.currentTurn] || '未知玩家';
const myState = gameState.playerStates && gameState.playerStates[gameState.playerId];
const reason = gameState.actions.reason || '';
let statusMessage = '等待行动中...';
let statusColor = 'text-gray-400';
if (reason.includes('allin')) {
statusMessage = '🎯 你已 ALL-IN';
statusColor = 'text-purple-400';
} else if (reason.includes('folded')) {
statusMessage = '❌ 你已弃牌';
statusColor = 'text-red-400';
}
return React.createElement('div', { className: 'bg-gray-800 rounded-lg p-4 text-center text-white' },
React.createElement('div', { className: `text-lg mb-2 ${statusColor} font-bold` }, statusMessage),
React.createElement('div', { className: 'text-gray-400' }, `等待 ${currentPlayerName} 行动`),
React.createElement('div', { className: 'text-xs text-gray-500 mt-2' },
`状态: ${reason || '等待中'}`)
);
}
return React.createElement('div', { className: 'bg-gray-800 rounded-lg p-4' },
React.createElement('h3', { className: 'text-white font-bold mb-4 text-center' }, '选择行动'),
// Action Buttons Row 1
React.createElement('div', { className: 'flex gap-2 mb-4 justify-center' },
gameState.actions.can_fold && React.createElement('button', {
className: 'action-button bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg font-bold',
onClick: () => applyAction('fold'),
disabled: gameState.loading
}, gameState.loading ? '⏳' : '弃牌'),
gameState.actions.can_check && React.createElement('button', {
className: 'action-button bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold',
onClick: () => applyAction('check'),
disabled: gameState.loading
}, gameState.loading ? '⏳' : '过牌'),
gameState.actions.can_call && React.createElement('button', {
className: 'action-button bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg font-bold',
onClick: () => applyAction('call'),
disabled: gameState.loading
}, gameState.loading ? '⏳' : `跟注 $${gameState.actions.call_amount || 0}`),
gameState.actions.can_allin && React.createElement('button', {
className: 'action-button bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg font-bold',
onClick: () => applyAction('allin'),
disabled: gameState.loading
}, gameState.loading ? '⏳' : `All-in $${gameState.stacks[gameState.playerId] || 0}`)
),
// Betting Controls
(gameState.actions.can_bet || gameState.actions.can_raise) && React.createElement('div', { className: 'border-t border-gray-600 pt-4' },
React.createElement('div', { className: 'flex items-center gap-2 mb-3' },
React.createElement('label', { className: 'text-white text-sm font-medium' }, '下注金额:'),
React.createElement('input', {
type: 'range',
min: gameState.actions.min_raise || 0,
max: gameState.actions.max_raise || gameState.stacks[gameState.playerId] || 0,
value: betAmount,
onChange: (e) => setBetAmount(parseInt(e.target.value)),
className: 'flex-1'
}),
React.createElement('input', {
type: 'number',
value: betAmount,
onChange: (e) => setBetAmount(parseInt(e.target.value) || 0),
min: gameState.actions.min_raise || 0,
max: gameState.actions.max_raise || 0,
className: 'w-20 px-2 py-1 bg-gray-700 text-white rounded text-center'
})
),
React.createElement('div', { className: 'flex gap-2 justify-center' },
gameState.actions.can_bet && React.createElement('button', {
className: 'action-button bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg font-bold',
onClick: () => applyAction('bet', betAmount),
disabled: gameState.loading
}, gameState.loading ? '⏳' : `下注 $${betAmount}`),
gameState.actions.can_raise && React.createElement('button', {
className: 'action-button bg-orange-600 hover:bg-orange-700 text-white px-4 py-2 rounded-lg font-bold',
onClick: () => applyAction('raise', betAmount),
disabled: gameState.loading
}, gameState.loading ? '⏳' : `加注至 $${betAmount}`)
)
)
);
};
// Player Seats Layout Component
const PlayersLayout = () => {
const { gameState } = useGame();
if (!gameState.players || gameState.players.length === 0) {
return React.createElement('div', { className: 'text-white text-center' }, '等待玩家加入...');
}
return React.createElement('div', { className: 'grid grid-cols-3 gap-4 mb-8' },
gameState.players.map((player, index) =>
React.createElement(PlayerSeat, {
key: index,
player: player || '未知玩家',
index,
isCurrentPlayer: index === gameState.playerId,
isDealer: index === gameState.dealerIndex,
isCurrentTurn: index === gameState.currentTurn,
stack: (gameState.stacks && gameState.stacks[index]) || 0,
currentBet: (gameState.currentPot && gameState.currentPot[index]) || 0,
state: (gameState.playerStates && gameState.playerStates[index]) || 'WAITING',
cards: index === gameState.playerId ? (() => {
console.log('PlayersList Debug - gameState.playerCards:', gameState.playerCards);
console.log('PlayersList Debug - index:', index, 'playerId:', gameState.playerId);
return gameState.playerCards || [];
})() : []
})
)
);
};
// Join Game Form Component
const JoinGameForm = () => {
const { joinGame, gameState } = useGame();
const [playerName, setPlayerName] = useState('');
const handleJoinGame = (e) => {
e.preventDefault();
if (playerName.trim()) {
joinGame(playerName.trim());
}
};
return React.createElement('div', { className: 'max-w-md mx-auto bg-gray-800 rounded-lg p-6' },
React.createElement('h2', { className: 'text-2xl font-bold text-white mb-6 text-center' }, 'ShortDeck Poker'),
React.createElement('form', {},
React.createElement('div', { className: 'mb-6' },
React.createElement('label', { className: 'block text-white text-sm font-bold mb-2' }, '玩家名称'),
React.createElement('input', {
type: 'text',
value: playerName,
onChange: (e) => setPlayerName(e.target.value),
placeholder: '输入你的名称',
className: 'w-full px-3 py-2 bg-gray-700 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500',
disabled: gameState.loading
})
),
React.createElement('div', { className: 'space-y-3' },
React.createElement('button', {
type: 'button',
onClick: handleJoinGame,
className: 'w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition-colors',
disabled: gameState.loading || !playerName.trim()
}, gameState.loading ? '加入中...' : '🎰 加入游戏'),
React.createElement('div', {
className: 'mt-4 p-3 bg-gray-700 rounded-lg text-sm text-gray-300'
}, React.createElement('div', { className: 'font-bold mb-2' }, '💡 如何与AI对战'),
React.createElement('ol', { className: 'list-decimal list-inside space-y-1 text-xs' },
React.createElement('li', null, '点击"加入游戏"按钮'),
React.createElement('li', null, '在新终端中运行: python client2_random_agent.py'),
React.createElement('li', null, 'RandomAgent会自动加入游戏与您对战')))
)
),
gameState.error && React.createElement('div', {
className: 'mt-4 p-3 bg-red-600 text-white rounded-lg text-sm'
}, gameState.error)
);
};
// Main Game Component
const GameApp = () => {
const { gameState, resetGame, leaveTable } = useGame();
if (gameState.playerId === null) {
return React.createElement(JoinGameForm);
}
const handleLeaveTable = () => {
if (confirm('确定要离开牌桌吗?你的筹码将被重置,需要重新加入游戏。')) {
leaveTable();
}
};
return React.createElement('div', { className: 'container mx-auto px-4 py-8' },
// Header
React.createElement('div', { className: 'flex justify-between items-center mb-8' },
React.createElement('h1', { className: 'text-3xl font-bold text-white' }, 'ShortDeck Poker'),
React.createElement('div', { className: 'flex gap-4 items-center' },
React.createElement('div', { className: 'text-white text-sm' },
React.createElement('div', null, `游戏ID: ${(gameState.gameId || '').slice(0, 8) || '未知'}...`),
React.createElement('div', null, `玩家: ${gameState.playerName || '未知'}`)
),
React.createElement('div', { className: 'flex gap-2' },
React.createElement('button', {
onClick: () => resetGame(true),
className: 'bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg text-sm transition-colors'
}, '🔄 重置游戏'),
React.createElement('button', {
onClick: handleLeaveTable,
className: 'bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm transition-colors'
}, '🚪 离开牌桌')
)
)
),
// Game Table
React.createElement(GameTable),
// Players Layout
React.createElement(PlayersLayout),
// Hand Strength (if available)
React.createElement(HandStrengthDisplay),
// Action Panel
React.createElement(ActionPanel),
// Error Display
gameState.error && React.createElement('div', {
className: 'fixed top-4 right-4 bg-red-600 text-white p-4 rounded-lg shadow-lg max-w-sm'
}, gameState.error)
);
};
// App Initialization
const App = () => {
return React.createElement(GameProvider,
null,
React.createElement(GameApp)
);
};
// Render the App
ReactDOM.render(React.createElement(App), document.getElementById('root'));
console.log('🚀 ShortDeck Poker Client v1.0 - Professional Edition Loaded');
</script>
</body>
</html>