diff --git a/QWEN.md b/QWEN.md index dd23887..f366bf4 100644 --- a/QWEN.md +++ b/QWEN.md @@ -61,15 +61,12 @@ hexo/ ├── server.js # Simple HTTP server for development ├── .gitignore # Git ignore rules ├── jsdom-pkg/ # Local jsdom library copy -├── public/ # Web application files +├── public/ # All application files │ ├── index.html # Main HTML page with start screen │ ├── styles.css # Game UI styles │ ├── game.js # Main game logic and rendering -│ ├── map.js # HexMap module (browser version) +│ ├── map.js # HexMap module │ └── ai-bot.js # AI bot player logic -├── src/ # Server-side modules -│ ├── index.js # Console demo entry point -│ └── map.js # HexMap module (Node.js version) └── test/ # Unit tests └── map.test.js # Map and cell tests ``` diff --git a/package.json b/package.json index 079b69b..72e93d5 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "hexo", "version": "0.1.0", "description": "Educational dice game - DiceWars clone", - "main": "src/index.js", + "main": "public/game.js", "scripts": { - "start": "node src/index.js", + "start": "node server.js", "serve": "node server.js", "test": "node --test" }, diff --git a/src/index.js b/src/index.js deleted file mode 100644 index fde38af..0000000 --- a/src/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Hexo - DiceWars Clone - * Main entry point - */ - -const { HexMap, CELL_TYPES } = require('./map.js'); - -console.log('=== HEXO - DiceWars Clone ===\n'); - -// Create and display the map -const map = new HexMap(20); - -console.log('Generated Map (20x20 hexagonal grid):\n'); -console.log('Legend: [██]=Blocked, [P1]=Player1, [P2]=Player2, [nn]=Strength, [ ]=Empty\n'); - -map.render(); - -// Show some statistics -const passableCells = map.getPassableCells().length; -const blockedCells = Array.from(map.cells.values()).filter( - cell => cell.type === CELL_TYPES.BLOCKED -).length; - -console.log(`\n=== Map Statistics ===`); -console.log(`Total cells: ${map.size * map.size}`); -console.log(`Passable: ${passableCells}`); -console.log(`Blocked: ${blockedCells}`); -console.log(`Blocked %: ${((blockedCells / (map.size * map.size)) * 100).toFixed(1)}%`); diff --git a/src/map.js b/src/map.js deleted file mode 100644 index d096f8e..0000000 --- a/src/map.js +++ /dev/null @@ -1,304 +0,0 @@ -/** - * 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 }; -} diff --git a/test/map.test.js b/test/map.test.js index 87710d3..fd66fae 100644 --- a/test/map.test.js +++ b/test/map.test.js @@ -1,6 +1,6 @@ const { describe, it } = require('node:test'); const assert = require('node:assert'); -const { HexMap, HexCell, CELL_TYPES, MAP_SIZE } = require('../src/map.js'); +const { HexMap, HexCell, CELL_TYPES, MAP_SIZE } = require('../public/map.js'); describe('HexCell', () => { it('should create a cell with axial coordinates', () => {