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 +15 -0
- package/README.md +238 -0
- package/dist/Dots.d.ts +66 -0
- package/dist/Dots.js +156 -0
- package/dist/Edge.d.ts +15 -0
- package/dist/Edge.js +35 -0
- package/dist/GameConstants.d.ts +7 -0
- package/dist/GameConstants.js +13 -0
- package/dist/Grid.d.ts +87 -0
- package/dist/Grid.js +229 -0
- package/dist/Point.d.ts +8 -0
- package/dist/Point.js +15 -0
- package/dist/Square.d.ts +19 -0
- package/dist/Square.js +45 -0
- package/dist/address.d.ts +11 -0
- package/dist/address.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +10 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +2 -0
- package/package.json +34 -0
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,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;
|
package/dist/Point.d.ts
ADDED
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;
|
package/dist/Square.d.ts
ADDED
|
@@ -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;
|
package/dist/address.js
ADDED
|
@@ -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;
|
package/dist/index.d.ts
ADDED
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; } });
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|