12 KiB
12 KiB
Project Context: hexo
Project Overview
hexo is an educational game project — a clone of DiceWars.
Game Concept
A strategy dice game played on a hexagonal grid where players command armies of dice and battle to conquer territories. The last player standing wins.
Features
- 2-4 Players: Support for multiple human and/or AI players
- Dynamic Map Sizes: 10×10, 15×15, 20×20, 25×25 hexagonal grids
- AI Bots: Computer-controlled players with smart move selection and thinking delay
- Makes multiple moves per turn until no valid moves remain
- Prioritizes favorable attacks, then expansion, then reinforcement
- 1000ms thinking delay between moves for natural gameplay
- Hexagonal Grid: Proper axial coordinate system with 6-direction adjacency
- Dice Combat: Roll-based battle system with strength calculation
- Solid Territory Supply: Supply = size of largest connected territory
- Flexible Setup: Any combination of human/AI players (hotseat, full AI, or mixed)
- 88 Passing Tests: Comprehensive test coverage for all game mechanics
Directory Structure
hexo/
├── README.md # Game rules and documentation (Russian)
├── QWEN.md # This file - project context
├── package.json # NPM configuration
├── server.js # Simple HTTP server for development
├── .gitignore # Git ignore rules
├── jsdom-pkg/ # Local jsdom library copy
├── public/ # All application files (no src/ directory)
│ ├── index.html # Main HTML page with start screen and game UI
│ ├── styles.css # Game UI styles
│ ├── game.js # Main game logic and canvas rendering (GameUI class)
│ ├── map.js # HexMap module (map generation, cells, supply)
│ └── ai-bot.js # AI bot player logic (AIBot class)
└── test/ # Unit tests (Node.js built-in test runner)
├── map.test.js # Map and cell tests
└── ai-bot.test.js # AI bot tests
Technology Stack
| Layer | Technology |
|---|---|
| Runtime | Node.js |
| Frontend | Vanilla JavaScript (ES Modules), HTML5 Canvas, CSS3 |
| Backend | Simple HTTP server (server.js) |
| Testing | Node.js built-in test runner (node --test) |
| Dependencies | jsdom (local copy in jsdom-pkg/) |
Building and Running
# Start web server (http://localhost:8080)
npm run serve
# Run console demo
npm start
# Run tests (88 tests)
npm test
Development Conventions
- ES Modules for browser code (
import/export) - CommonJS for Node.js code (
require/module.exports) - Dual exports in
map.jsfor both ES and CommonJS compatibility - Tests use Node.js built-in
node:testmodule
Core Game Mechanics
Map System
The map system supports dynamic sizes (10×10, 15×15, 20×20, 25×25):
// Create map with custom size
const map = new HexMap(15); // 15x15 grid
// Default size is 20x20
const defaultMap = new HexMap();
Key characteristics:
- Hexagonal grid using axial coordinates (q, r)
- Each cell can be passable or blocked/impassable (~15% blocked)
- Each cell can hold up to 8 dice
- Cells connect to 6 neighbors (hexagonal adjacency)
- Neighbor calculation respects map boundaries
Dice System
- Standard 6-sided dice
- Unit strength calculation formula:
Where:
F = (cnt - 1) × 6 + current_dicecnt= number of dice on the cellcurrent_dice= top die value (1-6)
Examples:
| Dice Array | Calculation | Strength |
|---|---|---|
[4] |
(1-1)×6 + 4 | 4 |
[6] |
(1-1)×6 + 6 | 6 |
[6, 1] |
(2-1)×6 + 1 | 7 |
[6, 6, 2] |
(3-1)×6 + 2 | 14 |
[6, 6, 6, 6, 6, 6, 6, 6] |
(8-1)×6 + 6 | 48 (max) |
Game Rules
- Setup: Each player starts with strength 8 at their starting position
- Movement: Can move if strength > 1
- Source cell left with 1, target receives strength-1
- Combat: Both sides roll dice (1 to their strength)
- Attacker wins: Takes cell with
attack_roll - 1, source becomes 1 - Defender wins: Attacker reduced to 1, defender keeps
defense_roll - attack_roll(min 1)
- Attacker wins: Takes cell with
- Supply: After turn ends, player receives supply = largest connected territory size
- Distributed 1 by 1 to random non-max cells
- Max per cell: 48 (8 dice × 6)
- Victory: Last player with cells on the map wins
Player Configuration
| Player | Color | HEX Code | Starting Position |
|---|---|---|---|
| P1 | 🟢 Green | #4ecca3 |
(offset, offset) |
| P2 | 🔴 Red | #e94560 |
(size-1-offset, size-1-offset) |
| P3 | 🟡 Yellow | #f9ed69 |
(offset, size-1-offset) |
| P4 | 🔵 Cyan | #00adb5 |
(size-1-offset, offset) |
Where offset = max(1, floor(mapSize / 10))
Each player starts with strength 8 at their starting position.
Starting Positions by Map Size
| Map Size | Offset | P1 | P2 | P3 | P4 |
|---|---|---|---|---|---|
| 10×10 | 1 | (1,1) | (8,8) | (1,8) | (8,1) |
| 15×15 | 1 | (1,1) | (13,13) | (1,13) | (13,1) |
| 20×20 | 2 | (2,2) | (17,17) | (2,17) | (17,2) |
| 25×25 | 2 | (2,2) | (22,22) | (2,22) | (22,2) |
AI Bot Logic
Implementation Details (ai-bot.js)
The AI bot is implemented in the AIBot class with the following behavior:
Multiple Moves Per Turn
Unlike a simple single-move AI, this bot makes multiple moves per turn:
async playTurn() {
let moveCount = 0;
const maxMovesPerTurn = 50; // Prevent infinite loops
const maxConsecutiveNoMoves = 3;
while (consecutiveNoMoves < maxConsecutiveNoMoves && moveCount < maxMovesPerTurn) {
const moves = this.findPossibleMoves(playerCells);
if (moves.length === 0) {
consecutiveNoMoves++;
continue;
}
consecutiveNoMoves = 0;
moves.sort((a, b) => this.movePriority(b) - this.movePriority(a));
await this.wait(this.thinkingTime); // 1000ms delay
this.executeMove(moves[0]);
moveCount++;
}
this.gameUI.endTurn();
}
Thinking Delay
- Delay: 1000ms between moves
- Purpose: Creates natural gameplay feel, allows human players to follow AI actions
Move Evaluation
The AI evaluates all possible moves using a priority system:
movePriority(move) {
let priority = 0;
// Attack weak enemies (highest priority)
if (move.type === 'attack') {
if (move.attackStrength > move.defenseStrength) {
priority += 100; // Likely to win
priority += move.attackStrength - move.defenseStrength;
} else {
priority -= 50; // Risky attack
}
}
// Expand to empty cells (medium priority)
if (move.type === 'expand') {
priority += 50;
priority += move.attackStrength; // Stronger placement = better
}
// Prefer moves that create strong positions
priority += move.attackStrength * 0.5;
return priority;
}
Move Types and Priorities
| Type | Condition | Priority | Description |
|---|---|---|---|
| Attack (favorable) | attack > defense | 100+ | Attack enemy with higher strength |
| Expand | empty cell | 50+ | Capture empty cells |
| Attack (risky) | attack ≤ defense | -50 | Attack enemy with equal/higher strength |
Turn Flow
- Get all player cells
- Find all possible moves (neighbors that are not own cells)
- Sort moves by priority
- Wait for thinking delay (1000ms)
- Execute best move
- Repeat until no moves available (max 50 moves per turn)
- End turn
Integration with Game UI
The AI bot integrates with the main game through:
gameUI.selectedCell/gameUI.currentTarget: Set before executing movesgameUI.executeAttack(): Called to perform the actual attackgameUI.endTurn(): Called when AI has no more movesgameUI.isAIThinking: Flag to prevent user interaction during AI turn
Key Implementation Areas
1. Map Generator (map.js)
class HexMap {
constructor(size = 20) {
this.size = size;
this.cells = new Map(); // Key: "q,r", Value: HexCell
this.generate();
}
generate() {
for (let q = 0; q < this.size; q++) {
for (let r = 0; r < this.size; r++) {
const type = Math.random() < 0.15 ? CELL_TYPES.BLOCKED : CELL_TYPES.EMPTY;
this.cells.set(`${q},${r}`, new HexCell(q, r, type));
}
}
}
}
Features:
- Hexagonal grid creation with passable/impassable cells
- Cell ownership tracking
- Neighbor calculation (6 directions)
- Supply calculation (largest connected territory via BFS)
2. Dice Engine (game.js)
// Strength calculation
getStrength() {
if (this.dice.length === 0) return 0;
const cnt = this.dice.length;
const currentDice = this.dice[this.dice.length - 1];
return (cnt - 1) * 6 + currentDice;
}
// Set strength (reconstruct dice array)
setStrength(targetStrength) {
if (targetStrength <= 0) {
this.dice = [];
return;
}
const cnt = Math.floor((targetStrength - 1) / 6) + 1;
const remainder = targetStrength - (cnt - 1) * 6;
this.dice = new Array(cnt - 1).fill(6);
if (remainder > 0) this.dice.push(remainder);
}
3. Combat System (game.js)
executeAttack() {
const attackStrength = attacker.getStrength() - 1;
const defenseStrength = defender.getStrength();
const attackRoll = Math.floor(Math.random() * attackStrength) + 1;
const defenseRoll = Math.floor(Math.random() * defenseStrength) + 1;
if (attackRoll > defenseRoll) {
// Attacker wins
defender.setStrength(attackRoll - 1);
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
} else {
// Defender wins
defender.setStrength(Math.max(1, defenseRoll - attackRoll));
}
attacker.setStrength(1);
}
4. Game State (game.js)
- Player turns management: Circular turn order (P1 → P2 → P3 → P4 → P1)
- Unit positions tracking: Via
HexMapcell ownership - Victory conditions: Last player standing (checked when player loses all cells)
- AI turn handling: Async/await pattern for sequential AI moves
5. UI/Rendering (game.js)
class GameUI {
constructor() {
this.canvas = document.getElementById('game-canvas');
this.ctx = this.canvas.getContext('2d');
this.mapSize = 20; // Dynamic, configurable
this.playerCount = 2; // 2-4 players
this.playerTypes = {}; // {1: 'human', 2: 'ai', ...}
}
// Hex to pixel conversion for rendering
hexToPixel(q, r) {
const sqrt3 = Math.sqrt(3);
const x = this.offsetX + HEX_SIZE * sqrt3 * (q + r/2);
const y = this.offsetY + HEX_SIZE * 1.5 * r;
return { x, y };
}
}
Features:
- HTML5 Canvas rendering
- Hexagon drawing with proper coordinates
- Player color indicators
- Dice visualization (strength + die count dots)
- Battle log
6. AI Bot (ai-bot.js)
Key features:
- Move evaluation and prioritization
- Thinking delay for natural gameplay
- Multiple moves per turn
- Integration with game UI for move execution
- Boundary and blocked cell awareness
Test Coverage
The project has 88 passing tests covering:
Map Tests (map.test.js)
| Category | Tests |
|---|---|
| Dynamic Map Sizes | 7 tests (10×10, 15×15, 20×20, 25×25) |
| HexCell | 6 tests (creation, strength, max strength, dice, passability) |
| HexMap | 9 tests (generation, neighbors, ownership, supply) |
| Target Selection | 5 tests (attack enemies, capture empty, block own cells) |
AI Bot Tests (ai-bot.test.js)
| Category | Tests |
|---|---|
| Instantiation | 3 tests |
| findPossibleMoves | 8 tests |
| movePriority | 6 tests |
| AI prefers attacking weak enemies | 3 tests |
| AI does not select moves with strength ≤ 1 | 4 tests |
| AI respects map boundaries | 4 tests |
| executeMove | 2 tests |
| playTurn | 2 tests |
| Integration tests | 3 tests |
| Four AI bots stress test | 1 test |
| Different map sizes | 8 tests |
Running Tests
npm test
# Output:
# ✔ tests 88
# ✔ suites 27
# ✔ pass 88
# ✔ fail 0
Notes
- The
.gitignorefile appears to be a Python template and may need to be updated for a JavaScript project - The
jsdom-pkgdirectory contains a local copy of jsdom, possibly for offline development or custom modifications - Game rules are documented in Russian in README.md
- All game logic runs in the browser (no server-side game state)
- The project uses no
src/directory — all source files are inpublic/