@@ -26,6 +26,7 @@ function createGameState(roomId: string): GameState {
|
||||
isGameOver: false,
|
||||
turnsPassed: 0,
|
||||
gameMode: 'waiting',
|
||||
rematchRequests: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function Home() {
|
||||
leaveRoom,
|
||||
startAIGame,
|
||||
setError,
|
||||
requestRematch,
|
||||
} = useGameStore();
|
||||
|
||||
const [showRules, setShowRules] = useState(false);
|
||||
@@ -362,8 +363,12 @@ export default function Home() {
|
||||
<GameOver
|
||||
winner={gameState.players.find(p => p.id === gameState.winner) || null}
|
||||
players={gameState.players}
|
||||
currentPlayerId={currentPlayerId}
|
||||
rematchRequests={gameState.rematchRequests || []}
|
||||
onPlayAgain={handlePlayAgain}
|
||||
onRequestRematch={requestRematch}
|
||||
onLeave={leaveRoom}
|
||||
isAIGame={roomId?.startsWith('AI-') || false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,19 @@ import { Player } from '@/lib/types';
|
||||
interface GameOverProps {
|
||||
winner: Player | null;
|
||||
players: Player[];
|
||||
currentPlayerId: string | null;
|
||||
rematchRequests: string[];
|
||||
onPlayAgain: () => void;
|
||||
onRequestRematch: () => 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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -57,6 +65,7 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
.map((player, index) => {
|
||||
const score = player.tiles.reduce((sum, t) => sum + t.left + t.right, 0);
|
||||
const isWinner = player.id === winner?.id;
|
||||
const hasRequestedRematchPlayer = rematchRequests.includes(player.id);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -71,7 +80,14 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
<div className="flex items-center gap-3">
|
||||
{isWinner && <span className="text-2xl">👑</span>}
|
||||
<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">
|
||||
{player.tiles.length} tiles remaining
|
||||
</div>
|
||||
@@ -89,15 +105,35 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
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"
|
||||
aria-label="Start a new game"
|
||||
>
|
||||
Play Again
|
||||
</motion.button>
|
||||
{isAIGame ? (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
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"
|
||||
aria-label="Play again against AI"
|
||||
>
|
||||
Play Again
|
||||
</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
|
||||
whileHover={{ scale: 1.02 }}
|
||||
|
||||
@@ -33,6 +33,7 @@ function createGameState(roomId: string): GameState {
|
||||
isGameOver: false,
|
||||
turnsPassed: 0,
|
||||
gameMode: 'waiting',
|
||||
rematchRequests: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
33
lib/store.ts
33
lib/store.ts
@@ -26,6 +26,7 @@ interface GameStore {
|
||||
selectTile: (tile: DominoTile | null) => void;
|
||||
leaveRoom: () => void;
|
||||
setError: (error: string | null) => void;
|
||||
requestRematch: () => void;
|
||||
|
||||
// AI actions
|
||||
startAIGame: (playerName: string) => void;
|
||||
@@ -134,6 +135,18 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
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 });
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
set({ error });
|
||||
if (error) {
|
||||
@@ -297,6 +329,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
isGameOver: false,
|
||||
turnsPassed: 0,
|
||||
gameMode: 'playing',
|
||||
rematchRequests: [],
|
||||
};
|
||||
|
||||
set({ gameState, currentPlayerId: 'human', roomId });
|
||||
|
||||
@@ -47,6 +47,7 @@ export type GameState = {
|
||||
isGameOver: boolean;
|
||||
turnsPassed: number;
|
||||
gameMode: 'waiting' | 'playing' | 'finished';
|
||||
rematchRequests: string[]; // Player IDs who requested rematch
|
||||
};
|
||||
|
||||
export type GameMove = {
|
||||
@@ -64,6 +65,7 @@ export type SocketEvents = {
|
||||
'make-move': (roomId: string, move: GameMove) => void;
|
||||
'draw-tile': (roomId: string) => void;
|
||||
'leave-room': (roomId: string) => void;
|
||||
'request-rematch': (roomId: string) => void;
|
||||
|
||||
// Server to Client
|
||||
'room-created': (roomId: string) => void;
|
||||
@@ -74,4 +76,6 @@ export type SocketEvents = {
|
||||
'player-joined': (player: Player) => void;
|
||||
'player-left': (playerId: string) => void;
|
||||
'error': (message: string) => void;
|
||||
'rematch-requested': (playerId: string) => void;
|
||||
'rematch-started': (gameState: GameState) => void;
|
||||
};
|
||||
|
||||
56
server.mjs
56
server.mjs
@@ -32,6 +32,7 @@ function createGameState(roomId) {
|
||||
isGameOver: false,
|
||||
turnsPassed: 0,
|
||||
gameMode: 'waiting',
|
||||
rematchRequests: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,12 +116,41 @@ function startGame(roomId) {
|
||||
currentPlayerIndex: startingPlayerIndex,
|
||||
boneyard,
|
||||
gameMode: 'playing',
|
||||
rematchRequests: [],
|
||||
};
|
||||
|
||||
gameRooms.set(roomId, 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(() => {
|
||||
const server = createServer(async (req, res) => {
|
||||
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', () => {
|
||||
const roomId = playerRooms.get(socket.id);
|
||||
if (roomId) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
Referencia en una nueva incidencia
Block a user