t3core 1.1.1 → 1.1.2

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.
@@ -2,13 +2,16 @@ import type { IBoard } from "./types/Board";
2
2
  import type { PlayerSymbol } from "./types/Symbol";
3
3
  export declare const BOARD_SIZE = 9;
4
4
  export declare class Board implements IBoard {
5
- private fields;
5
+ private _fields;
6
+ private _snapshot;
6
7
  /**
7
- * Returns the current board state.
8
- * @returns The current board state.
9
- * @type {number[] | PlayerSymbol[]}
8
+ * Returns a stable snapshot of the current board state.
9
+ * The same array reference is returned on repeated calls until a field is
10
+ * mutated or the board is reset, making it safe for referential-equality
11
+ * checks (e.g. `useSyncExternalStore`).
12
+ * @returns A cached shallow copy of the board fields.
10
13
  */
11
- getFields(): (number | "O" | "X")[];
14
+ get fields(): (number | "O" | "X")[];
12
15
  /**
13
16
  * Returns the value of a field by its number.
14
17
  * @param fieldNumber The field number (1-9) to get.
@@ -31,6 +34,7 @@ export declare class Board implements IBoard {
31
34
  isFull(): boolean;
32
35
  /**
33
36
  * Sets a field's value by its number.
37
+ * Invalidates the cached snapshot so the next `fields` access returns a new reference.
34
38
  * @param fieldNumber The field number (1-9) to set.
35
39
  * @param symbol The symbol to set.
36
40
  * @deprecated Use `setFieldByIndex` instead.
@@ -38,12 +42,14 @@ export declare class Board implements IBoard {
38
42
  setFieldByNumber(fieldNumber: number, symbol: PlayerSymbol): void;
39
43
  /**
40
44
  * Sets a field's value by its index.
45
+ * Invalidates the cached snapshot so the next `fields` access returns a new reference.
41
46
  * @param index The index of the field to set.
42
47
  * @param symbol The symbol to set.
43
48
  */
44
49
  setFieldByIndex(index: number, symbol: PlayerSymbol): void;
45
50
  /**
46
51
  * Resets the board to its initial state.
52
+ * Invalidates the cached snapshot so the next `fields` access returns a new reference.
47
53
  */
48
54
  reset(): void;
49
55
  }
@@ -5,17 +5,23 @@ const fillFields = (_, idx) => idx + 1;
5
5
  exports.BOARD_SIZE = 9;
