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

585
client/simple.html Normal file
View File

@@ -0,0 +1,585 @@
<!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>