@johnmorrisdotca/domino 1.0.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/mexicanTrain/dominoes.d.ts +27 -0
- package/dist/mexicanTrain/dominoes.js +58 -0
- package/dist/mexicanTrain/mexicanTrain.constants.d.ts +42 -0
- package/dist/mexicanTrain/mexicanTrain.constants.js +40 -0
- package/dist/mexicanTrain/mexicanTrain.d.ts +92 -0
- package/dist/mexicanTrain/mexicanTrain.js +339 -0
- package/dist/mexicanTrain/mexicanTrain.types.d.ts +154 -0
- package/dist/mexicanTrain/mexicanTrain.types.js +17 -0
- package/dist/mexicanTrain/trainCodec.d.ts +4 -0
- package/dist/mexicanTrain/trainCodec.js +90 -0
- package/dist/mexicanTrain/trainComputer.d.ts +10 -0
- package/dist/mexicanTrain/trainComputer.js +113 -0
- package/dist/random.d.ts +17 -0
- package/dist/random.js +31 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +85 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Relative, like the rest of lib/party: the browser specs import this, and Playwright resolves no alias.
|
|
2
|
+
import { endsOf, fits, isDouble, pipsOf } from "./dominoes.js";
|
|
3
|
+
import { mexicanOf, openEnd, trainMoves } from "./mexicanTrain.js";
|
|
4
|
+
/**
|
|
5
|
+
* A COMPUTER AT THE TABLE: the move a computer player makes, worked out in
|
|
6
|
+
* the browser from the game alone, the same move every time for the same
|
|
7
|
+
* game. It sees only what its seat may see — its own hand and the table —
|
|
8
|
+
* and never the boneyard's order or another hand.
|
|
9
|
+
*
|
|
10
|
+
* How it plays, the way a sensible player at a real table does:
|
|
11
|
+
*
|
|
12
|
+
* - It PLANS ITS OWN TRAIN. From its train's open end it finds the longest
|
|
13
|
+
* run its hand can lay there (`longestRun`), and keeps those tiles for its
|
|
14
|
+
* own train, laying them one a turn, first to last.
|
|
15
|
+
* - Everything not in that run it would rather lay somewhere else — the
|
|
16
|
+
* Mexican Train, or a train left open — and the heavier the better, since
|
|
17
|
+
* a tile laid is pips that cannot count against it.
|
|
18
|
+
* - Its own marker out is a train anybody can block, so it lays at home
|
|
19
|
+
* first when it can, and takes the marker in.
|
|
20
|
+
* - A DOUBLE it lays only when it holds a tile to cover it with, so the
|
|
21
|
+
* double is a free second tile rather than a turn spent drawing; when it
|
|
22
|
+
* must cover somebody else's, it covers with a tile its run can spare.
|
|
23
|
+
* - Going out wins the round, so a last tile is always laid.
|
|
24
|
+
*/
|
|
25
|
+
/** How many runs the planner looks at before settling for the longest found: enough for any hand, bounded for a huge one. */
|
|
26
|
+
const PLAN_STEPS = 4000;
|
|
27
|
+
/**
|
|
28
|
+
* The longest run of tiles from `hand` that can be laid one after another
|
|
29
|
+
* against `end`, in the order they would be laid. A depth-first search over
|
|
30
|
+
* the hand, bounded by `PLAN_STEPS`, preferring the heavier run of two the
|
|
31
|
+
* same length: pips laid are pips that do not count.
|
|
32
|
+
*/
|
|
33
|
+
export function longestRun(hand, end) {
|
|
34
|
+
let best = [];
|
|
35
|
+
let bestPips = -1;
|
|
36
|
+
let steps = 0;
|
|
37
|
+
const used = new Set();
|
|
38
|
+
const run = [];
|
|
39
|
+
const walk = (at, pips) => {
|
|
40
|
+
steps += 1;
|
|
41
|
+
if (run.length > best.length || (run.length === best.length && pips > bestPips)) {
|
|
42
|
+
best = [...run];
|
|
43
|
+
bestPips = pips;
|
|
44
|
+
}
|
|
45
|
+
if (steps > PLAN_STEPS)
|
|
46
|
+
return;
|
|
47
|
+
for (const tile of hand) {
|
|
48
|
+
if (used.has(tile) || !fits(tile, at))
|
|
49
|
+
continue;
|
|
50
|
+
const [low, high] = endsOf(tile);
|
|
51
|
+
used.add(tile);
|
|
52
|
+
run.push(tile);
|
|
53
|
+
walk(low === at ? high : low, pips + pipsOf(tile));
|
|
54
|
+
run.pop();
|
|
55
|
+
used.delete(tile);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
walk(end, 0);
|
|
59
|
+
return best;
|
|
60
|
+
}
|
|
61
|
+
/** How good a lay looks to the computer: higher is better. */
|
|
62
|
+
function scoreOf(game, move, plan) {
|
|
63
|
+
const seat = game.toPlay;
|
|
64
|
+
const hand = game.hands[seat];
|
|
65
|
+
const { tile, train } = move;
|
|
66
|
+
if (hand.length === 1)
|
|
67
|
+
return 10000;
|
|
68
|
+
const own = train === seat;
|
|
69
|
+
const planned = plan.indexOf(tile);
|
|
70
|
+
let score = pipsOf(tile);
|
|
71
|
+
if (own) {
|
|
72
|
+
if (planned === 0)
|
|
73
|
+
score += 40;
|
|
74
|
+
else if (planned > 0)
|
|
75
|
+
score -= 25;
|
|
76
|
+
else
|
|
77
|
+
score += 5;
|
|
78
|
+
if (game.trains[seat].open)
|
|
79
|
+
score += 30;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
score += planned >= 0 ? -30 : 12;
|
|
83
|
+
if (train === mexicanOf(game))
|
|
84
|
+
score += 2;
|
|
85
|
+
}
|
|
86
|
+
if (isDouble(tile)) {
|
|
87
|
+
const [value] = endsOf(tile);
|
|
88
|
+
const cover = hand.some((other) => other !== tile && fits(other, value));
|
|
89
|
+
score += cover ? 25 : -20;
|
|
90
|
+
}
|
|
91
|
+
return score;
|
|
92
|
+
}
|
|
93
|
+
/** The move the computer in the seat to move makes now: the best-scored lay, or the draw, pass or next round it must take. */
|
|
94
|
+
export function computerMove(game) {
|
|
95
|
+
const offered = trainMoves(game);
|
|
96
|
+
if (offered.length === 0)
|
|
97
|
+
throw new Error("no move to make: the game is over");
|
|
98
|
+
const plays = offered.filter((move) => move.kind === "play");
|
|
99
|
+
if (plays.length === 0)
|
|
100
|
+
return offered[0];
|
|
101
|
+
const seat = game.toPlay;
|
|
102
|
+
const plan = longestRun(game.hands[seat], openEnd(game, seat));
|
|
103
|
+
let best = plays[0];
|
|
104
|
+
let bestScore = -Infinity;
|
|
105
|
+
for (const move of plays) {
|
|
106
|
+
const score = scoreOf(game, move, plan);
|
|
107
|
+
if (score > bestScore || (score === bestScore && (move.tile > best.tile || (move.tile === best.tile && move.train < best.train)))) {
|
|
108
|
+
best = move;
|
|
109
|
+
bestScore = score;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return best;
|
|
113
|
+
}
|
package/dist/random.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEEDED RANDOMNESS, the one thing every deal is made from.
|
|
3
|
+
*
|
|
4
|
+
* mulberry32: small, fast, and the same stream for the same seed in every
|
|
5
|
+
* browser and every Node. It decides every shuffle, so it must never change —
|
|
6
|
+
* a game kept as its seed and its moves is dealt again from it, and a changed
|
|
7
|
+
* stream would deal somebody a different hand from the one they were playing.
|
|
8
|
+
* `random.test.ts` pins its first numbers for that reason.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here is a credential: it is for fair-looking deals, not secrets.
|
|
11
|
+
*/
|
|
12
|
+
/** A number in [0, 1), like `Math.random`, from a stream a seed fixes. */
|
|
13
|
+
export type Random = () => number;
|
|
14
|
+
/** A stream of numbers in [0, 1) fixed by a seed. */
|
|
15
|
+
export declare function seededRandom(seed: number): Random;
|
|
16
|
+
/** A copy of the list in a random order (Fisher–Yates); the list given is left alone. */
|
|
17
|
+
export declare function shuffled<T>(items: readonly T[], random: Random): T[];
|
package/dist/random.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEEDED RANDOMNESS, the one thing every deal is made from.
|
|
3
|
+
*
|
|
4
|
+
* mulberry32: small, fast, and the same stream for the same seed in every
|
|
5
|
+
* browser and every Node. It decides every shuffle, so it must never change —
|
|
6
|
+
* a game kept as its seed and its moves is dealt again from it, and a changed
|
|
7
|
+
* stream would deal somebody a different hand from the one they were playing.
|
|
8
|
+
* `random.test.ts` pins its first numbers for that reason.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here is a credential: it is for fair-looking deals, not secrets.
|
|
11
|
+
*/
|
|
12
|
+
/** A stream of numbers in [0, 1) fixed by a seed. */
|
|
13
|
+
export function seededRandom(seed) {
|
|
14
|
+
let state = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
17
|
+
let t = state;
|
|
18
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
19
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** A copy of the list in a random order (Fisher–Yates); the list given is left alone. */
|
|
24
|
+
export function shuffled(items, random) {
|
|
25
|
+
const out = [...items];
|
|
26
|
+
for (let i = out.length - 1; i > 0; i -= 1) {
|
|
27
|
+
const j = Math.floor(random() * (i + 1));
|
|
28
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@johnmorrisdotca/domino",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dominoes for JavaScript and TypeScript: double-nine, double-twelve and double-fifteen sets, and the full rules of Mexican Train for two to eight players, with a computer player, seeded deals that replay exactly, and a compact saved-game format. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dominoes",
|
|
7
|
+
"domino",
|
|
8
|
+
"mexican-train",
|
|
9
|
+
"mexican-train-dominoes",
|
|
10
|
+
"train-dominoes",
|
|
11
|
+
"double-twelve",
|
|
12
|
+
"double-nine",
|
|
13
|
+
"double-fifteen",
|
|
14
|
+
"domino-game",
|
|
15
|
+
"tile-game",
|
|
16
|
+
"board-game",
|
|
17
|
+
"game-engine",
|
|
18
|
+
"game-rules",
|
|
19
|
+
"rules-engine",
|
|
20
|
+
"computer-player",
|
|
21
|
+
"ai-player",
|
|
22
|
+
"bot",
|
|
23
|
+
"seeded",
|
|
24
|
+
"seeded-random",
|
|
25
|
+
"deterministic",
|
|
26
|
+
"replay",
|
|
27
|
+
"save-game",
|
|
28
|
+
"typescript",
|
|
29
|
+
"javascript",
|
|
30
|
+
"zero-dependencies",
|
|
31
|
+
"browser",
|
|
32
|
+
"node",
|
|
33
|
+
"party-game",
|
|
34
|
+
"family-game",
|
|
35
|
+
"itsutsu"
|
|
36
|
+
],
|
|
37
|
+
"homepage": "https://github.com/johnmorrisdotca/domino#readme",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/johnmorrisdotca/domino.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/johnmorrisdotca/domino/issues"
|
|
44
|
+
},
|
|
45
|
+
"author": "John Morris",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"type": "module",
|
|
48
|
+
"sideEffects": false,
|
|
49
|
+
"main": "./dist/index.js",
|
|
50
|
+
"module": "./dist/index.js",
|
|
51
|
+
"types": "./dist/index.d.ts",
|
|
52
|
+
"exports": {
|
|
53
|
+
".": {
|
|
54
|
+
"types": "./dist/index.d.ts",
|
|
55
|
+
"import": "./dist/index.js",
|
|
56
|
+
"default": "./dist/index.js"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"files": [
|
|
60
|
+
"dist",
|
|
61
|
+
"README.md",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
"CHANGELOG.md"
|
|
64
|
+
],
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=20"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc -p tsconfig.json",
|
|
70
|
+
"lint": "eslint .",
|
|
71
|
+
"typecheck": "tsc -p tsconfig.check.json",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:package": "pnpm build && node scripts/check-package.mjs",
|
|
74
|
+
"check": "pnpm lint && pnpm typecheck && pnpm test",
|
|
75
|
+
"prepublishOnly": "pnpm check && pnpm build"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@eslint/js": "^9.0.0",
|
|
79
|
+
"eslint": "^9.0.0",
|
|
80
|
+
"typescript": "^5.9.0",
|
|
81
|
+
"typescript-eslint": "^8.0.0",
|
|
82
|
+
"vitest": "^5.0.0"
|
|
83
|
+
},
|
|
84
|
+
"packageManager": "pnpm@10.11.0"
|
|
85
|
+
}
|