6
6
  class Board {
7
7
  constructor() {
8
- this.fields = new Array(exports.BOARD_SIZE)
8
+ this._fields = new Array(exports.BOARD_SIZE)
9
9
  .fill(0)
10
10
  .map(fillFields);
11
+ this._snapshot = null;
11
12
  }
12
13
  /**
13
- * Returns the current board state.
14
- * @returns The current board state.
15
- * @type {number[] | PlayerSymbol[]}
14
+ * Returns a stable snapshot of the current board state.
15
+ * The same array reference is returned on repeated calls until a field is
16
+ * mutated or the board is reset, making it safe for referential-equality
17
+ * checks (e.g. `useSyncExternalStore`).
18
+ * @returns A cached shallow copy of the board fields.
16
19
  */
17
- getFields() {
18
- return [...this.fields];
20
+ get fields() {
21
+ if (!this._snapshot) {
22
+ this._snapshot = [...this._fields];
23
+ }
24
+ return this._snapshot;
19
25
  }
20
26
  /**
21
27
  * Returns the value of a field by its number.
@@ -25,7 +31,7 @@ class Board {
25
31
  * @deprecated Use `getFieldByIndex` instead.
26
32
  */
27
33
  getFieldByNumber(fieldNumber) {
28
- return this.fields[fieldNumber - 1];
34
+ return this._fields[fieldNumber - 1];
29
35
  }
30
36
  /**
31
37
  * Returns the value of a field by its index.
@@ -34,37 +40,43 @@ class Board {
34
40
  * @type {number | TSymbol}
35
41
  */
36
42
  getFieldByIndex(index) {
37
- return this.fields[index];
43
+ return this._fields[index];
38
44
  }
39
45
  /**
40
46
  * Checks if the board is full.
41
47
  * @returns `true` if the board is full, `false` otherwise.
42
48
  */
43
49
  isFull() {
44
- return this.fields.every((field) => typeof field === "string");
50
+ return this._fields.every((field) => typeof field === "string");
45
51
  }
46
52
  /**
47
53
  * Sets a field's value by its number.
54
+ * Invalidates the cached snapshot so the next `fields` access returns a new reference.
48
55
  * @param fieldNumber The field number (1-9) to set.
49
56
  * @param symbol The symbol to set.
50
57
  * @deprecated Use `setFieldByIndex` instead.
51
58
  */
52
59
  setFieldByNumber(fieldNumber, symbol) {
53
- this.fields[fieldNumber - 1] = symbol;
60
+ this._fields[fieldNumber - 1] = symbol;
61
+ this._snapshot = null;
54
62
  }
55
63
  /**
56
64
  * Sets a field's value by its index.
65
+ * Invalidates the cached snapshot so the next `fields` access returns a new reference.
57
66
  * @param index The index of the field to set.
58
67
  * @param symbol The symbol to set.
59
68
  */
60
69
  setFieldByIndex(index, symbol) {
61
- this.fields[index] = symbol;
70
+ this._fields[index] = symbol;
71
+ this._snapshot = null;
62
72
  }
63
73
  /**
64
74
  * Resets the board to its initial state.
75
+ * Invalidates the cached snapshot so the next `fields` access returns a new reference.
65
76
  */
66
77
  reset() {
67
- this.fields = new Array(exports.BOARD_SIZE).fill(0).map(fillFields);
78
+ this._fields = new Array(exports.BOARD_SIZE).fill(0).map(fillFields);
79
+ this._snapshot = null;
68
80
  }
69
81
  }
70
82
  exports.Board = Board;
@@ -1,13 +1,20 @@
1
1
  import type { PlayerSymbol } from "./types/Symbol";
2
- import { type EventEmitHandler, type GameEventMap, type GameStatus, type IGame } from "./types/Game";
2
+ import { type GameEventMap, type GameEventPayload, type GameStatus, type IGame } from "./types/Game";
3
3
  export declare class Game implements IGame {
4
4
  private _currentPlayer;
5
5
  private _gameStatus;
6
6
  private _symbols;
7
7
  private _board;
8
8
  private _emitter;
9
+ private _snapshot;
9
10
  private _togglePlayer;
10
11
  private _updateGameStatus;
12
+ /**
13
+ * Returns a stable snapshot of the current game state.
14
+ * The same object reference is returned between moves, making it safe
15
+ * as a `getSnapshot` argument for `useSyncExternalStore`.
16
+ */
17
+ get snapshot(): GameEventPayload;
11
18
  /**
12
19
  * Returns the current player.
13
20
  * @returns The current player.
@@ -31,14 +38,14 @@ export declare class Game implements IGame {
31
38
  * @param fn The function to call when the event is emitted.
32
39
  * @returns This Game instance for method chaining.
33
40
  */
34
- on<K extends keyof GameEventMap>({ event, fn }: EventEmitHandler<K>): this;
41
+ on<K extends keyof GameEventMap>(event: K, fn: (...args: GameEventMap[K]) => void): this;
35
42
  /**
36
43
  * Removes an event listener for the specified event.
37
44
  * @param event The event to remove the listener from.
38
45
  * @param fn The function to remove.
39
46
  * @returns This Game instance for method chaining.
40
47
  */
41
- off<K extends keyof GameEventMap>({ event, fn }: EventEmitHandler<K>): this;
48
+ off<K extends keyof GameEventMap>(event: K, fn: (...args: GameEventMap[K]) => void): this;
42
49
  /**
43
50
  * Returns the current board state.
44
51
  * @returns The current board state.
@@ -16,6 +16,11 @@ class Game {
16
16
  this._symbols = constants_1.DEFAULT_GAME_SYMBOLS;
17
17
  this._board = new Board_1.Board();
18
18
  this._emitter = new eventemitter3_1.default();
19
+ this._snapshot = {
20
+ board: this._board.fields,
21
+ currentPlayer: this._currentPlayer,
22
+ gameStatus: this._gameStatus,
23
+ };
19
24
  }
20
25
  _togglePlayer() {
21
26
  this._currentPlayer =
@@ -25,7 +30,7 @@ class Game {
25
30
  }
26
31
  _updateGameStatus() {
27
32
  const board = this._board;
28
- const winner = (0, getWinnerFromFields_1.getWinnerFromFields)(board.getFields());
33
+ const winner = (0, getWinnerFromFields_1.getWinnerFromFields)(board.fields);
29
34
  const isDraw = board.isFull() && !winner;
30
35
  if (winner) {
31
36
  this._gameStatus = { status: "win", winner };
@@ -37,6 +42,14 @@ class Game {
37
42
  }
38
43
  this._gameStatus = { status: "running" };
39
44
  }
45
+ /**
46
+ * Returns a stable snapshot of the current game state.
47
+ * The same object reference is returned between moves, making it safe
48
+ * as a `getSnapshot` argument for `useSyncExternalStore`.
49
+ */
50
+ get snapshot() {
51
+ return this._snapshot;
52
+ }
40
53
  /**
41
54
  * Returns the current player.
42
55
  * @returns The current player.
@@ -58,7 +71,7 @@ class Game {
58
71
  * @type {(number | PlayerSymbol)[]}
59
72
  */
60
73
  get board() {
61
- return this._board.getFields();
74
+ return this._board.fields;
62
75
  }
63
76
  /**
64
77
  * Registers an event listener for the specified event.
@@ -66,7 +79,7 @@ class Game {
66
79
  * @param fn The function to call when the event is emitted.
67
80
  * @returns This Game instance for method chaining.
68
81
  */
69
- on({ event, fn }) {
82
+ on(event, fn) {
70
83
  this._emitter.on(event, fn);
71
84
  return this;
72
85
  }
@@ -76,7 +89,7 @@ class Game {
76
89
  * @param fn The function to remove.
77
90
  * @returns This Game instance for method chaining.
78
91
  */
79
- off({ event, fn }) {
92
+ off(event, fn) {
80
93
  this._emitter.off(event, fn);
81
94
  return this;
82
95
  }
@@ -87,7 +100,7 @@ class Game {
87
100
  * @deprecated Use `board` instead.
88
101
  */
89
102
  getBoard() {
90
- return this._board.getFields();
103
+ return this._board.fields;
91
104
  }
92
105
  /**
93
106
  * Checks if a field is already selected by a player.
@@ -133,12 +146,12 @@ class Game {
133
146
  this._board.setFieldByIndex(index, this._currentPlayer);
134
147
  this._togglePlayer();
135
148
  this._updateGameStatus();
136
- this._emitter.emit(Game_1.GameEvent.PLAYER_MOVE, {
137
- index,
138
- board: this._board.getFields(),
149
+ this._snapshot = {
150
+ board: this._board.fields,
139
151
  currentPlayer: this._currentPlayer,
140
152
  gameStatus: this._gameStatus,
141
- });
153
+ };
154
+ this._emitter.emit(Game_1.GameEvent.PLAYER_MOVE, Object.assign({ index }, this._snapshot));
142
155
  return Game_1.PlayerMoveStatus.SUCCESS;
143
156
  }
144
157
  /**
@@ -149,11 +162,12 @@ class Game {
149
162
  this._gameStatus = { status: "running" };
150
163
  this._currentPlayer = this._symbols[0];
151
164
  this._board.reset();
152
- this._emitter.emit(Game_1.GameEvent.RESET, {
153
- board: this._board.getFields(),
165
+ this._snapshot = {
166
+ board: this._board.fields,
154
167
  currentPlayer: this._currentPlayer,
155
168
  gameStatus: this._gameStatus,
156
- });
169
+ };
170
+ this._emitter.emit(Game_1.GameEvent.RESET, this._snapshot);
157
171
  }
158
172
  }
159
173
  exports.Game = Game;
@@ -52,7 +52,7 @@ const Game_2 = require("../types/Game");
52
52
  (0, vitest_1.test)("PLAYER_MOVE event fires with correct payload on savePlayerMove", () => {
53
53
  const game = new Game_1.Game();
54
54
  const listener = vitest_1.vi.fn();
55
- game.on({ event: Game_2.GameEvent.PLAYER_MOVE, fn: listener });
55
+ game.on(Game_2.GameEvent.PLAYER_MOVE, listener);
56
56
  game.savePlayerMove(4);
57
57
  (0, vitest_1.expect)(listener).toHaveBeenCalledOnce();
58
58
  const payload = listener.mock.calls[0][0];
@@ -66,7 +66,7 @@ const Game_2 = require("../types/Game");
66
66
  game.savePlayerMove(0);
67
67
  game.savePlayerMove(1);
68
68
  const listener = vitest_1.vi.fn();
69
- game.on({ event: Game_2.GameEvent.RESET, fn: listener });
69
+ game.on(Game_2.GameEvent.RESET, listener);
70
70
  game.reset();
71
71
  (0, vitest_1.expect)(listener).toHaveBeenCalledOnce();
72
72
  const payload = listener.mock.calls[0][0];
@@ -78,7 +78,7 @@ const Game_2 = require("../types/Game");
78
78
  const game = new Game_1.Game();
79
79
  game.savePlayerMove(0);
80
80
  const listener = vitest_1.vi.fn();
81
- game.on({ event: Game_2.GameEvent.PLAYER_MOVE, fn: listener });
81
+ game.on(Game_2.GameEvent.PLAYER_MOVE, listener);
82
82
  game.savePlayerMove(0);
83
83
  (0, vitest_1.expect)(listener).not.toHaveBeenCalled();
84
84
  });
@@ -1,9 +1,8 @@
1
1
  import type { PlayerSymbol } from "./Symbol";
2
2
  export interface IBoard {
3
- /** Cell value for field **1–9** (grid label shown to the player). */
3
+ fields: (number | PlayerSymbol)[];
4
4
  getFieldByNumber: (fieldNumber: number) => number | PlayerSymbol;
5
5
  setFieldByNumber: (fieldNumber: number, symbol: PlayerSymbol) => void;
6
- getFields: () => (number | PlayerSymbol)[];
7
6
  isFull: () => boolean;
8
7
  reset: () => void;
9
8
  }
@@ -32,14 +32,11 @@ export type GameEventMap = {
32
32
  }];
33
33
  [GameEvent.RESET]: [payload: GameEventPayload];
34
34
  };
35
- export type EventEmitHandler<K extends keyof GameEventMap> = {
36
- event: K;
37
- fn: (...args: GameEventMap[K]) => void;
38
- };
39
- export type EventEmit = <K extends keyof GameEventMap>(emitter: EventEmitHandler<K>) => void;
35
+ export type EventEmit = <K extends keyof GameEventMap>(event: K, fn: (...args: GameEventMap[K]) => void) => void;
40
36
  export interface IGame {
41
37
  on: EventEmit;
42
38
  off: EventEmit;
39
+ readonly snapshot: GameEventPayload;
43
40
  readonly gameStatus: GameStatus;
44
41
  readonly currentPlayer: PlayerSymbol;
45
42
  savePlayerSelection: (field: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "t3core",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "tic tac toe core - tttc - t3c",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {