Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-12-11 02:56:49 +01:00
padre dcd0aecd54
commit f5df98c544
Se han modificado 8 ficheros con 148 adiciones y 12 borrados

Ver fichero

@@ -26,6 +26,7 @@ function createGameState(roomId: string): GameState {
isGameOver: false, isGameOver: false,
turnsPassed: 0, turnsPassed: 0,
gameMode: 'waiting', gameMode: 'waiting',
rematchRequests: [],
}; };
} }

Ver fichero

@@ -27,6 +27,7 @@ export default function Home() {
leaveRoom, leaveRoom,
startAIGame, startAIGame,
setError, setError,
requestRematch,
} = useGameStore(); } = useGameStore();
const [showRules, setShowRules] = useState(false); const [showRules, setShowRules] = useState(false);
@@ -362,8 +363,12 @@ export default function Home() {
<GameOver <GameOver
winner={gameState.players.find(p => p.id === gameState.winner) || null} winner={gameState.players.find(p => p.id === gameState.winner) || null}
players={gameState.players} players={gameState.players}
currentPlayerId={currentPlayerId}
rematchRequests={gameState.rematchRequests || []}
onPlayAgain={handlePlayAgain} onPlayAgain={handlePlayAgain}
onRequestRematch={requestRematch}
onLeave={leaveRoom} onLeave={leaveRoom}
isAIGame={roomId?.startsWith('AI-') || false}
/> />
)} )}
</div> </div>

Ver fichero

