@@ -26,6 +26,7 @@ function createGameState(roomId: string): GameState {
|
|||||||
isGameOver: false,
|
isGameOver: false,
|
||||||
turnsPassed: 0,
|
turnsPassed: 0,
|
||||||
gameMode: 'waiting',
|
gameMode: 'waiting',
|
||||||
|
rematchRequests: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
<motion.button
|
{isAIGame ? (
|
||||||
whileHover={{ scale: 1.02 }}
|
<motion.button
|
||||||
whileTap={{ scale: 0.98 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
onClick={onPlayAgain}
|
whileTap={{ scale: 0.98 }}
|
||||||
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"
|
onClick={onPlayAgain}
|
||||||
aria-label="Start a new game"
|
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>
|
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
|
<motion.button
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function createGameState(roomId: string): GameState {
|
|||||||
isGameOver: false,
|
isGameOver: false,
|
||||||
turnsPassed: 0,
|
turnsPassed: 0,
|
||||||
gameMode: 'waiting',
|
gameMode: 'waiting',
|
||||||
|
rematchRequests: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
lib/store.ts
33
lib/store.ts
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
56
server.mjs
56
server.mjs
@@ -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) {
|
||||||
|
|||||||
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user