153
lib/aiLogic.ts
Archivo normal
153
lib/aiLogic.ts
Archivo normal
@@ -0,0 +1,153 @@
|
||||
import { Player, GameState, GameMove, DominoTile, BoardEnd } from './types';
|
||||
import { getValidMoves, canPlaceTile } from './gameLogic';
|
||||
|
||||
// AI difficulty levels
|
||||
export type AIDifficulty = 'easy' | 'medium' | 'hard';
|
||||
|
||||
// Evaluate tile value for strategic play
|
||||
function evaluateTileValue(tile: DominoTile, boardEnds: BoardEnd[]): number {
|
||||
let value = 0;
|
||||
|
||||
// Prefer higher value tiles early in the game
|
||||
value += tile.left + tile.right;
|
||||
|
||||
// Doubles are slightly more valuable
|
||||
if (tile.isDouble) {
|
||||
value += 2;
|
||||
}
|
||||
|
||||
// Tiles that match both ends are very valuable
|
||||
const matchesLeft = boardEnds.some(end => end.side === 'left' && canPlaceTile(tile, end.value));
|
||||
const matchesRight = boardEnds.some(end => end.side === 'right' && canPlaceTile(tile, end.value));
|
||||
|
||||
if (matchesLeft && matchesRight) {
|
||||
value += 10;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Count remaining tiles with specific values
|
||||
function countRemainingTiles(value: number, allPlayerTiles: DominoTile[][]): number {
|
||||
let count = 0;
|
||||
allPlayerTiles.forEach(tiles => {
|
||||
tiles.forEach(tile => {
|
||||
if (tile.left === value || tile.right === value) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// Choose the best move for AI based on difficulty
|
||||
export function chooseAIMove(
|
||||
gameState: GameState,
|
||||
aiPlayer: Player,
|
||||
difficulty: AIDifficulty = 'medium'
|
||||
): GameMove | null {
|
||||
const validMoves = getValidMoves(aiPlayer, gameState.boardEnds);
|
||||
|
||||
if (validMoves.length === 0) {
|
||||
// Try to draw from boneyard if possible
|
||||
if (gameState.boneyard.length > 0) {
|
||||
return null; // Will trigger draw action
|
||||
}
|
||||
// Pass if can't move and no tiles to draw
|
||||
return {
|
||||
playerId: aiPlayer.id,
|
||||
tile: aiPlayer.tiles[0], // Dummy tile
|
||||
side: 'left',
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Easy: Random move
|
||||
if (difficulty === 'easy') {
|
||||
const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)];
|
||||
return {
|
||||
playerId: aiPlayer.id,
|
||||
tile: randomMove.tile,
|
||||
side: randomMove.side,
|
||||
};
|
||||
}
|
||||
|
||||
// Medium: Prefer higher value tiles
|
||||
if (difficulty === 'medium') {
|
||||
let bestMove = validMoves[0];
|
||||
let bestValue = evaluateTileValue(bestMove.tile, gameState.boardEnds);
|
||||
|
||||
validMoves.forEach(move => {
|
||||
const value = evaluateTileValue(move.tile, gameState.boardEnds);
|
||||
if (value > bestValue) {
|
||||
bestValue = value;
|
||||
bestMove = move;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
playerId: aiPlayer.id,
|
||||
tile: bestMove.tile,
|
||||
side: bestMove.side,
|
||||
};
|
||||
}
|
||||
|
||||
// Hard: Strategic play
|
||||
// Consider opponent's possible tiles and blocking strategies
|
||||
let bestMove = validMoves[0];
|
||||
let bestScore = -Infinity;
|
||||
|
||||
validMoves.forEach(move => {
|
||||
let score = evaluateTileValue(move.tile, gameState.boardEnds);
|
||||
|
||||
// Prefer moves that limit opponent's options
|
||||
const resultingValue = move.tile.left === gameState.boardEnds.find(e => e.side === move.side)?.value
|
||||
? move.tile.right
|
||||
: move.tile.left;
|
||||
|
||||
// Check how common this value is among remaining tiles
|
||||
const allTiles = gameState.players.map(p => p.tiles);
|
||||
const commonality = countRemainingTiles(resultingValue, allTiles);
|
||||
|
||||
// Prefer less common values to block opponents
|
||||
score -= commonality * 3;
|
||||
|
||||
// Try to get rid of high-value tiles first (defensive play)
|
||||
const tileValue = move.tile.left + move.tile.right;
|
||||
score += tileValue * 0.5;
|
||||
|
||||
// Prefer doubles near the end of the game
|
||||
if (move.tile.isDouble && aiPlayer.tiles.length <= 3) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestMove = move;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
playerId: aiPlayer.id,
|
||||
tile: bestMove.tile,
|
||||
side: bestMove.side,
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate AI thinking delay
|
||||
export async function aiThinkingDelay(difficulty: AIDifficulty): Promise<void> {
|
||||
const delays = {
|
||||
easy: 500,
|
||||
medium: 1000,
|
||||
hard: 1500,
|
||||
};
|
||||
|
||||
const delay = delays[difficulty] + Math.random() * 500;
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
// Check if AI should draw a tile
|
||||
export function shouldAIDraw(gameState: GameState, aiPlayer: Player): boolean {
|
||||
const validMoves = getValidMoves(aiPlayer, gameState.boardEnds);
|
||||
return validMoves.length === 0 && gameState.boneyard.length > 0;
|
||||
}
|
||||
364
lib/gameLogic.ts
Archivo normal
364
lib/gameLogic.ts
Archivo normal
@@ -0,0 +1,364 @@
|
||||
import { DominoTile, GameState, Player, PlacedTile, BoardEnd, Position, GameMove } from './types';
|
||||
|
||||
// Generate a complete domino set (0-0 to 6-6)
|
||||
export function generateDominoSet(): DominoTile[] {
|
||||
const tiles: DominoTile[] = [];
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
for (let j = i; j <= 6; j++) {
|
||||
tiles.push({
|
||||
id: `${i}-${j}`,
|
||||
left: i,
|
||||
right: j,
|
||||
isDouble: i === j,
|
||||
});
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
// Shuffle array using Fisher-Yates algorithm
|
||||
export function shuffleArray<T>(array: T[]): T[] {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
// Deal tiles to players
|
||||
export function dealTiles(numPlayers: number): { playerTiles: DominoTile[][], boneyard: DominoTile[] } {
|
||||
const allTiles = shuffleArray(generateDominoSet());
|
||||
const tilesPerPlayer = 7;
|
||||
const playerTiles: DominoTile[][] = [];
|
||||
|
||||
for (let i = 0; i < numPlayers; i++) {
|
||||
playerTiles.push(allTiles.splice(0, tilesPerPlayer));
|
||||
}
|
||||
|
||||
return {
|
||||
playerTiles,
|
||||
boneyard: allTiles,
|
||||
};
|
||||
}
|
||||
|
||||
// Find the player with the highest double
|
||||
export function findStartingPlayer(players: Player[]): number {
|
||||
let highestDouble = -1;
|
||||
let startingPlayerIndex = 0;
|
||||
|
||||
players.forEach((player, index) => {
|
||||
player.tiles.forEach(tile => {
|
||||
if (tile.isDouble && tile.left > highestDouble) {
|
||||
highestDouble = tile.left;
|
||||
startingPlayerIndex = index;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return startingPlayerIndex;
|
||||
}
|
||||
|
||||
// Check if a tile can be placed at a specific board end
|
||||
export function canPlaceTile(tile: DominoTile, endValue: number): boolean {
|
||||
return tile.left === endValue || tile.right === endValue;
|
||||
}
|
||||
|
||||
// Check if a player can make any move
|
||||
export function canPlayerMove(player: Player, boardEnds: BoardEnd[]): boolean {
|
||||
if (boardEnds.length === 0) return true;
|
||||
|
||||
return player.tiles.some(tile =>
|
||||
boardEnds.some(end => canPlaceTile(tile, end.value))
|
||||
);
|
||||
}
|
||||
|
||||
// Get valid moves for a player
|
||||
export function getValidMoves(player: Player, boardEnds: BoardEnd[]): { tile: DominoTile; side: 'left' | 'right' }[] {
|
||||
if (boardEnds.length === 0) {
|
||||
return player.tiles.map(tile => ({ tile, side: 'left' as const }));
|
||||
}
|
||||
|
||||
const validMoves: { tile: DominoTile; side: 'left' | 'right' }[] = [];
|
||||
const addedTiles = new Set<string>();
|
||||
|
||||
player.tiles.forEach(tile => {
|
||||
boardEnds.forEach(end => {
|
||||
if (canPlaceTile(tile, end.value)) {
|
||||
const moveKey = `${tile.id}-${end.side}`;
|
||||
if (!addedTiles.has(moveKey)) {
|
||||
validMoves.push({ tile, side: end.side === 'left' ? 'left' : 'right' });
|
||||
addedTiles.add(moveKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return validMoves;
|
||||
}
|
||||
|
||||
// Get playable tiles for a player (returns just tile IDs)
|
||||
export function getPlayableTiles(player: Player, boardEnds: BoardEnd[]): string[] {
|
||||
if (boardEnds.length === 0) {
|
||||
return player.tiles.map(t => t.id);
|
||||
}
|
||||
|
||||
const playableTileIds = new Set<string>();
|
||||
|
||||
player.tiles.forEach(tile => {
|
||||
boardEnds.forEach(end => {
|
||||
if (canPlaceTile(tile, end.value)) {
|
||||
playableTileIds.add(tile.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(playableTileIds);
|
||||
}
|
||||
|
||||
// Check if a specific move is valid
|
||||
export function isValidMove(tile: DominoTile, side: 'left' | 'right', boardEnds: BoardEnd[]): boolean {
|
||||
if (boardEnds.length === 0) return true;
|
||||
|
||||
const targetEnd = boardEnds.find(end =>
|
||||
(side === 'left' && end.side === 'left') ||
|
||||
(side === 'right' && end.side === 'right')
|
||||
);
|
||||
|
||||
if (!targetEnd) return false;
|
||||
|
||||
return canPlaceTile(tile, targetEnd.value);
|
||||
}
|
||||
|
||||
// Calculate tile position on board
|
||||
export function calculateTilePosition(
|
||||
board: PlacedTile[],
|
||||
side: 'left' | 'right',
|
||||
tileWidth: number,
|
||||
tileHeight: number,
|
||||
isDouble: boolean
|
||||
): { position: Position; orientation: 'horizontal' | 'vertical'; rotation: number } {
|
||||
const spacing = 5;
|
||||
|
||||
if (board.length === 0) {
|
||||
return {
|
||||
position: { x: 400, y: 300 },
|
||||
orientation: 'horizontal',
|
||||
rotation: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const lastTile = side === 'right' ? board[board.length - 1] : board[0];
|
||||
let position: Position;
|
||||
let orientation: 'horizontal' | 'vertical' = 'horizontal';
|
||||
let rotation = 0;
|
||||
|
||||
if (side === 'right') {
|
||||
const offset = lastTile.orientation === 'horizontal' ? tileWidth : tileHeight;
|
||||
position = {
|
||||
x: lastTile.position.x + offset + spacing,
|
||||
y: lastTile.position.y,
|
||||
};
|
||||
orientation = isDouble ? 'vertical' : 'horizontal';
|
||||
} else {
|
||||
const offset = lastTile.orientation === 'horizontal' ? tileWidth : tileHeight;
|
||||
position = {
|
||||
x: lastTile.position.x - offset - spacing,
|
||||
y: lastTile.position.y,
|
||||
};
|
||||
orientation = isDouble ? 'vertical' : 'horizontal';
|
||||
}
|
||||
|
||||
return { position, orientation, rotation };
|
||||
}
|
||||
|
||||
// Update board ends after placing a tile
|
||||
export function updateBoardEnds(
|
||||
board: PlacedTile[],
|
||||
newTile: PlacedTile,
|
||||
side: 'left' | 'right',
|
||||
matchedValue: number
|
||||
): BoardEnd[] {
|
||||
if (board.length === 0) {
|
||||
return [
|
||||
{ value: newTile.tile.left, position: newTile.position, side: 'left' },
|
||||
{ value: newTile.tile.right, position: newTile.position, side: 'right' },
|
||||
];
|
||||
}
|
||||
|
||||
const newEnds: BoardEnd[] = [];
|
||||
|
||||
if (side === 'left') {
|
||||
const newValue = newTile.tile.left === matchedValue ? newTile.tile.right : newTile.tile.left;
|
||||
newEnds.push({ value: newValue, position: newTile.position, side: 'left' });
|
||||
|
||||
// Keep the right end
|
||||
const rightTile = board[board.length - 1];
|
||||
const rightValue = board.length === 1
|
||||
? (rightTile.tile.left === matchedValue ? rightTile.tile.right : rightTile.tile.left)
|
||||
: board[board.length - 1].tile.right;
|
||||
newEnds.push({ value: rightValue, position: rightTile.position, side: 'right' });
|
||||
} else {
|
||||
// Keep the left end
|
||||
const leftTile = board[0];
|
||||
const leftValue = board.length === 1
|
||||
? (leftTile.tile.left === matchedValue ? leftTile.tile.right : leftTile.tile.left)
|
||||
: board[0].tile.left;
|
||||
newEnds.push({ value: leftValue, position: leftTile.position, side: 'left' });
|
||||
|
||||
const newValue = newTile.tile.left === matchedValue ? newTile.tile.right : newTile.tile.left;
|
||||
newEnds.push({ value: newValue, position: newTile.position, side: 'right' });
|
||||
}
|
||||
|
||||
return newEnds;
|
||||
}
|
||||
|
||||
// Execute a move
|
||||
export function executeMove(
|
||||
gameState: GameState,
|
||||
move: GameMove,
|
||||
tileWidth: number = 60,
|
||||
tileHeight: number = 30
|
||||
): GameState {
|
||||
const player = gameState.players.find(p => p.id === move.playerId);
|
||||
if (!player) return gameState;
|
||||
|
||||
if (move.pass) {
|
||||
return {
|
||||
...gameState,
|
||||
currentPlayerIndex: (gameState.currentPlayerIndex + 1) % gameState.players.length,
|
||||
turnsPassed: gameState.turnsPassed + 1,
|
||||
};
|
||||
}
|
||||
|
||||
const tileIndex = player.tiles.findIndex(t => t.id === move.tile.id);
|
||||
if (tileIndex === -1) return gameState;
|
||||
|
||||
// Remove tile from player's hand
|
||||
const newTiles = [...player.tiles];
|
||||
newTiles.splice(tileIndex, 1);
|
||||
|
||||
// Determine if we need to flip the tile
|
||||
let tileToPlace = { ...move.tile };
|
||||
|
||||
if (gameState.board.length > 0) {
|
||||
const targetEnd = gameState.boardEnds.find(end => end.side === move.side);
|
||||
const matchValue = targetEnd?.value || 0;
|
||||
|
||||
// When placing on the RIGHT side:
|
||||
// - The LEFT value of the new tile should match the board end
|
||||
// - If tile.right matches, we need to flip it
|
||||
// When placing on the LEFT side:
|
||||
// - The RIGHT value of the new tile should match the board end
|
||||
// - If tile.left matches, we need to flip it
|
||||
|
||||
let needsFlip = false;
|
||||
if (move.side === 'right') {
|
||||
// On right side, left value of tile should match
|
||||
if (move.tile.right === matchValue && move.tile.left !== matchValue) {
|
||||
needsFlip = true;
|
||||
}
|
||||
} else {
|
||||
// On left side, right value of tile should match
|
||||
if (move.tile.left === matchValue && move.tile.right !== matchValue) {
|
||||
needsFlip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Flip the tile if needed
|
||||
if (needsFlip) {
|
||||
tileToPlace = {
|
||||
...move.tile,
|
||||
left: move.tile.right,
|
||||
right: move.tile.left,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate position and place tile
|
||||
const { position, orientation, rotation } = calculateTilePosition(
|
||||
gameState.board,
|
||||
move.side,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
tileToPlace.isDouble
|
||||
);
|
||||
|
||||
const placedTile: PlacedTile = {
|
||||
tile: tileToPlace,
|
||||
position,
|
||||
orientation,
|
||||
rotation,
|
||||
};
|
||||
|
||||
const newBoard = move.side === 'right'
|
||||
? [...gameState.board, placedTile]
|
||||
: [placedTile, ...gameState.board];
|
||||
|
||||
// Update board ends - after placing, it's straightforward:
|
||||
// Left end is the left value of the leftmost tile
|
||||
// Right end is the right value of the rightmost tile
|
||||
let newBoardEnds: BoardEnd[];
|
||||
|
||||
if (newBoard.length === 1) {
|
||||
newBoardEnds = [
|
||||
{ value: tileToPlace.left, position: placedTile.position, side: 'left' },
|
||||
{ value: tileToPlace.right, position: placedTile.position, side: 'right' },
|
||||
];
|
||||
} else {
|
||||
const leftTile = newBoard[0];
|
||||
const rightTile = newBoard[newBoard.length - 1];
|
||||
|
||||
newBoardEnds = [
|
||||
{ value: leftTile.tile.left, position: leftTile.position, side: 'left' },
|
||||
{ value: rightTile.tile.right, position: rightTile.position, side: 'right' },
|
||||
];
|
||||
}
|
||||
|
||||
const updatedPlayers = gameState.players.map(p =>
|
||||
p.id === player.id ? { ...p, tiles: newTiles } : p
|
||||
);
|
||||
|
||||
const isGameOver = newTiles.length === 0;
|
||||
const winner = isGameOver ? player.id : null;
|
||||
|
||||
return {
|
||||
...gameState,
|
||||
players: updatedPlayers,
|
||||
board: newBoard,
|
||||
boardEnds: newBoardEnds,
|
||||
currentPlayerIndex: (gameState.currentPlayerIndex + 1) % gameState.players.length,
|
||||
isGameOver,
|
||||
winner,
|
||||
turnsPassed: 0,
|
||||
gameMode: isGameOver ? 'finished' : 'playing',
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate score for a player
|
||||
export function calculateScore(tiles: DominoTile[]): number {
|
||||
return tiles.reduce((sum, tile) => sum + tile.left + tile.right, 0);
|
||||
}
|
||||
|
||||
// Check if game is blocked (no one can move)
|
||||
export function isGameBlocked(gameState: GameState): boolean {
|
||||
if (gameState.boneyard.length > 0) return false;
|
||||
|
||||
return gameState.players.every(player => !canPlayerMove(player, gameState.boardEnds));
|
||||
}
|
||||
|
||||
// Determine winner when game is blocked
|
||||
export function determineBlockedWinner(gameState: GameState): string {
|
||||
let lowestScore = Infinity;
|
||||
let winnerId = '';
|
||||
|
||||
gameState.players.forEach(player => {
|
||||
const score = calculateScore(player.tiles);
|
||||
if (score < lowestScore) {
|
||||
lowestScore = score;
|
||||
winnerId = player.id;
|
||||
}
|
||||
});
|
||||
|
||||
return winnerId;
|
||||
}
|
||||
238
lib/socket-server.ts
Archivo normal
238
lib/socket-server.ts
Archivo normal
@@ -0,0 +1,238 @@
|
||||
import { createServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { GameState, Player, GameMove } from '@/lib/types';
|
||||
import { dealTiles, findStartingPlayer, executeMove, canPlayerMove, isGameBlocked, determineBlockedWinner } from '@/lib/gameLogic';
|
||||
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
});
|
||||
|
||||
// Store active game rooms
|
||||
const gameRooms = new Map<string, GameState>();
|
||||
const playerRooms = new Map<string, string>(); // playerId -> roomId
|
||||
|
||||
// Generate unique room ID
|
||||
function generateRoomId(): string {
|
||||
return Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
}
|
||||
|
||||
// Create a new game state
|
||||
function createGameState(roomId: string): GameState {
|
||||
return {
|
||||
id: roomId,
|
||||
players: [],
|
||||
currentPlayerIndex: 0,
|
||||
board: [],
|
||||
boneyard: [],
|
||||
boardEnds: [],
|
||||
winner: null,
|
||||
isGameOver: false,
|
||||
turnsPassed: 0,
|
||||
gameMode: 'waiting',
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize game when all players are ready
|
||||
function startGame(roomId: string): GameState {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) throw new Error('Game not found');
|
||||
|
||||
const { playerTiles, boneyard } = dealTiles(gameState.players.length);
|
||||
|
||||
const updatedPlayers = gameState.players.map((player, index) => ({
|
||||
...player,
|
||||
tiles: playerTiles[index],
|
||||
score: 0,
|
||||
}));
|
||||
|
||||
const startingPlayerIndex = findStartingPlayer(updatedPlayers);
|
||||
|
||||
const newGameState: GameState = {
|
||||
...gameState,
|
||||
players: updatedPlayers,
|
||||
currentPlayerIndex: startingPlayerIndex,
|
||||
boneyard,
|
||||
gameMode: 'playing',
|
||||
};
|
||||
|
||||
gameRooms.set(roomId, newGameState);
|
||||
return newGameState;
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected:', socket.id);
|
||||
|
||||
// Create room
|
||||
socket.on('create-room', () => {
|
||||
const roomId = generateRoomId();
|
||||
const gameState = createGameState(roomId);
|
||||
gameRooms.set(roomId, gameState);
|
||||
|
||||
socket.join(roomId);
|
||||
socket.emit('room-created', roomId);
|
||||
console.log('Room created:', roomId);
|
||||
});
|
||||
|
||||
// Join room
|
||||
socket.on('join-room', (roomId: string, playerName: string) => {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
|
||||
if (!gameState) {
|
||||
socket.emit('error', 'Room not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.players.length >= 4) {
|
||||
socket.emit('error', 'Room is full');
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.gameMode !== 'waiting') {
|
||||
socket.emit('error', 'Game already started');
|
||||
return;
|
||||
}
|
||||
|
||||
const player: Player = {
|
||||
id: socket.id,
|
||||
name: playerName,
|
||||
tiles: [],
|
||||
score: 0,
|
||||
isAI: false,
|
||||
isReady: false,
|
||||
};
|
||||
|
||||
gameState.players.push(player);
|
||||
playerRooms.set(socket.id, roomId);
|
||||
|
||||
socket.join(roomId);
|
||||
socket.emit('room-joined', gameState, socket.id);
|
||||
socket.to(roomId).emit('player-joined', player);
|
||||
|
||||
console.log(`Player ${playerName} joined room ${roomId}`);
|
||||
});
|
||||
|
||||
// Player ready
|
||||
socket.on('player-ready', (roomId: string) => {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
const player = gameState.players.find(p => p.id === socket.id);
|
||||
if (!player) return;
|
||||
|
||||
player.isReady = true;
|
||||
gameRooms.set(roomId, gameState);
|
||||
|
||||
// Start game if all players are ready and at least 2 players
|
||||
const allReady = gameState.players.length >= 2 && gameState.players.every(p => p.isReady);
|
||||
|
||||
if (allReady) {
|
||||
const startedGame = startGame(roomId);
|
||||
io.to(roomId).emit('game-started', startedGame);
|
||||
console.log('Game started in room:', roomId);
|
||||
} else {
|
||||
io.to(roomId).emit('game-state-updated', gameState);
|
||||
}
|
||||
});
|
||||
|
||||
// Make move
|
||||
socket.on('make-move', (roomId: string, move: GameMove) => {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
const currentPlayer = gameState.players[gameState.currentPlayerIndex];
|
||||
|
||||
if (currentPlayer.id !== socket.id) {
|
||||
socket.emit('invalid-move', 'Not your turn');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newGameState = executeMove(gameState, move);
|
||||
|
||||
// Check if game is blocked
|
||||
if (isGameBlocked(newGameState)) {
|
||||
const winnerId = determineBlockedWinner(newGameState);
|
||||
newGameState.winner = winnerId;
|
||||
newGameState.isGameOver = true;
|
||||
newGameState.gameMode = 'finished';
|
||||
}
|
||||
|
||||
gameRooms.set(roomId, newGameState);
|
||||
io.to(roomId).emit('game-state-updated', newGameState);
|
||||
} catch (error) {
|
||||
socket.emit('invalid-move', 'Invalid move');
|
||||
}
|
||||
});
|
||||
|
||||
// Draw tile
|
||||
socket.on('draw-tile', (roomId: string) => {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
const currentPlayer = gameState.players[gameState.currentPlayerIndex];
|
||||
|
||||
if (currentPlayer.id !== socket.id) {
|
||||
socket.emit('invalid-move', 'Not your turn');
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.boneyard.length === 0) {
|
||||
socket.emit('invalid-move', 'No tiles left to draw');
|
||||
return;
|
||||
}
|
||||
|
||||
const drawnTile = gameState.boneyard.pop()!;
|
||||
currentPlayer.tiles.push(drawnTile);
|
||||
|
||||
// Check if player can move now
|
||||
if (!canPlayerMove(currentPlayer, gameState.boardEnds)) {
|
||||
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
||||
}
|
||||
|
||||
gameRooms.set(roomId, gameState);
|
||||
io.to(roomId).emit('game-state-updated', gameState);
|
||||
});
|
||||
|
||||
// Leave room
|
||||
socket.on('leave-room', (roomId: string) => {
|
||||
handlePlayerLeave(socket.id, roomId);
|
||||
});
|
||||
|
||||
// Disconnect
|
||||
socket.on('disconnect', () => {
|
||||
const roomId = playerRooms.get(socket.id);
|
||||
if (roomId) {
|
||||
handlePlayerLeave(socket.id, roomId);
|
||||
}
|
||||
console.log('Client disconnected:', socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
function handlePlayerLeave(playerId: string, roomId: string) {
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
gameState.players = gameState.players.filter(p => p.id !== playerId);
|
||||
playerRooms.delete(playerId);
|
||||
|
||||
if (gameState.players.length === 0) {
|
||||
gameRooms.delete(roomId);
|
||||
console.log('Room deleted:', roomId);
|
||||
} else {
|
||||
gameRooms.set(roomId, gameState);
|
||||
io.to(roomId).emit('player-left', playerId);
|
||||
io.to(roomId).emit('game-state-updated', gameState);
|
||||
}
|
||||
}
|
||||
|
||||
const PORT = process.env.SOCKET_PORT || 3001;
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`Socket.IO server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export { io };
|
||||
357
lib/store.ts
Archivo normal
357
lib/store.ts
Archivo normal
@@ -0,0 +1,357 @@
|
||||
'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 socket = io('http://localhost:3000', {
|
||||
transports: ['websocket', 'polling'],
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to socket server');
|
||||
set({ isConnected: true, socket });
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from socket server');
|
||||
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) {
|
||||
console.log('AI mode: executing move', move);
|
||||
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) {
|
||||
console.log('Online mode: sending move to server', move);
|
||||
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) {
|
||||
console.log('No tiles in boneyard');
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Drew tile in AI mode:', drawnTile);
|
||||
}
|
||||
// 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 });
|
||||
}
|
||||
},
|
||||
}));
|
||||
77
lib/types.ts
Archivo normal
77
lib/types.ts
Archivo normal
@@ -0,0 +1,77 @@
|
||||
// Domino game types and interfaces
|
||||
|
||||
export type DominoTile = {
|
||||
id: string;
|
||||
left: number;
|
||||
right: number;
|
||||
isDouble: boolean;
|
||||
};
|
||||
|
||||
export type Position = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
export type PlacedTile = {
|
||||
tile: DominoTile;
|
||||
position: Position;
|
||||
orientation: Orientation;
|
||||
rotation: number;
|
||||
};
|
||||
|
||||
export type Player = {
|
||||
id: string;
|
||||
name: string;
|
||||
tiles: DominoTile[];
|
||||
score: number;
|
||||
isAI: boolean;
|
||||
isReady: boolean;
|
||||
};
|
||||
|
||||
export type BoardEnd = {
|
||||
value: number;
|
||||
position: Position;
|
||||
side: 'left' | 'right' | 'top' | 'bottom';
|
||||
};
|
||||
|
||||
export type GameState = {
|
||||
id: string;
|
||||
players: Player[];
|
||||
currentPlayerIndex: number;
|
||||
board: PlacedTile[];
|
||||
boneyard: DominoTile[];
|
||||
boardEnds: BoardEnd[];
|
||||
winner: string | null;
|
||||
isGameOver: boolean;
|
||||
turnsPassed: number;
|
||||
gameMode: 'waiting' | 'playing' | 'finished';
|
||||
};
|
||||
|
||||
export type GameMove = {
|
||||
playerId: string;
|
||||
tile: DominoTile;
|
||||
side: 'left' | 'right';
|
||||
pass?: boolean;
|
||||
};
|
||||
|
||||
export type SocketEvents = {
|
||||
// Client to Server
|
||||
'create-room': () => void;
|
||||
'join-room': (roomId: string, playerName: string) => void;
|
||||
'player-ready': (roomId: string) => void;
|
||||
'make-move': (roomId: string, move: GameMove) => void;
|
||||
'draw-tile': (roomId: string) => void;
|
||||
'leave-room': (roomId: string) => void;
|
||||
|
||||
// Server to Client
|
||||
'room-created': (roomId: string) => void;
|
||||
'room-joined': (gameState: GameState, playerId: string) => void;
|
||||
'game-state-updated': (gameState: GameState) => void;
|
||||
'game-started': (gameState: GameState) => void;
|
||||
'invalid-move': (message: string) => void;
|
||||
'player-joined': (player: Player) => void;
|
||||
'player-left': (playerId: string) => void;
|
||||
'error': (message: string) => void;
|
||||
};
|
||||
Referencia en una nueva incidencia
Block a user