@johnmorrisdotca/kumimoji 1.3.0 → 1.4.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 +22 -1
- package/CONTRIBUTING.md +11 -2
- package/README.md +9 -16
- package/dist/check-reasons.d.ts +71 -0
- package/dist/check-reasons.js +47 -0
- package/dist/check.js +14 -11
- package/dist/computerPlay.js +11 -4
- package/dist/computerTurn.js +5 -2
- package/dist/daily.js +1 -1
- package/dist/element.d.ts +4 -1
- package/dist/element.js +332 -162
- package/dist/export.js +18 -5
- package/dist/generate.js +18 -5
- package/dist/grid.js +26 -4
- package/dist/index.d.ts +23 -22
- package/dist/index.js +23 -22
- package/dist/kana.js +39 -6
- package/dist/kumimoji.types.d.ts +5 -2
- package/dist/party.js +28 -5
- package/dist/partyKept.js +53 -8
- package/dist/partySeats.js +11 -4
- package/dist/partyTurns.js +16 -3
- package/dist/placement.js +5 -2
- package/dist/play.js +14 -3
- package/dist/react.js +33 -3
- package/dist/showcase.js +12 -3
- package/dist/tileWords.js +11 -6
- package/dist/tileWordsModule.d.ts +1 -1
- package/dist/tileWordsModule.js +3 -1
- package/dist/tiles.constants.js +72 -8
- package/dist/turn.js +6 -1
- package/dist/ui/board.js +16 -2
- package/dist/ui/cloth.d.ts +72 -0
- package/dist/ui/cloth.js +49 -0
- package/dist/ui/draw.d.ts +62 -0
- package/dist/ui/draw.js +229 -0
- package/dist/ui/mount.d.ts +4 -1
- package/dist/ui/mount.js +176 -279
- package/dist/ui/style.d.ts +2 -2
- package/dist/ui/style.js +2 -1
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wilds.js +3 -2
- package/package.json +12 -13
package/dist/generate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { seededRandom, shuffled } from "./random.js";
|
|
2
1
|
import { encodeGrid, squareAt } from "./grid.js";
|
|
3
2
|
import { crossingFit } from "./placement.js";
|
|
3
|
+
import { seededRandom, shuffled } from "./random.js";
|
|
4
4
|
import { kumimojiTileCount, kumimojiWildCount, TILE_MIX_TOTAL } from "./tiles.constants.js";
|
|
5
5
|
import { tileWords } from "./tileWords.js";
|
|
6
6
|
/**
|
|
@@ -58,10 +58,12 @@ export function generateKumimoji(size, level, seed, options = {}) {
|
|
|
58
58
|
const side = layingSideFor(tiles);
|
|
59
59
|
let bestProgress = 0;
|
|
60
60
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
61
|
-
const squares = layCrossword(tiles, random, words, side, multiplier, words.mix, (laid) => {
|
|
61
|
+
const squares = layCrossword(tiles, random, words, side, multiplier, words.mix, (laid) => {
|
|
62
|
+
bestProgress = Math.max(bestProgress, laid);
|
|
63
|
+
}, diagonals);
|
|
62
64
|
if (squares === null)
|
|
63
65
|
continue;
|
|
64
|
-
const positions = shuffled(squares.flatMap((tile, at) => tile === "" ? [] : [at]), random);
|
|
66
|
+
const positions = shuffled(squares.flatMap((tile, at) => (tile === "" ? [] : [at])), random);
|
|
65
67
|
const wildAt = new Set(positions.slice(0, wilds));
|
|
66
68
|
const bag = squares.flatMap((tile, at) => {
|
|
67
69
|
if (tile === "")
|
|
@@ -74,7 +76,18 @@ export function generateKumimoji(size, level, seed, options = {}) {
|
|
|
74
76
|
squares[at] = assigned;
|
|
75
77
|
return ["*"];
|
|
76
78
|
});
|
|
77
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
kind: "kumimoji",
|
|
81
|
+
size,
|
|
82
|
+
level,
|
|
83
|
+
seed,
|
|
84
|
+
givens: shuffled(bag, random).join(""),
|
|
85
|
+
solution: encodeGrid(tilesOf(squares, side)),
|
|
86
|
+
gameLength,
|
|
87
|
+
doubleSet,
|
|
88
|
+
language,
|
|
89
|
+
...(diagonals ? { diagonals } : {}),
|
|
90
|
+
};
|
|
78
91
|
}
|
|
79
92
|
throw new Error(`Could not lay a Kumimoji of ${tiles} tiles from seed ${seed}; furthest attempt laid ${bestProgress}.`);
|
|
80
93
|
}
|
|
@@ -102,7 +115,7 @@ const LONGEST_LAID = 7;
|
|
|
102
115
|
const ANCHORS_TRIED = 8;
|
|
103
116
|
const WORDS_READ = 60;
|
|
104
117
|
const FINISH_BRANCHES = 40;
|
|
105
|
-
const FINISH_NODES =
|
|
118
|
+
const FINISH_NODES = 2_000;
|
|
106
119
|
/**
|
|
107
120
|
* With Diagonals the last tiles are placed from every tile but a sample of
|
|
108
121
|
* words, and a try that cannot finish is given up sooner: a crossword read
|
package/dist/grid.js
CHANGED
|
@@ -48,7 +48,12 @@ export function boundsOf(tiles) {
|
|
|
48
48
|
bounds =
|
|
49
49
|
bounds === null
|
|
50
50
|
? { top: row, left: col, bottom: row, right: col }
|
|
51
|
-
: {
|
|
51
|
+
: {
|
|
52
|
+
top: Math.min(bounds.top, row),
|
|
53
|
+
left: Math.min(bounds.left, col),
|
|
54
|
+
bottom: Math.max(bounds.bottom, row),
|
|
55
|
+
right: Math.max(bounds.right, col),
|
|
56
|
+
};
|
|
52
57
|
}
|
|
53
58
|
return bounds;
|
|
54
59
|
}
|
|
@@ -188,7 +193,12 @@ export function sameLetters(a, b) {
|
|
|
188
193
|
return true;
|
|
189
194
|
}
|
|
190
195
|
/** One step along each line, as rows and columns. */
|
|
191
|
-
const STEP = {
|
|
196
|
+
const STEP = {
|
|
197
|
+
across: [0, 1],
|
|
198
|
+
down: [1, 0],
|
|
199
|
+
downRight: [1, 1],
|
|
200
|
+
downLeft: [1, -1],
|
|
201
|
+
};
|
|
192
202
|
/**
|
|
193
203
|
* The fewest tiles a diagonal run needs before it is read. Two tiles touching
|
|
194
204
|
* at a corner are what every crossword is full of — the letter above a word
|
|
@@ -283,7 +293,13 @@ export function groupsOf(tiles, links = []) {
|
|
|
283
293
|
const at = queue.pop();
|
|
284
294
|
group.push(at);
|
|
285
295
|
const { row, col } = placeOf(at);
|
|
286
|
-
for (const near of [
|
|
296
|
+
for (const near of [
|
|
297
|
+
squareAt(row - 1, col),
|
|
298
|
+
squareAt(row + 1, col),
|
|
299
|
+
squareAt(row, col - 1),
|
|
300
|
+
squareAt(row, col + 1),
|
|
301
|
+
...(joined.get(at) ?? []),
|
|
302
|
+
]) {
|
|
287
303
|
if (tiles.has(near) && !seen.has(near)) {
|
|
288
304
|
seen.add(near);
|
|
289
305
|
queue.push(near);
|
|
@@ -322,5 +338,11 @@ export function judgeGrid(tiles, isWord, readable = (word) => word, rules = {})
|
|
|
322
338
|
}
|
|
323
339
|
const groups = groupsOf(tiles, runs.filter((run) => run.line === "downRight" || run.line === "downLeft"));
|
|
324
340
|
const apart = new Set(groups.slice(1).flat());
|
|
325
|
-
return {
|
|
341
|
+
return {
|
|
342
|
+
sound: tiles.size >= 2 && groups.length === 1 && notWords.length === 0,
|
|
343
|
+
tiles: tiles.size,
|
|
344
|
+
misspelt,
|
|
345
|
+
apart,
|
|
346
|
+
notWords,
|
|
347
|
+
};
|
|
326
348
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,33 +16,34 @@
|
|
|
16
16
|
* a test through `@johnmorrisdotca/kumimoji/words`, which reads them from
|
|
17
17
|
* disk.
|
|
18
18
|
*/
|
|
19
|
-
export * from "./kumimoji.types.ts";
|
|
20
|
-
export * from "./tiles.constants.ts";
|
|
21
|
-
export * from "./kana.ts";
|
|
22
|
-
export * from "./tileFace.ts";
|
|
23
|
-
export * from "./tileFamily.ts";
|
|
24
|
-
export * from "./tileWords.ts";
|
|
25
|
-
export * from "./grid.ts";
|
|
26
|
-
export * from "./placement.ts";
|
|
27
|
-
export * from "./wilds.ts";
|
|
28
|
-
export * from "./judge.ts";
|
|
29
19
|
export * from "./check.ts";
|
|
30
|
-
export * from "./
|
|
31
|
-
export * from "./play.ts";
|
|
32
|
-
export * from "./help.ts";
|
|
33
|
-
export * from "./turn.ts";
|
|
34
|
-
export * from "./tableView.ts";
|
|
35
|
-
export * from "./showcase.ts";
|
|
36
|
-
export * from "./party.types.ts";
|
|
37
|
-
export * from "./party.ts";
|
|
38
|
-
export * from "./partyTurns.ts";
|
|
39
|
-
export * from "./partySeats.ts";
|
|
40
|
-
export * from "./partyKept.ts";
|
|
20
|
+
export * from "./check-reasons.ts";
|
|
41
21
|
export * from "./computer.types.ts";
|
|
42
22
|
export * from "./computerPlay.ts";
|
|
43
23
|
export * from "./computerTurn.ts";
|
|
44
|
-
export * from "./random.ts";
|
|
45
24
|
export * from "./daily.ts";
|
|
46
25
|
export * from "./export.ts";
|
|
26
|
+
export * from "./generate.ts";
|
|
27
|
+
export * from "./grid.ts";
|
|
28
|
+
export * from "./help.ts";
|
|
29
|
+
export * from "./judge.ts";
|
|
30
|
+
export * from "./kana.ts";
|
|
31
|
+
export * from "./kumimoji.types.ts";
|
|
32
|
+
export * from "./party.ts";
|
|
33
|
+
export * from "./party.types.ts";
|
|
34
|
+
export * from "./partyKept.ts";
|
|
35
|
+
export * from "./partySeats.ts";
|
|
36
|
+
export * from "./partyTurns.ts";
|
|
37
|
+
export * from "./placement.ts";
|
|
38
|
+
export * from "./play.ts";
|
|
39
|
+
export * from "./random.ts";
|
|
40
|
+
export * from "./showcase.ts";
|
|
47
41
|
export * from "./strings.ts";
|
|
42
|
+
export * from "./tableView.ts";
|
|
43
|
+
export * from "./tileFace.ts";
|
|
44
|
+
export * from "./tileFamily.ts";
|
|
45
|
+
export * from "./tiles.constants.ts";
|
|
46
|
+
export * from "./tileWords.ts";
|
|
47
|
+
export * from "./turn.ts";
|
|
48
48
|
export { KUMIMOJI_VERSION } from "./version.ts";
|
|
49
|
+
export * from "./wilds.ts";
|
package/dist/index.js
CHANGED
|
@@ -16,33 +16,34 @@
|
|
|
16
16
|
* a test through `@johnmorrisdotca/kumimoji/words`, which reads them from
|
|
17
17
|
* disk.
|
|
18
18
|
*/
|
|
19
|
-
export * from "./kumimoji.types.js";
|
|
20
|
-
export * from "./tiles.constants.js";
|
|
21
|
-
export * from "./kana.js";
|
|
22
|
-
export * from "./tileFace.js";
|
|
23
|
-
export * from "./tileFamily.js";
|
|
24
|
-
export * from "./tileWords.js";
|
|
25
|
-
export * from "./grid.js";
|
|
26
|
-
export * from "./placement.js";
|
|
27
|
-
export * from "./wilds.js";
|
|
28
|
-
export * from "./judge.js";
|
|
29
19
|
export * from "./check.js";
|
|
30
|
-
export * from "./
|
|
31
|
-
export * from "./play.js";
|
|
32
|
-
export * from "./help.js";
|
|
33
|
-
export * from "./turn.js";
|
|
34
|
-
export * from "./tableView.js";
|
|
35
|
-
export * from "./showcase.js";
|
|
36
|
-
export * from "./party.types.js";
|
|
37
|
-
export * from "./party.js";
|
|
38
|
-
export * from "./partyTurns.js";
|
|
39
|
-
export * from "./partySeats.js";
|
|
40
|
-
export * from "./partyKept.js";
|
|
20
|
+
export * from "./check-reasons.js";
|
|
41
21
|
export * from "./computer.types.js";
|
|
42
22
|
export * from "./computerPlay.js";
|
|
43
23
|
export * from "./computerTurn.js";
|
|
44
|
-
export * from "./random.js";
|
|
45
24
|
export * from "./daily.js";
|
|
46
25
|
export * from "./export.js";
|
|
26
|
+
export * from "./generate.js";
|
|
27
|
+
export * from "./grid.js";
|
|
28
|
+
export * from "./help.js";
|
|
29
|
+
export * from "./judge.js";
|
|
30
|
+
export * from "./kana.js";
|
|
31
|
+
export * from "./kumimoji.types.js";
|
|
32
|
+
export * from "./party.js";
|
|
33
|
+
export * from "./party.types.js";
|
|
34
|
+
export * from "./partyKept.js";
|
|
35
|
+
export * from "./partySeats.js";
|
|
36
|
+
export * from "./partyTurns.js";
|
|
37
|
+
export * from "./placement.js";
|
|
38
|
+
export * from "./play.js";
|
|
39
|
+
export * from "./random.js";
|
|
40
|
+
export * from "./showcase.js";
|
|
47
41
|
export * from "./strings.js";
|
|
42
|
+
export * from "./tableView.js";
|
|
43
|
+
export * from "./tileFace.js";
|
|
44
|
+
export * from "./tileFamily.js";
|
|
45
|
+
export * from "./tiles.constants.js";
|
|
46
|
+
export * from "./tileWords.js";
|
|
47
|
+
export * from "./turn.js";
|
|
48
48
|
export { KUMIMOJI_VERSION } from "./version.js";
|
|
49
|
+
export * from "./wilds.js";
|
package/dist/kana.js
CHANGED
|
@@ -21,7 +21,21 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export const BASE_KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわん";
|
|
23
23
|
const SMALL_AND_OLD = {
|
|
24
|
-
ぁ: "あ",
|
|
24
|
+
ぁ: "あ",
|
|
25
|
+
ぃ: "い",
|
|
26
|
+
ぅ: "う",
|
|
27
|
+
ぇ: "え",
|
|
28
|
+
ぉ: "お",
|
|
29
|
+
っ: "つ",
|
|
30
|
+
ゃ: "や",
|
|
31
|
+
ゅ: "ゆ",
|
|
32
|
+
ょ: "よ",
|
|
33
|
+
ゎ: "わ",
|
|
34
|
+
ゕ: "か",
|
|
35
|
+
ゖ: "け",
|
|
36
|
+
を: "お",
|
|
37
|
+
ゐ: "い",
|
|
38
|
+
ゑ: "え",
|
|
25
39
|
};
|
|
26
40
|
/** The tile a kana is played with, or null for anything that is not hiragana (ー, katakana, a letter).
|
|
27
41
|
*
|
|
@@ -52,9 +66,28 @@ export function tileKana(kana) {
|
|
|
52
66
|
* ```
|
|
53
67
|
*/
|
|
54
68
|
export const CORNER_FORMS = {
|
|
55
|
-
か: "が",
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
か: "が",
|
|
70
|
+
き: "ぎ",
|
|
71
|
+
く: "ぐ",
|
|
72
|
+
け: "げ",
|
|
73
|
+
こ: "ご",
|
|
74
|
+
さ: "ざ",
|
|
75
|
+
し: "じ",
|
|
76
|
+
す: "ず",
|
|
77
|
+
せ: "ぜ",
|
|
78
|
+
そ: "ぞ",
|
|
79
|
+
た: "だ",
|
|
80
|
+
ち: "ぢ",
|
|
81
|
+
つ: "っづ",
|
|
82
|
+
て: "で",
|
|
83
|
+
と: "ど",
|
|
84
|
+
は: "ばぱ",
|
|
85
|
+
ひ: "びぴ",
|
|
86
|
+
ふ: "ぶぷ",
|
|
87
|
+
へ: "べぺ",
|
|
88
|
+
ほ: "ぼぽ",
|
|
89
|
+
や: "ゃ",
|
|
90
|
+
ゆ: "ゅ",
|
|
91
|
+
よ: "ょ",
|
|
92
|
+
お: "を",
|
|
60
93
|
};
|
package/dist/kumimoji.types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KumimojiCheckCode, KumimojiCheckValues } from "./check-reasons.ts";
|
|
1
2
|
/** How much of the set a game's bag holds: a short game, half the set, or all of it.
|
|
2
3
|
*
|
|
3
4
|
* @example
|
|
@@ -68,13 +69,13 @@ export type KumimojiOptions = {
|
|
|
68
69
|
*/
|
|
69
70
|
diagonals?: boolean;
|
|
70
71
|
};
|
|
71
|
-
/** The answer to "does this grid finish the game?": yes, or no and why.
|
|
72
|
+
/** The answer to "does this grid finish the game?": yes, or no and why, in English (`reason`) and as a stable `code` with `values` a site can say in its own language (`KUMIMOJI_CHECK_REASONS`).
|
|
72
73
|
*
|
|
73
74
|
* @example
|
|
74
75
|
* ```ts
|
|
75
76
|
* import type { KumimojiCheck } from "@johnmorrisdotca/kumimoji";
|
|
76
77
|
*
|
|
77
|
-
* const value: KumimojiCheck = {ok:false,reason:"
|
|
78
|
+
* const value: KumimojiCheck = {ok:false,reason:"the grid is not finished",code:"grid-not-finished"};
|
|
78
79
|
* console.log(value);
|
|
79
80
|
* ```
|
|
80
81
|
*/
|
|
@@ -83,6 +84,8 @@ export type KumimojiCheck = {
|
|
|
83
84
|
} | {
|
|
84
85
|
ok: false;
|
|
85
86
|
reason: string;
|
|
87
|
+
code: KumimojiCheckCode;
|
|
88
|
+
values?: KumimojiCheckValues;
|
|
86
89
|
};
|
|
87
90
|
/**
|
|
88
91
|
* A GAME DEALT (`generateKumimoji`): the bag, in the order its tiles come out,
|
package/dist/party.js
CHANGED
|
@@ -118,9 +118,28 @@ export function startParty(settings, bag, seats) {
|
|
|
118
118
|
const seat = seats[at] ?? "";
|
|
119
119
|
const asked = typeof seat === "string" ? { name: seat } : seat;
|
|
120
120
|
const dealt = { hand: [...bag.slice(at * hand, (at + 1) * hand)], tiles: new Map() };
|
|
121
|
-
players.push(asked.computer === true
|
|
121
|
+
players.push(asked.computer === true
|
|
122
|
+
? { name: computerName(players), computer: true, ...dealt }
|
|
123
|
+
: { name: tidyName(asked.name), ...dealt });
|
|
122
124
|
}
|
|
123
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
settings,
|
|
127
|
+
bag,
|
|
128
|
+
returned: "",
|
|
129
|
+
taken: count * hand,
|
|
130
|
+
players,
|
|
131
|
+
dealt: count,
|
|
132
|
+
turn: 0,
|
|
133
|
+
turns: 0,
|
|
134
|
+
out: [],
|
|
135
|
+
lastTurns: null,
|
|
136
|
+
resigned: [],
|
|
137
|
+
resignRun: [],
|
|
138
|
+
ending: null,
|
|
139
|
+
startTable: "",
|
|
140
|
+
traded: false,
|
|
141
|
+
passedBy: null,
|
|
142
|
+
};
|
|
124
143
|
}
|
|
125
144
|
/** The name a new computer takes: "Computer" and the lowest number no player at the table already goes by.
|
|
126
145
|
*
|
|
@@ -269,7 +288,7 @@ export function withSeatPlay(game, play) {
|
|
|
269
288
|
return game;
|
|
270
289
|
if (game.traded && play.returned.length > game.returned.length)
|
|
271
290
|
return game;
|
|
272
|
-
const players = game.players.map((player, at) =>
|
|
291
|
+
const players = game.players.map((player, at) => at === game.turn ? { ...player, hand: play.hand, tiles: play.tiles } : player);
|
|
273
292
|
const traded = game.traded || play.returned.length > game.returned.length;
|
|
274
293
|
return { ...game, returned: play.returned, taken: play.taken, players, traded };
|
|
275
294
|
}
|
|
@@ -353,7 +372,11 @@ export function nextIn(game, from) {
|
|
|
353
372
|
* ```
|
|
354
373
|
*/
|
|
355
374
|
export function mayDrawAll(game, verdict) {
|
|
356
|
-
return game.ending === null &&
|
|
375
|
+
return (game.ending === null &&
|
|
376
|
+
game.lastTurns === null &&
|
|
377
|
+
game.players[game.turn].hand.length === 0 &&
|
|
378
|
+
verdict.sound &&
|
|
379
|
+
partyTilesLeft(game) >= stillIn(game).length);
|
|
357
380
|
}
|
|
358
381
|
/**
|
|
359
382
|
* DRAW: every player still in takes one tile, in turn order from the one who
|
|
@@ -378,7 +401,7 @@ export function drawAll(game) {
|
|
|
378
401
|
if (partyTilesLeft(game) < order.length)
|
|
379
402
|
return game;
|
|
380
403
|
const coming = (game.bag + game.returned).slice(game.taken, game.taken + order.length);
|
|
381
|
-
const players = game.players.map((player, at) =>
|
|
404
|
+
const players = game.players.map((player, at) => order.includes(at) ? { ...player, hand: [...player.hand, coming[order.indexOf(at)]] } : player);
|
|
382
405
|
return { ...game, taken: game.taken + order.length, players };
|
|
383
406
|
}
|
|
384
407
|
/**
|
package/dist/partyKept.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { decodeGrid, encodeGrid, lettersOf, sameLetters } from "./grid.js";
|
|
2
|
-
import {
|
|
2
|
+
import { KUMIMOJI_HANDS, KUMIMOJI_PARTY } from "./tiles.constants.js";
|
|
3
3
|
/**
|
|
4
4
|
* A PASS-AND-PLAY GAME KEPT IN THE BROWSER, as one string of JSON: the
|
|
5
5
|
* settings, the whole bag in its order, what was traded back and taken, whose
|
|
@@ -50,7 +50,12 @@ export function encodeParty(game) {
|
|
|
50
50
|
traded: game.traded,
|
|
51
51
|
passedBy: game.passedBy,
|
|
52
52
|
...(game.dealt === game.players.length ? {} : { dealt: game.dealt }),
|
|
53
|
-
players: game.players.map((player) => ({
|
|
53
|
+
players: game.players.map((player) => ({
|
|
54
|
+
name: player.name,
|
|
55
|
+
hand: player.hand.join(""),
|
|
56
|
+
grid: encodeGrid(player.tiles),
|
|
57
|
+
...(player.computer === true ? { computer: true } : {}),
|
|
58
|
+
})),
|
|
54
59
|
};
|
|
55
60
|
return JSON.stringify(kept);
|
|
56
61
|
}
|
|
@@ -88,7 +93,16 @@ export function readSettings(value) {
|
|
|
88
93
|
// A game kept before Diagonals existed has no word for it, and was played without.
|
|
89
94
|
if (s.diagonals !== undefined && typeof s.diagonals !== "boolean")
|
|
90
95
|
return null;
|
|
91
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
size: s.size,
|
|
98
|
+
level: s.level,
|
|
99
|
+
seed: s.seed,
|
|
100
|
+
gameLength: s.gameLength,
|
|
101
|
+
language: s.language,
|
|
102
|
+
doubleSet: s.doubleSet,
|
|
103
|
+
diagonals: s.diagonals === true,
|
|
104
|
+
hints: s.hints,
|
|
105
|
+
};
|
|
92
106
|
}
|
|
93
107
|
/**
|
|
94
108
|
* A kept game read back, or null for anything that is not one. With
|
|
@@ -138,23 +152,54 @@ export function decodeParty(code, familyKey) {
|
|
|
138
152
|
const dealt = kept.dealt ?? count;
|
|
139
153
|
if (!Number.isInteger(dealt) || dealt < KUMIMOJI_PARTY.least || dealt > KUMIMOJI_PARTY.most)
|
|
140
154
|
return null;
|
|
141
|
-
if (!isCount(kept.turn, count - 1) ||
|
|
155
|
+
if (!isCount(kept.turn, count - 1) ||
|
|
156
|
+
!isCount(kept.turns, 1_000_000) ||
|
|
157
|
+
(kept.ending !== null && kept.ending !== "out" && kept.ending !== "standing" && kept.ending !== "tied") ||
|
|
158
|
+
typeof kept.traded !== "boolean" ||
|
|
159
|
+
!isTiles(kept.startTable) ||
|
|
160
|
+
(kept.passedBy !== null && !isCount(kept.passedBy, count - 1)))
|
|
142
161
|
return null;
|
|
143
162
|
const seats = (list) => Array.isArray(list) && list.every((at) => isCount(at, count - 1)) && new Set(list).size === list.length;
|
|
144
|
-
if (!seats(kept.out) ||
|
|
163
|
+
if (!seats(kept.out) ||
|
|
164
|
+
(kept.lastTurns !== null && !seats(kept.lastTurns)) ||
|
|
165
|
+
!seats(kept.resigned) ||
|
|
166
|
+
!seats(kept.resignRun))
|
|
145
167
|
return null;
|
|
146
168
|
const players = [];
|
|
147
169
|
for (const player of kept.players) {
|
|
148
|
-
if (typeof player !== "object" ||
|
|
170
|
+
if (typeof player !== "object" ||
|
|
171
|
+
player === null ||
|
|
172
|
+
typeof player.name !== "string" ||
|
|
173
|
+
player.name.length > KUMIMOJI_PARTY.nameMost ||
|
|
174
|
+
!isTiles(player.hand))
|
|
149
175
|
return null;
|
|
150
176
|
if (player.computer !== undefined && player.computer !== true)
|
|
151
177
|
return null;
|
|
152
178
|
const tiles = decodeGrid(player.grid);
|
|
153
179
|
if (tiles === null)
|
|
154
180
|
return null;
|
|
155
|
-
players.push(player.computer === true
|
|
181
|
+
players.push(player.computer === true
|
|
182
|
+
? { name: player.name, hand: [...player.hand], tiles, computer: true }
|
|
183
|
+
: { name: player.name, hand: [...player.hand], tiles });
|
|
156
184
|
}
|
|
157
|
-
const game = {
|
|
185
|
+
const game = {
|
|
186
|
+
settings,
|
|
187
|
+
bag: kept.bag,
|
|
188
|
+
returned: kept.returned,
|
|
189
|
+
taken: kept.taken,
|
|
190
|
+
players,
|
|
191
|
+
dealt,
|
|
192
|
+
turn: kept.turn,
|
|
193
|
+
turns: kept.turns,
|
|
194
|
+
out: kept.out,
|
|
195
|
+
lastTurns: kept.lastTurns,
|
|
196
|
+
resigned: kept.resigned,
|
|
197
|
+
resignRun: kept.resignRun,
|
|
198
|
+
ending: kept.ending,
|
|
199
|
+
startTable: kept.startTable,
|
|
200
|
+
traded: kept.traded,
|
|
201
|
+
passedBy: kept.passedBy,
|
|
202
|
+
};
|
|
158
203
|
if (familyKey !== undefined && !holdsItsBag(game, familyKey))
|
|
159
204
|
return null;
|
|
160
205
|
return game;
|
package/dist/partySeats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { seededRandom, shuffled } from "./random.js";
|
|
2
1
|
import { computerName, isComputer, nextIn, partyTilesLeft, stillIn, tableTally, tidyName } from "./party.js";
|
|
2
|
+
import { seededRandom, shuffled } from "./random.js";
|
|
3
3
|
import { tileFace } from "./tileFace.js";
|
|
4
4
|
import { KUMIMOJI_PARTY } from "./tiles.constants.js";
|
|
5
5
|
/** Why nobody can join the table now, or null when a hand can be dealt.
|
|
@@ -47,7 +47,9 @@ export function joinParty(game, seat) {
|
|
|
47
47
|
return game;
|
|
48
48
|
const hand = [...(game.bag + game.returned).slice(game.taken, game.taken + game.settings.size)];
|
|
49
49
|
const tiles = new Map();
|
|
50
|
-
const player = seat.computer === true
|
|
50
|
+
const player = seat.computer === true
|
|
51
|
+
? { name: computerName(game.players), computer: true, hand, tiles }
|
|
52
|
+
: { name: tidyName(seat.name) || freePlayerName(game.players), hand, tiles };
|
|
51
53
|
return { ...game, players: [...game.players, player], taken: game.taken + hand.length };
|
|
52
54
|
}
|
|
53
55
|
/** "Player" and the next seat's number, or the lowest number nobody at the table goes by. */
|
|
@@ -137,7 +139,7 @@ export function leaveParty(game, at) {
|
|
|
137
139
|
return game;
|
|
138
140
|
const leaver = game.players[at];
|
|
139
141
|
const back = [...leaver.hand, ...leaver.tiles.values()].map((tile) => (tileFace(tile).wild ? "*" : tile));
|
|
140
|
-
const players = game.players.flatMap((player, seat) =>
|
|
142
|
+
const players = game.players.flatMap((player, seat) => seat === at ? [] : [{ ...player, name: player.name || `Player ${seat + 1}` }]);
|
|
141
143
|
const seatOf = (seat) => (seat > at ? seat - 1 : seat);
|
|
142
144
|
const without = (seats) => seats.filter((seat) => seat !== at).map(seatOf);
|
|
143
145
|
const held = players.flatMap((player) => [...player.hand, ...player.tiles.values()]);
|
|
@@ -179,7 +181,12 @@ export function leaveParty(game, at) {
|
|
|
179
181
|
const moved = at === game.turn ? { startTable: tableTally(players[turn].tiles), traded: false } : {};
|
|
180
182
|
if (ending === null && lastTurns === null && stillIn(after).length === 0) {
|
|
181
183
|
// Only players who resigned are left: tied, the last run of them, or all of them where that run has left.
|
|
182
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
...after,
|
|
186
|
+
...moved,
|
|
187
|
+
ending: "tied",
|
|
188
|
+
resignRun: after.resignRun.length > 0 ? after.resignRun : after.resigned,
|
|
189
|
+
};
|
|
183
190
|
}
|
|
184
191
|
return { ...after, ...moved };
|
|
185
192
|
}
|
package/dist/partyTurns.js
CHANGED
|
@@ -47,7 +47,10 @@ export function isOver(game) {
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
export function goesOut(game, verdict) {
|
|
50
|
-
return !isOver(game) &&
|
|
50
|
+
return (!isOver(game) &&
|
|
51
|
+
game.players[game.turn].hand.length === 0 &&
|
|
52
|
+
verdict.sound &&
|
|
53
|
+
partyTilesLeft(game) < stillIn(game).length);
|
|
51
54
|
}
|
|
52
55
|
/** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began.
|
|
53
56
|
*
|
|
@@ -163,7 +166,15 @@ export function doneRefused(game, verdict, handSpells) {
|
|
|
163
166
|
}
|
|
164
167
|
/** The game passed to `turn`, a new turn with nothing traded and its table as it stands. */
|
|
165
168
|
function passTo(game, turn, rest) {
|
|
166
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
...game,
|
|
171
|
+
...rest,
|
|
172
|
+
turns: game.turns + 1,
|
|
173
|
+
turn,
|
|
174
|
+
traded: false,
|
|
175
|
+
startTable: tableTally(game.players[turn].tiles),
|
|
176
|
+
passedBy: game.turn,
|
|
177
|
+
};
|
|
167
178
|
}
|
|
168
179
|
/**
|
|
169
180
|
* DONE: the end of a turn, and the device passed on. Before anybody is out,
|
|
@@ -202,7 +213,9 @@ export function endTurn(game, verdict, handSpells) {
|
|
|
202
213
|
if (game.lastTurns === null) {
|
|
203
214
|
if (out.length === 0)
|
|
204
215
|
return passTo(game, nextIn(game, game.turn), { resignRun: [] });
|
|
205
|
-
const lastTurns = stillIn(game)
|
|
216
|
+
const lastTurns = stillIn(game)
|
|
217
|
+
.filter((at) => at !== game.turn)
|
|
218
|
+
.sort((a, b) => fromTurn(game, a) - fromTurn(game, b));
|
|
206
219
|
if (lastTurns.length === 0)
|
|
207
220
|
return passTo(game, game.turn, { out, lastTurns, ending: "out" });
|
|
208
221
|
return passTo(game, lastTurns[0], { out, lastTurns, resignRun: [] });
|
package/dist/placement.js
CHANGED
|
@@ -99,8 +99,11 @@ export function crossingFit(letterAt, inside, word, anchor, at, across, diagonal
|
|
|
99
99
|
* ```
|
|
100
100
|
*/
|
|
101
101
|
export function diagonalsRead(letterAt, inside, word, fit, isWord) {
|
|
102
|
-
const freshAt = new Map(fit.fresh.map((square) => [
|
|
103
|
-
|
|
102
|
+
const freshAt = new Map(fit.fresh.map((square) => [
|
|
103
|
+
`${square.row},${square.col}`,
|
|
104
|
+
word[square.row - fit.first.row + (square.col - fit.first.col)],
|
|
105
|
+
]));
|
|
106
|
+
const at = (row, col) => inside(row, col) ? letterAt(row, col) || (freshAt.get(`${row},${col}`) ?? "") : "";
|
|
104
107
|
for (const { row, col } of fit.fresh) {
|
|
105
108
|
for (const lean of [1, -1]) {
|
|
106
109
|
let top = 0;
|
package/dist/play.js
CHANGED
|
@@ -57,7 +57,11 @@ export function placeFromHand(play, handAt, square) {
|
|
|
57
57
|
const letter = play.hand[handAt];
|
|
58
58
|
if (letter === undefined || play.tiles.has(square))
|
|
59
59
|
return play;
|
|
60
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
...play,
|
|
62
|
+
tiles: withTiles(play, (tiles) => tiles.set(square, letter)),
|
|
63
|
+
hand: play.hand.filter((_, at) => at !== handAt),
|
|
64
|
+
};
|
|
61
65
|
}
|
|
62
66
|
/** Give a hand tile its chosen reading, without changing which physical tile it is.
|
|
63
67
|
*
|
|
@@ -276,7 +280,12 @@ export function trade(play, handAt) {
|
|
|
276
280
|
return play;
|
|
277
281
|
const returned = play.returned + letter;
|
|
278
282
|
const coming = (play.bag + returned).slice(play.taken, play.taken + KUMIMOJI_TRADE.take);
|
|
279
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
...play,
|
|
285
|
+
returned,
|
|
286
|
+
taken: play.taken + coming.length,
|
|
287
|
+
hand: [...play.hand.filter((_, at) => at !== handAt), ...coming],
|
|
288
|
+
};
|
|
280
289
|
}
|
|
281
290
|
/** Whether the game is over: the bag empty, the hand used, and the grid sound.
|
|
282
291
|
*
|
|
@@ -334,7 +343,9 @@ export function readTileProgress(code) {
|
|
|
334
343
|
if (parts.length !== 4)
|
|
335
344
|
return null;
|
|
336
345
|
const [takenText, returned, hand, grid] = parts;
|
|
337
|
-
if (!/^\d{1,3}$/.test(takenText) ||
|
|
346
|
+
if (!/^\d{1,3}$/.test(takenText) ||
|
|
347
|
+
!/^[a-zA-Z*\uE000-\uF8FF]*$/.test(returned) ||
|
|
348
|
+
!/^[a-zA-Z*\uE000-\uF8FF]*$/.test(hand))
|
|
338
349
|
return null;
|
|
339
350
|
const tiles = decodeGrid(grid);
|
|
340
351
|
if (tiles === null)
|
package/dist/react.js
CHANGED
|
@@ -29,11 +29,25 @@ const TILE = {
|
|
|
29
29
|
*/
|
|
30
30
|
export function KumimojiBoard({ tiles, verdict = null, glyphOf, onSquare, held = null, square = 36, style, ...element }) {
|
|
31
31
|
const model = boardModel(tiles, verdict, glyphOf);
|
|
32
|
-
return (_jsx("div", { role: "grid", style: {
|
|
32
|
+
return (_jsx("div", { role: "grid", style: {
|
|
33
|
+
display: "grid",
|
|
34
|
+
gap: 2,
|
|
35
|
+
gridTemplateColumns: `repeat(${model.cols}, ${square}px)`,
|
|
36
|
+
width: "max-content",
|
|
37
|
+
...style,
|
|
38
|
+
}, ...element, children: model.squares.map((cell) => {
|
|
33
39
|
const ring = cell.square === held ? "0 0 0 3px #ffcf3f" : cell.mark === "misspelt" ? "0 0 0 3px #d9534f inset" : undefined;
|
|
34
40
|
const look = cell.tile === null
|
|
35
41
|
? { width: square, height: square, borderRadius: 5, background: "rgba(127,127,127,.18)", boxShadow: ring }
|
|
36
|
-
: {
|
|
42
|
+
: {
|
|
43
|
+
...TILE,
|
|
44
|
+
width: square,
|
|
45
|
+
height: square,
|
|
46
|
+
fontSize: square * 0.55,
|
|
47
|
+
background: cell.wild ? "#f6d27a" : TILE.background,
|
|
48
|
+
opacity: cell.mark === "apart" ? 0.6 : 1,
|
|
49
|
+
boxShadow: ring ?? TILE.boxShadow,
|
|
50
|
+
};
|
|
37
51
|
return (_jsx("button", { type: "button", "data-square": cell.square, "aria-label": cell.tile === null ? "Empty square" : cell.glyph, onClick: onSquare === undefined ? undefined : () => onSquare(cell.square), style: { border: 0, padding: 0, cursor: onSquare === undefined ? "default" : "pointer", ...look }, children: cell.glyph.toUpperCase() }, cell.square));
|
|
38
52
|
}) }));
|
|
39
53
|
}
|
|
@@ -61,7 +75,23 @@ export function KumimojiTable(props) {
|
|
|
61
75
|
const target = host.current;
|
|
62
76
|
if (target === null)
|
|
63
77
|
return;
|
|
64
|
-
const table = mountKumimoji(target, {
|
|
78
|
+
const table = mountKumimoji(target, {
|
|
79
|
+
language,
|
|
80
|
+
hand,
|
|
81
|
+
level,
|
|
82
|
+
gameLength,
|
|
83
|
+
diagonals,
|
|
84
|
+
seed,
|
|
85
|
+
locale,
|
|
86
|
+
strings,
|
|
87
|
+
theme,
|
|
88
|
+
keep,
|
|
89
|
+
controls,
|
|
90
|
+
saved,
|
|
91
|
+
onError: (error) => latest.current.onError?.(error),
|
|
92
|
+
onFinish: (result) => latest.current.onFinish?.(result),
|
|
93
|
+
onChange: (saved) => latest.current.onChange?.(saved),
|
|
94
|
+
});
|
|
65
95
|
return () => table.destroy();
|
|
66
96
|
// Mounted once per key, as documented above.
|
|
67
97
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|