@@ -7,11 +7,19 @@ import { Player } from '@/lib/types';
interface GameOverProps { interface GameOverProps {
winner: Player | null; winner: Player | null;
players: Player[]; players: Player[];
currentPlayerId: string | null;
rematchRequests: string[];
onPlayAgain: () => void; onPlayAgain: () => void;
onRequestRematch: () => void;
onLeave: () => void; onLeave: () => void;
isAIGame: boolean;
} }
export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProps) { export function GameOver({ winner, players, currentPlayerId, rematchRequests, onPlayAgain, onRequestRematch, onLeave, isAIGame }: GameOverProps) {
const hasRequestedRematch = currentPlayerId ? rematchRequests.includes(currentPlayerId) : false;
const otherPlayersRequested = rematchRequests.filter(id => id !== currentPlayerId);
const waitingForOthers = hasRequestedRematch && otherPlayersRequested.length < players.length - 1;
return ( return (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@@ -57,6 +65,7 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
.map((player, index) => { .map((player, index) => {
const score = player.tiles.reduce((sum, t) => sum + t.left + t.right, 0); const score = player.tiles.reduce((sum, t) => sum + t.left + t.right, 0);
const isWinner = player.id === winner?.id; const isWinner = player.id === winner?.id;
const hasRequestedRematchPlayer = rematchRequests.includes(player.id);
return ( return (
<motion.div <motion.div
@@ -71,7 +80,14 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isWinner && <span className="text-2xl">👑</span>} {isWinner && <span className="text-2xl">👑</span>}
<div> <div>
<div className="font-semibold text-gray-800">{player.name}</div> <div className="font-semibold text-gray-800 flex items-center gap-2">
{player.name}
{hasRequestedRematchPlayer && !isAIGame && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">
Wants rematch
</span>
)}
</div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{player.tiles.length} tiles remaining {player.tiles.length} tiles remaining
</div> </div>
@@ -89,15 +105,35 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{isAIGame ? (
<motion.button <motion.button
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
onClick={onPlayAgain} onClick={onPlayAgain}
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow" className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow"
aria-label="Start a new game" aria-label="Play again against AI"
> >
Play Again Play Again
</motion.button> </motion.button>
) : (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onRequestRematch}
disabled={hasRequestedRematch}
className={`w-full py-3 rounded-lg font-semibold shadow-lg transition-shadow ${
hasRequestedRematch
? 'bg-gray-400 text-white cursor-not-allowed'
: 'bg-gradient-to-r from-green-500 to-green-600 text-white hover:shadow-xl'
}`}
aria-label={hasRequestedRematch ? 'Waiting for others to accept rematch' : 'Request rematch'}
>
{hasRequestedRematch
? (waitingForOthers ? 'Waiting for others...' : 'Starting rematch...')
: 'Request Rematch'
}
</motion.button>
)}
<motion.button <motion.button
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}

Ver fichero

@@ -33,6 +33,7 @@ function createGameState(roomId: string): GameState {
isGameOver: false, isGameOver: false,
turnsPassed: 0, turnsPassed: 0,
gameMode: 'waiting', gameMode: 'waiting',
rematchRequests: [],
}; };
} }

Ver fichero

@@ -26,6 +26,7 @@ interface GameStore {
selectTile: (tile: DominoTile | null) => void; selectTile: (tile: DominoTile | null) => void;
leaveRoom: () => void; leaveRoom: () => void;
setError: (error: string | null) => void; setError: (error: string | null) => void;
requestRematch: () => void;
// AI actions // AI actions
startAIGame: (playerName: string) => void; startAIGame: (playerName: string) => void;
@@ -134,6 +135,18 @@ export const useGameStore = create<GameStore>((set, get) => ({
setTimeout(() => set({ error: null }), 3000); setTimeout(() => set({ error: null }), 3000);
}); });
socket.on('rematch-started', (gameState: GameState) => {
set({ gameState, selectedTile: null });
// Check if AI starts
const currentPlayer = gameState.players[gameState.currentPlayerIndex];
if (currentPlayer?.isAI) {
setTimeout(() => {
get().executeAITurn();
}, 1500);
}
});
set({ socket }); set({ socket });
}, },
@@ -246,6 +259,25 @@ export const useGameStore = create<GameStore>((set, get) => ({
}); });
}, },
requestRematch: () => {
const { socket, roomId, gameState, currentPlayerId } = get();
// AI mode - start new game immediately
if (roomId?.startsWith('AI-') && gameState && currentPlayerId) {
const humanPlayer = gameState.players.find(p => !p.isAI);
if (humanPlayer) {
// Just restart with same settings
get().startAIGame(humanPlayer.name);
}
return;
}
// Online mode - send rematch request to server
if (socket && roomId) {
socket.emit('request-rematch', roomId);
}
},
setError: (error: string | null) => { setError: (error: string | null) => {
set({ error }); set({ error });
if (error) { if (error) {
@@ -297,6 +329,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
isGameOver: false, isGameOver: false,
turnsPassed: 0, turnsPassed: 0,
gameMode: 'playing', gameMode: 'playing',
rematchRequests: [],
}; };
set({ gameState, currentPlayerId: 'human', roomId }); set({ gameState, currentPlayerId: 'human', roomId });

Ver fichero

@@ -47,6 +47,7 @@ export type GameState = {
isGameOver: boolean; isGameOver: boolean;
turnsPassed: number; turnsPassed: number;
gameMode: 'waiting' | 'playing' | 'finished'; gameMode: 'waiting' | 'playing' | 'finished';
rematchRequests: string[]; // Player IDs who requested rematch
}; };
export type GameMove = { export type GameMove = {
@@ -64,6 +65,7 @@ export type SocketEvents = {
'make-move': (roomId: string, move: GameMove) => void; 'make-move': (roomId: string, move: GameMove) => void;
'draw-tile': (roomId: string) => void; 'draw-tile': (roomId: string) => void;
'leave-room': (roomId: string) => void; 'leave-room': (roomId: string) => void;
'request-rematch': (roomId: string) => void;
// Server to Client // Server to Client
'room-created': (roomId: string) => void; 'room-created': (roomId: string) => void;
@@ -74,4 +76,6 @@ export type SocketEvents = {
'player-joined': (player: Player) => void; 'player-joined': (player: Player) => void;
'player-left': (playerId: string) => void; 'player-left': (playerId: string) => void;
'error': (message: string) => void; 'error': (message: string) => void;
'rematch-requested': (playerId: string) => void;
'rematch-started': (gameState: GameState) => void;
}; };

Ver fichero

@@ -32,6 +32,7 @@ function createGameState(roomId) {
isGameOver: false, isGameOver: false,
turnsPassed: 0, turnsPassed: 0,
gameMode: 'waiting', gameMode: 'waiting',
rematchRequests: [],
}; };
} }
@@ -115,12 +116,41 @@ function startGame(roomId) {
currentPlayerIndex: startingPlayerIndex, currentPlayerIndex: startingPlayerIndex,
boneyard, boneyard,
gameMode: 'playing', gameMode: 'playing',
rematchRequests: [],
}; };
gameRooms.set(roomId, newGameState); gameRooms.set(roomId, newGameState);
return newGameState; return newGameState;
} }
// Start rematch with same players
function startRematch(roomId, oldGameState) {
const { playerTiles, boneyard } = dealTiles(oldGameState.players.length);
const updatedPlayers = oldGameState.players.map((player, index) => ({
...player,
tiles: playerTiles[index],
score: 0,
isReady: true,
}));
const startingPlayerIndex = findStartingPlayer(updatedPlayers);
return {
id: roomId,
players: updatedPlayers,
currentPlayerIndex: startingPlayerIndex,
board: [],
boneyard,
boardEnds: [],
winner: null,
isGameOver: false,
turnsPassed: 0,
gameMode: 'playing',
rematchRequests: [],
};
}
app.prepare().then(() => { app.prepare().then(() => {
const server = createServer(async (req, res) => { const server = createServer(async (req, res) => {
try { try {
@@ -437,6 +467,32 @@ app.prepare().then(() => {
} }
}); });
socket.on('request-rematch', (roomId) => {
const gameState = gameRooms.get(roomId);
if (!gameState || !gameState.isGameOver) return;
// Add player to rematch requests if not already there
if (!gameState.rematchRequests.includes(socket.id)) {
gameState.rematchRequests.push(socket.id);
}
// Check if all players have requested rematch
const allPlayersRequested = gameState.players.every(p =>
gameState.rematchRequests.includes(p.id)
);
if (allPlayersRequested) {
// Start a new game with the same players
const newGameState = startRematch(roomId, gameState);
gameRooms.set(roomId, newGameState);
io.to(roomId).emit('rematch-started', newGameState);
} else {
// Update state to show who has requested
gameRooms.set(roomId, gameState);
io.to(roomId).emit('game-state-updated', gameState);
}
});
socket.on('disconnect', () => { socket.on('disconnect', () => {
const roomId = playerRooms.get(socket.id); const roomId = playerRooms.get(socket.id);
if (roomId) { if (roomId) {

Ver fichero

@@ -15,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {