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.
- package/dist/src/core/Board.d.ts +11 -5
- package/dist/src/core/Board.js +24 -12
- package/dist/src/core/Game.d.ts +10 -3
- package/dist/src/core/Game.js +26 -12
- package/dist/src/core/tests/gameCore.test.js +3 -3
- package/dist/src/core/types/Board.d.ts +1 -2
- package/dist/src/core/types/Game.d.ts +2 -5
- package/package.json +1 -1
package/dist/src/core/Board.d.ts
CHANGED
|
@@ -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
|
|
5
|
+
private _fields;
|
|
6
|
+
private _snapshot;
|
|
6
7
|
/**
|
|
7
|
-
* Returns the current board state.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/dist/src/core/Board.js
CHANGED
|
@@ -5,17 +5,23 @@ const fillFields = (_, idx) => idx + 1;
|
|
|
5
5
|
exports.BOARD_SIZE = 9;
|
|
6
6
|
class Board {
|
|
7
7
|
constructor() {
|
|
8
|
-
this.
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|
package/dist/src/core/Game.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { PlayerSymbol } from "./types/Symbol";
|
|
2
|
-
import { type
|
|
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>(
|
|
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>(
|
|
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.
|
package/dist/src/core/Game.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
137
|
-
|
|
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.
|
|
153
|
-
board: this._board.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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;
|