352 líneas
9.3 KiB
TypeScript
352 líneas
9.3 KiB
TypeScript
'use client';
|
|
|
|
import { create } from 'zustand';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { GameState, Player, GameMove, DominoTile } from '@/lib/types';
|
|
import { chooseAIMove, aiThinkingDelay, shouldAIDraw } from '@/lib/aiLogic';
|
|
import { dealTiles, findStartingPlayer, executeMove, canPlayerMove } from '@/lib/gameLogic';
|
|
|
|
interface GameStore {
|
|
socket: Socket | null;
|
|
gameState: GameState | null;
|
|
currentPlayerId: string | null;
|
|
roomId: string | null;
|
|
error: string | null;
|
|
isConnected: boolean;
|
|
selectedTile: DominoTile | null;
|
|
pendingPlayerName: string | null;
|
|
|
|
// Actions
|
|
initSocket: () => void;
|
|
createRoom: (playerName: string) => void;
|
|
joinRoom: (roomId: string, playerName: string) => void;
|
|
setPlayerReady: () => void;
|
|
makeMove: (move: GameMove) => void;
|
|
drawTile: () => void;
|
|
selectTile: (tile: DominoTile | null) => void;
|
|
leaveRoom: () => void;
|
|
setError: (error: string | null) => void;
|
|
|
|
// AI actions
|
|
startAIGame: (playerName: string) => void;
|
|
executeAITurn: () => void;
|
|
}
|
|
|
|
export const useGameStore = create<GameStore>((set, get) => ({
|
|
socket: null,
|
|
gameState: null,
|
|
currentPlayerId: null,
|
|
roomId: null,
|
|
error: null,
|
|
isConnected: false,
|
|
selectedTile: null,
|
|
pendingPlayerName: null,
|
|
|
|
initSocket: () => {
|
|
const socketUrl = process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3000';
|
|
const socket = io(socketUrl, {
|
|
transports: ['websocket', 'polling'],
|
|
});
|
|
|
|
socket.on('connect', () => {
|
|
set({ isConnected: true, socket });
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
set({ isConnected: false });
|
|
});
|
|
|
|
socket.on('room-created', (roomId: string) => {
|
|
const { pendingPlayerName, socket } = get();
|
|
set({ roomId, error: null });
|
|
|
|
// Automatically join the room we just created
|
|
if (pendingPlayerName && socket) {
|
|
socket.emit('join-room', roomId, pendingPlayerName);
|
|
}
|
|
});
|
|
|
|
socket.on('room-joined', (gameState: GameState, playerId: string) => {
|
|
set({ gameState, currentPlayerId: playerId, error: null, pendingPlayerName: null });
|
|
});
|
|
|
|
socket.on('game-state-updated', (gameState: GameState) => {
|
|
set({ gameState });
|
|
|
|
// Check if it's AI's turn
|
|
const currentPlayer = gameState.players[gameState.currentPlayerIndex];
|
|
if (currentPlayer?.isAI && gameState.gameMode === 'playing' && !gameState.isGameOver) {
|
|
setTimeout(() => {
|
|
get().executeAITurn();
|
|
}, 1000);
|
|
}
|
|
});
|
|
|
|
socket.on('game-started', (gameState: GameState) => {
|
|
set({ gameState });
|
|
|
|
// Check if AI starts
|
|
const currentPlayer = gameState.players[gameState.currentPlayerIndex];
|
|
if (currentPlayer?.isAI) {
|
|
setTimeout(() => {
|
|
get().executeAITurn();
|
|
}, 1500);
|
|
}
|
|
});
|
|
|
|
socket.on('player-joined', (player: Player) => {
|
|
const { gameState } = get();
|
|
if (gameState) {
|
|
set({
|
|
gameState: {
|
|
...gameState,
|
|
players: [...gameState.players, player],
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
socket.on('player-left', (playerId: string) => {
|
|
const { currentPlayerId } = get();
|
|
|
|
// If we are the player who left (kicked or left from another tab), clear our state
|
|
if (playerId === currentPlayerId) {
|
|
set({
|
|
gameState: null,
|
|
roomId: null,
|
|
currentPlayerId: null,
|
|
selectedTile: null,
|
|
pendingPlayerName: null,
|
|
error: null
|
|
});
|
|
}
|
|
// Note: Don't update gameState here - the server will send game-state-updated
|
|
// with the updated state including potential winner if only 1 player remains
|
|
});
|
|
|
|
socket.on('invalid-move', (message: string) => {
|
|
set({ error: message });
|
|
setTimeout(() => set({ error: null }), 3000);
|
|
});
|
|
|
|
socket.on('error', (message: string) => {
|
|
set({ error: message });
|
|
setTimeout(() => set({ error: null }), 3000);
|
|
});
|
|
|
|
set({ socket });
|
|
},
|
|
|
|
createRoom: (playerName: string) => {
|
|
const { socket } = get();
|
|
if (socket) {
|
|
set({ pendingPlayerName: playerName });
|
|
socket.emit('create-room');
|
|
}
|
|
},
|
|
|
|
joinRoom: (roomId: string, playerName: string) => {
|
|
const { socket } = get();
|
|
if (socket) {
|
|
set({ roomId });
|
|
socket.emit('join-room', roomId, playerName);
|
|
}
|
|
},
|
|
|
|
setPlayerReady: () => {
|
|
const { socket, roomId } = get();
|
|
if (socket && roomId) {
|
|
socket.emit('player-ready', roomId);
|
|
}
|
|
},
|
|
|
|
makeMove: (move: GameMove) => {
|
|
const { socket, roomId, gameState } = get();
|
|
|
|
// Modo AI (offline)
|
|
if (roomId?.startsWith('AI-') && gameState) {
|
|
const newGameState = executeMove(gameState, move);
|
|
set({ gameState: newGameState, selectedTile: null });
|
|
|
|
// Si es turno de la IA, ejecutar su movimiento
|
|
const currentPlayer = newGameState.players[newGameState.currentPlayerIndex];
|
|
if (currentPlayer?.isAI && !newGameState.isGameOver) {
|
|
setTimeout(() => {
|
|
get().executeAITurn();
|
|
}, 1000);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Modo multijugador (online)
|
|
if (socket && roomId) {
|
|
socket.emit('make-move', roomId, move);
|
|
set({ selectedTile: null });
|
|
}
|
|
},
|
|
|
|
drawTile: () => {
|
|
const { socket, roomId, gameState, currentPlayerId } = get();
|
|
|
|
// AI mode - execute locally
|
|
if (roomId?.startsWith('AI-')) {
|
|
if (!gameState || !currentPlayerId) return;
|
|
|
|
const player = gameState.players.find(p => p.id === currentPlayerId);
|
|
if (!player) return;
|
|
|
|
// Check if there are tiles in the boneyard
|
|
if (gameState.boneyard.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Draw a tile from the boneyard
|
|
const drawnTile = gameState.boneyard[0];
|
|
const newBoneyard = gameState.boneyard.slice(1);
|
|
|
|
// Add to player's hand
|
|
const updatedPlayers = gameState.players.map(p =>
|
|
p.id === currentPlayerId
|
|
? { ...p, tiles: [...p.tiles, drawnTile] }
|
|
: p
|
|
);
|
|
|
|
// Update game state
|
|
set({
|
|
gameState: {
|
|
...gameState,
|
|
boneyard: newBoneyard,
|
|
players: updatedPlayers,
|
|
}
|
|
});
|
|
}
|
|
// Online mode - send to server
|
|
else if (socket && roomId) {
|
|
socket.emit('draw-tile', roomId);
|
|
}
|
|
},
|
|
|
|
selectTile: (tile: DominoTile | null) => {
|
|
set({ selectedTile: tile });
|
|
},
|
|
|
|
leaveRoom: () => {
|
|
const { socket, roomId } = get();
|
|
if (socket && roomId) {
|
|
socket.emit('leave-room', roomId);
|
|
}
|
|
// Clear state immediately on client side
|
|
set({
|
|
gameState: null,
|
|
roomId: null,
|
|
currentPlayerId: null,
|
|
selectedTile: null,
|
|
pendingPlayerName: null,
|
|
error: null
|
|
});
|
|
},
|
|
|
|
setError: (error: string | null) => {
|
|
set({ error });
|
|
if (error) {
|
|
// Auto-clear error after 3 seconds
|
|
setTimeout(() => {
|
|
set({ error: null });
|
|
}, 3000);
|
|
}
|
|
},
|
|
|
|
// Start AI game (offline mode)
|
|
startAIGame: (playerName: string) => {
|
|
const roomId = 'AI-' + Math.random().toString(36).substring(2, 8);
|
|
|
|
const humanPlayer: Player = {
|
|
id: 'human',
|
|
name: playerName,
|
|
tiles: [],
|
|
score: 0,
|
|
isAI: false,
|
|
isReady: true,
|
|
};
|
|
|
|
const aiPlayer: Player = {
|
|
id: 'ai',
|
|
name: 'AI Opponent',
|
|
tiles: [],
|
|
score: 0,
|
|
isAI: true,
|
|
isReady: true,
|
|
};
|
|
|
|
const { playerTiles, boneyard } = dealTiles(2);
|
|
|
|
humanPlayer.tiles = playerTiles[0];
|
|
aiPlayer.tiles = playerTiles[1];
|
|
|
|
const players = [humanPlayer, aiPlayer];
|
|
const startingPlayerIndex = findStartingPlayer(players);
|
|
|
|
const gameState: GameState = {
|
|
id: roomId,
|
|
players,
|
|
currentPlayerIndex: startingPlayerIndex,
|
|
board: [],
|
|
boneyard,
|
|
boardEnds: [],
|
|
winner: null,
|
|
isGameOver: false,
|
|
turnsPassed: 0,
|
|
gameMode: 'playing',
|
|
};
|
|
|
|
set({ gameState, currentPlayerId: 'human', roomId });
|
|
|
|
// If AI starts, make first move
|
|
if (startingPlayerIndex === 1) {
|
|
setTimeout(() => {
|
|
get().executeAITurn();
|
|
}, 1500);
|
|
}
|
|
},
|
|
|
|
// Execute AI turn
|
|
executeAITurn: async () => {
|
|
const { gameState } = get();
|
|
if (!gameState || gameState.isGameOver) return;
|
|
|
|
const aiPlayer = gameState.players.find(p => p.isAI);
|
|
if (!aiPlayer) return;
|
|
|
|
const currentPlayer = gameState.players[gameState.currentPlayerIndex];
|
|
if (currentPlayer.id !== aiPlayer.id) return;
|
|
|
|
await aiThinkingDelay('medium');
|
|
|
|
// Check if AI should draw
|
|
if (shouldAIDraw(gameState, aiPlayer)) {
|
|
if (gameState.boneyard.length > 0) {
|
|
const drawnTile = gameState.boneyard.pop()!;
|
|
aiPlayer.tiles.push(drawnTile);
|
|
|
|
if (!canPlayerMove(aiPlayer, gameState.boardEnds)) {
|
|
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
|
set({ gameState: { ...gameState } });
|
|
return;
|
|
}
|
|
} else {
|
|
// Pass turn
|
|
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
|
gameState.turnsPassed++;
|
|
set({ gameState: { ...gameState } });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const move = chooseAIMove(gameState, aiPlayer, 'medium');
|
|
if (move) {
|
|
const newGameState = executeMove(gameState, move);
|
|
set({ gameState: newGameState });
|
|
}
|
|
},
|
|
}));
|