t3core 1.0.3 → 1.1.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.
@@ -14,8 +14,16 @@ export declare class Board implements IBoard {
14
14
  * @param fieldNumber The field number (1-9) to get.
15
15
  * @returns The value of the field.
16
16
  * @type {number | TSymbol}
17
+ * @deprecated Use `getFieldByIndex` instead.
17
18
  */
18
19
  getFieldByNumber(fieldNumber: number): number | "O" | "X";
20
+ /**
21
+ * Returns the value of a field by its index.
22
+ * @param index The index of the field to get.
23
+ * @returns The value of the field.
24
+ * @type {number | TSymbol}
25
+ */
26
+ getFieldByIndex(index: number): number | "O" | "X";
19
27
  /**
20
28
  * Checks if the board is full.
21
29
  * @returns `true` if the board is full, `false` otherwise.
@@ -25,8 +33,15 @@ export declare class Board implements IBoard {
25
33
  * Sets a field's value by its number.
26
34
  * @param fieldNumber The field number (1-9) to set.
27
35
  * @param symbol The symbol to set.
36
+ * @deprecated Use `setFieldByIndex` instead.
28
37
  */
29
38
  setFieldByNumber(fieldNumber: number, symbol: PlayerSymbol): void;
39
+ /**
40
+ * Sets a field's value by its index.
41
+ * @param index The index of the field to set.
42
+ * @param symbol The symbol to set.
43
+ */
44
+ setFieldByIndex(index: number, symbol: PlayerSymbol): void;
30
45
  /**
31
46
  * Resets the board to its initial state.
32
47
  */
@@ -22,10 +22,20 @@ class Board {
22
22
  * @param fieldNumber The field number (1-9) to get.
23
23
  * @returns The value of the field.
24
24
  * @type {number | TSymbol}
25
+ * @deprecated Use `getFieldByIndex` instead.
25
26
  */
26
27
  getFieldByNumber(fieldNumber) {
27
28
  return this.fields[fieldNumber - 1];
28
29
  }
30
+ /**
31
+ * Returns the value of a field by its index.
32
+ * @param index The index of the field to get.
33
+ * @returns The value of the field.
34
+ * @type {number | TSymbol}
35
+ */
36
+ getFieldByIndex(index) {
37
+ return this.fields[index];
38
+ }
29
39
  /**
30
40
  * Checks if the board is full.
31
41
  * @returns `true` if the board is full, `false` otherwise.
@@ -37,10 +47,19 @@ class Board {
37
47
  * Sets a field's value by its number.
38
48
  * @param fieldNumber The field number (1-9) to set.
39
49
  * @param symbol The symbol to set.
50
+ * @deprecated Use `setFieldByIndex` instead.
40
51
  */
41
52
  setFieldByNumber(fieldNumber, symbol) {
42
53
  this.fields[fieldNumber - 1] = symbol;
43
54
  }
55
+ /**
56
+ * Sets a field's value by its index.
57
+ * @param index The index of the field to set.
58
+ * @param symbol The symbol to set.
59
+ */
60
+ setFieldByIndex(index, symbol) {
61
+ this.fields[index] = symbol;
62
+ }
44
63
  /**
45
64
  * Resets the board to its initial state.
46
65
  */
@@ -1,10 +1,11 @@
1
- import type { GameStatus, IGame } from "./types/Game";
2
1
  import type { PlayerSymbol } from "./types/Symbol";
2
+ import { type EventEmitHandler, type GameEventMap, 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
+ private _emitter;
8
9
  private _togglePlayer;
9
10
  private _updateGameStatus;
10
11
  /**
@@ -23,20 +24,56 @@ export declare class Game implements IGame {
23
24
  * @returns The current board state.
24
25
  * @type {(number | PlayerSymbol)[]}
25
26
  */
27
+ get board(): (number | "O" | "X")[];
28
+ /**
29
+ * Registers an event listener for the specified event.
30
+ * @param event The event to listen for.
31
+ * @param fn The function to call when the event is emitted.
32
+ * @returns This Game instance for method chaining.
33
+ */
34
+ on<K extends keyof GameEventMap>({ event, fn }: EventEmitHandler<K>): this;
35
+ /**
36
+ * Removes an event listener for the specified event.
37
+ * @param event The event to remove the listener from.
38
+ * @param fn The function to remove.
39
+ * @returns This Game instance for method chaining.
40
+ */
41
+ off<K extends keyof GameEventMap>({ event, fn }: EventEmitHandler<K>): this;
42
+ /**
43
+ * Returns the current board state.
44
+ * @returns The current board state.
45
+ * @type {(number | PlayerSymbol)[]}
46
+ * @deprecated Use `board` instead.
47
+ */
26
48
  getBoard(): (number | "O" | "X")[];
27
49
  /**
28
50
  * Checks if a field is already selected by a player.
29
51
  * @param field The field number to check.
30
52
  * @returns `true` if the field is selected, `false` otherwise.
53
+ * @deprecated Use `isFieldSelectedByIndex` instead.
31
54
  */
32
55
  isFieldSelected(field: number): boolean;
56
+ /**
57
+ * Checks if a field is already selected by a player.
58
+ * @param index The index of the field to check.
59
+ * @returns `true` if the field is selected, `false` otherwise.
60
+ */
61
+ isFieldSelectedByIndex(index: number): boolean;
33
62
  /**
34
63
  * Saves a player's selection on the board.
35
64
  * @param field The field number to mark.
65
+ * @deprecated Use `savePlayerMove` instead.
36
66
  */
37
67
  savePlayerSelection(field: number): void;
68
+ /**
69
+ * Saves a player's selection on the board by index.
70
+ * emit PLAYER_MOVE event
71
+ * @param index The index of the field to mark.
72
+ */
73
+ savePlayerMove(index: number): "success" | "already_selected" | "game_not_running";
38
74
  /**
39
75
  * Resets the game to its initial state.
76
+ * emit RESET event
40
77
  */
41
78
  reset(): void;
42
79
  }
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.Game = void 0;
7
+ const eventemitter3_1 = __importDefault(require("eventemitter3"));
4
8
  const Board_1 = require("./Board");
5
9
  const constants_1 = require("./constants");
10
+ const Game_1 = require("./types/Game");
6
11
  const getWinnerFromFields_1 = require("./utils/getWinnerFromFields");
7
12
  class Game {
8
13
  constructor() {
@@ -10,6 +15,7 @@ class Game {
10
15
  this._gameStatus = { status: "running" };
11
16
  this._symbols = constants_1.DEFAULT_GAME_SYMBOLS;
12
17
  this._board = new Board_1.Board();
18
+ this._emitter = new eventemitter3_1.default();
13
19
  }
14
20
  _togglePlayer() {
15
21
  this._currentPlayer =
@@ -51,6 +57,35 @@ class Game {
51
57
  * @returns The current board state.
52
58
  * @type {(number | PlayerSymbol)[]}
53
59
  */
60
+ get board() {
61
+ return this._board.getFields();
62
+ }
63
+ /**
64
+ * Registers an event listener for the specified event.
65
+ * @param event The event to listen for.
66
+ * @param fn The function to call when the event is emitted.
67
+ * @returns This Game instance for method chaining.
68
+ */
69
+ on({ event, fn }) {
70
+ this._emitter.on(event, fn);
71
+ return this;
72
+ }
73
+ /**
74
+ * Removes an event listener for the specified event.
75
+ * @param event The event to remove the listener from.
76
+ * @param fn The function to remove.
77
+ * @returns This Game instance for method chaining.
78
+ */
79
+ off({ event, fn }) {
80
+ this._emitter.off(event, fn);
81
+ return this;
82
+ }
83
+ /**
84
+ * Returns the current board state.
85
+ * @returns The current board state.
86
+ * @type {(number | PlayerSymbol)[]}
87
+ * @deprecated Use `board` instead.
88
+ */
54
89
  getBoard() {
55
90
  return this._board.getFields();
56
91
  }
@@ -58,26 +93,67 @@ class Game {
58
93
  * Checks if a field is already selected by a player.
59
94
  * @param field The field number to check.
60
95
  * @returns `true` if the field is selected, `false` otherwise.
96
+ * @deprecated Use `isFieldSelectedByIndex` instead.
61
97
  */
62
98
  isFieldSelected(field) {
63
99
  return typeof this._board.getFieldByNumber(field) === "string";
64
100
  }
101
+ /**
102
+ * Checks if a field is already selected by a player.
103
+ * @param index The index of the field to check.
104
+ * @returns `true` if the field is selected, `false` otherwise.
105
+ */
106
+ isFieldSelectedByIndex(index) {
107
+ return typeof this._board.getFieldByIndex(index) === "string";
108
+ }
65
109
  /**
66
110
  * Saves a player's selection on the board.
67
111
  * @param field The field number to mark.
112
+ * @deprecated Use `savePlayerMove` instead.
68
113
  */
69
114
  savePlayerSelection(field) {
70
115
  this._board.setFieldByNumber(field, this._currentPlayer);
71
116
  this._togglePlayer();
72
117
  this._updateGameStatus();
73
118
  }
119
+ /**
120
+ * Saves a player's selection on the board by index.
121
+ * emit PLAYER_MOVE event
122
+ * @param index The index of the field to mark.
123
+ */
124
+ savePlayerMove(index) {
125
+ if (this.isFieldSelected(index + 1)) {
126
+ console.warn("Field is already selected");
127
+ return Game_1.PlayerMoveStatus.ALREADY_SELECTED;
128
+ }
129
+ if (this._gameStatus.status !== "running") {
130
+ console.warn("Game is not running");
131
+ return Game_1.PlayerMoveStatus.GAME_NOT_RUNNING;
132
+ }
133
+ this._board.setFieldByIndex(index, this._currentPlayer);
134
+ this._togglePlayer();
135
+ this._updateGameStatus();
136
+ this._emitter.emit(Game_1.GameEvent.PLAYER_MOVE, {
137
+ index,
138
+ board: this._board.getFields(),
139
+ currentPlayer: this._currentPlayer,
140
+ gameStatus: this._gameStatus,
141
+ });
142
+ return Game_1.PlayerMoveStatus.SUCCESS;
143
+ }
74
144
  /**
75
145
  * Resets the game to its initial state.
146
+ * emit RESET event
76
147
  */
77
148
  reset() {
78
149
  this._gameStatus = { status: "running" };
79
150
  this._currentPlayer = this._symbols[0];
80
151
  this._board.reset();
152
+ this._emitter.emit(Game_1.GameEvent.RESET, {
153
+ board: this._board.getFields(),
154
+ currentPlayer: this._currentPlayer,
155
+ gameStatus: this._gameStatus,
156
+ });
81
157
  }
82
158
  }
83
159
  exports.Game = Game;
@@ -11,6 +11,14 @@ export { DEFAULT_GAME_SYMBOLS } from "./constants";
11
11
  * Interface describing the Game class contract.
12
12
  */
13
13
  export type { IGame } from "./types/Game";
14
+ /**
15
+ * Game event names: `PLAYER_MOVE`, `RESET`.
16
+ */
17
+ export { GameEvent } from "./types/Game";
18
+ /**
19
+ * Typed event map for EventEmitter3 — use with `Game` listeners.
20
+ */
21
+ export type { GameEventMap, GameEventPayload } from "./types/Game";
14
22
  /**
15
23
  * Union type representing possible game states:
16
24
  * - `{ status: 'running' }` - Game in progress
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_GAME_SYMBOLS = exports.Game = void 0;
3
+ exports.GameEvent = exports.DEFAULT_GAME_SYMBOLS = exports.Game = void 0;
4
4
  /**
5
5
  * Core game logic for Tic Tac Toe.
6
6
  * Manages player turns, board state, win/draw detection, and game lifecycle.
@@ -12,3 +12,8 @@ Object.defineProperty(exports, "Game", { enumerable: true, get: function () { re
12
12
  */
13
13
  var constants_1 = require("./constants");
14
14
  Object.defineProperty(exports, "DEFAULT_GAME_SYMBOLS", { enumerable: true, get: function () { return constants_1.DEFAULT_GAME_SYMBOLS; } });
15
+ /**
16
+ * Game event names: `PLAYER_MOVE`, `RESET`.
17
+ */
18
+ var Game_2 = require("./types/Game");
19
+ Object.defineProperty(exports, "GameEvent", { enumerable: true, get: function () { return Game_2.GameEvent; } });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const vitest_1 = require("vitest");
4
4
  const Game_1 = require("../Game");
5
+ const Game_2 = require("../types/Game");
5
6
  /*
6
7
  O X O
7
8
  O X X
@@ -9,26 +10,26 @@ const Game_1 = require("../Game");
9
10
  */
10
11
  (0, vitest_1.test)("Check if the game ends in a draw", () => {
11
12
  const game = new Game_1.Game();
12
- game.savePlayerSelection(1);
13
- game.savePlayerSelection(2);
14
- game.savePlayerSelection(3);
15
- game.savePlayerSelection(5);
16
- game.savePlayerSelection(4);
17
- game.savePlayerSelection(6);
18
- game.savePlayerSelection(8);
19
- game.savePlayerSelection(7);
20
- game.savePlayerSelection(9);
13
+ game.savePlayerMove(0);
14
+ game.savePlayerMove(1);
15
+ game.savePlayerMove(2);
16
+ game.savePlayerMove(4);
17
+ game.savePlayerMove(3);
18
+ game.savePlayerMove(5);
19
+ game.savePlayerMove(7);
20
+ game.savePlayerMove(6);
21
+ game.savePlayerMove(8);
21
22
  (0, vitest_1.expect)(game.gameStatus).toEqual({ status: "draw" });
22
23
  });
23
24
  (0, vitest_1.test)("Reset the game", () => {
24
25
  const game = new Game_1.Game();
25
- game.savePlayerSelection(5);
26
- game.savePlayerSelection(1);
27
- game.savePlayerSelection(2);
28
- game.savePlayerSelection(3);
29
- game.savePlayerSelection(8);
26
+ game.savePlayerMove(4);
27
+ game.savePlayerMove(0);
28
+ game.savePlayerMove(1);
29
+ game.savePlayerMove(2);
30
+ game.savePlayerMove(7);
30
31
  game.reset();
31
- (0, vitest_1.expect)(game.getBoard()).toEqual(new Array(9).fill(0).map((_, idx) => idx + 1));
32
+ (0, vitest_1.expect)(game.board).toEqual(new Array(9).fill(0).map((_, idx) => idx + 1));
32
33
  (0, vitest_1.expect)(game.gameStatus.status).toBe("running");
33
34
  (0, vitest_1.expect)(game.currentPlayer).toBe("O");
34
35
  });
@@ -39,12 +40,45 @@ const Game_1 = require("../Game");
39
40
  */
40
41
  (0, vitest_1.test)("Check if the game is won", () => {
41
42
  const game = new Game_1.Game();
42
- game.savePlayerSelection(5);
43
- game.savePlayerSelection(1);
44
- game.savePlayerSelection(2);
45
- game.savePlayerSelection(3);
46
- game.savePlayerSelection(8);
43
+ game.savePlayerMove(4);
44
+ game.savePlayerMove(0);
45
+ game.savePlayerMove(1);
46
+ game.savePlayerMove(2);
47
+ game.savePlayerMove(7);
47
48
  (0, vitest_1.expect)(game.gameStatus).toEqual({ status: "win", winner: "O" });
48
- (0, vitest_1.expect)(game.isFieldSelected(5)).toBe(true);
49
- (0, vitest_1.expect)(game.isFieldSelected(9)).toBe(false);
49
+ (0, vitest_1.expect)(game.isFieldSelectedByIndex(4)).toBe(true);
50
+ (0, vitest_1.expect)(game.isFieldSelectedByIndex(8)).toBe(false);
51
+ });
52
+ (0, vitest_1.test)("PLAYER_MOVE event fires with correct payload on savePlayerMove", () => {
53
+ const game = new Game_1.Game();
54
+ const listener = vitest_1.vi.fn();
55
+ game.on({ event: Game_2.GameEvent.PLAYER_MOVE, fn: listener });
56
+ game.savePlayerMove(4);
57
+ (0, vitest_1.expect)(listener).toHaveBeenCalledOnce();
58
+ const payload = listener.mock.calls[0][0];
59
+ (0, vitest_1.expect)(payload.index).toBe(4);
60
+ (0, vitest_1.expect)(payload.board[4]).toBe("O");
61
+ (0, vitest_1.expect)(payload.currentPlayer).toBe("X");
62
+ (0, vitest_1.expect)(payload.gameStatus).toEqual({ status: "running" });
63
+ });
64
+ (0, vitest_1.test)("RESET event fires with correct payload", () => {
65
+ const game = new Game_1.Game();
66
+ game.savePlayerMove(0);
67
+ game.savePlayerMove(1);
68
+ const listener = vitest_1.vi.fn();
69
+ game.on({ event: Game_2.GameEvent.RESET, fn: listener });
70
+ game.reset();
71
+ (0, vitest_1.expect)(listener).toHaveBeenCalledOnce();
72
+ const payload = listener.mock.calls[0][0];
73
+ (0, vitest_1.expect)(payload.board).toEqual(new Array(9).fill(0).map((_, idx) => idx + 1));
74
+ (0, vitest_1.expect)(payload.currentPlayer).toBe("O");
75
+ (0, vitest_1.expect)(payload.gameStatus).toEqual({ status: "running" });
76
+ });
77
+ (0, vitest_1.test)("PLAYER_MOVE event is not fired when move is invalid", () => {
78
+ const game = new Game_1.Game();
79
+ game.savePlayerMove(0);
80
+ const listener = vitest_1.vi.fn();
81
+ game.on({ event: Game_2.GameEvent.PLAYER_MOVE, fn: listener });
82
+ game.savePlayerMove(0);
83
+ (0, vitest_1.expect)(listener).not.toHaveBeenCalled();
50
84
  });
@@ -10,12 +10,43 @@ type GameStatusRunning = {
10
10
  status: "running";
11
11
  };
12
12
  export type GameStatus = GameStatusWin | GameStatusDraw | GameStatusRunning;
13
+ export declare const PlayerMoveStatus: {
14
+ readonly SUCCESS: "success";
15
+ readonly ALREADY_SELECTED: "already_selected";
16
+ readonly GAME_NOT_RUNNING: "game_not_running";
17
+ };
18
+ export type PlayerMoveStatus = (typeof PlayerMoveStatus)[keyof typeof PlayerMoveStatus];
19
+ export declare const GameEvent: {
20
+ readonly PLAYER_MOVE: "PLAYER_MOVE";
21
+ readonly RESET: "RESET";
22
+ };
23
+ export type GameEvent = (typeof GameEvent)[keyof typeof GameEvent];
24
+ export type GameEventPayload = {
25
+ board: (number | PlayerSymbol)[];
26
+ currentPlayer: PlayerSymbol;
27
+ gameStatus: GameStatus;
28
+ };
29
+ export type GameEventMap = {
30
+ [GameEvent.PLAYER_MOVE]: [payload: GameEventPayload & {
31
+ index: number;
32
+ }];
33
+ [GameEvent.RESET]: [payload: GameEventPayload];
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;
13
40
  export interface IGame {
41
+ on: EventEmit;
42
+ off: EventEmit;
14
43
  readonly gameStatus: GameStatus;
15
44
  readonly currentPlayer: PlayerSymbol;
16
45
  savePlayerSelection: (field: number) => void;
17
46
  reset: () => void;
18
47
  isFieldSelected: (field: number) => boolean;
48
+ isFieldSelectedByIndex: (index: number) => boolean;
49
+ savePlayerMove: (index: number) => PlayerMoveStatus;
19
50
  getBoard: () => (number | PlayerSymbol)[];
20
51
  }
21
52
  export {};
@@ -1,2 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GameEvent = exports.PlayerMoveStatus = void 0;
4
+ exports.PlayerMoveStatus = {
5
+ SUCCESS: "success",
6
+ ALREADY_SELECTED: "already_selected",
7
+ GAME_NOT_RUNNING: "game_not_running",
8
+ };
9
+ exports.GameEvent = {
10
+ PLAYER_MOVE: "PLAYER_MOVE",
11
+ RESET: "RESET",
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "t3core",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "tic tac toe core - tttc - t3c",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -10,7 +10,8 @@
10
10
  "test": "vitest",
11
11
  "lint": "eslint ./src/**/*.ts",
12
12
  "ts:check": "tsc --noEmit",
13
- "publish:package": "npm version patch && npm publish"
13
+ "publish:dry": "npm run build && npm publish --dry-run",
14
+ "release:tag": "env -i HOME=$HOME PATH=$PATH bash scripts/release.sh"
14
15
  },
15
16
  "keywords": [
16
17
  "tic-tac-toe",
@@ -59,5 +60,8 @@
59
60
  "typescript": "^6.0.3",
60
61
  "typescript-eslint": "^8.59.1",
61
62
  "vitest": "^4.1.5"
63
+ },
64
+ "dependencies": {
65
+ "eventemitter3": "^5.0.4"
62
66
  }
63
67
  }