@pompidup/kingdomino-engine 3.1.0 → 3.2.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 CHANGED
@@ -17,7 +17,7 @@ A simple, lightweight TypeScript engine for the Kingdomino board game.
17
17
  - [Installation](#installation)
18
18
  - [Usage](#usage)
19
19
  - [Basic Setup](#basic-setup)
20
- - [Options for GameEngine](#options-for-gameengine)
20
+ - [Engine Options](#engine-options)
21
21
  - [Adding Players](#adding-players)
22
22
  - [Getting Available Game Modes](#getting-available-game-modes)
23
23
  - [Getting Available Extra Rules](#getting-available-extra-rules)
@@ -26,6 +26,8 @@ A simple, lightweight TypeScript engine for the Kingdomino board game.
26
26
  - [Player Actions](#player-actions)
27
27
  - [Game Flow](#game-flow)
28
28
  - [Scoring](#scoring)
29
+ - [Valid Placements](#valid-placements)
30
+ - [Serialization](#serialization)
29
31
  - [Complete Game Example](#complete-game-example)
30
32
  - [Extra Rules](#extra-rules)
31
33
  - [API Documentation](#api-documentation)
@@ -34,6 +36,7 @@ A simple, lightweight TypeScript engine for the Kingdomino board game.
34
36
  - [Development](#development)
35
37
  - [Setup](#setup)
36
38
  - [Testing](#testing)
39
+ - [Linting and Type Checking](#linting-and-type-checking)
37
40
  - [Building](#building)
38
41
  - [Contributing](#contributing)
39
42
 
@@ -43,9 +46,11 @@ This is a simple and lightweight TypeScript engine designed to facilitate the ga
43
46
 
44
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.
45
48
 
49
+ 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
+
46
51
  ## Requirements
47
52
 
48
- - Node v20.6.0 or higher
53
+ - Node.js >= 18
49
54
 
50
55
  ## Features
51
56
 
@@ -55,7 +60,10 @@ Kingdomino is a tile-placement game where players build kingdoms by connecting d
55
60
  - **Extra Rules Support**: Implement and manage additional game rules for enhanced gameplay.
56
61
  - **Multiple Game Modes**: Support for different game modes, including Classic and potentially others.
57
62
  - **Scoring System**: Automatic calculation of scores based on kingdom layouts and rule sets.
58
- - **Typescript Support**: Full TypeScript support with comprehensive type definitions.
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.
59
67
 
60
68
  ## Installation
61
69
 
@@ -80,42 +88,52 @@ Each method returns an updated game state object, which contains the current sta
80
88
  To start using the Kingdomino Engine, import it and initialize a new game instance:
81
89
 
82
90
  ```typescript
83
- import createGameEngine from "@pompidup/kingdomino-engine";
91
+ import { createGameEngine } from "@pompidup/kingdomino-engine";
84
92
 
85
93
  const engine = createGameEngine({});
86
94
  let gameState = engine.createGame({ mode: "Classic" });
87
95
  ```
88
96
 
89
- ### Options for GameEngine
90
-
91
- You can pass options to the `createGameEngine` function to customize the engine's behavior. The available options are:
97
+ ### Engine Options
92
98
 
93
- - `logging`: Enable or disable logging for debugging purposes. Default is `false`.
94
- - `shuffleMethod`: Specify the method used to shuffle dominoes. If not specified, a default shuffle method will be used.
95
- - `uuidMethod`: Specify the method used to generate UUIDs. If not specified, a default UUID method will be used.
99
+ You can pass options to the `createGameEngine` function to customize the engine's behavior:
96
100
 
97
- Here's an example of how to pass options to the engine:
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 |
98
107
 
99
108
  ```typescript
100
- // Full custom options
101
- const options: EngineConfig = {
102
- logging: true,
103
- shuffleMethod: myCustomShuffleMethod,
104
- uuidMethod: myCustomUuidMethod,
105
- };
109
+ import { createGameEngine, type EngineConfig } from "@pompidup/kingdomino-engine";
106
110
 
107
- const engine = createGameEngine(options);
111
+ // With logging enabled
112
+ const engine = createGameEngine({ logging: true });
108
113
 
109
- // If you want default options
110
- const engine = createGameEngine({});
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
+ });
111
126
  ```
112
127
 
113
128
  ### Adding Players
114
129
 
115
- Add players to the game by passing an array of player names:
130
+ Add players to the game by passing an array of player names (2-4 players, minimum 3 characters per name):
116
131
 
117
132
  ```typescript
118
- gameState = engine.addPlayers({ game: gameState, players: ["Player 1", "Player 2", "Player 3"] });
133
+ gameState = engine.addPlayers({
134
+ game: gameState,
135
+ players: ["Alice", "Bob", "Carol"],
136
+ });
119
137
  ```
120
138
 
121
139
  ### Getting Available Game Modes
@@ -137,7 +155,7 @@ const extraRules = engine.getExtraRules({ mode: "Classic", players: 2 });
137
155
 
138
156
  ### Setting Up Extra Rules
139
157
 
140
- You can add extra rules to the game to modify gameplay. For example, you can add the "The Middle Kingdom" rule:
158
+ You can add extra rules to the game to modify gameplay:
141
159
 
142
160
  ```typescript
143
161
  gameState = engine.addExtraRules({
@@ -148,7 +166,7 @@ gameState = engine.addExtraRules({
148
166
 
149
167
  ### Starting the Game
150
168
 
151
- 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.
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`.
152
170
 
153
171
  ```typescript
154
172
  gameState = engine.startGame({ game: gameState });
@@ -158,28 +176,28 @@ gameState = engine.startGame({ game: gameState });
158
176
 
159
177
  Players can perform actions like picking a domino, placing a domino, and discarding a domino.
160
178
 
161
- - Pick Domino:
179
+ **Pick a domino** from the currently revealed set:
162
180
 
163
181
  ```typescript
164
182
  gameState = engine.chooseDomino({
165
183
  game: gameState,
166
184
  lordId: "lordId",
167
- dominoPick: 12,
185
+ dominoPick: 12, // domino number
168
186
  });
169
187
  ```
170
188
 
171
- - Place Domino:
189
+ **Place a domino** on the kingdom grid:
172
190
 
173
191
  ```typescript
174
192
  gameState = engine.placeDomino({
175
193
  game: gameState,
176
194
  lordId: "lordId",
177
- position: { x: 0, y: 0 },
178
- rotation: 0,
195
+ position: { x: 5, y: 4 },
196
+ rotation: 0, // 0, 90, 180, or 270
179
197
  });
180
198
  ```
181
199
 
182
- - Discard Domino:
200
+ **Discard a domino** when no valid placement exists:
183
201
 
184
202
  ```typescript
185
203
  gameState = engine.discardDomino({
@@ -190,33 +208,94 @@ gameState = engine.discardDomino({
190
208
 
191
209
  ### Game Flow
192
210
 
193
- The game progresses through various steps and actions. The `nextAction` property in the game state indicates what action should be taken next. This could be a step (like "addPlayers", "options", "start", or "result") or an action (like "pickDomino" or "placeDomino").
194
- Once the game is started you will receive the nextAction and nextLord, when you receive a nextStep the game isOver.
195
- You can use utility functions to check the game state and determine the next action to take.
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:
196
218
 
197
219
  ```typescript
220
+ import { isGameWithNextAction, isGameWithNextStep } from "@pompidup/kingdomino-engine";
221
+
198
222
  while (isGameWithNextAction(gameState)) {
199
- // Manage player actions
223
+ const { nextLord, nextAction } = gameState.nextAction;
224
+ // Handle pickDomino, placeDomino, or pass
200
225
  }
201
226
 
202
227
  if (isGameWithNextStep(gameState)) {
203
- // Manage game steps, like adding players or showing results
228
+ // Game has ended, get results
204
229
  }
205
230
  ```
206
231
 
207
232
  ### Scoring
208
233
 
209
- When the last turn is played, the game will automatically calculate the final score for each player and their position.
210
- You can get the results by calling the `getResults` method:
234
+ When the last turn is played, the game transitions to the `result` step. Get final results with rankings:
211
235
 
212
236
  ```typescript
213
- gameState = engine.getResults({ game: gameState });
237
+ const gameWithResults = engine.getResults({ game: gameState });
238
+ console.log(gameWithResults.result);
239
+ // [{ playerId, playerName, details: { points, maxPropertiesSize, totalCrowns }, position }]
214
240
  ```
215
241
 
216
- You have the possibility to get the results at any time of a kingdom, you can use `calculateScore` method:
242
+ You can also calculate the score of any kingdom at any time:
217
243
 
218
244
  ```typescript
219
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 });
220
299
  ```
221
300
 
222
301
  ## Complete Game Example
@@ -227,52 +306,64 @@ Here's a complete example of how to simulate a game from start to finish:
227
306
  import { createGameEngine, isGameWithNextAction } from "@pompidup/kingdomino-engine";
228
307
 
229
308
  // Create the game engine
230
- const engine = createGameEngine({
231
- logging: true,
232
- // Optional: provide custom shuffle method
233
- shuffleMethod: (array) => array,
234
- });
309
+ const engine = createGameEngine({});
235
310
 
236
311
  // Create a new game with Classic mode
237
312
  let game = engine.createGame({ mode: "Classic" });
238
313
 
239
314
  // Add players
240
- const players = ["Alice", "Bob"];
241
- game = engine.addPlayers({ game, players });
315
+ game = engine.addPlayers({ game, players: ["Alice", "Bob"] });
316
+
317
+ // Optionally add extra rules
318
+ game = engine.addExtraRules({ game, extraRules: ["The middle Kingdom", "Harmony"] });
242
319
 
243
320
  // Start the game
244
321
  game = engine.startGame({ game });
245
322
 
246
- // Game loop - continue until there are no more actions
323
+ // Game loop
247
324
  while (isGameWithNextAction(game)) {
248
325
  const currentLordId = game.nextAction.nextLord;
249
326
  const nextAction = game.nextAction.nextAction;
250
327
 
251
- // Handle different actions
252
328
  if (nextAction === "pickDomino") {
253
- // Choose a domino from the available ones
254
- const dominoToChoose = game.currentDominoes[0].domino.number;
255
- game = engine.chooseDomino({
256
- game,
257
- lordId: currentLordId,
258
- dominoPick: dominoToChoose,
259
- });
260
- } else if (nextAction === "placeDomino") {
261
- // Try to place the domino
262
- try {
263
- game = engine.placeDomino({
329
+ // Pick the first available domino
330
+ const availableDomino = game.currentDominoes.find((d) => !d.picked);
331
+ if (availableDomino) {
332
+ game = engine.chooseDomino({
264
333
  game,
265
334
  lordId: currentLordId,
266
- position: { x: 0, y: 0 }, // Position to place the domino
267
- rotation: 0, // Rotation of the domino (0, 90, 180, 270)
335
+ dominoPick: availableDomino.domino.number,
268
336
  });
269
- } catch (error) {
270
- // If placement fails, discard the domino
271
- game = engine.discardDomino({
272
- game,
273
- lordId: currentLordId,
337
+ }
338
+ } else if (nextAction === "placeDomino") {
339
+ // Check if placement is possible
340
+ const lord = game.lords.find((l) => l.id === currentLordId);
341
+ const player = game.players.find((p) => p.id === lord?.playerId);
342
+
343
+ if (lord?.dominoPicked && player) {
344
+ const canPlace = engine.canPlaceDomino({
345
+ kingdom: player.kingdom,
346
+ domino: lord.dominoPicked,
274
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
+ });
360
+ } else {
361
+ game = engine.discardDomino({ game, lordId: currentLordId });
362
+ }
275
363
  }
364
+ } else {
365
+ // pass action
366
+ game = engine.discardDomino({ game, lordId: currentLordId });
276
367
  }
277
368
  }
278
369
 
@@ -283,7 +374,7 @@ console.log("Game results:", gameResult.result);
283
374
 
284
375
  ## Extra Rules
285
376
 
286
- The engine supports extra rules that can modify gameplay. Some examples include:
377
+ The engine supports extra rules that can modify gameplay:
287
378
 
288
379
  - **The Middle Kingdom**: Gain 10 additional points if your castle is in the middle of the kingdom.
289
380
  - **Harmony**: Gain 5 additional points if your kingdom is complete (no discarded dominoes).
@@ -292,7 +383,6 @@ You can get all available extra rules for a specific game mode and number of pla
292
383
 
293
384
  ```typescript
294
385
  const extraRules = engine.getExtraRules({ mode: "Classic", players: 2 });
295
- console.log("Available extra rules:", extraRules);
296
386
  ```
297
387
 
298
388
  ## API Documentation
@@ -316,6 +406,10 @@ type GameEngine = {
316
406
  discardDomino: (command: DiscardDominoCommand) => GameState;
317
407
  getResults: (command: GetResultCommand) => GameWithResults;
318
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;
319
413
  };
320
414
  ```
321
415
 
@@ -327,7 +421,7 @@ Represents the current state of a Kingdomino game.
327
421
  type Game = {
328
422
  id: string;
329
423
  dominoes: Domino[];
330
- currentDominoes: RevealedDomino[];
424
+ currentDominoes: RevealsDomino[];
331
425
  players: Player[];
332
426
  lords: Lord[];
333
427
  turn: number;
@@ -354,6 +448,18 @@ type NextAction = {
354
448
  };
355
449
  ```
356
450
 
451
+ #### Score
452
+
453
+ Represents the score calculation for a kingdom.
454
+
455
+ ```typescript
456
+ type Score = {
457
+ points: number;
458
+ maxPropertiesSize: number;
459
+ totalCrowns: number;
460
+ };
461
+ ```
462
+
357
463
  #### ExtraRule
358
464
 
359
465
  Represents an additional rule that can be applied to the game.
@@ -367,43 +473,54 @@ type ExtraRule = {
367
473
  };
368
474
  ```
369
475
 
370
- ### Methods
371
-
372
- #### createGame(command: CreateGameCommand): GameWithNextStep
373
-
374
- Creates a new game with the specified game mode.
375
-
376
- #### addPlayers(command: AddPlayersCommand): GameWithNextStep
377
-
378
- Adds players to the game.
379
-
380
- #### addExtraRules(command: AddExtraRulesCommand): GameWithNextStep
381
-
382
- Adds extra rules to the game.
383
-
384
- #### startGame(command: StartGameCommand): GameWithNextAction
385
-
386
- Starts the game, setting up the initial game state.
476
+ #### ValidPlacement
387
477
 
388
- #### chooseDomino(command: ChooseDominoCommand): GameWithNextAction
478
+ Represents a valid position and rotation for placing a domino.
389
479
 
390
- Allows a player to choose a domino.
391
-
392
- #### placeDomino(command: PlaceDominoCommand): GameState
393
-
394
- Allows a player to place a domino on their kingdom.
395
-
396
- #### discardDomino(command: DiscardDominoCommand): GameState
480
+ ```typescript
481
+ type ValidPlacement = {
482
+ position: Position;
483
+ rotation: Rotation; // 0 | 90 | 180 | 270
484
+ };
485
+ ```
397
486
 
398
- Allows a player to discard a domino if they cannot place it.
487
+ #### Domain Errors
399
488
 
400
- #### getResults(command: GetResultCommand): GameWithResults
489
+ The engine throws typed `DomainException` errors with error codes for programmatic handling:
401
490
 
402
- Calculates and returns the final game results.
491
+ ```typescript
492
+ import { DomainException, ErrorCode } from "@pompidup/kingdomino-engine";
493
+
494
+ try {
495
+ game = engine.placeDomino({ game, lordId, position, rotation });
496
+ } catch (error) {
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
+ }
502
+ }
503
+ ```
403
504
 
404
- #### calculateScore(command: CalculateScoreCommand): Score
505
+ ### Methods
405
506
 
406
- Calculates and returns the score of a kingdom.
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 |
407
524
 
408
525
  ## Development
409
526
 
@@ -424,19 +541,35 @@ To set up the project for development:
424
541
 
425
542
  ### Testing
426
543
 
427
- The project uses Vitest for testing. To run the tests:
544
+ The project uses Vitest for testing:
428
545
 
429
546
  ```bash
430
- # Run all tests
547
+ # Run tests in watch mode
431
548
  pnpm test
432
549
 
550
+ # Run tests once
551
+ pnpm test -- --run
552
+
433
553
  # Run tests with coverage
434
554
  pnpm coverage
555
+
556
+ # Run a specific test file
557
+ pnpm test -- src/tests/unit/useCases/calculateScore.test.ts --run
558
+ ```
559
+
560
+ ### Linting and Type Checking
561
+
562
+ ```bash
563
+ # Lint with Biome
564
+ pnpm lint
565
+
566
+ # Type check with TypeScript
567
+ pnpm typecheck
435
568
  ```
436
569
 
437
570
  ### Building
438
571
 
439
- To build the project:
572
+ To build the project (ESM output to `dist/`):
440
573
 
441
574
  ```bash
442
575
  pnpm build
@@ -449,13 +582,10 @@ Contributions are welcome! Here's how you can contribute:
449
582
  1. Fork the repository
450
583
  2. Create a new branch (`git checkout -b feature/your-feature-name`)
451
584
  3. Make your changes
452
- 4. Run tests to ensure everything works (`pnpm test`)
453
- 5. Commit your changes (`git commit -m 'Add some feature'`)
454
- 6. Push to the branch (`git push origin feature/your-feature-name`)
455
- 7. Open a Pull Request
585
+ 4. Run tests to ensure everything works (`pnpm test -- --run`)
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
456
590
 
457
591
  Please make sure your code follows the existing style and includes appropriate tests.
458
-
459
- ## Advanced Usage
460
-
461
- For more advanced usage and detailed API documentation, please refer to the source code and inline comments.