@pompidup/kingdomino-engine 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -492
- package/dist/index.d.ts +658 -72
- package/dist/index.js +6118 -348
- package/dist/index.js.map +1 -1
- package/dist/index.universal.js +6118 -348
- package/dist/wasm-bridge.js +6967 -1574
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,585 +7,268 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
10
|
-
A
|
|
11
|
-
|
|
12
|
-
## Table of Contents
|
|
13
|
-
|
|
14
|
-
- [Overview](#overview)
|
|
15
|
-
- [Requirements](#requirements)
|
|
16
|
-
- [Features](#features)
|
|
17
|
-
- [Installation](#installation)
|
|
18
|
-
- [Usage](#usage)
|
|
19
|
-
- [Basic Setup](#basic-setup)
|
|
20
|
-
- [Engine Options](#engine-options)
|
|
21
|
-
- [Adding Players](#adding-players)
|
|
22
|
-
- [Getting Available Game Modes](#getting-available-game-modes)
|
|
23
|
-
- [Getting Available Extra Rules](#getting-available-extra-rules)
|
|
24
|
-
- [Setting Up Extra Rules](#setting-up-extra-rules)
|
|
25
|
-
- [Starting the Game](#starting-the-game)
|
|
26
|
-
- [Player Actions](#player-actions)
|
|
27
|
-
- [Game Flow](#game-flow)
|
|
28
|
-
- [Scoring](#scoring)
|
|
29
|
-
- [Valid Placements](#valid-placements)
|
|
30
|
-
- [Serialization](#serialization)
|
|
31
|
-
- [Complete Game Example](#complete-game-example)
|
|
32
|
-
- [Extra Rules](#extra-rules)
|
|
33
|
-
- [API Documentation](#api-documentation)
|
|
34
|
-
- [Key Types and Interfaces](#key-types-and-interfaces)
|
|
35
|
-
- [Methods](#methods)
|
|
36
|
-
- [Development](#development)
|
|
37
|
-
- [Setup](#setup)
|
|
38
|
-
- [Testing](#testing)
|
|
39
|
-
- [Linting and Type Checking](#linting-and-type-checking)
|
|
40
|
-
- [Building](#building)
|
|
41
|
-
- [Contributing](#contributing)
|
|
10
|
+
A TypeScript engine for the Kingdomino board game and its expansions.
|
|
42
11
|
|
|
43
12
|
## Overview
|
|
44
13
|
|
|
45
|
-
This
|
|
46
|
-
|
|
47
|
-
Kingdomino is a tile-placement game where players build kingdoms by connecting domino-like tiles with different terrains. This engine handles all the game logic, allowing you to focus on building the user interface and experience.
|
|
14
|
+
This engine provides the core logic for Kingdomino: state management, rule enforcement, scoring, and turn flow. It is **not** a UI — consumers drive the game by calling engine methods and passing game state back in.
|
|
48
15
|
|
|
49
16
|
The engine is **stateless**: each method takes the current game state as input and returns a new game state. It is your responsibility to persist the state between calls.
|
|
50
17
|
|
|
51
18
|
## Requirements
|
|
52
19
|
|
|
53
|
-
- Node.js >=
|
|
54
|
-
|
|
55
|
-
## Features
|
|
56
|
-
|
|
57
|
-
- **Game State Management**: Efficiently manage game states, including player turns, game rules, and game progression.
|
|
58
|
-
- **Rule Enforcement**: Automatically enforce game rules, ensuring a fair and consistent gameplay experience.
|
|
59
|
-
- **Player Interaction**: Simplify player interactions, including drawing tiles, placing dominoes, and scoring.
|
|
60
|
-
- **Extra Rules Support**: Implement and manage additional game rules for enhanced gameplay.
|
|
61
|
-
- **Multiple Game Modes**: Support for different game modes, including Classic and potentially others.
|
|
62
|
-
- **Scoring System**: Automatic calculation of scores based on kingdom layouts and rule sets.
|
|
63
|
-
- **Valid Placement Detection**: Find all valid positions for a domino or check if any placement exists.
|
|
64
|
-
- **Serialization**: Save and restore game state to/from JSON for persistence.
|
|
65
|
-
- **TypeScript Support**: Full TypeScript support with comprehensive type definitions.
|
|
66
|
-
- **Dependency Injection**: Custom shuffle, UUID, and logger implementations can be injected.
|
|
20
|
+
- Node.js >= 20
|
|
67
21
|
|
|
68
22
|
## Installation
|
|
69
23
|
|
|
70
|
-
To install the Kingdomino Engine, you can use npm, yarn or pnpm. Run one of the following commands in your terminal:
|
|
71
|
-
|
|
72
24
|
```bash
|
|
73
25
|
npm install @pompidup/kingdomino-engine
|
|
74
|
-
yarn add @pompidup/kingdomino-engine
|
|
75
|
-
pnpm add @pompidup/kingdomino-engine
|
|
76
26
|
```
|
|
77
27
|
|
|
78
|
-
##
|
|
79
|
-
|
|
80
|
-
The Kingdomino Engine is designed to be easy to use and integrate into any application. Below are examples of how you can use the engine to create a new game, add players, set rules, and manage gameplay.
|
|
81
|
-
|
|
82
|
-
Each method returns an updated game state object, which contains the current state of the game, including the board, players, and dominoes. You will also receive the nextLord (the next player to play) and the nextAction (the next action to be performed by the next player).
|
|
83
|
-
|
|
84
|
-
**It's your responsibility to save and pass the updated game state to the next method call.**
|
|
85
|
-
|
|
86
|
-
### Basic Setup
|
|
87
|
-
|
|
88
|
-
To start using the Kingdomino Engine, import it and initialize a new game instance:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { createGameEngine } from "@pompidup/kingdomino-engine";
|
|
92
|
-
|
|
93
|
-
const engine = createGameEngine({});
|
|
94
|
-
let gameState = engine.createGame({ mode: "Classic" });
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Engine Options
|
|
98
|
-
|
|
99
|
-
You can pass options to the `createGameEngine` function to customize the engine's behavior:
|
|
100
|
-
|
|
101
|
-
| Option | Type | Default | Description |
|
|
102
|
-
|--------|------|---------|-------------|
|
|
103
|
-
| `logging` | `boolean` | `false` | Enable console logging for debugging |
|
|
104
|
-
| `logger` | `Logger` | — | Custom logger instance (takes precedence over `logging`) |
|
|
105
|
-
| `shuffleMethod` | `ShuffleMethod` | Fisher-Yates | Custom shuffle function for dominoes and lords |
|
|
106
|
-
| `uuidMethod` | `UuidMethod` | `crypto.randomUUID()` | Custom UUID generator |
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
import { createGameEngine, type EngineConfig } from "@pompidup/kingdomino-engine";
|
|
110
|
-
|
|
111
|
-
// With logging enabled
|
|
112
|
-
const engine = createGameEngine({ logging: true });
|
|
113
|
-
|
|
114
|
-
// With custom logger
|
|
115
|
-
const engine = createGameEngine({
|
|
116
|
-
logger: {
|
|
117
|
-
info: (msg) => console.log(msg),
|
|
118
|
-
error: (msg) => console.error(msg),
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// With deterministic shuffle (useful for testing)
|
|
123
|
-
const engine = createGameEngine({
|
|
124
|
-
shuffleMethod: (array) => array, // No shuffle
|
|
125
|
-
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Adding Players
|
|
129
|
-
|
|
130
|
-
Add players to the game by passing an array of player names (2-4 players, minimum 3 characters per name):
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
gameState = engine.addPlayers({
|
|
134
|
-
game: gameState,
|
|
135
|
-
players: ["Alice", "Bob", "Carol"],
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Getting Available Game Modes
|
|
140
|
-
|
|
141
|
-
You can get available game modes and select one for the game:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
const modes = engine.getModes({});
|
|
145
|
-
gameState = engine.createGame({ mode: modes[0].name });
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Getting Available Extra Rules
|
|
149
|
-
|
|
150
|
-
You can get available extra rules for a specific game mode and number of players:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
const extraRules = engine.getExtraRules({ mode: "Classic", players: 2 });
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Setting Up Extra Rules
|
|
157
|
-
|
|
158
|
-
You can add extra rules to the game to modify gameplay:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
gameState = engine.addExtraRules({
|
|
162
|
-
game: gameState,
|
|
163
|
-
extraRules: ["The middle Kingdom"],
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Starting the Game
|
|
168
|
-
|
|
169
|
-
After setting up players and rules, start the game. The engine will automatically set up the initial game state and determine the first player (lord) to play in `nextAction`.
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
gameState = engine.startGame({ game: gameState });
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Player Actions
|
|
176
|
-
|
|
177
|
-
Players can perform actions like picking a domino, placing a domino, and discarding a domino.
|
|
178
|
-
|
|
179
|
-
**Pick a domino** from the currently revealed set:
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
gameState = engine.chooseDomino({
|
|
183
|
-
game: gameState,
|
|
184
|
-
lordId: "lordId",
|
|
185
|
-
dominoPick: 12, // domino number
|
|
186
|
-
});
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
**Place a domino** on the kingdom grid:
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
gameState = engine.placeDomino({
|
|
193
|
-
game: gameState,
|
|
194
|
-
lordId: "lordId",
|
|
195
|
-
position: { x: 5, y: 4 },
|
|
196
|
-
rotation: 0, // 0, 90, 180, or 270
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**Discard a domino** when no valid placement exists:
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
gameState = engine.discardDomino({
|
|
204
|
-
game: gameState,
|
|
205
|
-
lordId: "lordId",
|
|
206
|
-
});
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### Game Flow
|
|
210
|
-
|
|
211
|
-
The game progresses through steps and actions. The `nextAction` property in the game state indicates what should happen next.
|
|
212
|
-
|
|
213
|
-
**Steps** (setup and completion phases): `addPlayers` → `options` → `start` → `result`
|
|
214
|
-
|
|
215
|
-
**Actions** (during gameplay): `pickDomino`, `placeDomino`, `pass`
|
|
216
|
-
|
|
217
|
-
Use the provided type guards to determine the current state:
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
import { isGameWithNextAction, isGameWithNextStep } from "@pompidup/kingdomino-engine";
|
|
221
|
-
|
|
222
|
-
while (isGameWithNextAction(gameState)) {
|
|
223
|
-
const { nextLord, nextAction } = gameState.nextAction;
|
|
224
|
-
// Handle pickDomino, placeDomino, or pass
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (isGameWithNextStep(gameState)) {
|
|
228
|
-
// Game has ended, get results
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### Scoring
|
|
233
|
-
|
|
234
|
-
When the last turn is played, the game transitions to the `result` step. Get final results with rankings:
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
const gameWithResults = engine.getResults({ game: gameState });
|
|
238
|
-
console.log(gameWithResults.result);
|
|
239
|
-
// [{ playerId, playerName, details: { points, maxPropertiesSize, totalCrowns }, position }]
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
You can also calculate the score of any kingdom at any time:
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
const score = engine.calculateScore({ kingdom: gameState.players[0].kingdom });
|
|
246
|
-
// { points: 42, maxPropertiesSize: 5, totalCrowns: 8 }
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Valid Placements
|
|
250
|
-
|
|
251
|
-
The engine provides helpers to determine where a domino can be placed:
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
// Get all valid positions and rotations for a domino
|
|
255
|
-
const placements = engine.getValidPlacements({
|
|
256
|
-
kingdom: player.kingdom,
|
|
257
|
-
domino: myDomino,
|
|
258
|
-
});
|
|
259
|
-
// [{ position: { x: 3, y: 4 }, rotation: 0 }, ...]
|
|
260
|
-
|
|
261
|
-
// Quick check: can this domino be placed at all?
|
|
262
|
-
const canPlace = engine.canPlaceDomino({
|
|
263
|
-
kingdom: player.kingdom,
|
|
264
|
-
domino: myDomino,
|
|
265
|
-
});
|
|
266
|
-
// true or false
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### Serialization
|
|
270
|
-
|
|
271
|
-
Save and restore game state for persistence:
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
import {
|
|
275
|
-
serializeGame,
|
|
276
|
-
deserializeGame,
|
|
277
|
-
createSavePoint,
|
|
278
|
-
restoreFromSavePoint,
|
|
279
|
-
} from "@pompidup/kingdomino-engine";
|
|
280
|
-
|
|
281
|
-
// Serialize to JSON string
|
|
282
|
-
const json = serializeGame(gameState);
|
|
283
|
-
|
|
284
|
-
// Deserialize back (returns a Result type)
|
|
285
|
-
const result = deserializeGame(json);
|
|
286
|
-
|
|
287
|
-
// Or use save points with metadata
|
|
288
|
-
const savePoint = createSavePoint(gameState);
|
|
289
|
-
// { serialized, createdAt, gameId, turn }
|
|
290
|
-
|
|
291
|
-
const restored = restoreFromSavePoint(savePoint);
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
The engine also provides `serialize` and `deserialize` methods on the engine instance:
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
const json = engine.serialize({ game: gameState });
|
|
298
|
-
const restored = engine.deserialize({ json });
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
## Complete Game Example
|
|
302
|
-
|
|
303
|
-
Here's a complete example of how to simulate a game from start to finish:
|
|
28
|
+
## Quick Start
|
|
304
29
|
|
|
305
30
|
```typescript
|
|
306
31
|
import { createGameEngine, isGameWithNextAction } from "@pompidup/kingdomino-engine";
|
|
307
32
|
|
|
308
|
-
// Create the game engine
|
|
309
33
|
const engine = createGameEngine({});
|
|
310
34
|
|
|
311
|
-
// Create
|
|
35
|
+
// Create and set up a game
|
|
312
36
|
let game = engine.createGame({ mode: "Classic" });
|
|
313
|
-
|
|
314
|
-
// Add players
|
|
315
37
|
game = engine.addPlayers({ game, players: ["Alice", "Bob"] });
|
|
316
|
-
|
|
317
|
-
// Optionally add extra rules
|
|
318
|
-
game = engine.addExtraRules({ game, extraRules: ["The middle Kingdom", "Harmony"] });
|
|
319
|
-
|
|
320
|
-
// Start the game
|
|
321
38
|
game = engine.startGame({ game });
|
|
322
39
|
|
|
323
40
|
// Game loop
|
|
324
41
|
while (isGameWithNextAction(game)) {
|
|
325
|
-
const
|
|
326
|
-
const nextAction = game.nextAction.nextAction;
|
|
42
|
+
const { nextLord, nextAction } = game.nextAction;
|
|
327
43
|
|
|
328
44
|
if (nextAction === "pickDomino") {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
game = engine.chooseDomino({
|
|
333
|
-
game,
|
|
334
|
-
lordId: currentLordId,
|
|
335
|
-
dominoPick: availableDomino.domino.number,
|
|
336
|
-
});
|
|
45
|
+
const domino = game.currentDominoes.find((d) => !d.picked);
|
|
46
|
+
if (domino) {
|
|
47
|
+
game = engine.chooseDomino({ game, lordId: nextLord, dominoPick: domino.domino.number });
|
|
337
48
|
}
|
|
338
49
|
} else if (nextAction === "placeDomino") {
|
|
339
|
-
|
|
340
|
-
const lord = game.lords.find((l) => l.id === currentLordId);
|
|
50
|
+
const lord = game.lords.find((l) => l.id === nextLord);
|
|
341
51
|
const player = game.players.find((p) => p.id === lord?.playerId);
|
|
342
|
-
|
|
343
52
|
if (lord?.dominoPicked && player) {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
if (canPlace) {
|
|
350
|
-
const placements = engine.getValidPlacements({
|
|
351
|
-
kingdom: player.kingdom,
|
|
352
|
-
domino: lord.dominoPicked,
|
|
353
|
-
});
|
|
354
|
-
game = engine.placeDomino({
|
|
355
|
-
game,
|
|
356
|
-
lordId: currentLordId,
|
|
357
|
-
position: placements[0].position,
|
|
358
|
-
rotation: placements[0].rotation,
|
|
359
|
-
});
|
|
53
|
+
const placements = engine.getValidPlacements({ kingdom: player.kingdom, domino: lord.dominoPicked });
|
|
54
|
+
if (placements.length > 0) {
|
|
55
|
+
game = engine.placeDomino({ game, lordId: nextLord, position: placements[0].position, rotation: placements[0].rotation });
|
|
360
56
|
} else {
|
|
361
|
-
game = engine.discardDomino({ game, lordId:
|
|
57
|
+
game = engine.discardDomino({ game, lordId: nextLord });
|
|
362
58
|
}
|
|
363
59
|
}
|
|
364
60
|
} else {
|
|
365
|
-
|
|
366
|
-
game = engine.discardDomino({ game, lordId: currentLordId });
|
|
61
|
+
game = engine.discardDomino({ game, lordId: nextLord });
|
|
367
62
|
}
|
|
368
63
|
}
|
|
369
64
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
console.log("Game results:", gameResult.result);
|
|
65
|
+
const results = engine.getResults({ game });
|
|
66
|
+
console.log(results.result);
|
|
373
67
|
```
|
|
374
68
|
|
|
375
|
-
##
|
|
376
|
-
|
|
377
|
-
The engine supports extra rules that can modify gameplay:
|
|
378
|
-
|
|
379
|
-
- **The Middle Kingdom**: Gain 10 additional points if your castle is in the middle of the kingdom.
|
|
380
|
-
- **Harmony**: Gain 5 additional points if your kingdom is complete (no discarded dominoes).
|
|
69
|
+
## Game Modes
|
|
381
70
|
|
|
382
|
-
|
|
71
|
+
| Mode | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| **Classic** | Original Kingdomino rules. 1-4 players, 5x5 kingdom, terrain-based scoring. |
|
|
74
|
+
| **QueenDomino** | Full Queendomino expansion with buildings, coins, knights, the Queen and the Dragon. |
|
|
75
|
+
| **KingdominoOrigins-Discovery** | Origins expansion — volcanoes, fire tokens, fire-based scoring. |
|
|
76
|
+
| **KingdominoOrigins-Totem** | Origins — Discovery + wooden resources + totem majority tiles. |
|
|
77
|
+
| **KingdominoOrigins-Tribe** | Origins — Discovery + resources + cave board + cavemen recruitment. |
|
|
78
|
+
| **AgeOfGiants** | Age of Giants extension — giants, quest tiles, 5-player support, 12 new dominos on Classic. |
|
|
79
|
+
| **AgeOfGiants-QueenDomino** | Age of Giants combined with QueenDomino. |
|
|
383
80
|
|
|
384
81
|
```typescript
|
|
385
|
-
|
|
82
|
+
let game = engine.createGame({ mode: "Classic" });
|
|
83
|
+
let game = engine.createGame({ mode: "QueenDomino" });
|
|
84
|
+
let game = engine.createGame({ mode: "KingdominoOrigins-Discovery" });
|
|
85
|
+
let game = engine.createGame({ mode: "AgeOfGiants" });
|
|
386
86
|
```
|
|
387
87
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
### Key Types and Interfaces
|
|
391
|
-
|
|
392
|
-
#### GameEngine
|
|
88
|
+
See [Game Modes](docs/game-modes.md) for rules, player counts, and extra rules details.
|
|
393
89
|
|
|
394
|
-
|
|
90
|
+
## Queendomino
|
|
395
91
|
|
|
396
|
-
|
|
397
|
-
type GameEngine = {
|
|
398
|
-
getModes: (command: GetModesCommand) => GameMode[];
|
|
399
|
-
getExtraRules: (command: GetExtraRulesCommand) => ExtraRule[];
|
|
400
|
-
createGame: (command: CreateGameCommand) => GameWithNextStep;
|
|
401
|
-
addPlayers: (command: AddPlayersCommand) => GameWithNextStep;
|
|
402
|
-
addExtraRules: (command: AddExtraRulesCommand) => GameWithNextStep;
|
|
403
|
-
startGame: (command: StartGameCommand) => GameWithNextAction;
|
|
404
|
-
chooseDomino: (command: ChooseDominoCommand) => GameWithNextAction;
|
|
405
|
-
placeDomino: (command: PlaceDominoCommand) => GameState;
|
|
406
|
-
discardDomino: (command: DiscardDominoCommand) => GameState;
|
|
407
|
-
getResults: (command: GetResultCommand) => GameWithResults;
|
|
408
|
-
calculateScore: (command: CalculateScoreCommand) => Score;
|
|
409
|
-
getValidPlacements: (command: GetValidPlacementsCommand) => ValidPlacement[];
|
|
410
|
-
canPlaceDomino: (command: CanPlaceDominoCommand) => boolean;
|
|
411
|
-
serialize: (command: SerializeGameCommand) => string;
|
|
412
|
-
deserialize: (command: DeserializeGameCommand) => GameState;
|
|
413
|
-
};
|
|
414
|
-
```
|
|
92
|
+
The QueenDomino mode adds significant new mechanics to the base game:
|
|
415
93
|
|
|
416
|
-
|
|
94
|
+
- **Construction squares** on some domino tiles where buildings can be placed
|
|
95
|
+
- **Coins** (7 starting) used to purchase buildings or use the Dragon
|
|
96
|
+
- **Knights** (max 3) placed on dominos to collect tax from construction squares
|
|
97
|
+
- **Buildings** (18 types) purchased from the Builders Board, granting crowns, towers, and end-game bonuses
|
|
98
|
+
- **The Queen** goes to the player with the most towers, granting +1 crown to the best territory
|
|
99
|
+
- **The Dragon** burns a building tile from the Builders Board
|
|
417
100
|
|
|
418
|
-
|
|
101
|
+
The turn flow is extended with optional actions after placing a domino:
|
|
419
102
|
|
|
420
|
-
```typescript
|
|
421
|
-
type Game = {
|
|
422
|
-
id: string;
|
|
423
|
-
dominoes: Domino[];
|
|
424
|
-
currentDominoes: RevealsDomino[];
|
|
425
|
-
players: Player[];
|
|
426
|
-
lords: Lord[];
|
|
427
|
-
turn: number;
|
|
428
|
-
nextAction: NextAction | NextStep;
|
|
429
|
-
rules: SelectedRules;
|
|
430
|
-
mode: GameMode;
|
|
431
|
-
};
|
|
432
103
|
```
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
Represent the next step or action in the game flow.
|
|
104
|
+
placeDomino → placeKnight → constructBuilding → useDragon → pickDomino
|
|
105
|
+
(optional) (optional) (optional)
|
|
106
|
+
```
|
|
437
107
|
|
|
438
108
|
```typescript
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
type NextAction = {
|
|
445
|
-
type: "action";
|
|
446
|
-
nextLord: string;
|
|
447
|
-
nextAction: "pickDomino" | "placeDomino" | "pass";
|
|
448
|
-
};
|
|
109
|
+
// Handle QueenDomino optional actions in your game loop
|
|
110
|
+
if (nextAction === "placeKnight" || nextAction === "constructBuilding" || nextAction === "useDragon") {
|
|
111
|
+
game = engine.skipOptionalAction({ game, lordId: nextLord });
|
|
112
|
+
}
|
|
449
113
|
```
|
|
450
114
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
Represents the score calculation for a kingdom.
|
|
115
|
+
See [Queendomino Guide](docs/queendomino.md) for the complete rules, building list, and game loop example.
|
|
454
116
|
|
|
455
|
-
|
|
456
|
-
type Score = {
|
|
457
|
-
points: number;
|
|
458
|
-
maxPropertiesSize: number;
|
|
459
|
-
totalCrowns: number;
|
|
460
|
-
};
|
|
461
|
-
```
|
|
117
|
+
## Kingdomino Origins
|
|
462
118
|
|
|
463
|
-
|
|
119
|
+
Origins introduces prehistoric terrains and fire-based scoring across three modes:
|
|
464
120
|
|
|
465
|
-
|
|
121
|
+
- **Discovery**: Volcanoes provide fire tokens. Scoring = region size × fire symbols.
|
|
122
|
+
- **Totem**: Discovery + wooden resources + totem majority tiles for bonus points.
|
|
123
|
+
- **Tribe**: Discovery + resources + cave board with 22 cavemen to recruit.
|
|
466
124
|
|
|
467
125
|
```typescript
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
126
|
+
if (nextAction === "placeFireToken") {
|
|
127
|
+
game = engine.placeFireToken({ game, lordId: nextLord, position });
|
|
128
|
+
}
|
|
129
|
+
if (nextAction === "recruitCaveman") {
|
|
130
|
+
game = engine.recruitCaveman({ game, lordId: nextLord, cavemanId, position, resourcePositions });
|
|
131
|
+
}
|
|
474
132
|
```
|
|
475
133
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
Represents a valid position and rotation for placing a domino.
|
|
134
|
+
See [Origins Guide](docs/origins.md) for the complete rules and mechanics.
|
|
479
135
|
|
|
480
|
-
|
|
481
|
-
type ValidPlacement = {
|
|
482
|
-
position: Position;
|
|
483
|
-
rotation: Rotation; // 0 | 90 | 180 | 270
|
|
484
|
-
};
|
|
485
|
-
```
|
|
136
|
+
## Age of Giants
|
|
486
137
|
|
|
487
|
-
|
|
138
|
+
Age of Giants extends Classic (or QueenDomino) with giants, quest tiles, and 5-player support:
|
|
488
139
|
|
|
489
|
-
|
|
140
|
+
- **60 dominos** (48 original + 12 new with giant/footprint symbols)
|
|
141
|
+
- **Giants** cover crowns on your kingdom, reducing opponents' scores
|
|
142
|
+
- **Quest tiles** (2 per game) provide end-game bonus scoring conditions
|
|
143
|
+
- **5-player support** with 5 dominos revealed per turn
|
|
490
144
|
|
|
491
145
|
```typescript
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (error instanceof DomainException) {
|
|
498
|
-
console.log(error.code); // e.g. "INVALID_PLACEMENT"
|
|
499
|
-
console.log(error.message); // Human-readable message
|
|
500
|
-
console.log(error.context); // Optional debug context
|
|
501
|
-
}
|
|
146
|
+
if (nextAction === "placeGiant") {
|
|
147
|
+
game = engine.placeGiant({ game, lordId: nextLord, position });
|
|
148
|
+
}
|
|
149
|
+
if (nextAction === "sendGiant") {
|
|
150
|
+
game = engine.sendGiant({ game, lordId: nextLord, giantIndex, targetPlayerId, targetCrownPosition });
|
|
502
151
|
}
|
|
503
152
|
```
|
|
504
153
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
| Method | Input | Output | Description |
|
|
508
|
-
|--------|-------|--------|-------------|
|
|
509
|
-
| `getModes` | `{}` | `GameMode[]` | Get available game modes |
|
|
510
|
-
| `getExtraRules` | `{ mode, players }` | `ExtraRule[]` | Get extra rules for a mode/player count |
|
|
511
|
-
| `createGame` | `{ mode }` | `GameWithNextStep` | Create a new game |
|
|
512
|
-
| `addPlayers` | `{ game, players }` | `GameWithNextStep` | Add 2-4 players |
|
|
513
|
-
| `addExtraRules` | `{ game, extraRules }` | `GameWithNextStep` | Add optional extra rules |
|
|
514
|
-
| `startGame` | `{ game }` | `GameWithNextAction` | Start the game |
|
|
515
|
-
| `chooseDomino` | `{ game, lordId, dominoPick }` | `GameWithNextAction` | Pick a domino |
|
|
516
|
-
| `placeDomino` | `{ game, lordId, position, rotation }` | `GameState` | Place a domino on kingdom |
|
|
517
|
-
| `discardDomino` | `{ game, lordId }` | `GameState` | Discard when no placement exists |
|
|
518
|
-
| `getResults` | `{ game }` | `GameWithResults` | Get final rankings and scores |
|
|
519
|
-
| `calculateScore` | `{ kingdom }` | `Score` | Calculate score for a kingdom |
|
|
520
|
-
| `getValidPlacements` | `{ kingdom, domino }` | `ValidPlacement[]` | Find all valid placements |
|
|
521
|
-
| `canPlaceDomino` | `{ kingdom, domino }` | `boolean` | Check if any placement exists |
|
|
522
|
-
| `serialize` | `{ game }` | `string` | Serialize game state to JSON |
|
|
523
|
-
| `deserialize` | `{ json }` | `GameState` | Restore game state from JSON |
|
|
524
|
-
|
|
525
|
-
## Development
|
|
526
|
-
|
|
527
|
-
### Setup
|
|
528
|
-
|
|
529
|
-
To set up the project for development:
|
|
530
|
-
|
|
531
|
-
1. Clone the repository:
|
|
532
|
-
```bash
|
|
533
|
-
git clone https://github.com/Pompidup/KDomino.git
|
|
534
|
-
cd KDomino
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
2. Install dependencies:
|
|
538
|
-
```bash
|
|
539
|
-
pnpm install
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
### Testing
|
|
543
|
-
|
|
544
|
-
The project uses Vitest for testing:
|
|
154
|
+
See [Age of Giants Guide](docs/age-of-giants.md) for the complete rules and mechanics.
|
|
545
155
|
|
|
546
|
-
|
|
547
|
-
# Run tests in watch mode
|
|
548
|
-
pnpm test
|
|
156
|
+
## Extra Rules
|
|
549
157
|
|
|
550
|
-
|
|
551
|
-
|
|
158
|
+
| Rule | Description | Restriction |
|
|
159
|
+
|------|-------------|-------------|
|
|
160
|
+
| **The Middle Kingdom** | +10 points if castle is centered | — |
|
|
161
|
+
| **Harmony** | +5 points if no dominos were discarded | — |
|
|
162
|
+
| **The Mighty Duel** | All 48 dominos, 7x7 kingdom | 2 players only |
|
|
163
|
+
| **Dynasty** | Play 3 games, highest total wins | — |
|
|
552
164
|
|
|
553
|
-
|
|
554
|
-
|
|
165
|
+
```typescript
|
|
166
|
+
game = engine.addExtraRules({ game, extraRules: ["The middle Kingdom", "Harmony"] });
|
|
555
167
|
|
|
556
|
-
|
|
557
|
-
|
|
168
|
+
// Dynasty: aggregate results from multiple games
|
|
169
|
+
const dynastyResults = engine.getDynastyResults({ games: [game1, game2, game3] });
|
|
558
170
|
```
|
|
559
171
|
|
|
560
|
-
|
|
172
|
+
See [Game Modes](docs/game-modes.md#extra-rules) for details.
|
|
561
173
|
|
|
562
|
-
|
|
563
|
-
# Lint with Biome
|
|
564
|
-
pnpm lint
|
|
174
|
+
## Engine Options
|
|
565
175
|
|
|
566
|
-
|
|
567
|
-
|
|
176
|
+
```typescript
|
|
177
|
+
const engine = createGameEngine({
|
|
178
|
+
logging: true, // Console logging
|
|
179
|
+
logger: { info: console.log, error: console.error }, // Custom logger
|
|
180
|
+
shuffleMethod: (arr) => arr, // Custom shuffle (e.g. no-op for testing)
|
|
181
|
+
uuidMethod: () => "custom-uuid", // Custom UUID generator
|
|
182
|
+
events: { onGameEnd: () => {} }, // Event callbacks
|
|
183
|
+
debug: true, // Debug mode
|
|
184
|
+
});
|
|
568
185
|
```
|
|
569
186
|
|
|
570
|
-
|
|
187
|
+
## API Overview
|
|
188
|
+
|
|
189
|
+
### Core Methods
|
|
190
|
+
|
|
191
|
+
| Method | Description |
|
|
192
|
+
|--------|-------------|
|
|
193
|
+
| `getModes` | Get available game modes |
|
|
194
|
+
| `createGame` | Create a new game |
|
|
195
|
+
| `addPlayers` | Add 1-4 players |
|
|
196
|
+
| `addExtraRules` | Add optional extra rules |
|
|
197
|
+
| `startGame` | Start the game |
|
|
198
|
+
| `chooseDomino` | Pick a domino from the revealed set |
|
|
199
|
+
| `placeDomino` | Place a domino on the kingdom |
|
|
200
|
+
| `discardDomino` | Discard when no valid placement exists |
|
|
201
|
+
| `getResults` | Get final rankings and scores |
|
|
202
|
+
| `calculateScore` | Calculate score for any kingdom |
|
|
203
|
+
| `getValidPlacements` | Find all valid placements for a domino |
|
|
204
|
+
| `canPlaceDomino` | Check if any placement exists |
|
|
205
|
+
| `serialize` / `deserialize` | Save and restore game state |
|
|
206
|
+
| `getDynastyResults` | Aggregate scores from multiple games |
|
|
207
|
+
|
|
208
|
+
### Queendomino Methods
|
|
209
|
+
|
|
210
|
+
| Method | Description |
|
|
211
|
+
|--------|-------------|
|
|
212
|
+
| `placeKnight` | Place a knight and collect tax |
|
|
213
|
+
| `constructBuilding` | Buy and place a building |
|
|
214
|
+
| `useDragon` | Destroy a building tile from the board |
|
|
215
|
+
| `skipOptionalAction` | Skip the current optional action |
|
|
216
|
+
|
|
217
|
+
### Origins Methods
|
|
218
|
+
|
|
219
|
+
| Method | Description |
|
|
220
|
+
|--------|-------------|
|
|
221
|
+
| `placeFireToken` | Place a fire token after placing a volcano domino |
|
|
222
|
+
| `recruitCaveman` | Recruit a caveman from the cave board (Tribe mode) |
|
|
223
|
+
|
|
224
|
+
### Age of Giants Methods
|
|
225
|
+
|
|
226
|
+
| Method | Description |
|
|
227
|
+
|--------|-------------|
|
|
228
|
+
| `placeGiant` | Place a giant on a crown in your kingdom |
|
|
229
|
+
| `sendGiant` | Send a giant to an opponent's kingdom |
|
|
230
|
+
|
|
231
|
+
See [API Reference](docs/api-reference.md) for complete method signatures, types, and error codes.
|
|
232
|
+
|
|
233
|
+
## Additional Features
|
|
234
|
+
|
|
235
|
+
- **Bot / AI**: 4 built-in strategies (`randomStrategy`, `greedyStrategy`, `advancedStrategy`, `expertStrategy`) with `playBotTurn()` helper
|
|
236
|
+
- **Mixed games**: Human and bot players in the same game via `bot: { strategyName }` on player input
|
|
237
|
+
- **Undo / Redo**: Snapshot-based history with `createGameHistory()`, `pushState()`, `undo()`, `redo()`
|
|
238
|
+
- **Action Log**: Immutable log via `wrapWithActionLog(engine)` for replay with `replayActions()`
|
|
239
|
+
- **Events**: Callbacks via `EngineConfig.events` (`onDominoPlaced`, `onGameEnd`, etc.)
|
|
240
|
+
- **Debug Mode**: `wrapWithDebug(engine)` with 3 verbosity levels
|
|
241
|
+
- **Seeded games**: Deterministic shuffle via `createGame({ mode, seed: "my-seed" })`
|
|
242
|
+
- **Serialization**: `serializeGame()` / `deserializeGame()` and save points
|
|
243
|
+
- **WASM Build**: `pnpm build:wasm` produces a WebAssembly module usable from any language
|
|
244
|
+
|
|
245
|
+
## Documentation
|
|
246
|
+
|
|
247
|
+
| Document | Content |
|
|
248
|
+
|----------|---------|
|
|
249
|
+
| [Game Modes](docs/game-modes.md) | Modes, rules by player count, extra rules, game flow |
|
|
250
|
+
| [Queendomino Guide](docs/queendomino.md) | Complete Queendomino rules, mechanics, and examples |
|
|
251
|
+
| [Origins Guide](docs/origins.md) | Kingdomino Origins rules, fire tokens, resources, cavemen |
|
|
252
|
+
| [Age of Giants Guide](docs/age-of-giants.md) | Age of Giants rules, giants, quest tiles, 5-player support |
|
|
253
|
+
| [API Reference](docs/api-reference.md) | All methods, types, error codes, and utilities |
|
|
254
|
+
| [WASM Build](wasm/README.md) | Cross-language usage via WebAssembly |
|
|
571
255
|
|
|
572
|
-
|
|
256
|
+
## Development
|
|
573
257
|
|
|
574
258
|
```bash
|
|
575
|
-
pnpm
|
|
259
|
+
pnpm install # Install dependencies
|
|
260
|
+
pnpm test # Run tests (watch mode)
|
|
261
|
+
pnpm test -- --run # Run tests once
|
|
262
|
+
pnpm coverage # Tests with coverage
|
|
263
|
+
pnpm typecheck # Type check (tsc)
|
|
264
|
+
pnpm lint # Lint (biome)
|
|
265
|
+
pnpm build # Build (ESM to dist/)
|
|
266
|
+
pnpm build:wasm # Build WASM module (requires javy)
|
|
576
267
|
```
|
|
577
268
|
|
|
578
269
|
## Contributing
|
|
579
270
|
|
|
580
|
-
Contributions are welcome! Here's how you can contribute:
|
|
581
|
-
|
|
582
271
|
1. Fork the repository
|
|
583
|
-
2. Create a
|
|
584
|
-
3. Make
|
|
585
|
-
4.
|
|
586
|
-
5. Run type checking (`pnpm typecheck`)
|
|
587
|
-
6. Commit your changes (`git commit -m 'Add some feature'`)
|
|
588
|
-
7. Push to the branch (`git push origin feature/your-feature-name`)
|
|
589
|
-
8. Open a Pull Request
|
|
590
|
-
|
|
591
|
-
Please make sure your code follows the existing style and includes appropriate tests.
|
|
272
|
+
2. Create a branch (`git checkout -b feature/your-feature`)
|
|
273
|
+
3. Make changes with tests (`pnpm test -- --run && pnpm typecheck`)
|
|
274
|
+
4. Open a Pull Request
|