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

585 lines
26 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;
margin: 0;
padding: 0;
}
.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 style="color: white; text-align: center; padding: 20px;">
正在加载...
</div>
</div>
<script type="text/babel">
console.log('🚀 开始加载 ShortDeck Poker Client');
const { useState, useEffect, useContext, createContext } = React;
if (typeof React === 'undefined') {
document.getElementById('root').innerHTML = '<div style="color: red; text-align: center; padding: 20px;">React加载失败</div>';
}
// API Configuration
const API_BASE_URL = 'http://127.0.0.1:8001';
// Game Context
const GameContext = createContext();
// Game Provider Component
const GameProvider = ({ children }) => {
const [gameState, setGameState] = useState({
playerId: null,
playerName: '',
players: [],
loading: false,
error: null,
});
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
}));
setTimeout(fetchGameInfo, 500);
} 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;
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 },
error: null
}));
if (data.player_cards && data.player_cards.length > 0) {
fetchHandStrength();
}
} catch (error) {
console.error('Failed to fetch game info:', error);
}
};
const fetchHandStrength = async () => {
if (gameState.playerId === null) 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 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);
setTimeout(fetchGameInfo, 500);
} catch (error) {
setGameState(prev => ({
...prev,
error: error.response?.data?.detail || '行动失败',
loading: false
}));
}
};
const resetGame = async () => {
try {
await axios.post(`${API_BASE_URL}/reset`);
setGameState({
playerId: null,
playerName: '',
gameId: '',
players: [],
dealerIndex: 0,
currentTurn: 0,
stage: 'waiting',
totalPot: 0,
sidePots: [],
boardCards: [],
playerCards: [],
stacks: [],
playerStates: [],
currentPot: [],
actions: { can_act: false },
handStrength: null,
loading: false,
error: null,
});
} catch (error) {
console.error('Reset failed:', error);
}
};
useEffect(() => {
if (gameState.playerId !== null) {
const interval = setInterval(fetchGameInfo, 2000);
return () => clearInterval(interval);
}
}, [gameState.playerId]);
return React.createElement(GameContext.Provider, {
value: { gameState, joinGame, applyAction, resetGame, fetchGameInfo }
}, children);
};
const useGame = () => {
const context = useContext(GameContext);
if (!context) {
throw new Error('useGame must be used within a GameProvider');
}
return context;
};
// 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: 'min-h-screen flex items-center justify-center px-4'
},
React.createElement('div', { className: 'max-w-md w-full 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', { onSubmit: handleJoinGame },
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('button', {
type: 'submit',
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)
)
);
};
// Card format conversion function
const formatCard = (card) => {
if (!card) return '';
// Convert backend format (e.g., 'Kd', 'Ah') to frontend format (e.g., 'K♦', 'A♥')
const suitMap = {
's': '♠',
'h': '♥',
'd': '♦',
'c': '♣'
};
// Extract rank and suit from card string like 'Kd'
const rank = card.slice(0, -1); // Everything except last character
const suit = card.slice(-1).toLowerCase(); // Last character, lowercase
return rank + (suitMap[suit] || suit);
};
// Card Component
const Card = ({ card, isBack = false }) => {
if (isBack || !card) {
return React.createElement('div', {
className: 'card back'
}, '🂠');
}
const formattedCard = formatCard(card);
const isRed = formattedCard.includes('♥') || formattedCard.includes('♦');
return React.createElement('div', {
className: `card ${isRed ? 'red' : ''}`
}, formattedCard);
};
// Player Seat Component
const PlayerSeat = ({ player, index, isCurrentPlayer, isDealer, isCurrentTurn, stack, currentBet, state, cards }) => {
return React.createElement('div', {
className: `bg-gray-800 rounded-lg p-4 text-white relative transition-all ${isCurrentTurn ? 'player-seat active' : ''} ${state !== '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 && cards && cards.length > 0
? cards.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, `筹码: $${stack || 0}`),
currentBet > 0 && React.createElement('div', { className: 'text-yellow-400' }, `下注: $${currentBet}`),
React.createElement('div', { className: 'text-xs text-gray-400' }, state || 'WAITING')
)
);
};
// Action Panel Component
const ActionPanel = () => {
const { gameState, applyAction } = useGame();
const [raiseAmount, setRaiseAmount] = useState('');
if (!gameState.actions.can_act || gameState.loading) {
return React.createElement('div', {
className: 'bg-gray-800 rounded-lg p-4 text-center text-gray-400'
}, gameState.loading ? '处理中...' : '等待其他玩家行动...');
}
const { actions } = gameState;
return React.createElement('div', { className: 'bg-gray-800 rounded-lg p-4' },
React.createElement('h3', { className: 'text-white font-bold mb-4 text-center' }, '选择你的行动'),
React.createElement('div', { className: 'grid grid-cols-2 gap-3 mb-4' },
// Fold
actions.can_fold && React.createElement('button', {
onClick: () => applyAction('fold'),
className: 'action-button bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded-lg font-bold transition-all'
}, '弃牌 (Fold)'),
// Check
actions.can_check && React.createElement('button', {
onClick: () => applyAction('check'),
className: 'action-button bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg font-bold transition-all'
}, '过牌 (Check)'),
// Call
actions.can_call && React.createElement('button', {
onClick: () => applyAction('call'),
className: 'action-button bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg font-bold transition-all'
}, `跟注 $${actions.call_amount || 0}`),
// Bet
actions.can_bet && React.createElement('button', {
onClick: () => applyAction('bet', parseInt(raiseAmount) || actions.min_raise),
className: 'action-button bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded-lg font-bold transition-all'
}, '下注 (Bet)')
),
// Raise section
actions.can_raise && React.createElement('div', { className: 'border-t border-gray-600 pt-4' },
React.createElement('div', { className: 'flex gap-2 items-center mb-2' },
React.createElement('input', {
type: 'number',
value: raiseAmount,
onChange: (e) => setRaiseAmount(e.target.value),
placeholder: `最小: ${actions.min_raise || 0}`,
min: actions.min_raise || 0,
max: actions.max_raise || 1000,
className: 'flex-1 px-3 py-2 bg-gray-700 text-white rounded border-0 focus:ring-2 focus:ring-blue-500'
}),
React.createElement('button', {
onClick: () => applyAction('raise', parseInt(raiseAmount) || actions.min_raise),
className: 'action-button bg-orange-600 hover:bg-orange-700 text-white py-2 px-4 rounded-lg font-bold transition-all'
}, '加注 (Raise)')
),
React.createElement('div', { className: 'text-xs text-gray-400' },
`加注范围: $${actions.min_raise || 0} - $${actions.max_raise || 1000}`
)
)
);
};
// Game Table Component
const GameTable = () => {
const { gameState } = useGame();
return React.createElement('div', { className: 'poker-table rounded-full w-96 h-64 mx-auto mb-8 flex items-center justify-center relative' },
// Community Cards
React.createElement('div', { className: 'flex gap-2' },
gameState.boardCards && gameState.boardCards.length > 0
? gameState.boardCards.map((card, idx) => React.createElement(Card, { key: idx, card }))
: [1, 2, 3, 4, 5].map(i => React.createElement('div', {
key: i,
className: 'w-12 h-16 border-2 border-dashed border-gray-400 rounded opacity-30'
}))
),
// Pot Info
React.createElement('div', {
className: 'absolute top-4 left-1/2 transform -translate-x-1/2 bg-yellow-600 text-white px-4 py-2 rounded-full font-bold'
}, `底池: $${gameState.totalPot || 0}`)
);
};
// Players Layout
const PlayersLayout = () => {
const { gameState } = useGame();
if (gameState.players.length === 0) {
return React.createElement('div', {
className: 'text-center text-gray-400 py-8'
}, '等待其他玩家加入...');
}
return React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-2 gap-4 mb-8' },
gameState.players.map((player, index) =>
React.createElement(PlayerSeat, {
key: index,
player,
index,
isCurrentPlayer: index === gameState.playerId,
isDealer: index === gameState.dealerIndex,
isCurrentTurn: index === gameState.currentTurn,
stack: gameState.stacks[index],
currentBet: gameState.currentPot[index],
state: gameState.playerStates[index],
cards: index === gameState.playerId ? gameState.playerCards : []
})
)
);
};
// Hand Strength Display
const HandStrengthDisplay = () => {
const { gameState } = useGame();
if (!gameState.handStrength) return null;
const { handStrength } = gameState;
const strengthPercent = (handStrength.strength * 100).toFixed(1);
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: 'mb-2' },
React.createElement('div', { className: 'text-lg font-bold text-yellow-400' }, handStrength.hand_type),
React.createElement('div', { className: 'text-sm text-gray-300' }, handStrength.description)
),
React.createElement('div', { className: 'bg-gray-700 rounded-full h-2 mb-2' },
React.createElement('div', {
className: 'bg-gradient-to-r from-red-500 via-yellow-500 to-green-500 h-2 rounded-full transition-all',
style: { width: `${strengthPercent}%` }
})
),
React.createElement('div', { className: 'text-xs text-gray-400 text-center' },
`强度: ${strengthPercent}%`
)
);
};
// Game View Component
const GameView = () => {
const { gameState, resetGame } = useGame();
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' },
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', null, `阶段: ${gameState.stage}`)
),
React.createElement('button', {
onClick: resetGame,
className: 'bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg text-sm'
}, '重置游戏')
)
),
// 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)
);
};
// Main App Component
const GameApp = () => {
const { gameState } = useGame();
if (gameState.playerId === null) {
return React.createElement(JoinGameForm);
}
return React.createElement(GameView);
};
// App Initialization
const App = () => {
return React.createElement(GameProvider, null,
React.createElement(GameApp)
);
};
try {
ReactDOM.render(React.createElement(App), document.getElementById('root'));
console.log('✅ ShortDeck Poker Client 加载成功');
} catch (error) {
console.error('❌ React 渲染失败:', error);
document.getElementById('root').innerHTML = `
<div style="color: red; text-align: center; padding: 20px;">
<h2>渲染失败</h2>
<p>错误: ${error.message}</p>
</div>
`;
}
</script>
</body>
</html>