Files
dominoes/lib/gameLogic.ts
2025-11-12 18:22:30 +01:00

456 líneas
14 KiB
TypeScript

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 with smart wrapping to keep tiles visible
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;
// Count tiles on current side to determine wrapping
const tilesOnSide = side === 'right' ? board.length - 1 : 0;
const segmentLength = 5; // Change direction every 5 tiles
const segmentIndex = Math.floor(tilesOnSide / segmentLength);
if (side === 'right') {
// Alternate between horizontal right and vertical down
const isVerticalSegment = segmentIndex % 2 === 1;
if (isVerticalSegment && lastTile.orientation === 'horizontal') {
// Transition from horizontal to vertical (going down)
orientation = isDouble ? 'horizontal' : 'vertical';
const offset = lastTile.orientation === 'horizontal' ? tileWidth : tileHeight;
position = {
x: lastTile.position.x + offset + spacing,
y: lastTile.position.y,
};
} else if (!isVerticalSegment && lastTile.orientation === 'vertical') {
// Transition from vertical to horizontal (going right)
orientation = isDouble ? 'vertical' : 'horizontal';
const offset = lastTile.orientation === 'vertical' ? tileHeight : tileWidth;
position = {
x: lastTile.position.x,
y: lastTile.position.y + offset + spacing,
};
} else if (isVerticalSegment) {
// Continue vertical (going down)
orientation = isDouble ? 'horizontal' : 'vertical';
const offset = lastTile.orientation === 'vertical' ? tileHeight : tileWidth;
position = {
x: lastTile.position.x,
y: lastTile.position.y + offset + spacing,
};
} else {
// Continue horizontal (going right)
orientation = isDouble ? 'vertical' : 'horizontal';
const offset = lastTile.orientation === 'horizontal' ? tileWidth : tileHeight;
position = {
x: lastTile.position.x + offset + spacing,
y: lastTile.position.y,
};
}
} else {
// Left side: alternate between horizontal left and vertical up
const isVerticalSegment = segmentIndex % 2 === 1;
if (isVerticalSegment && lastTile.orientation === 'horizontal') {
// Transition from horizontal to vertical (going up)
orientation = isDouble ? 'horizontal' : 'vertical';
const offset = lastTile.orientation === 'horizontal' ? tileWidth : tileHeight;
position = {
x: lastTile.position.x - offset - spacing,
y: lastTile.position.y,
};
} else if (!isVerticalSegment && lastTile.orientation === 'vertical') {
// Transition from vertical to horizontal (going left)
orientation = isDouble ? 'vertical' : 'horizontal';
const offset = lastTile.orientation === 'vertical' ? tileHeight : tileWidth;
position = {
x: lastTile.position.x,
y: lastTile.position.y - offset - spacing,
};
} else if (isVerticalSegment) {
// Continue vertical (going up)
orientation = isDouble ? 'horizontal' : 'vertical';
const offset = lastTile.orientation === 'vertical' ? tileHeight : tileWidth;
position = {
x: lastTile.position.x,
y: lastTile.position.y - offset - spacing,
};
} else {
// Continue horizontal (going left)
orientation = isDouble ? 'vertical' : 'horizontal';
const offset = lastTile.orientation === 'horizontal' ? tileWidth : tileHeight;
position = {
x: lastTile.position.x - offset - spacing,
y: lastTile.position.y,
};
}
}
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) {
const newTurnsPassed = gameState.turnsPassed + 1;
// Check if game is blocked (all players have passed consecutively)
if (newTurnsPassed >= gameState.players.length) {
// Game is blocked - determine winner by lowest score
let lowestScore = Infinity;
let winnerId = '';
gameState.players.forEach(p => {
const score = calculateScore(p.tiles);
if (score < lowestScore) {
lowestScore = score;
winnerId = p.id;
}
});
return {
...gameState,
currentPlayerIndex: (gameState.currentPlayerIndex + 1) % gameState.players.length,
turnsPassed: newTurnsPassed,
isGameOver: true,
winner: winnerId,
gameMode: 'finished',
};
}
return {
...gameState,
currentPlayerIndex: (gameState.currentPlayerIndex + 1) % gameState.players.length,
turnsPassed: newTurnsPassed,
};
}
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;
}