/** * Hexagonal grid map for the DiceWars game. * * Map is a 20x20 hexagonal grid where each cell can be: * - passable (playable) * - impassable (blocked) * * Uses axial coordinates (q, r) for hexagon positioning. */ const MAP_SIZE = 20; const CELL_TYPES = { EMPTY: 0, // Passable, unowned BLOCKED: 1, // Impassable terrain PLAYER1: 2, // Player 1 owned PLAYER2: 3, // Player 2 owned }; /** * Represents a single hex cell on the map */ class HexCell { constructor(q, r, type = CELL_TYPES.EMPTY) { this.q = q; // Axial coordinate q this.r = r; // Axial coordinate r this.type = type; // Cell ownership/type this.dice = []; // Array of dice values on this cell } /** * Calculate unit strength: F = (cnt-1)*full_dice + current_dice */ getStrength() { if (this.dice.length === 0) return 0; const cnt = this.dice.length; const fullDice = 6; const currentDice = this.dice[this.dice.length - 1]; // Top die return (cnt - 1) * fullDice + currentDice; } /** * Check if cell has maximum strength (8 dice with 6 on top) */ isMaxStrength() { return this.dice.length >= 8 && this.getStrength() >= 48; } /** * Add a die to this cell */ addDie(value) { if (this.dice.length < 8) { this.dice.push(value); return true; } return false; } /** * Remove dice from cell, leaving specified strength */ setStrength(targetStrength) { if (targetStrength <= 0) { this.dice = []; return; } const fullDice = 6; const cnt = Math.floor((targetStrength - 1) / fullDice) + 1; const remainder = targetStrength - (cnt - 1) * fullDice; this.dice = new Array(cnt - 1).fill(6); if (remainder > 0) { this.dice.push(remainder); } } isPassable() { return this.type !== CELL_TYPES.BLOCKED; } isOwned() { return this.type === CELL_TYPES.PLAYER1 || this.type === CELL_TYPES.PLAYER2; } getOwner() { if (this.type === CELL_TYPES.PLAYER1) return 1; if (this.type === CELL_TYPES.PLAYER2) return 2; return 0; } } /** * Hexagonal map generator and manager */ class HexMap { constructor(size = MAP_SIZE) { this.size = size; this.cells = new Map(); // Key: "q,r", Value: HexCell this.generate(); } /** * Generate the hexagonal grid */ generate() { this.cells.clear(); for (let q = 0; q < this.size; q++) { for (let r = 0; r < this.size; r++) { const key = this.getKey(q, r); const type = Math.random() < 0.15 ? CELL_TYPES.BLOCKED : CELL_TYPES.EMPTY; this.cells.set(key, new HexCell(q, r, type)); } } } /** * Get cell by axial coordinates */ getCell(q, r) { const key = this.getKey(q, r); return this.cells.get(key); } /** * Get cell by key string */ getCellByKey(key) { return this.cells.get(key); } /** * Generate key from coordinates */ getKey(q, r) { return `${q},${r}`; } /** * Get all passable cells */ getPassableCells() { return Array.from(this.cells.values()).filter(cell => cell.isPassable()); } /** * Get all empty (unowned) passable cells */ getEmptyCells() { return Array.from(this.cells.values()).filter( cell => cell.isPassable() && !cell.isOwned() ); } /** * Get all cells owned by a player */ getPlayerCells(playerId) { const targetType = playerId === 1 ? CELL_TYPES.PLAYER1 : CELL_TYPES.PLAYER2; return Array.from(this.cells.values()).filter( cell => cell.type === targetType ); } /** * Get neighboring cells (6 directions in hex grid) */ getNeighbors(q, r) { const directions = [ [1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1] ]; const neighbors = []; for (const [dq, dr] of directions) { const nq = q + dq; const nr = r + dr; if (nq >= 0 && nq < this.size && nr >= 0 && nr < this.size) { const cell = this.getCell(nq, nr); if (cell && cell.isPassable()) { neighbors.push(cell); } } } return neighbors; } /** * Calculate supply for a player: S = size of largest solid (connected) territory * A solid territory is a connected region of player's cells */ calculateSupply(playerId) { const playerCells = this.getPlayerCells(playerId); if (playerCells.length === 0) return 0; // Build adjacency map for player cells const cellMap = new Map(); for (const cell of playerCells) { cellMap.set(this.getKey(cell.q, cell.r), cell); } // Find connected components using BFS const visited = new Set(); let maxTerritory = 0; for (const cell of playerCells) { const key = this.getKey(cell.q, cell.r); if (visited.has(key)) continue; // BFS to find size of this territory let territorySize = 0; const queue = [cell]; visited.add(key); while (queue.length > 0) { const current = queue.shift(); territorySize++; // Get all neighbors that are also player cells const neighbors = this.getNeighbors(current.q, current.r); for (const neighbor of neighbors) { if (neighbor.getOwner() === playerId) { const nKey = this.getKey(neighbor.q, neighbor.r); if (!visited.has(nKey)) { visited.add(nKey); queue.push(neighbor); } } } } maxTerritory = Math.max(maxTerritory, territorySize); } return maxTerritory; } /** * Render map to console (simplified ASCII representation) */ render() { let output = ''; for (let r = 0; r < this.size; r++) { // Offset every other row for hex appearance const offset = r % 2 === 0 ? 0 : 2; output += ' '.repeat(offset); for (let q = 0; q < this.size; q++) { const cell = this.getCell(q, r); const symbol = this.getCellSymbol(cell); output += `[${symbol}]`; } output += '\n'; } console.log(output); } /** * Get symbol for cell visualization */ getCellSymbol(cell) { if (cell.type === CELL_TYPES.BLOCKED) return '██'; if (cell.type === CELL_TYPES.PLAYER1) return 'P1'; if (cell.type === CELL_TYPES.PLAYER2) return 'P2'; if (cell.dice.length > 0) return cell.getStrength().toString().padStart(2, ' '); return ' '; } /** * Set cell ownership */ setOwner(q, r, playerId) { const cell = this.getCell(q, r); if (cell && cell.isPassable()) { cell.type = playerId === 1 ? CELL_TYPES.PLAYER1 : CELL_TYPES.PLAYER2; return true; } return false; } /** * Clear cell ownership */ clearOwner(q, r) { const cell = this.getCell(q, r); if (cell) { cell.type = CELL_TYPES.EMPTY; cell.dice = []; return true; } return false; } } // ES Module exports for browser export { HexMap, HexCell, CELL_TYPES, MAP_SIZE }; // CommonJS exports for Node.js if (typeof module !== 'undefined' && module.exports) { module.exports = { HexMap, HexCell, CELL_TYPES, MAP_SIZE }; }