shortdeck1.3:ui and fix
This commit is contained in:
856
client/index.html
Normal file
856
client/index.html
Normal 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>
|
||||
585
client/simple.html
Normal file
585
client/simple.html
Normal 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>
|
||||
Reference in New Issue
Block a user