diff --git a/QWEN.md b/QWEN.md index b5c6b81..3253823 100644 --- a/QWEN.md +++ b/QWEN.md @@ -6,16 +6,21 @@ ### Game Concept -A strategy dice game played on a hexagonal grid where players command armies of dice and battle to conquer territories. +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 -- **Hexagonal Grid**: 20×20 map with proper adjacency -- **Dice Combat**: Roll-based battle system + - 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 @@ -27,23 +32,26 @@ hexo/ ├── server.js # Simple HTTP server for development ├── .gitignore # Git ignore rules ├── jsdom-pkg/ # Local jsdom library copy -├── public/ # All application files +├── 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 +│ ├── 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 -└── test/ # Unit tests +│ └── 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 -- **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`) +| 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 @@ -54,41 +62,61 @@ npm run serve # Run console demo npm start -# Run tests +# Run tests (88 tests) npm test ``` ## Development Conventions -- ES Modules for browser code (`import`/`export`) -- CommonJS for Node.js code (`require`/`module.exports`) -- Map module exports both ES and CommonJS for compatibility -- Tests use Node.js built-in `node:test` module +- **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 -- Generatable hexagonal grid map (20×20 cells) -- Each cell can be passable or blocked/impassable -- Each field can hold up to 8 dice -- Cells are connected to 6 neighbors (hexagonal adjacency) +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: +- **Unit strength calculation formula:** ``` - F = (cnt - 1) × full_dice + current_dice + F = (cnt - 1) × 6 + current_dice ``` Where: - - `cnt` = number of dice on the field - - `full_dice` = maximum die value (6) - - `current_dice` = top die current value (1-6) + - `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 dice on their starting position +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) @@ -97,25 +125,29 @@ npm test 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 | -|--------|-------|----------| -| P1 | Green | `#4ecca3` | -| P2 | Red | `#e94560` | -| P3 | Yellow | `#f9ed69` | -| P4 | Cyan | `#a8e6cf` | +| 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) | -### Starting Positions +Where `offset = max(1, floor(mapSize / 10))` -Players are placed at fixed positions on the map: -- **P1**: Top area (q: 2, r: 2) -- **P2**: Bottom area (q: MAP_SIZE-3, r: MAP_SIZE-3) -- **P3**: Bottom-left (q: 2, r: MAP_SIZE-3) -- **P4**: Top-right (q: MAP_SIZE-3, r: 2) +Each player starts with **strength 8** at their starting position. -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 @@ -123,6 +155,37 @@ Each player starts with strength 8 at their starting position. 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 @@ -159,13 +222,13 @@ movePriority(move) { } ``` -#### Move Types +#### Move Types and Priorities -| Type | Priority | Description | -|------|----------|-------------| -| **Attack (favorable)** | 100+ | Attack enemy with higher strength | -| **Expand** | 50+ | Capture empty cells | -| **Attack (risky)** | -50 | Attack enemy with equal/higher strength | +| 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 @@ -174,7 +237,8 @@ movePriority(move) { 3. Sort moves by priority 4. Wait for thinking delay (1000ms) 5. Execute best move -6. Repeat until no moves available, then end turn +6. Repeat until no moves available (max 50 moves per turn) +7. End turn ### Integration with Game UI @@ -189,42 +253,162 @@ The AI bot integrates with the main game through: ### 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) +- Supply calculation (largest connected territory via BFS) ### 2. Dice Engine (`game.js`) -- Randomization for combat rolls -- Strength calculation -- Dice distribution during supply phase +```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`) -- Attack/defense resolution logic -- Victory/defeat outcomes -- Cell ownership transfer +```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 -- Unit positions tracking -- Victory conditions (last player standing) +- **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 +- 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 @@ -232,3 +416,4 @@ The AI bot integrates with the main game through: - 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/` diff --git a/README.md b/README.md index abaef40..8de3e79 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ```bash npm run serve # Запустить веб-сервер (http://localhost:8080) -npm test # Запустить тесты +npm test # Запустить тесты (88 тестов) npm start # Консольная версия карты ``` @@ -15,10 +15,15 @@ npm start # Консольная версия карты При запуске игры открывается экран настройки: 1. **Выберите количество игроков**: 2, 3 или 4 -2. **Настройте тип каждого игрока**: +2. **Выберите размер карты**: + - **Small (10×10)** — Быстрая игра на маленькой карте + - **Medium (15×15)** — Сбалансированный размер + - **Large (20×20)** — Стандартный размер (по умолчанию) + - **Extra Large (25×25)** — Большая карта для длительных игр +3. **Настройте тип каждого игрока**: - **Human** — управление человеком (клики мышью) - **AI Bot** — управление компьютером -3. Нажмите **Start Game** для начала игры +4. Нажмите **Start Game** для начала игры ### Комбинации игроков @@ -34,7 +39,7 @@ npm start # Консольная версия карты | P1 | 🟢 Зелёный | `#4ecca3` | | P2 | 🔴 Красный | `#e94560` | | P3 | 🟡 Жёлтый | `#f9ed69` | -| P4 | 🔵 Бирюзовый | `#a8e6cf` | +| P4 | 🔵 Бирюзовый | `#00adb5` | ## AI Bot @@ -44,10 +49,11 @@ AI бот автоматически играет за выбранного иг 1. **Анализ поля** — бот оценивает все возможные ходы 2. **Приоритеты ходов**: - - 🎯 Атака слабого противника (высокий шанс победы) - - 📈 Захват пустых клеток (расширение территории) + - 🎯 Атака слабого противника (высокий шанс победы) — приоритет 100+ + - 📈 Захват пустых клеток (расширение территории) — приоритет 50+ - 💪 Укрепление позиций (перемещение к сильным клеткам) 3. **Задержка мышления** — 1000 мс перед каждым ходом для естественности геймплея +4. **Несколько ходов за turn** — AI делает все возможные ходы подряд, затем завершает ход ### Индикаторы AI @@ -55,14 +61,43 @@ AI бот автоматически играет за выбранного иг - Во время хода AI в панели действий показано: *"AI is thinking..."* - Кнопки управления отключены во время хода AI +### Логика принятия решений + +```javascript +movePriority(move) { + let priority = 0; + + // Атака слабого врага (высший приоритет) + if (move.type === 'attack') { + if (move.attackStrength > move.defenseStrength) { + priority += 100; // Высокий шанс победы + priority += move.attackStrength - move.defenseStrength; + } else { + priority -= 50; // Рискованная атака + } + } + + // Расширение на пустые клетки (средний приоритет) + if (move.type === 'expand') { + priority += 50; + priority += move.attackStrength; + } + + // Бонус за сильную позицию + priority += move.attackStrength * 0.5; + + return priority; +} +``` + ## Правила игры ### 1. Карта -- Гексагональная сетка 20×20 ячеек +- Гексагональная сетка **10×10**, **15×15**, **20×20** или **25×25** ячеек - Каждая ячейка может быть: - **Проходима** (пустая или принадлежит игроку) - - **Непроходима** (заблокирована, серый цвет) + - **Непроходима** (заблокирована, серый цвет, ~15% карты) ### 2. Игровые единицы @@ -72,6 +107,15 @@ AI бот автоматически играет за выбранного иг - `cnt` — количество кубиков - `current_dice` — значение верхнего кубика +**Примеры расчёта силы:** +| Кубики | Сила | +|--------|------| +| [4] | 4 | +| [6] | 6 | +| [6, 1] | 7 | +| [6, 6, 2] | 14 | +| [6, 6, 6, 6, 6, 6, 6, 6] | 48 (максимум) | + ### 3. Ход игры #### Перемещение/Атака @@ -124,5 +168,38 @@ AI бот автоматически играет за выбранного иг Игра поддерживает: - **2-4 игрока** (любая комбинация людей и AI) +- **4 размера карты**: 10×10, 15×15, 20×20, 25×25 - **Случайная генерация карты** при каждом запуске - **Случайные стартовые позиции** для игроков + +## Стартовые позиции + +Игроки размещаются в углах карты (позиции масштабируются с размером карты): + +| Игрок | Позиция | Описание | +|-------|---------|----------| +| P1 | (offset, offset) | Верхний-левый угол | +| P2 | (size-1-offset, size-1-offset) | Нижний-правый угол | +| P3 | (offset, size-1-offset) | Нижний-левый угол | +| P4 | (size-1-offset, offset) | Верхний-правый угол | + +`offset = max(1, floor(size / 10))` + +Каждый игрок начинает с **силой 8** на своей стартовой позиции. + +## Тестирование + +Проект покрыт **88 тестами**, проверяющими: + +- **Динамические размеры карт** (10×10, 15×15, 20×20, 25×25) +- **HexCell**: создание, расчёт силы, максимальная сила, добавление кубиков +- **HexMap**: генерация, соседи, снабжение, владение +- **Логика выбора целей**: атака врагов, захват пустых клеток, блокировка своих клеток +- **AIBot**: поиск ходов, приоритеты, выполнение ходов, завершение хода +- **Интеграционные тесты**: 4 AI бота одновременно, разные размеры карт + +```bash +npm test +``` + +Все тесты проходят успешно ✅