Files
hexo/QWEN.md
2026-02-22 11:08:02 +03:00

420 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Project Context: hexo
## Project Overview
**hexo** is an educational game project — a clone of [DiceWars](https://www.gamedesign.jp/games/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
```bash
# 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.js` for both ES and CommonJS compatibility
- **Tests** use Node.js built-in `node:test` module
## Core Game Mechanics
### Map System
The map system supports **dynamic sizes** (10×10, 15×15, 20×20, 25×25):
```javascript
// 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:**
```
F = (cnt - 1) × 6 + current_dice
```
Where:
- `cnt` = number of dice on the cell
- `current_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
1. **Setup**: Each player starts with strength 8 at their starting position
2. **Movement**: Can move if strength > 1
- Source cell left with 1, target receives strength-1
3. **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)
4. **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)
5. **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**:
```javascript
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:
```javascript
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
1. Get all player cells
2. Find all possible moves (neighbors that are not own cells)
3. Sort moves by priority
4. Wait for thinking delay (1000ms)
5. Execute best move
6. Repeat until no moves available (max 50 moves per turn)
7. End turn
### Integration with Game UI
The AI bot integrates with the main game through:
- **`gameUI.selectedCell`** / **`gameUI.currentTarget`**: Set before executing moves
- **`gameUI.executeAttack()`**: Called to perform the actual attack
- **`gameUI.endTurn()`**: Called when AI has no more moves
- **`gameUI.isAIThinking`**: Flag to prevent user interaction during AI turn
## Key Implementation Areas
### 1. Map Generator (`map.js`)
```javascript
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`)
```javascript
// 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`)
```javascript
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 `HexMap` cell 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`)
```javascript
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
```bash
npm test
# Output:
# ✔ tests 88
# ✔ suites 27
# ✔ pass 88
# ✔ fail 0
```
## Notes
- The `.gitignore` file appears to be a Python template and may need to be updated for a JavaScript project
- The `jsdom-pkg` directory 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 in `public/`