Remove src directory - simplify project structure
This commit is contained in:
7
QWEN.md
7
QWEN.md
@@ -61,15 +61,12 @@ hexo/
|
|||||||
├── server.js # Simple HTTP server for development
|
├── server.js # Simple HTTP server for development
|
||||||
├── .gitignore # Git ignore rules
|
├── .gitignore # Git ignore rules
|
||||||
├── jsdom-pkg/ # Local jsdom library copy
|
├── jsdom-pkg/ # Local jsdom library copy
|
||||||
├── public/ # Web application files
|
├── public/ # All application files
|
||||||
│ ├── index.html # Main HTML page with start screen
|
│ ├── index.html # Main HTML page with start screen
|
||||||
│ ├── styles.css # Game UI styles
|
│ ├── styles.css # Game UI styles
|
||||||
│ ├── game.js # Main game logic and rendering
|
│ ├── game.js # Main game logic and rendering
|
||||||
│ ├── map.js # HexMap module (browser version)
|
│ ├── map.js # HexMap module
|
||||||
│ └── ai-bot.js # AI bot player logic
|
│ └── 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
|
└── test/ # Unit tests
|
||||||
└── map.test.js # Map and cell tests
|
└── map.test.js # Map and cell tests
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
"name": "hexo",
|
"name": "hexo",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Educational dice game - DiceWars clone",
|
"description": "Educational dice game - DiceWars clone",
|
||||||
"main": "src/index.js",
|
"main": "public/game.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node server.js",
|
||||||
"serve": "node server.js",
|
"serve": "node server.js",
|
||||||
"test": "node --test"
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
|
|||||||
28
src/index.js
28
src/index.js
@@ -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)}%`);
|
|
||||||
304
src/map.js
304
src/map.js
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const { describe, it } = require('node:test');
|
const { describe, it } = require('node:test');
|
||||||
const assert = require('node:assert');
|
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', () => {
|
describe('HexCell', () => {
|
||||||
it('should create a cell with axial coordinates', () => {
|
it('should create a cell with axial coordinates', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user