dots-engine 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Cláudio Silva
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # dots-engine
2
+
3
+ A TypeScript engine for the classic "Dots" game (a.k.a. Dots and Boxes) — a
4
+ paper-and-pencil strategy game, played here as a strict 1v1 turn-based match.
5
+
6
+ The engine is designed to be **deterministic**: given the same sequence of
7
+ moves, it always produces the same result, which makes it suitable for
8
+ decentralized applications as well as any ordinary client/server setup.
9
+
10
+ ## About the Game
11
+
12
+ - Start with a grid of dots.
13
+ - Players take turns connecting two adjacent dots with a horizontal or
14
+ vertical line.
15
+ - When a move completes a square (its fourth edge), that square is owned by
16
+ whoever drew the line, and that player gets an extra move.
17
+ - The game ends when no more lines can be drawn.
18
+ - The player who owns the most completed squares wins; a tie is a draw.
19
+
20
+
21
+
22
+ ## Features
23
+
24
+ - **Complete Game Logic**: Full implementation of the dots-and-boxes rules.
25
+ - **Configurable Grid**: Support for different grid sizes (minimum 2x2).
26
+ - **Strict 1v1 Matches**: Exactly two players per match, alternating turns,
27
+ with an extra move for the player who closes a square.
28
+ - **Score Tracking**: Automatic per-player tally of closed squares.
29
+ - **King-of-the-Chain Tracking**: Each player's longest run of squares closed
30
+ in one uninterrupted turn.
31
+ - **Game State Management**: Tracks game status (in progress, over, draw).
32
+ - **Move Validation**: Enforces turn order, grid bounds, dot adjacency, and
33
+ rejects already-drawn edges.
34
+ - **Move Log**: Records every applied move as a JSON-serializable log, ready
35
+ for a client to replay or persist.
36
+ - **Player Identity Normalization**: Case-insensitive player IDs, so the same
37
+ player never forks into separate score buckets.
38
+ - **Point-Based Model**: Players connect dots using coordinate pairs.
39
+ - **Square-Based Logic**: Internal square tracking for scoring and game state.
40
+
41
+
42
+
43
+ ## Key Components
44
+
45
+ - `Dots.ts`: Main game class that orchestrates a match — turns, scoring,
46
+ status, and the move log.
47
+ - `Grid.ts`: Manages the game board, points, squares, and edges.
48
+ - `Edge.ts`: Represents connections between dots using coordinate pairs.
49
+ - `Point.ts`: Represents individual dots on the grid.
50
+ - `Square.ts`: Represents the squares that can be completed.
51
+ - `GameConstants.ts`: Game constants and status definitions.
52
+ - `address.ts`: Normalizes player identifiers so identity is consistent
53
+ regardless of input casing.
54
+
55
+
56
+
57
+ ## Game Model
58
+
59
+ The engine uses a hybrid model that matches how the game is actually played:
60
+
61
+ - **Points/Dots**: The primary entities that players interact with.
62
+ - **Edges**: Connections between adjacent points specified by coordinate pairs.
63
+ - **Squares**: Internal entities for tracking completed squares and scoring.
64
+ - **Coordinate System**: Players specify moves as two adjacent dots, `[x1, y1]` and `[x2, y2]`.
65
+
66
+
67
+
68
+ ### Example: 3x3 Grid
69
+
70
+ ```
71
+ Points: (0,0) (1,0) (2,0)
72
+ (0,1) (1,1) (2,1)
73
+ (0,2) (1,2) (2,2)
74
+
75
+ Squares: 4 squares arranged as 2x2
76
+ Square 0 at (0,0) Square 1 at (0,1)
77
+ Square 2 at (1,0) Square 3 at (1,1)
78
+ ```
79
+
80
+
81
+
82
+ ## Installation
83
+
84
+ ```bash
85
+ npm install dots-engine
86
+ ```
87
+
88
+
89
+
90
+ ## Usage
91
+
92
+
93
+
94
+ ### Basic Example
95
+
96
+ ```typescript
97
+ import { Dots } from "dots-engine";
98
+
99
+ const alice = "0xAl1ce";
100
+ const bob = "0xB0b";
101
+
102
+ // Create a 3x3 grid (2x2 squares) for a match between alice and bob.
103
+ // alice is players[0], so she moves first.
104
+ const dots = new Dots(3, [alice, bob], "match-1");
105
+
106
+ // Each move connects two adjacent dots, submitted by the player on turn.
107
+ dots.play([0, 0], [0, 1], alice);
108
+ dots.play([0, 0], [1, 0], bob);
109
+ dots.play([0, 1], [1, 1], alice);
110
+
111
+ // play() returns what changed, so you can re-render straight from it.
112
+ const result = dots.play([1, 0], [1, 1], bob); // Completes square 0, owned by bob
113
+ console.log(result); // { squaresClosed: 1, submitter: "0xb0b", status: 2 }
114
+
115
+ console.log("Game over:", dots.isOVer());
116
+ console.log("Score:", dots.getScore()); // { "0xb0b": 1 }
117
+ ```
118
+
119
+
120
+
121
+ ### Advanced Example
122
+
123
+ ```typescript
124
+ import { Dots } from "dots-engine";
125
+
126
+ const alice = "0xAl1ce";
127
+ const bob = "0xB0b";
128
+ const dots = new Dots(4, [alice, bob], "match-2"); // Create a 4x4 grid
129
+
130
+ dots.play([0, 0], [0, 1], alice);
131
+ dots.play([0, 0], [1, 0], bob);
132
+ dots.play([0, 1], [1, 1], alice);
133
+ dots.play([1, 0], [1, 1], bob);
134
+
135
+ console.log("Game over:", dots.isOVer());
136
+ console.log("Score:", dots.getScore());
137
+ console.log("Is draw:", dots.isDraw());
138
+ console.log("Winner:", dots.getWinner());
139
+ console.log("Longest chains:", dots.getLongestChain());
140
+ console.log("Move log:", dots.moveLog);
141
+
142
+ // Get square position from ID
143
+ const grid = dots.grid;
144
+ const [row, col] = grid.getSquarePosition(5); // Square 5
145
+ console.log("Square 5 is at position:", row, col);
146
+
147
+ // Get square ID from position
148
+ const squareId = grid.getSquareId(1, 2); // Position (1,2)
149
+ console.log("Position (1,2) corresponds to square:", squareId);
150
+
151
+ // Show all points in the grid
152
+ console.log("All points in the grid:");
153
+ for (let point of grid.getPoints()) {
154
+ console.log(`Point: ${point.toString()}`);
155
+ }
156
+ ```
157
+
158
+
159
+
160
+ ### Frontend usage
161
+
162
+ The engine speaks plain coordinate tuples, so the same `Coord` type can
163
+ describe your UI state and feed `play` directly — no engine-internal classes
164
+ to import, and everything is JSON-serializable.
165
+
166
+ ```typescript
167
+ import { Dots, type Coord } from "dots-engine";
168
+
169
+ const dots = new Dots(3, ["0xAl1ce", "0xB0b"], "match-3");
170
+ let selected: Coord | null = null;
171
+ const myAddress = "0xAl1ce"; // the connected wallet's address
172
+
173
+ // Call this whenever the user clicks a dot.
174
+ function onDotClick(dot: Coord) {
175
+ if (!selected) {
176
+ selected = dot; // first click: remember the start dot
177
+ return;
178
+ }
179
+ try {
180
+ const result = dots.play(selected, dot, myAddress); // second click: complete the move
181
+ // re-render from result.submitter / result.status / dots.getScore()
182
+ } catch (err) {
183
+ // not this player's turn / not adjacent / out of bounds — show feedback to the user
184
+ } finally {
185
+ selected = null;
186
+ }
187
+ }
188
+ ```
189
+
190
+ A move is always an *edge* (two dots), so the two-click selection lives in
191
+ your UI; the engine only ever receives one complete `play(from, to, submitter)` call.
192
+
193
+ ## Determinism
194
+
195
+ The engine never reads the wall clock or uses randomness — `Date.now()` and
196
+ `Math.random()` are both banned by lint rules — so two independent
197
+ executions of the same sequence of `play()` calls always produce the same
198
+ state, score, and outcome, whether that's in a browser, on a server, or
199
+ inside a blockchain's execution environment.
200
+
201
+ The engine itself doesn't timestamp moves, detect a stalled turn, or forfeit
202
+ a non-responding player. Timing, turn timeouts, and replaying a match's
203
+ history from a log are all client/orchestration concerns built on top of
204
+ `Dots` (e.g. by pairing each `play()` call with your own timestamp), not
205
+ things this engine implements.
206
+
207
+ ## Coordinate System
208
+
209
+ - **Dots**: A dot is a `Coord` tuple `[x, y]`, where `x` is the column and `y` the row (both 0-based), ranging from `[0, 0]` to `[gridSize-1, gridSize-1]`.
210
+ - **Moves**: A move connects two orthogonally adjacent dots (the edge between them).
211
+ - **Squares**: Internal tracking with IDs `0, 1, 2, …` arranged in rows.
212
+ - **Validation**: `play` enforces turn order, and that the two dots are adjacent and within grid bounds.
213
+
214
+
215
+
216
+ ## Development
217
+
218
+
219
+
220
+ ### Running Tests
221
+
222
+ ```bash
223
+ npm test
224
+ ```
225
+
226
+
227
+
228
+ ### Running Demo
229
+
230
+ ```bash
231
+ npm run dev
232
+ ```
233
+
234
+
235
+
236
+ ## License
237
+
238
+ ISC License
package/dist/Dots.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ import Grid from "./Grid";
2
+ import { Coord, MatchId, MoveRecord, MoveResult, PlayerId } from "./types";
3
+ declare class Dots {
4
+ grid: Grid;
5
+ /** Identifier of this match; stamped onto every {@link MoveRecord}. */
6
+ matchId: MatchId;
7
+ /** The two players in this match; `players[0]` moves first. */
8
+ players: [PlayerId, PlayerId];
9
+ /** Player allowed to submit the next move. */
10
+ turn: PlayerId;
11
+ /** Squares closed per submitter. JSON-serializable by design. */
12
+ scores: Record<PlayerId, number>;
13
+ /** Squares closed so far in the current uninterrupted turn; reset when the turn changes. */
14
+ private chainCounter;
15
+ /** Each player's longest uninterrupted-turn chain this match (king-of-the-chain). */
16
+ longestChain: Record<PlayerId, number>;
17
+ status: number;
18
+ /** Full move history — simultaneously the per-move notice payload and the replay input (F6). */
19
+ moveLog: MoveRecord[];
20
+ /**
21
+ * @param matchId Identifier of this match, assigned by the caller
22
+ * (today: tests; later: the Match orchestrator).
23
+ * @param players The two players, normalized on entry; `players[0]`
24
+ * moves first (the first joiner, per the matchmaking queue).
25
+ * @throws If the two players are not distinct once normalized.
26
+ */
27
+ constructor(gridsize: number, players: [string, string], matchId: MatchId);
28
+ /**
29
+ * Connect two adjacent dots, drawing the edge between them.
30
+ *
31
+ * A move is always an edge (two dots); collecting the two endpoints — e.g.
32
+ * the two-click selection in a UI — is the caller's responsibility, so the
33
+ * engine stays stateless about selection and receives one complete move.
34
+ *
35
+ * Only the player currently on turn may draw an edge, and any square the
36
+ * move closes is owned by that player.
37
+ *
38
+ * Extra move on close: a move that closes one or two squares keeps the
39
+ * turn with the same player instead of alternating (PRD-v5 §6.4).
40
+ *
41
+ * @param from Start dot as `[x, y]` (x = column, y = row, 0-based).
42
+ * @param to End dot as `[x, y]`; must be orthogonally adjacent to `from`.
43
+ * @param submitter Identifier of the player drawing the edge; owns any
44
+ * squares it closes. Normalized before use, so formatting
45
+ * differences never affect scoring or the returned `MoveResult`.
46
+ * @returns A {@link MoveResult} describing the outcome of the move.
47
+ * @throws If the game is over, `submitter` is not the player on turn, a
48
+ * coordinate is out of bounds, the two dots are not adjacent, or
49
+ * the edge has already been drawn.
50
+ */
51
+ play(from: Coord, to: Coord, submitter: string): MoveResult;
52
+ getScore(): Record<string, number>;
53
+ /** Each player's longest uninterrupted-turn chain this match so far. */
54
+ getLongestChain(): Record<string, number>;
55
+ isOVer(): boolean;
56
+ isDraw(): boolean;
57
+ /**
58
+ * The submitter who owns the most closed squares, or `null` when the top
59
+ * count is shared by two or more addresses (a draw).
60
+ */
61
+ getWinner(): PlayerId | null;
62
+ private addScore;
63
+ private otherPlayer;
64
+ private updateStatus;
65
+ }
66
+ export default Dots;
package/dist/Dots.js ADDED
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const address_1 = require("./address");
7
+ const GameConstants_1 = require("./GameConstants");
8
+ const Grid_1 = __importDefault(require("./Grid"));
9
+ class Dots {
10
+ /**
11
+ * @param matchId Identifier of this match, assigned by the caller
12
+ * (today: tests; later: the Match orchestrator).
13
+ * @param players The two players, normalized on entry; `players[0]`
14
+ * moves first (the first joiner, per the matchmaking queue).
15
+ * @throws If the two players are not distinct once normalized.
16
+ */
17
+ constructor(gridsize, players, matchId) {
18
+ /** Squares closed per submitter. JSON-serializable by design. */
19
+ this.scores = {};
20
+ /** Squares closed so far in the current uninterrupted turn; reset when the turn changes. */
21
+ this.chainCounter = 0;
22
+ /** Each player's longest uninterrupted-turn chain this match (king-of-the-chain). */
23
+ this.longestChain = {};
24
+ this.status = GameConstants_1.GameConstants.STATUS_NOT_INITIATED;
25
+ /** Full move history — simultaneously the per-move notice payload and the replay input (F6). */
26
+ this.moveLog = [];
27
+ this.grid = new Grid_1.default(gridsize);
28
+ this.matchId = matchId;
29
+ const p1 = (0, address_1.normalizePlayerId)(players[0]);
30
+ const p2 = (0, address_1.normalizePlayerId)(players[1]);
31
+ if (p1 === p2) {
32
+ throw new Error(`A match needs two distinct players, got the same player twice: ${p1}`);
33
+ }
34
+ this.players = [p1, p2];
35
+ this.turn = p1;
36
+ }
37
+ /**
38
+ * Connect two adjacent dots, drawing the edge between them.
39
+ *
40
+ * A move is always an edge (two dots); collecting the two endpoints — e.g.
41
+ * the two-click selection in a UI — is the caller's responsibility, so the
42
+ * engine stays stateless about selection and receives one complete move.
43
+ *
44
+ * Only the player currently on turn may draw an edge, and any square the
45
+ * move closes is owned by that player.
46
+ *
47
+ * Extra move on close: a move that closes one or two squares keeps the
48
+ * turn with the same player instead of alternating (PRD-v5 §6.4).
49
+ *
50
+ * @param from Start dot as `[x, y]` (x = column, y = row, 0-based).
51
+ * @param to End dot as `[x, y]`; must be orthogonally adjacent to `from`.
52
+ * @param submitter Identifier of the player drawing the edge; owns any
53
+ * squares it closes. Normalized before use, so formatting
54
+ * differences never affect scoring or the returned `MoveResult`.
55
+ * @returns A {@link MoveResult} describing the outcome of the move.
56
+ * @throws If the game is over, `submitter` is not the player on turn, a
57
+ * coordinate is out of bounds, the two dots are not adjacent, or
58
+ * the edge has already been drawn.
59
+ */
60
+ play(from, to, submitter) {
61
+ var _a;
62
+ if (this.isOVer()) {
63
+ throw new Error("Game is over");
64
+ }
65
+ submitter = (0, address_1.normalizePlayerId)(submitter);
66
+ if (submitter !== this.turn) {
67
+ throw new Error(`Not this player's turn: expected ${this.turn}, got ${submitter}`);
68
+ }
69
+ const edge = this.grid.buildEdge(from, to);
70
+ if (this.grid.isEdgeDrawn(edge)) {
71
+ const [x1, y1] = from;
72
+ const [x2, y2] = to;
73
+ throw new Error(`Edge already drawn: [${x1}, ${y1}] -> [${x2}, ${y2}]`);
74
+ }
75
+ const squaresClosed = this.grid.conquerEdge(edge, submitter);
76
+ if (squaresClosed > 0) {
77
+ this.addScore(submitter, squaresClosed);
78
+ this.chainCounter += squaresClosed;
79
+ // Update the max here, not on turn change: the move that ends the
80
+ // match closes squares without ever flipping the turn, so a
81
+ // turn-change-gated update would silently drop the final chain.
82
+ this.longestChain[submitter] = Math.max((_a = this.longestChain[submitter]) !== null && _a !== void 0 ? _a : 0, this.chainCounter);
83
+ }
84
+ this.updateStatus();
85
+ if (squaresClosed === 0) {
86
+ // The turn can only flip when nothing was closed
87
+ this.turn = this.otherPlayer(submitter);
88
+ this.chainCounter = 0;
89
+ }
90
+ this.moveLog.push({
91
+ matchId: this.matchId,
92
+ moveIndex: this.moveLog.length,
93
+ edge: [from, to],
94
+ submitter,
95
+ squaresClosed,
96
+ turnAfter: this.turn,
97
+ });
98
+ return {
99
+ squaresClosed,
100
+ submitter,
101
+ status: this.status,
102
+ };
103
+ }
104
+ getScore() {
105
+ return this.scores;
106
+ }
107
+ /** Each player's longest uninterrupted-turn chain this match so far. */
108
+ getLongestChain() {
109
+ return this.longestChain;
110
+ }
111
+ isOVer() {
112
+ return this.status === GameConstants_1.GameConstants.STATUS_OVER ||
113
+ this.status === GameConstants_1.GameConstants.STATUS_OVER_BY_DRAW;
114
+ }
115
+ isDraw() {
116
+ return this.isOVer() && this.getWinner() === null;
117
+ }
118
+ /**
119
+ * The submitter who owns the most closed squares, or `null` when the top
120
+ * count is shared by two or more addresses (a draw).
121
+ */
122
+ getWinner() {
123
+ let winner = null;
124
+ let topScore = 0;
125
+ let tied = false;
126
+ for (const [address, score] of Object.entries(this.scores)) {
127
+ if (score > topScore) {
128
+ topScore = score;
129
+ winner = address;
130
+ tied = false;
131
+ }
132
+ else if (score === topScore) {
133
+ tied = true;
134
+ }
135
+ }
136
+ return tied ? null : winner;
137
+ }
138
+ addScore(submitter, nClosedSquares) {
139
+ var _a;
140
+ this.scores[submitter] = ((_a = this.scores[submitter]) !== null && _a !== void 0 ? _a : 0) + nClosedSquares;
141
+ }
142
+ otherPlayer(player) {
143
+ return this.players[0] === player ? this.players[1] : this.players[0];
144
+ }
145
+ updateStatus() {
146
+ if (!this.grid.hasOpenSquare()) {
147
+ this.status = this.getWinner() !== null
148
+ ? GameConstants_1.GameConstants.STATUS_OVER
149
+ : GameConstants_1.GameConstants.STATUS_OVER_BY_DRAW;
150
+ }
151
+ else {
152
+ this.status = GameConstants_1.GameConstants.STATUS_IN_PROGRESS;
153
+ }
154
+ }
155
+ }
156
+ exports.default = Dots;
package/dist/Edge.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import Point from './Point';
2
+ import { PlayerId } from './types';
3
+ declare class Edge {
4
+ p1: Point;
5
+ p2: Point;
6
+ relatedSquareId: number[];
7
+ owner: PlayerId;
8
+ constructor(p1: Point, p2: Point);
9
+ setOwner(owner: PlayerId): void;
10
+ hasOwner(): boolean;
11
+ equals(other: Edge): boolean;
12
+ relatesTo(squareId: number): void;
13
+ getRelatedSquareOtherThan(id: number): number;
14
+ }
15
+ export default Edge;
package/dist/Edge.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class Edge {
4
+ constructor(p1, p2) {
5
+ this.relatedSquareId = [];
6
+ this.owner = "";
7
+ this.p1 = p1;
8
+ this.p2 = p2;
9
+ }
10
+ setOwner(owner) {
11
+ if (!this.hasOwner()) {
12
+ this.owner = owner;
13
+ }
14
+ }
15
+ hasOwner() {
16
+ return (this.owner.length > 0) ? true : false;
17
+ }
18
+ equals(other) {
19
+ // Check if edges are the same (in either direction)
20
+ return (this.p1.equals(other.p1) && this.p2.equals(other.p2)) ||
21
+ (this.p1.equals(other.p2) && this.p2.equals(other.p1));
22
+ }
23
+ relatesTo(squareId) {
24
+ this.relatedSquareId.push(squareId);
25
+ }
26
+ getRelatedSquareOtherThan(id) {
27
+ if (this.relatedSquareId[0] === id) {
28
+ return this.relatedSquareId[1];
29
+ }
30
+ else {
31
+ return this.relatedSquareId[0];
32
+ }
33
+ }
34
+ }
35
+ exports.default = Edge;
@@ -0,0 +1,7 @@
1
+ export declare class GameConstants {
2
+ static readonly GRID_SIZE = 6;
3
+ static readonly STATUS_NOT_INITIATED: number;
4
+ static readonly STATUS_IN_PROGRESS: number;
5
+ static readonly STATUS_OVER: number;
6
+ static readonly STATUS_OVER_BY_DRAW: number;
7
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GameConstants = void 0;
4
+ class GameConstants {
5
+ }
6
+ exports.GameConstants = GameConstants;
7
+ GameConstants.GRID_SIZE = 6;
8
+ // Game possible statuses
9
+ GameConstants.STATUS_NOT_INITIATED = 1;
10
+ GameConstants.STATUS_IN_PROGRESS = 2;
11
+ GameConstants.STATUS_OVER = 3;
12
+ GameConstants.STATUS_OVER_BY_DRAW = 4;
13
+ ;
package/dist/Grid.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ import Square from './Square';
2
+ import Point from './Point';
3
+ import Edge from './Edge';
4
+ import { Coord, PlayerId } from './types';
5
+ declare class Grid {
6
+ size: number;
7
+ squares: Square[];
8
+ points: Point[];
9
+ uniqueEdges: Edge[];
10
+ /**
11
+ * Constructor
12
+ * @param gridSize n. points (horizontal and vertical) for the grid
13
+ */
14
+ constructor(gridSize: number);
15
+ private build;
16
+ private createPoints;
17
+ private createSquares;
18
+ private createEdges;
19
+ private getEdge;
20
+ /**
21
+ * Get square position from square ID
22
+ * @param squareId The square ID
23
+ * @returns [row, col] position
24
+ */
25
+ getSquarePosition(squareId: number): [number, number];
26
+ /**
27
+ * Get square ID from position
28
+ * @param row Row position
29
+ * @param col Column position
30
+ * @returns Square ID
31
+ */
32
+ getSquareId(row: number, col: number): number;
33
+ /**
34
+ * Get all squares that share an edge
35
+ * @param edge The edge
36
+ * @returns Array of square IDs that share this edge
37
+ */
38
+ getSquaresForEdge(edge: Edge): number[];
39
+ /**
40
+ * Build the edge for a move, validating it first. Single source of truth
41
+ * for move geometry: bounds + orthogonal adjacency.
42
+ */
43
+ buildEdge(from: Coord, to: Coord): Edge;
44
+ /**
45
+ * Whether the given edge has already been drawn (claimed by a submitter).
46
+ */
47
+ isEdgeDrawn(edge: Edge): boolean;
48
+ /**
49
+ * Find an edge by its points
50
+ * @param p1 First point
51
+ * @param p2 Second point
52
+ * @returns The edge if found, null otherwise
53
+ */
54
+ findEdge(p1: Point, p2: Point): Edge | null;
55
+ /**
56
+ * Get squares that still have available edges
57
+ * @param squareIds List of squares' ids
58
+ */
59
+ private getAvailableSquaresbyId;
60
+ /**
61
+ * Close an edge
62
+ *
63
+ * @param edge Edge to close
64
+ * @param owner User who wants to own the edge
65
+ *
66
+ * @returns Number of closed squares by closing the edge provided.
67
+ */
68
+ conquerEdge(edge: Edge, owner: PlayerId): number;
69
+ private conquerSquare;
70
+ /**
71
+ * Check if there is any open square.
72
+ */
73
+ hasOpenSquare(): boolean;
74
+ /**
75
+ * Get all squares
76
+ */
77
+ getSquares(): Square[];
78
+ /**
79
+ * Get all edges
80
+ */
81
+ getEdges(): Edge[];
82
+ /**
83
+ * Get all points
84
+ */
85
+ getPoints(): Point[];
86
+ }
87
+ export default Grid;
package/dist/Grid.js ADDED
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const Square_1 = __importDefault(require("./Square"));
7
+ const Point_1 = __importDefault(require("./Point"));
8
+ const Edge_1 = __importDefault(require("./Edge"));
9
+ class Grid {
10
+ /**
11
+ * Constructor
12
+ * @param gridSize n. points (horizontal and vertical) for the grid
13
+ */
14
+ constructor(gridSize) {
15
+ this.size = 0;
16
+ this.squares = [];
17
+ this.points = [];
18
+ this.uniqueEdges = [];
19
+ if (gridSize < 2)
20
+ throw new Error("Grid size must be greater or equal to 2");
21
+ this.build(gridSize);
22
+ }
23
+ build(gridSize) {
24
+ this.size = gridSize;
25
+ this.createPoints(gridSize);
26
+ this.createSquares(gridSize);
27
+ }
28
+ createPoints(gridSize) {
29
+ for (let x = 0; x < gridSize; x++) {
30
+ for (let y = 0; y < gridSize; y++) {
31
+ this.points.push(new Point_1.default(x, y));
32
+ }
33
+ }
34
+ }
35
+ createSquares(gridSize) {
36
+ const squaresPerRow = gridSize - 1;
37
+ const totalRows = gridSize - 1;
38
+ for (let row = 0; row < totalRows; row++) {
39
+ for (let col = 0; col < squaresPerRow; col++) {
40
+ const squareId = row * squaresPerRow + col;
41
+ console.log('CreateSquare #' + squareId + ' at position (' + row + ',' + col + ')');
42
+ let edges = this.createEdges(row, col);
43
+ this.squares.push(new Square_1.default(squareId, edges));
44
+ }
45
+ }
46
+ console.log('createSquares - created #uniqueEdges=' + this.uniqueEdges.length);
47
+ }
48
+ createEdges(row, col) {
49
+ // Create the four edges for a square at position (row, col)
50
+ // Each edge connects two adjacent points
51
+ let leftEdge = this.getEdge(new Point_1.default(col, row), new Point_1.default(col, row + 1));
52
+ let bottomEdge = this.getEdge(new Point_1.default(col, row + 1), new Point_1.default(col + 1, row + 1));
53
+ let rightEdge = this.getEdge(new Point_1.default(col + 1, row + 1), new Point_1.default(col + 1, row));
54
+ let topEdge = this.getEdge(new Point_1.default(col + 1, row), new Point_1.default(col, row));
55
+ let edges = [leftEdge, bottomEdge, rightEdge, topEdge];
56
+ return edges;
57
+ }
58
+ getEdge(p1, p2) {
59
+ let tempEdge = new Edge_1.default(p1, p2);
60
+ for (let i = 0; i < this.uniqueEdges.length; i++) {
61
+ const existingEdge = this.uniqueEdges[i];
62
+ if (existingEdge.equals(tempEdge)) {
63
+ return existingEdge;
64
+ }
65
+ }
66
+ this.uniqueEdges.push(tempEdge);
67
+ return tempEdge;
68
+ }
69
+ /**
70
+ * Get square position from square ID
71
+ * @param squareId The square ID
72
+ * @returns [row, col] position
73
+ */
74
+ getSquarePosition(squareId) {
75
+ const squaresPerRow = this.size - 1;
76
+ const row = Math.floor(squareId / squaresPerRow);
77
+ const col = squareId % squaresPerRow;
78
+ return [row, col];
79
+ }
80
+ /**
81
+ * Get square ID from position
82
+ * @param row Row position
83
+ * @param col Column position
84
+ * @returns Square ID
85
+ */
86
+ getSquareId(row, col) {
87
+ const squaresPerRow = this.size - 1;
88
+ return row * squaresPerRow + col;
89
+ }
90
+ /**
91
+ * Get all squares that share an edge
92
+ * @param edge The edge
93
+ * @returns Array of square IDs that share this edge
94
+ */
95
+ getSquaresForEdge(edge) {
96
+ return edge.relatedSquareId;
97
+ }
98
+ /**
99
+ * Build the edge for a move, validating it first. Single source of truth
100
+ * for move geometry: bounds + orthogonal adjacency.
101
+ */
102
+ buildEdge(from, to) {
103
+ const size = this.size;
104
+ const endpoints = [["from", from], ["to", to]];
105
+ for (const [label, [x, y]] of endpoints) {
106
+ if (!Number.isInteger(x) || !Number.isInteger(y) ||
107
+ x < 0 || x >= size || y < 0 || y >= size) {
108
+ throw new Error(`Invalid "${label}" coordinate [${x}, ${y}]: out of bounds for a ${size}x${size} grid`);
109
+ }
110
+ }
111
+ const [x1, y1] = from;
112
+ const [x2, y2] = to;
113
+ const dx = Math.abs(x2 - x1);
114
+ const dy = Math.abs(y2 - y1);
115
+ if (!((dx === 1 && dy === 0) || (dx === 0 && dy === 1))) {
116
+ throw new Error(`Dots must be adjacent: [${x1}, ${y1}] -> [${x2}, ${y2}]`);
117
+ }
118
+ return new Edge_1.default(new Point_1.default(x1, y1), new Point_1.default(x2, y2));
119
+ }
120
+ /**
121
+ * Whether the given edge has already been drawn (claimed by a submitter).
122
+ */
123
+ isEdgeDrawn(edge) {
124
+ var _a;
125
+ const tracked = this.findEdge(edge.p1, edge.p2);
126
+ return (_a = tracked === null || tracked === void 0 ? void 0 : tracked.hasOwner()) !== null && _a !== void 0 ? _a : false;
127
+ }
128
+ /**
129
+ * Find an edge by its points
130
+ * @param p1 First point
131
+ * @param p2 Second point
132
+ * @returns The edge if found, null otherwise
133
+ */
134
+ findEdge(p1, p2) {
135
+ const searchEdge = new Edge_1.default(p1, p2);
136
+ for (let edge of this.uniqueEdges) {
137
+ if (edge.equals(searchEdge)) {
138
+ return edge;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * Get squares that still have available edges
145
+ * @param squareIds List of squares' ids
146
+ */
147
+ getAvailableSquaresbyId(squareIds) {
148
+ let squaresFound = [];
149
+ for (let i = 0; i < this.squares.length; i++) {
150
+ const currSquare = this.squares[i];
151
+ squareIds.forEach(id => {
152
+ if ((currSquare.id === id) && (currSquare.hasAvailableFace())) {
153
+ squaresFound.push(currSquare);
154
+ }
155
+ });
156
+ }
157
+ return squaresFound;
158
+ }
159
+ /**
160
+ * Close an edge
161
+ *
162
+ * @param edge Edge to close
163
+ * @param owner User who wants to own the edge
164
+ *
165
+ * @returns Number of closed squares by closing the edge provided.
166
+ */
167
+ conquerEdge(edge, owner) {
168
+ let nClosedSquares = 0;
169
+ for (let i = 0; i < this.uniqueEdges.length; i++) {
170
+ const gameEdge = this.uniqueEdges[i];
171
+ if ((gameEdge.equals(edge)) && (!gameEdge.hasOwner())) {
172
+ let squareIds = gameEdge.relatedSquareId;
173
+ let availSquaresBeforeClosing = this.getAvailableSquaresbyId(squareIds);
174
+ gameEdge.setOwner(owner);
175
+ let availSquaresAfterClosing = this.getAvailableSquaresbyId(squareIds);
176
+ nClosedSquares = availSquaresBeforeClosing.length - availSquaresAfterClosing.length;
177
+ if (nClosedSquares > 0) {
178
+ this.conquerSquare(availSquaresBeforeClosing, availSquaresAfterClosing, owner);
179
+ }
180
+ break;
181
+ }
182
+ }
183
+ return nClosedSquares;
184
+ }
185
+ conquerSquare(availSquaresBeforeClosing, availSquaresAfterClosing, owner) {
186
+ for (let i = 0; i < availSquaresBeforeClosing.length; i++) {
187
+ let foundSquare = false;
188
+ for (let j = 0; j < availSquaresAfterClosing.length; j++) {
189
+ if (availSquaresBeforeClosing[i].id === availSquaresAfterClosing[j].id) {
190
+ foundSquare = true;
191
+ break;
192
+ }
193
+ }
194
+ if (!foundSquare) {
195
+ availSquaresBeforeClosing[i].owner = owner;
196
+ }
197
+ }
198
+ }
199
+ /**
200
+ * Check if there is any open square.
201
+ */
202
+ hasOpenSquare() {
203
+ for (let i = 0; i < this.squares.length; i++) {
204
+ if (this.squares[i].hasAvailableFace()) {
205
+ return true;
206
+ }
207
+ }
208
+ return false;
209
+ }
210
+ /**
211
+ * Get all squares
212
+ */
213
+ getSquares() {
214
+ return this.squares;
215
+ }
216
+ /**
217
+ * Get all edges
218
+ */
219
+ getEdges() {
220
+ return this.uniqueEdges;
221
+ }
222
+ /**
223
+ * Get all points
224
+ */
225
+ getPoints() {
226
+ return this.points;
227
+ }
228
+ }
229
+ exports.default = Grid;
@@ -0,0 +1,8 @@
1
+ declare class Point {
2
+ x: number;
3
+ y: number;
4
+ constructor(x: number, y: number);
5
+ equals(other: Point): boolean;
6
+ toString(): string;
7
+ }
8
+ export default Point;
package/dist/Point.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class Point {
4
+ constructor(x, y) {
5
+ this.x = x;
6
+ this.y = y;
7
+ }
8
+ equals(other) {
9
+ return this.x === other.x && this.y === other.y;
10
+ }
11
+ toString() {
12
+ return `${this.x},${this.y}`;
13
+ }
14
+ }
15
+ exports.default = Point;
@@ -0,0 +1,19 @@
1
+ import Edge from './Edge';
2
+ import { PlayerId } from './types';
3
+ declare class Square {
4
+ id: number;
5
+ owner: PlayerId;
6
+ edges: Edge[];
7
+ constructor(id: number, edges: Edge[]);
8
+ /**
9
+ * Return true if there is, at least, one available face
10
+ * and false otherwise
11
+ */
12
+ hasAvailableFace(): boolean;
13
+ getNumberOfAvailableFaces(): number;
14
+ /**
15
+ * true if the square has owner or false otherwise
16
+ */
17
+ hasOwner(): boolean;
18
+ }
19
+ export default Square;
package/dist/Square.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SIDES = 4;
4
+ class Square {
5
+ constructor(id, edges) {
6
+ if (edges.length != SIDES) {
7
+ throw new Error("Edge array must have exactly 4 edges");
8
+ }
9
+ this.id = id;
10
+ this.owner = '';
11
+ edges.forEach(edge => {
12
+ edge.relatesTo(this.id);
13
+ });
14
+ this.edges = edges;
15
+ }
16
+ /**
17
+ * Return true if there is, at least, one available face
18
+ * and false otherwise
19
+ */
20
+ hasAvailableFace() {
21
+ for (let i = 0; i < this.edges.length; i++) {
22
+ const edge = this.edges[i];
23
+ if (edge.hasOwner() === false)
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ getNumberOfAvailableFaces() {
29
+ let cont = 0;
30
+ for (let i = 0; i < this.edges.length; i++) {
31
+ const edge = this.edges[i];
32
+ if (edge.hasOwner() === false)
33
+ cont++;
34
+ }
35
+ return cont;
36
+ }
37
+ /**
38
+ * true if the square has owner or false otherwise
39
+ */
40
+ hasOwner() {
41
+ var _a;
42
+ return ((_a = this.owner) === null || _a === void 0 ? void 0 : _a.length) > 0 ? true : false;
43
+ }
44
+ }
45
+ exports.default = Square;
@@ -0,0 +1,11 @@
1
+ import { PlayerId } from "./types";
2
+ /**
3
+ * Canonicalize an EOA address for use as a player identifier.
4
+ *
5
+ * Cartesi supplies `msg_sender` already lowercase, but wallets supply
6
+ * EIP-55 checksum-cased addresses. Without normalizing, the same player
7
+ * submitted under different casing would fork replays and score buckets
8
+ * between consumers. This is the only place in the engine allowed to know
9
+ * player identifiers are case-insensitive hex strings.
10
+ */
11
+ export declare function normalizePlayerId(id: string): PlayerId;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePlayerId = void 0;
4
+ /**
5
+ * Canonicalize an EOA address for use as a player identifier.
6
+ *
7
+ * Cartesi supplies `msg_sender` already lowercase, but wallets supply
8
+ * EIP-55 checksum-cased addresses. Without normalizing, the same player
9
+ * submitted under different casing would fork replays and score buckets
10
+ * between consumers. This is the only place in the engine allowed to know
11
+ * player identifiers are case-insensitive hex strings.
12
+ */
13
+ function normalizePlayerId(id) {
14
+ return id.toLowerCase();
15
+ }
16
+ exports.normalizePlayerId = normalizePlayerId;
@@ -0,0 +1,3 @@
1
+ export { default as Dots } from "./Dots";
2
+ export { GameConstants } from "./GameConstants";
3
+ export type { Coord, MatchId, MoveRecord, MoveResult, PlayerId } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GameConstants = exports.Dots = void 0;
7
+ var Dots_1 = require("./Dots");
8
+ Object.defineProperty(exports, "Dots", { enumerable: true, get: function () { return __importDefault(Dots_1).default; } });
9
+ var GameConstants_1 = require("./GameConstants");
10
+ Object.defineProperty(exports, "GameConstants", { enumerable: true, get: function () { return GameConstants_1.GameConstants; } });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A dot on the grid, expressed as `[x, y]` where `x` is the column and `y` the
3
+ * row (both 0-based). Ranges from `[0, 0]` to `[gridSize - 1, gridSize - 1]`.
4
+ *
5
+ * Plain tuples are intentionally the only move vocabulary: they are
6
+ * JSON-serializable and need no engine-internal classes, so the same `Coord`
7
+ * type can describe a frontend's state and feed {@link Dots.play} directly.
8
+ */
9
+ export type Coord = [number, number];
10
+ /**
11
+ * A player identifier, already normalized by the engine at every ingress so
12
+ * the same player never forks into separate score buckets. Opaque to game
13
+ * logic — see {@link ../address.normalizePlayerId} for the one place that
14
+ * knows its concrete format.
15
+ */
16
+ export type PlayerId = string;
17
+ /**
18
+ * A match identifier, opaque to game logic. Assigned once per match by
19
+ * whoever constructs the engine (today: tests; later: the Match
20
+ * orchestrator).
21
+ */
22
+ export type MatchId = string;
23
+ /**
24
+ * One applied move, in canonical form. Doubles as the per-move notice
25
+ * payload (F6) and, as a log, the replay input — the shape is shared on
26
+ * purpose so the two never drift.
27
+ */
28
+ export interface MoveRecord {
29
+ matchId: MatchId;
30
+ /** 0-based position of this move within the match's move log. */
31
+ moveIndex: number;
32
+ /** The move exactly as submitted: `[from, to]`. */
33
+ edge: [Coord, Coord];
34
+ /** Identifier of the player who submitted this move; already normalized. */
35
+ submitter: PlayerId;
36
+ /** Squares completed by this move (0, 1, or 2). */
37
+ squaresClosed: number;
38
+ /** Player on turn immediately after this move resolved. */
39
+ turnAfter: PlayerId;
40
+ }
41
+ /**
42
+ * Outcome of a single {@link Dots.play} call — everything a UI needs to
43
+ * re-render after a move, returned from the one call instead of forcing the
44
+ * caller to diff `getScore()` / `isOVer()` by hand.
45
+ */
46
+ export interface MoveResult {
47
+ /** Squares completed by this move (0, 1, or 2). */
48
+ squaresClosed: number;
49
+ /** Identifier of the player who submitted this move (owns any squares it closed); already normalized. */
50
+ submitter: PlayerId;
51
+ /** Game status after the move (see `GameConstants.STATUS_*`). */
52
+ status: number;
53
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "dots-engine",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "js engine for dots game",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.build.json",
18
+ "dev": "ts-node examples/demo.ts",
19
+ "test": "jest",
20
+ "lint": "eslint .",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "author": "claudioantonio",
24
+ "license": "ISC",
25
+ "devDependencies": {
26
+ "@types/jest": "^29.5.7",
27
+ "eslint": "^10.8.0",
28
+ "jest": "^29.7.0",
29
+ "ts-jest": "^29.1.1",
30
+ "ts-node": "^10.9.1",
31
+ "typescript": "^5.2.2",
32
+ "typescript-eslint": "^8.65.0"
33
+ }
34
+ }