Files
shortdeck/client/index.html
2025-10-11 18:24:24 +08:00

856 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>