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;
|
||||
}
|
||||
Referencia en una nueva incidencia
Block a user