@johnmorrisdotca/kumimoji 1.0.1
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 +53 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/NOTICE.md +38 -0
- package/README.md +233 -0
- package/dist/check.d.ts +19 -0
- package/dist/check.js +70 -0
- package/dist/computer.types.d.ts +23 -0
- package/dist/computer.types.js +1 -0
- package/dist/computerPlay.d.ts +17 -0
- package/dist/computerPlay.js +203 -0
- package/dist/computerTurn.d.ts +31 -0
- package/dist/computerTurn.js +94 -0
- package/dist/generate.d.ts +29 -0
- package/dist/generate.js +278 -0
- package/dist/grid.d.ts +97 -0
- package/dist/grid.js +208 -0
- package/dist/help.d.ts +20 -0
- package/dist/help.js +57 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +44 -0
- package/dist/judge.d.ts +13 -0
- package/dist/judge.js +19 -0
- package/dist/kana.d.ts +25 -0
- package/dist/kana.js +38 -0
- package/dist/kumimoji.types.d.ts +49 -0
- package/dist/kumimoji.types.js +1 -0
- package/dist/party.d.ts +91 -0
- package/dist/party.js +172 -0
- package/dist/party.types.d.ts +86 -0
- package/dist/party.types.js +1 -0
- package/dist/partyKept.d.ts +17 -0
- package/dist/partyKept.js +149 -0
- package/dist/partySeats.d.ts +53 -0
- package/dist/partySeats.js +119 -0
- package/dist/partyTurns.d.ts +75 -0
- package/dist/partyTurns.js +153 -0
- package/dist/placement.d.ts +26 -0
- package/dist/placement.js +87 -0
- package/dist/play.d.ts +76 -0
- package/dist/play.js +165 -0
- package/dist/random.d.ts +11 -0
- package/dist/random.js +25 -0
- package/dist/react.d.ts +32 -0
- package/dist/react.js +53 -0
- package/dist/showcase.d.ts +51 -0
- package/dist/showcase.js +59 -0
- package/dist/tableView.d.ts +92 -0
- package/dist/tableView.js +127 -0
- package/dist/tileFace.d.ts +25 -0
- package/dist/tileFace.js +39 -0
- package/dist/tileFamily.d.ts +11 -0
- package/dist/tileFamily.js +23 -0
- package/dist/tileWords.d.ts +70 -0
- package/dist/tileWords.js +164 -0
- package/dist/tileWordsModule.d.ts +4 -0
- package/dist/tileWordsModule.js +13 -0
- package/dist/tiles.constants.d.ts +108 -0
- package/dist/tiles.constants.js +132 -0
- package/dist/turn.d.ts +62 -0
- package/dist/turn.js +96 -0
- package/dist/ui/board.d.ts +29 -0
- package/dist/ui/board.js +37 -0
- package/dist/ui/mount.d.ts +36 -0
- package/dist/ui/mount.js +257 -0
- package/dist/ui/style.d.ts +6 -0
- package/dist/ui/style.js +40 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/wilds.d.ts +27 -0
- package/dist/wilds.js +111 -0
- package/dist/words.d.ts +6 -0
- package/dist/words.en.data.d.ts +23 -0
- package/dist/words.en.data.js +610 -0
- package/dist/words.ja.data.d.ts +29 -0
- package/dist/words.ja.data.js +29 -0
- package/dist/words.js +6 -0
- package/package.json +90 -0
package/dist/party.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { GridVerdict, Tiles } from "./grid.ts";
|
|
2
|
+
import type { KumimojiLength } from "./kumimoji.types.ts";
|
|
3
|
+
import type { PartyGame, PartyPlayer, PartySeat, PartySettings } from "./party.types.ts";
|
|
4
|
+
import { type TilePlay } from "./play.ts";
|
|
5
|
+
/**
|
|
6
|
+
* PASS AND PLAY: two to eight people round one device. John, 2026-09-28:
|
|
7
|
+
* "after you finish your move, you say you're done and… the screen will cover
|
|
8
|
+
* until the next person comes… then they can see their tiles and they play the
|
|
9
|
+
* game and then it rotates."
|
|
10
|
+
*
|
|
11
|
+
* One bag for everybody, read as a line exactly as the solo game reads it
|
|
12
|
+
* (`play.ts`), and a hand and a table for each player. A player's turn is
|
|
13
|
+
* played with the solo game's own moves on a `TilePlay` made of the shared bag
|
|
14
|
+
* and their own hand and table (`seatPlay`), and put back (`withSeatPlay`), so
|
|
15
|
+
* laying, moving, swapping, sorting and trading are the one set of rules.
|
|
16
|
+
* What is only the party's is here — the deal, whose turn it is, and Draw for
|
|
17
|
+
* everybody — and in `partyTurns.ts`: Done, going out, the last round,
|
|
18
|
+
* resigning and who won. Pure, like every rule here: each returns a new game.
|
|
19
|
+
*/
|
|
20
|
+
/** The tiles a bag must hold for this many players: a hand each and one round of draws. */
|
|
21
|
+
export declare function partyTilesNeeded(players: number, hand: number): number;
|
|
22
|
+
/** Whether a bag of this many tiles can be played by this many: one player can play any bag. */
|
|
23
|
+
export declare function partyFits(players: number, hand: number, tiles: number): boolean;
|
|
24
|
+
/** A players count from the address, or 1 (the solo game) for anything that is not two to eight. */
|
|
25
|
+
export declare function partyPlayersAsked(value: unknown): number;
|
|
26
|
+
/**
|
|
27
|
+
* The game length a set-up plays at: the one chosen where the bag holds this
|
|
28
|
+
* many players, else the shortest that does, so a choice made for fewer
|
|
29
|
+
* players comes back when there are fewer again. A Full bag of one set is 144
|
|
30
|
+
* tiles, and eight Classic hands with a round of draws are 96, so there is
|
|
31
|
+
* always one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function partyLength(players: number, hand: number, chosen: KumimojiLength, doubleSet: boolean): KumimojiLength;
|
|
34
|
+
/**
|
|
35
|
+
* The deal: each player the next hand from the bag in turn, player one first,
|
|
36
|
+
* and every table empty. A seat is a name, or a name and whether a computer
|
|
37
|
+
* plays it; a computer is named "Computer 1", "Computer 2"… in seat order.
|
|
38
|
+
*/
|
|
39
|
+
export declare function startParty(settings: PartySettings, bag: string, seats: readonly (string | PartySeat)[]): PartyGame;
|
|
40
|
+
/** The name a new computer takes: "Computer" and the lowest number no player at the table already goes by. */
|
|
41
|
+
export declare function computerName(players: readonly PartyPlayer[]): string;
|
|
42
|
+
/** Whether a computer plays this seat. */
|
|
43
|
+
export declare function isComputer(game: PartyGame, at: number): boolean;
|
|
44
|
+
/** A table's tiles as one string in order, wherever they stand: what a turn compares to see whether anything was laid or lifted. */
|
|
45
|
+
export declare function tableTally(tiles: Tiles): string;
|
|
46
|
+
/** A name as kept: trimmed, one line, and no longer than a line holds. */
|
|
47
|
+
export declare function tidyName(name: string): string;
|
|
48
|
+
/** What a player is called: their name, or "Player 2". */
|
|
49
|
+
export declare function nameOf(game: PartyGame, at: number): string;
|
|
50
|
+
/** The player whose turn it is, as the solo game's moves take a game: the shared bag, and their own hand and table. */
|
|
51
|
+
export declare function seatPlay(game: PartyGame): TilePlay;
|
|
52
|
+
/**
|
|
53
|
+
* ONE TRADE A TURN. John, 2026-09-29, after a player traded over and over and
|
|
54
|
+
* built up a hand: "if they swap tiles … give them a chance to lay some tiles
|
|
55
|
+
* but cannot ask for another swap until next turn." A trade gives one tile and
|
|
56
|
+
* takes three, so trading without end lets one player take the bag from the
|
|
57
|
+
* others. After a trade the player may still lay, lift and draw, and press
|
|
58
|
+
* Done; the next trade is on their next turn.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mayTradeThisTurn(game: PartyGame): boolean;
|
|
61
|
+
/** A move made on `seatPlay` put back: the bag as it left it, and the hand and table to the player whose turn it is. A second trade in one turn is refused, the game given back as it was. */
|
|
62
|
+
export declare function withSeatPlay(game: PartyGame, play: TilePlay): PartyGame;
|
|
63
|
+
/** Tiles still in the shared bag. */
|
|
64
|
+
export declare function partyTilesLeft(game: PartyGame): number;
|
|
65
|
+
/** The players still in — not resigned — by place, in turn order. Somebody who went out is still in until the game ends. */
|
|
66
|
+
export declare function stillIn(game: PartyGame): number[];
|
|
67
|
+
/** The next player still in after `from`, round the table; `from` itself when nobody else is. */
|
|
68
|
+
export declare function nextIn(game: PartyGame, from: number): number;
|
|
69
|
+
/**
|
|
70
|
+
* Whether Draw may be pressed: the hand of the player whose turn it is used,
|
|
71
|
+
* their grid sound, and a tile in the bag for every player still in. Never in
|
|
72
|
+
* the last round, when the bag already holds fewer than that.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mayDrawAll(game: PartyGame, verdict: GridVerdict): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* DRAW: every player still in takes one tile, in turn order from the one who
|
|
77
|
+
* pressed it — the race game's rule that when one player uses their hand,
|
|
78
|
+
* everybody draws.
|
|
79
|
+
*/
|
|
80
|
+
export declare function drawAll(game: PartyGame): PartyGame;
|
|
81
|
+
/**
|
|
82
|
+
* WHOSE TABLE IS SHOWN. Nothing in a pass-and-play game is secret — John,
|
|
83
|
+
* 2026-09-28: "there are no secrets because they are face up" — so every
|
|
84
|
+
* table and every hand can be looked at by anybody, read-only. The pass
|
|
85
|
+
* screen opens on the table of whoever has just played (the first player's,
|
|
86
|
+
* before anybody has), and a swipe steps round every player in seat order,
|
|
87
|
+
* resigned ones included.
|
|
88
|
+
*/
|
|
89
|
+
export declare function passViewStart(game: PartyGame): number;
|
|
90
|
+
/** The player a swipe or an arrow shows next: `by` seats on, round the table. */
|
|
91
|
+
export declare function stepView(game: PartyGame, from: number, by: number): number;
|
package/dist/party.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { mayTrade, tilesLeft } from "./play.js";
|
|
2
|
+
import { KUMIMOJI_PARTY, kumimojiTileCount } from "./tiles.constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* PASS AND PLAY: two to eight people round one device. John, 2026-09-28:
|
|
5
|
+
* "after you finish your move, you say you're done and… the screen will cover
|
|
6
|
+
* until the next person comes… then they can see their tiles and they play the
|
|
7
|
+
* game and then it rotates."
|
|
8
|
+
*
|
|
9
|
+
* One bag for everybody, read as a line exactly as the solo game reads it
|
|
10
|
+
* (`play.ts`), and a hand and a table for each player. A player's turn is
|
|
11
|
+
* played with the solo game's own moves on a `TilePlay` made of the shared bag
|
|
12
|
+
* and their own hand and table (`seatPlay`), and put back (`withSeatPlay`), so
|
|
13
|
+
* laying, moving, swapping, sorting and trading are the one set of rules.
|
|
14
|
+
* What is only the party's is here — the deal, whose turn it is, and Draw for
|
|
15
|
+
* everybody — and in `partyTurns.ts`: Done, going out, the last round,
|
|
16
|
+
* resigning and who won. Pure, like every rule here: each returns a new game.
|
|
17
|
+
*/
|
|
18
|
+
/** The tiles a bag must hold for this many players: a hand each and one round of draws. */
|
|
19
|
+
export function partyTilesNeeded(players, hand) {
|
|
20
|
+
return players * hand + players;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a bag of this many tiles can be played by this many: one player can play any bag. */
|
|
23
|
+
export function partyFits(players, hand, tiles) {
|
|
24
|
+
return players < KUMIMOJI_PARTY.least || tiles >= partyTilesNeeded(players, hand);
|
|
25
|
+
}
|
|
26
|
+
/** A players count from the address, or 1 (the solo game) for anything that is not two to eight. */
|
|
27
|
+
export function partyPlayersAsked(value) {
|
|
28
|
+
const count = Number(value);
|
|
29
|
+
return Number.isInteger(count) && count >= KUMIMOJI_PARTY.least && count <= KUMIMOJI_PARTY.most ? count : 1;
|
|
30
|
+
}
|
|
31
|
+
const LENGTHS = ["short", "medium", "full"];
|
|
32
|
+
/**
|
|
33
|
+
* The game length a set-up plays at: the one chosen where the bag holds this
|
|
34
|
+
* many players, else the shortest that does, so a choice made for fewer
|
|
35
|
+
* players comes back when there are fewer again. A Full bag of one set is 144
|
|
36
|
+
* tiles, and eight Classic hands with a round of draws are 96, so there is
|
|
37
|
+
* always one.
|
|
38
|
+
*/
|
|
39
|
+
export function partyLength(players, hand, chosen, doubleSet) {
|
|
40
|
+
// One player plays any length, and asks nothing of the size: the set-up screen asks this of every puzzle's.
|
|
41
|
+
if (players < KUMIMOJI_PARTY.least)
|
|
42
|
+
return chosen;
|
|
43
|
+
const fits = (length) => partyFits(players, hand, kumimojiTileCount(hand, length, undefined, doubleSet));
|
|
44
|
+
if (fits(chosen))
|
|
45
|
+
return chosen;
|
|
46
|
+
return LENGTHS.find(fits) ?? "full";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The deal: each player the next hand from the bag in turn, player one first,
|
|
50
|
+
* and every table empty. A seat is a name, or a name and whether a computer
|
|
51
|
+
* plays it; a computer is named "Computer 1", "Computer 2"… in seat order.
|
|
52
|
+
*/
|
|
53
|
+
export function startParty(settings, bag, seats) {
|
|
54
|
+
const count = Math.min(Math.max(seats.length, KUMIMOJI_PARTY.least), KUMIMOJI_PARTY.most);
|
|
55
|
+
const hand = settings.size;
|
|
56
|
+
if (bag.length < partyTilesNeeded(count, hand))
|
|
57
|
+
throw new Error(`A bag of ${bag.length} cannot be dealt to ${count} players.`);
|
|
58
|
+
const players = [];
|
|
59
|
+
for (let at = 0; at < count; at += 1) {
|
|
60
|
+
const seat = seats[at] ?? "";
|
|
61
|
+
const asked = typeof seat === "string" ? { name: seat } : seat;
|
|
62
|
+
const dealt = { hand: [...bag.slice(at * hand, (at + 1) * hand)], tiles: new Map() };
|
|
63
|
+
players.push(asked.computer === true ? { name: computerName(players), computer: true, ...dealt } : { name: tidyName(asked.name), ...dealt });
|
|
64
|
+
}
|
|
65
|
+
return { settings, bag, returned: "", taken: count * hand, players, dealt: count, turn: 0, turns: 0, out: [], lastTurns: null, resigned: [], resignRun: [], ending: null, startTable: "", traded: false, passedBy: null };
|
|
66
|
+
}
|
|
67
|
+
/** The name a new computer takes: "Computer" and the lowest number no player at the table already goes by. */
|
|
68
|
+
export function computerName(players) {
|
|
69
|
+
const taken = new Set(players.map((player) => player.name));
|
|
70
|
+
for (let number = 1;; number += 1)
|
|
71
|
+
if (!taken.has(`Computer ${number}`))
|
|
72
|
+
return `Computer ${number}`;
|
|
73
|
+
}
|
|
74
|
+
/** Whether a computer plays this seat. */
|
|
75
|
+
export function isComputer(game, at) {
|
|
76
|
+
return game.players[at]?.computer === true;
|
|
77
|
+
}
|
|
78
|
+
/** A table's tiles as one string in order, wherever they stand: what a turn compares to see whether anything was laid or lifted. */
|
|
79
|
+
export function tableTally(tiles) {
|
|
80
|
+
return [...tiles.values()].sort().join("");
|
|
81
|
+
}
|
|
82
|
+
/** A name as kept: trimmed, one line, and no longer than a line holds. */
|
|
83
|
+
export function tidyName(name) {
|
|
84
|
+
return name.replace(/\s+/g, " ").trim().slice(0, KUMIMOJI_PARTY.nameMost);
|
|
85
|
+
}
|
|
86
|
+
/** What a player is called: their name, or "Player 2". */
|
|
87
|
+
export function nameOf(game, at) {
|
|
88
|
+
const name = game.players[at]?.name ?? "";
|
|
89
|
+
return name === "" ? `Player ${at + 1}` : name;
|
|
90
|
+
}
|
|
91
|
+
/** The player whose turn it is, as the solo game's moves take a game: the shared bag, and their own hand and table. */
|
|
92
|
+
export function seatPlay(game) {
|
|
93
|
+
const player = game.players[game.turn];
|
|
94
|
+
return { bag: game.bag, returned: game.returned, taken: game.taken, tiles: player.tiles, hand: player.hand };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* ONE TRADE A TURN. John, 2026-09-29, after a player traded over and over and
|
|
98
|
+
* built up a hand: "if they swap tiles … give them a chance to lay some tiles
|
|
99
|
+
* but cannot ask for another swap until next turn." A trade gives one tile and
|
|
100
|
+
* takes three, so trading without end lets one player take the bag from the
|
|
101
|
+
* others. After a trade the player may still lay, lift and draw, and press
|
|
102
|
+
* Done; the next trade is on their next turn.
|
|
103
|
+
*/
|
|
104
|
+
export function mayTradeThisTurn(game) {
|
|
105
|
+
return game.ending === null && !game.traded && mayTrade(seatPlay(game));
|
|
106
|
+
}
|
|
107
|
+
/** A move made on `seatPlay` put back: the bag as it left it, and the hand and table to the player whose turn it is. A second trade in one turn is refused, the game given back as it was. */
|
|
108
|
+
export function withSeatPlay(game, play) {
|
|
109
|
+
if (game.ending !== null)
|
|
110
|
+
return game;
|
|
111
|
+
if (game.traded && play.returned.length > game.returned.length)
|
|
112
|
+
return game;
|
|
113
|
+
const players = game.players.map((player, at) => (at === game.turn ? { ...player, hand: play.hand, tiles: play.tiles } : player));
|
|
114
|
+
const traded = game.traded || play.returned.length > game.returned.length;
|
|
115
|
+
return { ...game, returned: play.returned, taken: play.taken, players, traded };
|
|
116
|
+
}
|
|
117
|
+
/** Tiles still in the shared bag. */
|
|
118
|
+
export function partyTilesLeft(game) {
|
|
119
|
+
return tilesLeft(seatPlay(game));
|
|
120
|
+
}
|
|
121
|
+
/** The players still in — not resigned — by place, in turn order. Somebody who went out is still in until the game ends. */
|
|
122
|
+
export function stillIn(game) {
|
|
123
|
+
return game.players.flatMap((_, at) => (game.resigned.includes(at) ? [] : [at]));
|
|
124
|
+
}
|
|
125
|
+
/** The next player still in after `from`, round the table; `from` itself when nobody else is. */
|
|
126
|
+
export function nextIn(game, from) {
|
|
127
|
+
const count = game.players.length;
|
|
128
|
+
for (let step = 1; step <= count; step += 1) {
|
|
129
|
+
const at = (from + step) % count;
|
|
130
|
+
if (!game.resigned.includes(at))
|
|
131
|
+
return at;
|
|
132
|
+
}
|
|
133
|
+
return from;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether Draw may be pressed: the hand of the player whose turn it is used,
|
|
137
|
+
* their grid sound, and a tile in the bag for every player still in. Never in
|
|
138
|
+
* the last round, when the bag already holds fewer than that.
|
|
139
|
+
*/
|
|
140
|
+
export function mayDrawAll(game, verdict) {
|
|
141
|
+
return game.ending === null && game.lastTurns === null && game.players[game.turn].hand.length === 0 && verdict.sound && partyTilesLeft(game) >= stillIn(game).length;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* DRAW: every player still in takes one tile, in turn order from the one who
|
|
145
|
+
* pressed it — the race game's rule that when one player uses their hand,
|
|
146
|
+
* everybody draws.
|
|
147
|
+
*/
|
|
148
|
+
export function drawAll(game) {
|
|
149
|
+
const count = game.players.length;
|
|
150
|
+
const order = stillIn(game).sort((a, b) => ((a - game.turn + count) % count) - ((b - game.turn + count) % count));
|
|
151
|
+
if (partyTilesLeft(game) < order.length)
|
|
152
|
+
return game;
|
|
153
|
+
const coming = (game.bag + game.returned).slice(game.taken, game.taken + order.length);
|
|
154
|
+
const players = game.players.map((player, at) => (order.includes(at) ? { ...player, hand: [...player.hand, coming[order.indexOf(at)]] } : player));
|
|
155
|
+
return { ...game, taken: game.taken + order.length, players };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* WHOSE TABLE IS SHOWN. Nothing in a pass-and-play game is secret — John,
|
|
159
|
+
* 2026-09-28: "there are no secrets because they are face up" — so every
|
|
160
|
+
* table and every hand can be looked at by anybody, read-only. The pass
|
|
161
|
+
* screen opens on the table of whoever has just played (the first player's,
|
|
162
|
+
* before anybody has), and a swipe steps round every player in seat order,
|
|
163
|
+
* resigned ones included.
|
|
164
|
+
*/
|
|
165
|
+
export function passViewStart(game) {
|
|
166
|
+
return game.passedBy ?? game.turn;
|
|
167
|
+
}
|
|
168
|
+
/** The player a swipe or an arrow shows next: `by` seats on, round the table. */
|
|
169
|
+
export function stepView(game, from, by) {
|
|
170
|
+
const count = game.players.length;
|
|
171
|
+
return (((from + by) % count) + count) % count;
|
|
172
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Tiles } from "./grid.ts";
|
|
2
|
+
import type { KumimojiLanguage, KumimojiLength, KumimojiLevel } from "./kumimoji.types.ts";
|
|
3
|
+
/** What a pass-and-play game was set up as: the address's own choices, which a kept game is matched to. */
|
|
4
|
+
export type PartySettings = {
|
|
5
|
+
/** The hand each player is dealt: 7 Quick or 11 Classic (3 in the browser tests alone). */
|
|
6
|
+
size: number;
|
|
7
|
+
level: KumimojiLevel;
|
|
8
|
+
/** The seed the bag was made from; the bag itself is kept too, so a kept game never depends on the generator. */
|
|
9
|
+
seed: number;
|
|
10
|
+
gameLength: KumimojiLength;
|
|
11
|
+
language: KumimojiLanguage;
|
|
12
|
+
doubleSet: boolean;
|
|
13
|
+
/** Whether Diagonals was chosen: every table in the game is read along its diagonals too (`judgeGrid`). */
|
|
14
|
+
diagonals: boolean;
|
|
15
|
+
/** Whether Help was chosen on the set-up screen. */
|
|
16
|
+
hints: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* One player: their name as typed (empty for "Player N"), their hand and
|
|
20
|
+
* their own table, and whether the seat is a computer's (`computerTurn.ts`),
|
|
21
|
+
* whose turns play themselves in the browser. A computer's name is always
|
|
22
|
+
* written out ("Computer 1"), never left for its number.
|
|
23
|
+
*/
|
|
24
|
+
export type PartyPlayer = {
|
|
25
|
+
name: string;
|
|
26
|
+
hand: readonly string[];
|
|
27
|
+
tiles: Tiles;
|
|
28
|
+
computer?: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** A seat asked for before the deal or at a join: a name (empty for its number) and whether a computer plays it. */
|
|
31
|
+
export type PartySeat = {
|
|
32
|
+
name: string;
|
|
33
|
+
computer?: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A PASS-AND-PLAY KUMIMOJI: one bag, read as a line the way the solo game
|
|
37
|
+
* reads it (`TilePlay`), and a hand and a table for each player.
|
|
38
|
+
*/
|
|
39
|
+
export type PartyGame = {
|
|
40
|
+
settings: PartySettings;
|
|
41
|
+
/** The tiles in the order they come out. */
|
|
42
|
+
bag: string;
|
|
43
|
+
/** Tiles traded back, in order; they come out after the bag. */
|
|
44
|
+
returned: string;
|
|
45
|
+
/** How many tiles have been taken from `bag + returned`, by every player together. */
|
|
46
|
+
taken: number;
|
|
47
|
+
/** Up to eight; two to eight when dealt, and one or more after players leave (`partySeats.ts`). */
|
|
48
|
+
players: readonly PartyPlayer[];
|
|
49
|
+
/**
|
|
50
|
+
* How many the game was dealt to: the address's own `players`, which a
|
|
51
|
+
* kept game is matched to, however many have joined or left since.
|
|
52
|
+
*/
|
|
53
|
+
dealt: number;
|
|
54
|
+
/** Whose turn it is, by place in `players`. */
|
|
55
|
+
turn: number;
|
|
56
|
+
/** How many turns have been ended, and seats left: a turn's own number, so each one opens on a fresh view. */
|
|
57
|
+
turns: number;
|
|
58
|
+
/** The players who went out, in the order they did; the first is the one who began the last round. */
|
|
59
|
+
out: readonly number[];
|
|
60
|
+
/**
|
|
61
|
+
* THE LAST ROUND: null until somebody goes out, then the players still to
|
|
62
|
+
* take their one last turn, in order. John, 2026-09-28: "once one person
|
|
63
|
+
* goes out the remaining players get one last move. That should make it
|
|
64
|
+
* fair."
|
|
65
|
+
*/
|
|
66
|
+
lastTurns: readonly number[] | null;
|
|
67
|
+
/** The players who resigned, in the order they did: their turns are passed over from then on. */
|
|
68
|
+
resigned: readonly number[];
|
|
69
|
+
/** The resignations since a player still in last pressed Done: who is tied if nobody is left standing. */
|
|
70
|
+
resignRun: readonly number[];
|
|
71
|
+
/** How the game ended (`PartyEnding`), or null while it is played. */
|
|
72
|
+
ending: PartyEnding | null;
|
|
73
|
+
/** The tiles on the table of the player whose turn it is when the turn began, in order (`tableTally`): whether they have laid anything since. */
|
|
74
|
+
startTable: string;
|
|
75
|
+
/** Whether the player whose turn it is has traded this turn. */
|
|
76
|
+
traded: boolean;
|
|
77
|
+
/** Who played the turn before this one — pressed Done or resigned — or null before anybody has: whose table the pass screen opens on. */
|
|
78
|
+
passedBy: number | null;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* HOW A PASS-AND-PLAY GAME ENDS. `out`: somebody went out and everybody else
|
|
82
|
+
* had a last turn; the winners are everybody who went out. `standing`: every
|
|
83
|
+
* other player resigned and the last one in laid a tile. `tied`: nobody was
|
|
84
|
+
* left standing, and the players who resigned in that final run are tied.
|
|
85
|
+
*/
|
|
86
|
+
export type PartyEnding = "out" | "standing" | "tied";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PartyGame, PartySettings } from "./party.types.ts";
|
|
2
|
+
export declare function encodeParty(game: PartyGame): string;
|
|
3
|
+
/** A game's settings as something outside this browser sent them — a table on several devices, a kept game — checked, or null. */
|
|
4
|
+
export declare function readSettings(value: unknown): PartySettings | null;
|
|
5
|
+
/**
|
|
6
|
+
* A kept game read back, or null for anything that is not one. With
|
|
7
|
+
* `familyKey` (the loaded word list's), it is also checked against its own
|
|
8
|
+
* bag: every tile held by anybody — hand and table — must be one taken out of
|
|
9
|
+
* it and not given back, as the solo game checks a kept run
|
|
10
|
+
* (`decodeTileProgress`). Without, its shape alone, which is enough to offer
|
|
11
|
+
* it on the set-up screen.
|
|
12
|
+
*/
|
|
13
|
+
export declare function decodeParty(code: string | null, familyKey?: (tile: string) => string | null): PartyGame | null;
|
|
14
|
+
/** Whether what everybody holds is exactly what was taken from the bag, less what was given back. */
|
|
15
|
+
export declare function holdsItsBag(game: PartyGame, familyKey: (tile: string) => string | null): boolean;
|
|
16
|
+
/** Whether a kept game is the one an address asks for: the same settings, and dealt to as many players, whoever has joined or left since. */
|
|
17
|
+
export declare function isPartyFor(game: PartyGame, settings: PartySettings, players: number): boolean;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { decodeGrid, encodeGrid, lettersOf, sameLetters } from "./grid.js";
|
|
2
|
+
import { KUMIMOJI_PARTY, KUMIMOJI_HANDS } from "./tiles.constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* A PASS-AND-PLAY GAME KEPT IN THE BROWSER, as one string of JSON: the
|
|
5
|
+
* settings, the whole bag in its order, what was traded back and taken, whose
|
|
6
|
+
* turn it is, who is out, who resigned and who still has a last turn, what
|
|
7
|
+
* this turn has done so far, and every player's
|
|
8
|
+
* name, hand and grid (`encodeGrid`), and whether a computer plays it. The
|
|
9
|
+
* bag is written out rather than made again from the seed, so a game left half
|
|
10
|
+
* way opens the same after the generator has changed.
|
|
11
|
+
*
|
|
12
|
+
* Joining, leaving and computer players came after the first version of this
|
|
13
|
+
* and did not change its number: `dealt` and a player's `computer` are
|
|
14
|
+
* written only when they say something, and a game kept without them reads
|
|
15
|
+
* as one dealt to as many as sit at it, every seat a person's.
|
|
16
|
+
*
|
|
17
|
+
* It is never sent to the server: a local game has no points, no record and
|
|
18
|
+
* nothing to check, and the names in it are the players' own business.
|
|
19
|
+
*/
|
|
20
|
+
const VERSION = 1;
|
|
21
|
+
export function encodeParty(game) {
|
|
22
|
+
const kept = {
|
|
23
|
+
v: VERSION,
|
|
24
|
+
settings: game.settings,
|
|
25
|
+
bag: game.bag,
|
|
26
|
+
returned: game.returned,
|
|
27
|
+
taken: game.taken,
|
|
28
|
+
turn: game.turn,
|
|
29
|
+
turns: game.turns,
|
|
30
|
+
out: [...game.out],
|
|
31
|
+
lastTurns: game.lastTurns === null ? null : [...game.lastTurns],
|
|
32
|
+
resigned: [...game.resigned],
|
|
33
|
+
resignRun: [...game.resignRun],
|
|
34
|
+
ending: game.ending,
|
|
35
|
+
startTable: game.startTable,
|
|
36
|
+
traded: game.traded,
|
|
37
|
+
passedBy: game.passedBy,
|
|
38
|
+
...(game.dealt === game.players.length ? {} : { dealt: game.dealt }),
|
|
39
|
+
players: game.players.map((player) => ({ name: player.name, hand: player.hand.join(""), grid: encodeGrid(player.tiles), ...(player.computer === true ? { computer: true } : {}) })),
|
|
40
|
+
};
|
|
41
|
+
return JSON.stringify(kept);
|
|
42
|
+
}
|
|
43
|
+
const isCount = (value, most) => Number.isInteger(value) && value >= 0 && value <= most;
|
|
44
|
+
const isTiles = (value) => typeof value === "string" && /^[a-zA-Z*-]*$/.test(value);
|
|
45
|
+
/** A game's settings as something outside this browser sent them — a table on several devices, a kept game — checked, or null. */
|
|
46
|
+
export function readSettings(value) {
|
|
47
|
+
if (typeof value !== "object" || value === null)
|
|
48
|
+
return null;
|
|
49
|
+
const s = value;
|
|
50
|
+
const sizes = Object.values(KUMIMOJI_HANDS);
|
|
51
|
+
if (!sizes.includes(s.size))
|
|
52
|
+
return null;
|
|
53
|
+
if (s.level !== "easy" && s.level !== "medium" && s.level !== "hard")
|
|
54
|
+
return null;
|
|
55
|
+
if (!Number.isInteger(s.seed))
|
|
56
|
+
return null;
|
|
57
|
+
if (s.gameLength !== "short" && s.gameLength !== "medium" && s.gameLength !== "full")
|
|
58
|
+
return null;
|
|
59
|
+
if (s.language !== "english" && s.language !== "japanese")
|
|
60
|
+
return null;
|
|
61
|
+
if (typeof s.doubleSet !== "boolean" || typeof s.hints !== "boolean")
|
|
62
|
+
return null;
|
|
63
|
+
// A game kept before Diagonals existed has no word for it, and was played without.
|
|
64
|
+
if (s.diagonals !== undefined && typeof s.diagonals !== "boolean")
|
|
65
|
+
return null;
|
|
66
|
+
return { size: s.size, level: s.level, seed: s.seed, gameLength: s.gameLength, language: s.language, doubleSet: s.doubleSet, diagonals: s.diagonals === true, hints: s.hints };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A kept game read back, or null for anything that is not one. With
|
|
70
|
+
* `familyKey` (the loaded word list's), it is also checked against its own
|
|
71
|
+
* bag: every tile held by anybody — hand and table — must be one taken out of
|
|
72
|
+
* it and not given back, as the solo game checks a kept run
|
|
73
|
+
* (`decodeTileProgress`). Without, its shape alone, which is enough to offer
|
|
74
|
+
* it on the set-up screen.
|
|
75
|
+
*/
|
|
76
|
+
export function decodeParty(code, familyKey) {
|
|
77
|
+
if (code === null)
|
|
78
|
+
return null;
|
|
79
|
+
let kept;
|
|
80
|
+
try {
|
|
81
|
+
kept = JSON.parse(code);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (typeof kept !== "object" || kept === null || kept.v !== VERSION)
|
|
87
|
+
return null;
|
|
88
|
+
const settings = readSettings(kept.settings);
|
|
89
|
+
if (settings === null || !isTiles(kept.bag) || !isTiles(kept.returned))
|
|
90
|
+
return null;
|
|
91
|
+
const line = kept.bag + kept.returned;
|
|
92
|
+
if (!isCount(kept.taken, line.length) || !Array.isArray(kept.players))
|
|
93
|
+
return null;
|
|
94
|
+
const count = kept.players.length;
|
|
95
|
+
// Dealt to two or more; one may be left at the table after the rest leave (`partySeats.ts`).
|
|
96
|
+
if (count < 1 || count > KUMIMOJI_PARTY.most)
|
|
97
|
+
return null;
|
|
98
|
+
const dealt = kept.dealt ?? count;
|
|
99
|
+
if (!Number.isInteger(dealt) || dealt < KUMIMOJI_PARTY.least || dealt > KUMIMOJI_PARTY.most)
|
|
100
|
+
return null;
|
|
101
|
+
if (!isCount(kept.turn, count - 1) || !isCount(kept.turns, 1000000) || (kept.ending !== null && kept.ending !== "out" && kept.ending !== "standing" && kept.ending !== "tied") || typeof kept.traded !== "boolean" || !isTiles(kept.startTable) || (kept.passedBy !== null && !isCount(kept.passedBy, count - 1)))
|
|
102
|
+
return null;
|
|
103
|
+
const seats = (list) => Array.isArray(list) && list.every((at) => isCount(at, count - 1)) && new Set(list).size === list.length;
|
|
104
|
+
if (!seats(kept.out) || (kept.lastTurns !== null && !seats(kept.lastTurns)) || !seats(kept.resigned) || !seats(kept.resignRun))
|
|
105
|
+
return null;
|
|
106
|
+
const players = [];
|
|
107
|
+
for (const player of kept.players) {
|
|
108
|
+
if (typeof player !== "object" || player === null || typeof player.name !== "string" || player.name.length > KUMIMOJI_PARTY.nameMost || !isTiles(player.hand))
|
|
109
|
+
return null;
|
|
110
|
+
if (player.computer !== undefined && player.computer !== true)
|
|
111
|
+
return null;
|
|
112
|
+
const tiles = decodeGrid(player.grid);
|
|
113
|
+
if (tiles === null)
|
|
114
|
+
return null;
|
|
115
|
+
players.push(player.computer === true ? { name: player.name, hand: [...player.hand], tiles, computer: true } : { name: player.name, hand: [...player.hand], tiles });
|
|
116
|
+
}
|
|
117
|
+
const game = { settings, bag: kept.bag, returned: kept.returned, taken: kept.taken, players, dealt, turn: kept.turn, turns: kept.turns, out: kept.out, lastTurns: kept.lastTurns, resigned: kept.resigned, resignRun: kept.resignRun, ending: kept.ending, startTable: kept.startTable, traded: kept.traded, passedBy: kept.passedBy };
|
|
118
|
+
if (familyKey !== undefined && !holdsItsBag(game, familyKey))
|
|
119
|
+
return null;
|
|
120
|
+
return game;
|
|
121
|
+
}
|
|
122
|
+
/** Whether what everybody holds is exactly what was taken from the bag, less what was given back. */
|
|
123
|
+
export function holdsItsBag(game, familyKey) {
|
|
124
|
+
const taken = [...(game.bag + game.returned).slice(0, game.taken)].map(familyKey);
|
|
125
|
+
const back = [...game.returned].map(familyKey);
|
|
126
|
+
const held = [...game.players.flatMap((player) => [...player.hand, ...player.tiles.values()])].map(familyKey);
|
|
127
|
+
if ([...taken, ...back, ...held].some((tile) => tile === null))
|
|
128
|
+
return false;
|
|
129
|
+
const tally = lettersOf(taken);
|
|
130
|
+
for (const tile of back)
|
|
131
|
+
tally.set(tile, (tally.get(tile) ?? 0) - 1);
|
|
132
|
+
for (const [tile, count] of tally)
|
|
133
|
+
if (count === 0)
|
|
134
|
+
tally.delete(tile);
|
|
135
|
+
return sameLetters(tally, lettersOf(held));
|
|
136
|
+
}
|
|
137
|
+
/** Whether a kept game is the one an address asks for: the same settings, and dealt to as many players, whoever has joined or left since. */
|
|
138
|
+
export function isPartyFor(game, settings, players) {
|
|
139
|
+
const a = game.settings;
|
|
140
|
+
return (game.dealt === players &&
|
|
141
|
+
a.size === settings.size &&
|
|
142
|
+
a.level === settings.level &&
|
|
143
|
+
a.seed === settings.seed &&
|
|
144
|
+
a.gameLength === settings.gameLength &&
|
|
145
|
+
a.language === settings.language &&
|
|
146
|
+
a.doubleSet === settings.doubleSet &&
|
|
147
|
+
a.diagonals === settings.diagonals &&
|
|
148
|
+
a.hints === settings.hints);
|
|
149
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PartyGame, PartySeat } from "./party.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* JUMPING IN AND OUT OF A PASS-AND-PLAY GAME. John, 2026-09-28: "People can
|
|
4
|
+
* jump in and out of a game at which point all their tiles go back to the
|
|
5
|
+
* pot. Also you can add computer bots."
|
|
6
|
+
*
|
|
7
|
+
* Both happen from the pass screen, between turns, never on somebody's desk.
|
|
8
|
+
* Pure, like every rule here: each returns a new game, or the one it was
|
|
9
|
+
* given where the change is refused.
|
|
10
|
+
*
|
|
11
|
+
* JOINING: a new player, or a computer, takes the next seat after the last
|
|
12
|
+
* and a hand of the game's size from the bag, and plays when the turn comes
|
|
13
|
+
* round to them. Offered while there are fewer than eight, the last round has
|
|
14
|
+
* not begun (a player who sat down then could never catch up, and would only
|
|
15
|
+
* be dealt tiles to lose with), and the bag holds a whole hand.
|
|
16
|
+
*
|
|
17
|
+
* LEAVING: every tile in the leaver's hand and on their table goes back into
|
|
18
|
+
* the bag, which is shuffled with them in, and play goes on with the next
|
|
19
|
+
* player. A player who has gone out has finished, and stays at the table until
|
|
20
|
+
* the game ends; and the last person at the table cannot leave it to the
|
|
21
|
+
* computers — they end the game instead.
|
|
22
|
+
*
|
|
23
|
+
* Fewer than two left is no new rule: the one still in is the last one
|
|
24
|
+
* standing (`lastStanding`), and wins by laying a tile on a sound grid, as
|
|
25
|
+
* when everybody else resigns — unless somebody joins first.
|
|
26
|
+
*/
|
|
27
|
+
/** Why nobody can join now, or null when a hand can be dealt. */
|
|
28
|
+
export type JoinRefusal = "over" | "full" | "lastRound" | "bag";
|
|
29
|
+
export declare function joinRefused(game: PartyGame): JoinRefusal | null;
|
|
30
|
+
/** A player, or a computer, sat down after the last seat with the next hand out of the bag. */
|
|
31
|
+
export declare function joinParty(game: PartyGame, seat: PartySeat): PartyGame;
|
|
32
|
+
/** Why this player cannot leave now, or null when they can. */
|
|
33
|
+
export type LeaveRefusal = "over" | "out" | "lastPerson";
|
|
34
|
+
export declare function leaveRefused(game: PartyGame, at: number): LeaveRefusal | null;
|
|
35
|
+
/** How many tiles a player would put back in the bag by leaving: their hand and their table. */
|
|
36
|
+
export declare function tilesHeldBy(game: PartyGame, at: number): number;
|
|
37
|
+
/**
|
|
38
|
+
* A PLAYER LEAVES. Their tiles go back into the bag and the bag is shuffled
|
|
39
|
+
* — with the game's own seed and how far it has gone, so the same game left
|
|
40
|
+
* the same way deals the same tiles after a reload. A wild goes back blank.
|
|
41
|
+
*
|
|
42
|
+
* The bag is kept as a line whose first `taken` tiles are the ones held, less
|
|
43
|
+
* what was traded back (`holdsItsBag`); after a leave it is written afresh as
|
|
44
|
+
* exactly that: the tiles everybody still holds, then the shuffled rest, with
|
|
45
|
+
* nothing traded back. Every seat after the leaver's moves up one, and the
|
|
46
|
+
* unnamed players keep the names they were shown ("Player 3" stays Player 3).
|
|
47
|
+
*
|
|
48
|
+
* If it was their turn, the turn passes to the next still in, or in the last
|
|
49
|
+
* round to the next still to have their last turn; a last round with nobody
|
|
50
|
+
* left to play ends. With nobody still in — everybody left has resigned —
|
|
51
|
+
* the game ends tied between them.
|
|
52
|
+
*/
|
|
53
|
+
export declare function leaveParty(game: PartyGame, at: number): PartyGame;
|