@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { seededRandom, shuffled } from "./random.js";
|
|
2
|
+
import { computerName, isComputer, nextIn, partyTilesLeft, stillIn, tableTally, tidyName } from "./party.js";
|
|
3
|
+
import { tileFace } from "./tileFace.js";
|
|
4
|
+
import { KUMIMOJI_PARTY } from "./tiles.constants.js";
|
|
5
|
+
export function joinRefused(game) {
|
|
6
|
+
if (game.ending !== null)
|
|
7
|
+
return "over";
|
|
8
|
+
if (game.players.length >= KUMIMOJI_PARTY.most)
|
|
9
|
+
return "full";
|
|
10
|
+
if (game.lastTurns !== null)
|
|
11
|
+
return "lastRound";
|
|
12
|
+
if (partyTilesLeft(game) < game.settings.size)
|
|
13
|
+
return "bag";
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/** A player, or a computer, sat down after the last seat with the next hand out of the bag. */
|
|
17
|
+
export function joinParty(game, seat) {
|
|
18
|
+
if (joinRefused(game) !== null)
|
|
19
|
+
return game;
|
|
20
|
+
const hand = [...(game.bag + game.returned).slice(game.taken, game.taken + game.settings.size)];
|
|
21
|
+
const tiles = new Map();
|
|
22
|
+
const player = seat.computer === true ? { name: computerName(game.players), computer: true, hand, tiles } : { name: tidyName(seat.name) || freePlayerName(game.players), hand, tiles };
|
|
23
|
+
return { ...game, players: [...game.players, player], taken: game.taken + hand.length };
|
|
24
|
+
}
|
|
25
|
+
/** "Player" and the next seat's number, or the lowest number nobody at the table goes by. */
|
|
26
|
+
function freePlayerName(players) {
|
|
27
|
+
const taken = new Set(players.map((player, at) => player.name || `Player ${at + 1}`));
|
|
28
|
+
const next = `Player ${players.length + 1}`;
|
|
29
|
+
if (!taken.has(next))
|
|
30
|
+
return next;
|
|
31
|
+
for (let number = 1;; number += 1)
|
|
32
|
+
if (!taken.has(`Player ${number}`))
|
|
33
|
+
return `Player ${number}`;
|
|
34
|
+
}
|
|
35
|
+
export function leaveRefused(game, at) {
|
|
36
|
+
if (game.ending !== null || game.players[at] === undefined)
|
|
37
|
+
return "over";
|
|
38
|
+
if (game.out.includes(at))
|
|
39
|
+
return "out";
|
|
40
|
+
if (!isComputer(game, at) && game.players.filter((player) => player.computer !== true).length <= 1)
|
|
41
|
+
return "lastPerson";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/** How many tiles a player would put back in the bag by leaving: their hand and their table. */
|
|
45
|
+
export function tilesHeldBy(game, at) {
|
|
46
|
+
const player = game.players[at];
|
|
47
|
+
return player === undefined ? 0 : player.hand.length + player.tiles.size;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A PLAYER LEAVES. Their tiles go back into the bag and the bag is shuffled
|
|
51
|
+
* — with the game's own seed and how far it has gone, so the same game left
|
|
52
|
+
* the same way deals the same tiles after a reload. A wild goes back blank.
|
|
53
|
+
*
|
|
54
|
+
* The bag is kept as a line whose first `taken` tiles are the ones held, less
|
|
55
|
+
* what was traded back (`holdsItsBag`); after a leave it is written afresh as
|
|
56
|
+
* exactly that: the tiles everybody still holds, then the shuffled rest, with
|
|
57
|
+
* nothing traded back. Every seat after the leaver's moves up one, and the
|
|
58
|
+
* unnamed players keep the names they were shown ("Player 3" stays Player 3).
|
|
59
|
+
*
|
|
60
|
+
* If it was their turn, the turn passes to the next still in, or in the last
|
|
61
|
+
* round to the next still to have their last turn; a last round with nobody
|
|
62
|
+
* left to play ends. With nobody still in — everybody left has resigned —
|
|
63
|
+
* the game ends tied between them.
|
|
64
|
+
*/
|
|
65
|
+
export function leaveParty(game, at) {
|
|
66
|
+
if (leaveRefused(game, at) !== null)
|
|
67
|
+
return game;
|
|
68
|
+
const leaver = game.players[at];
|
|
69
|
+
const back = [...leaver.hand, ...leaver.tiles.values()].map((tile) => (tileFace(tile).wild ? "*" : tile));
|
|
70
|
+
const players = game.players.flatMap((player, seat) => (seat === at ? [] : [{ ...player, name: player.name || `Player ${seat + 1}` }]));
|
|
71
|
+
const seatOf = (seat) => (seat > at ? seat - 1 : seat);
|
|
72
|
+
const without = (seats) => seats.filter((seat) => seat !== at).map(seatOf);
|
|
73
|
+
const held = players.flatMap((player) => [...player.hand, ...player.tiles.values()]);
|
|
74
|
+
const rest = [...(game.bag + game.returned).slice(game.taken), ...back];
|
|
75
|
+
const pot = shuffled(rest, seededRandom(potSeed(game)));
|
|
76
|
+
const lastTurns = game.lastTurns === null ? null : without(game.lastTurns);
|
|
77
|
+
let turn = seatOf(game.turn);
|
|
78
|
+
let ending = null;
|
|
79
|
+
if (at === game.turn) {
|
|
80
|
+
if (lastTurns !== null) {
|
|
81
|
+
// The leaver was first of the last round's list: the next on it plays, or the round is over.
|
|
82
|
+
if (lastTurns.length > 0)
|
|
83
|
+
turn = lastTurns[0];
|
|
84
|
+
else {
|
|
85
|
+
turn = seatOf(game.out[0] ?? 0);
|
|
86
|
+
ending = "out";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const next = nextIn(game, at);
|
|
91
|
+
turn = next === at ? 0 : seatOf(next);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const after = {
|
|
95
|
+
...game,
|
|
96
|
+
bag: held.join("") + pot.join(""),
|
|
97
|
+
returned: "",
|
|
98
|
+
taken: held.length,
|
|
99
|
+
players,
|
|
100
|
+
turn,
|
|
101
|
+
turns: game.turns + 1,
|
|
102
|
+
out: without(game.out),
|
|
103
|
+
lastTurns,
|
|
104
|
+
resigned: without(game.resigned),
|
|
105
|
+
resignRun: without(game.resignRun),
|
|
106
|
+
ending,
|
|
107
|
+
passedBy: game.passedBy === null || game.passedBy === at ? null : seatOf(game.passedBy),
|
|
108
|
+
};
|
|
109
|
+
const moved = at === game.turn ? { startTable: tableTally(players[turn].tiles), traded: false } : {};
|
|
110
|
+
if (ending === null && lastTurns === null && stillIn(after).length === 0) {
|
|
111
|
+
// Only players who resigned are left: tied, the last run of them, or all of them where that run has left.
|
|
112
|
+
return { ...after, ...moved, ending: "tied", resignRun: after.resignRun.length > 0 ? after.resignRun : after.resigned };
|
|
113
|
+
}
|
|
114
|
+
return { ...after, ...moved };
|
|
115
|
+
}
|
|
116
|
+
/** The seed a leave shuffles with: the game's own, stirred by how far it has gone. */
|
|
117
|
+
function potSeed(game) {
|
|
118
|
+
return (game.settings.seed ^ Math.imul(game.turns + 1, 0x9e3779b1) ^ Math.imul(game.taken + 1, 0x85ebca6b)) >>> 0;
|
|
119
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { GridVerdict } from "./grid.ts";
|
|
2
|
+
import type { PartyGame } from "./party.types.ts";
|
|
3
|
+
import type { TileWords } from "./tileWords.ts";
|
|
4
|
+
/**
|
|
5
|
+
* HOW A PASS-AND-PLAY TURN ENDS, AND HOW THE GAME DOES: Done, going out, the
|
|
6
|
+
* last round, resigning, and who won. The rules of the table itself are the
|
|
7
|
+
* solo game's (`play.ts`), and the deal and the draw are `party.ts`'s. Pure:
|
|
8
|
+
* each returns a new game, or the one it was given where the move is refused.
|
|
9
|
+
*/
|
|
10
|
+
/** Whether the game is over. */
|
|
11
|
+
export declare function isOver(game: PartyGame): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the player whose turn it is would go out with Done: their hand
|
|
14
|
+
* used, their grid sound, and too few tiles in the bag for everybody still in
|
|
15
|
+
* to draw.
|
|
16
|
+
*/
|
|
17
|
+
export declare function goesOut(game: PartyGame, verdict: GridVerdict): boolean;
|
|
18
|
+
/** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began. */
|
|
19
|
+
export declare function laidThisTurn(game: PartyGame): boolean;
|
|
20
|
+
/** Whether the player whose turn it is is the only one still in: everybody else has resigned. */
|
|
21
|
+
export declare function lastStanding(game: PartyGame): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* A HAND THAT SPELLS NOTHING MUST BE TRADED. John, 2026-09-28: "If no words
|
|
24
|
+
* can be formed which is possible, they must dump a tile to get 3 more. Easy
|
|
25
|
+
* rule." So Done waits while the player whose turn it is holds tiles that
|
|
26
|
+
* spell no word, has laid nothing on (or lifted nothing from) their table this
|
|
27
|
+
* turn, and has not traded — unless the bag cannot give three, when there is
|
|
28
|
+
* nothing to trade for. `handSpells` answers for a hand (`handCanSpell`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function mustTradeFirst(game: PartyGame, handSpells: (hand: readonly string[]) => boolean): boolean;
|
|
31
|
+
/** Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything. */
|
|
32
|
+
export declare function handCanSpell(hand: readonly string[], words: TileWords): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Why Done cannot be pressed now, or null when it can: a hand to trade first,
|
|
35
|
+
* or, for the last one standing, no tile laid on a sound grid yet (a hand
|
|
36
|
+
* already used on a sound grid has nothing left to lay, and wins as it is).
|
|
37
|
+
*/
|
|
38
|
+
export declare function doneRefused(game: PartyGame, verdict: GridVerdict, handSpells: (hand: readonly string[]) => boolean): "trade" | "standing" | null;
|
|
39
|
+
/**
|
|
40
|
+
* DONE: the end of a turn, and the device passed on. Before anybody is out,
|
|
41
|
+
* to the next player still in. A player who goes out with it starts the last
|
|
42
|
+
* round: everybody else still in, in order from the next, has one last turn,
|
|
43
|
+
* and any of them who goes out on it shares the win (John, 2026-09-28:
|
|
44
|
+
* "Sharing the win is correct"). After the last of those, the game is over.
|
|
45
|
+
*
|
|
46
|
+
* The last one standing, everybody else resigned, wins by laying a tile on a
|
|
47
|
+
* sound grid and pressing Done. `verdict` is the grid of the player whose turn
|
|
48
|
+
* it is. Refused, the game given back as it was, while `doneRefused` says so.
|
|
49
|
+
*/
|
|
50
|
+
export declare function endTurn(game: PartyGame, verdict: GridVerdict, handSpells: (hand: readonly string[]) => boolean): PartyGame;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the player whose turn it is may resign: only once the bag cannot
|
|
53
|
+
* give a trade, so nothing more can be got from it. John, 2026-09-28:
|
|
54
|
+
* "Perhaps we offer a resign button only when there are no remaining tiles
|
|
55
|
+
* they can get from the bag."
|
|
56
|
+
*/
|
|
57
|
+
export declare function mayResign(game: PartyGame): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* RESIGN: the player whose turn it is is out of the game, their table kept
|
|
60
|
+
* for the finish, and their turn passed over from then on. In the last round
|
|
61
|
+
* it is their last turn ended without going out. Otherwise the game goes on
|
|
62
|
+
* with whoever is still in; and when nobody is — the last one standing
|
|
63
|
+
* resigned too — it ends tied between everybody who resigned since a player
|
|
64
|
+
* still in last pressed Done. John: "if you resign, as the turns pass, if
|
|
65
|
+
* someone else can't go and has to resign then they are tied."
|
|
66
|
+
*/
|
|
67
|
+
export declare function resign(game: PartyGame): PartyGame;
|
|
68
|
+
/**
|
|
69
|
+
* The winners of a finished game, by place: everybody who went out; the last
|
|
70
|
+
* one standing; or, tied, the final run of resignations. Empty while it is
|
|
71
|
+
* played.
|
|
72
|
+
*/
|
|
73
|
+
export declare function winnersOf(game: PartyGame): readonly number[];
|
|
74
|
+
/** Whether the turn now being played is a player's last. */
|
|
75
|
+
export declare function isLastTurn(game: PartyGame): boolean;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { wordsInHand } from "./help.js";
|
|
2
|
+
import { nextIn, partyTilesLeft, seatPlay, stillIn, tableTally } from "./party.js";
|
|
3
|
+
import { mayTrade } from "./play.js";
|
|
4
|
+
import { KUMIMOJI_TRADE } from "./tiles.constants.js";
|
|
5
|
+
/**
|
|
6
|
+
* HOW A PASS-AND-PLAY TURN ENDS, AND HOW THE GAME DOES: Done, going out, the
|
|
7
|
+
* last round, resigning, and who won. The rules of the table itself are the
|
|
8
|
+
* solo game's (`play.ts`), and the deal and the draw are `party.ts`'s. Pure:
|
|
9
|
+
* each returns a new game, or the one it was given where the move is refused.
|
|
10
|
+
*/
|
|
11
|
+
/** Whether the game is over. */
|
|
12
|
+
export function isOver(game) {
|
|
13
|
+
return game.ending !== null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether the player whose turn it is would go out with Done: their hand
|
|
17
|
+
* used, their grid sound, and too few tiles in the bag for everybody still in
|
|
18
|
+
* to draw.
|
|
19
|
+
*/
|
|
20
|
+
export function goesOut(game, verdict) {
|
|
21
|
+
return !isOver(game) && game.players[game.turn].hand.length === 0 && verdict.sound && partyTilesLeft(game) < stillIn(game).length;
|
|
22
|
+
}
|
|
23
|
+
/** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began. */
|
|
24
|
+
export function laidThisTurn(game) {
|
|
25
|
+
return game.players[game.turn].tiles.size > [...game.startTable].length;
|
|
26
|
+
}
|
|
27
|
+
/** Whether the player whose turn it is is the only one still in: everybody else has resigned. */
|
|
28
|
+
export function lastStanding(game) {
|
|
29
|
+
return !isOver(game) && game.lastTurns === null && stillIn(game).length === 1;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A HAND THAT SPELLS NOTHING MUST BE TRADED. John, 2026-09-28: "If no words
|
|
33
|
+
* can be formed which is possible, they must dump a tile to get 3 more. Easy
|
|
34
|
+
* rule." So Done waits while the player whose turn it is holds tiles that
|
|
35
|
+
* spell no word, has laid nothing on (or lifted nothing from) their table this
|
|
36
|
+
* turn, and has not traded — unless the bag cannot give three, when there is
|
|
37
|
+
* nothing to trade for. `handSpells` answers for a hand (`handCanSpell`).
|
|
38
|
+
*/
|
|
39
|
+
export function mustTradeFirst(game, handSpells) {
|
|
40
|
+
if (isOver(game) || game.traded)
|
|
41
|
+
return false;
|
|
42
|
+
const play = seatPlay(game);
|
|
43
|
+
if (play.hand.length === 0 || tableTally(play.tiles) !== game.startTable || !mayTrade(play))
|
|
44
|
+
return false;
|
|
45
|
+
return !handSpells(play.hand);
|
|
46
|
+
}
|
|
47
|
+
/** Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything. */
|
|
48
|
+
export function handCanSpell(hand, words) {
|
|
49
|
+
return hand.some(words.isWild) || wordsInHand(hand, words).length > 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Why Done cannot be pressed now, or null when it can: a hand to trade first,
|
|
53
|
+
* or, for the last one standing, no tile laid on a sound grid yet (a hand
|
|
54
|
+
* already used on a sound grid has nothing left to lay, and wins as it is).
|
|
55
|
+
*/
|
|
56
|
+
export function doneRefused(game, verdict, handSpells) {
|
|
57
|
+
if (mustTradeFirst(game, handSpells))
|
|
58
|
+
return "trade";
|
|
59
|
+
const used = game.players[game.turn].hand.length === 0;
|
|
60
|
+
if (lastStanding(game) && !(verdict.sound && (laidThisTurn(game) || used)))
|
|
61
|
+
return "standing";
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** The game passed to `turn`, a new turn with nothing traded and its table as it stands. */
|
|
65
|
+
function passTo(game, turn, rest) {
|
|
66
|
+
return { ...game, ...rest, turns: game.turns + 1, turn, traded: false, startTable: tableTally(game.players[turn].tiles), passedBy: game.turn };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* DONE: the end of a turn, and the device passed on. Before anybody is out,
|
|
70
|
+
* to the next player still in. A player who goes out with it starts the last
|
|
71
|
+
* round: everybody else still in, in order from the next, has one last turn,
|
|
72
|
+
* and any of them who goes out on it shares the win (John, 2026-09-28:
|
|
73
|
+
* "Sharing the win is correct"). After the last of those, the game is over.
|
|
74
|
+
*
|
|
75
|
+
* The last one standing, everybody else resigned, wins by laying a tile on a
|
|
76
|
+
* sound grid and pressing Done. `verdict` is the grid of the player whose turn
|
|
77
|
+
* it is. Refused, the game given back as it was, while `doneRefused` says so.
|
|
78
|
+
*/
|
|
79
|
+
export function endTurn(game, verdict, handSpells) {
|
|
80
|
+
if (isOver(game) || doneRefused(game, verdict, handSpells) !== null)
|
|
81
|
+
return game;
|
|
82
|
+
if (lastStanding(game))
|
|
83
|
+
return passTo(game, game.turn, { ending: "standing", resignRun: [] });
|
|
84
|
+
const out = goesOut(game, verdict) ? [...game.out, game.turn] : game.out;
|
|
85
|
+
if (game.lastTurns === null) {
|
|
86
|
+
if (out.length === 0)
|
|
87
|
+
return passTo(game, nextIn(game, game.turn), { resignRun: [] });
|
|
88
|
+
const lastTurns = stillIn(game).filter((at) => at !== game.turn).sort((a, b) => fromTurn(game, a) - fromTurn(game, b));
|
|
89
|
+
if (lastTurns.length === 0)
|
|
90
|
+
return passTo(game, game.turn, { out, lastTurns, ending: "out" });
|
|
91
|
+
return passTo(game, lastTurns[0], { out, lastTurns, resignRun: [] });
|
|
92
|
+
}
|
|
93
|
+
return nextLastTurn(game, out);
|
|
94
|
+
}
|
|
95
|
+
/** How far round the table from the player whose turn it is. */
|
|
96
|
+
function fromTurn(game, at) {
|
|
97
|
+
return (at - game.turn + game.players.length) % game.players.length;
|
|
98
|
+
}
|
|
99
|
+
/** The last round moved on past the player whose turn it was: to the next still to play, or the end. */
|
|
100
|
+
function nextLastTurn(game, out, rest = {}) {
|
|
101
|
+
const lastTurns = (game.lastTurns ?? []).filter((at) => at !== game.turn);
|
|
102
|
+
if (lastTurns.length === 0)
|
|
103
|
+
return passTo(game, game.turn, { ...rest, out, lastTurns, ending: "out" });
|
|
104
|
+
return passTo(game, lastTurns[0], { ...rest, out, lastTurns });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether the player whose turn it is may resign: only once the bag cannot
|
|
108
|
+
* give a trade, so nothing more can be got from it. John, 2026-09-28:
|
|
109
|
+
* "Perhaps we offer a resign button only when there are no remaining tiles
|
|
110
|
+
* they can get from the bag."
|
|
111
|
+
*/
|
|
112
|
+
export function mayResign(game) {
|
|
113
|
+
return !isOver(game) && partyTilesLeft(game) < KUMIMOJI_TRADE.take;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* RESIGN: the player whose turn it is is out of the game, their table kept
|
|
117
|
+
* for the finish, and their turn passed over from then on. In the last round
|
|
118
|
+
* it is their last turn ended without going out. Otherwise the game goes on
|
|
119
|
+
* with whoever is still in; and when nobody is — the last one standing
|
|
120
|
+
* resigned too — it ends tied between everybody who resigned since a player
|
|
121
|
+
* still in last pressed Done. John: "if you resign, as the turns pass, if
|
|
122
|
+
* someone else can't go and has to resign then they are tied."
|
|
123
|
+
*/
|
|
124
|
+
export function resign(game) {
|
|
125
|
+
if (!mayResign(game))
|
|
126
|
+
return game;
|
|
127
|
+
const resigned = [...game.resigned, game.turn];
|
|
128
|
+
const resignRun = [...game.resignRun, game.turn];
|
|
129
|
+
const after = { ...game, resigned, resignRun };
|
|
130
|
+
if (game.lastTurns !== null)
|
|
131
|
+
return nextLastTurn(after, game.out);
|
|
132
|
+
if (stillIn(after).length === 0)
|
|
133
|
+
return passTo(after, game.turn, { ending: "tied" });
|
|
134
|
+
return passTo(after, nextIn(after, game.turn), {});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The winners of a finished game, by place: everybody who went out; the last
|
|
138
|
+
* one standing; or, tied, the final run of resignations. Empty while it is
|
|
139
|
+
* played.
|
|
140
|
+
*/
|
|
141
|
+
export function winnersOf(game) {
|
|
142
|
+
if (game.ending === "out")
|
|
143
|
+
return game.out;
|
|
144
|
+
if (game.ending === "standing")
|
|
145
|
+
return stillIn(game);
|
|
146
|
+
if (game.ending === "tied")
|
|
147
|
+
return game.resignRun;
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
/** Whether the turn now being played is a player's last. */
|
|
151
|
+
export function isLastTurn(game) {
|
|
152
|
+
return !isOver(game) && game.lastTurns !== null;
|
|
153
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Square = {
|
|
2
|
+
row: number;
|
|
3
|
+
col: number;
|
|
4
|
+
};
|
|
5
|
+
/** A word's place: its first square, and the squares it lays a new tile on, in the word's order. */
|
|
6
|
+
export type CrossingFit = {
|
|
7
|
+
first: Square;
|
|
8
|
+
fresh: Square[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Where `word` stands with its letter `at` on the tile at `anchor`, running
|
|
12
|
+
* across or down, or null where it cannot: a square outside what may be used,
|
|
13
|
+
* a tile at either end, a different letter in its way, a new tile with a
|
|
14
|
+
* neighbour at its side, or no new tile at all — and, given `diagonalWord`
|
|
15
|
+
* (a game with Diagonals), a new tile standing in a diagonal run of three or
|
|
16
|
+
* more that `diagonalWord` does not take (`diagonalsRead`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function crossingFit(letterAt: (row: number, col: number) => string, inside: (row: number, col: number) => boolean, word: string, anchor: Square, at: number, across: boolean, diagonalWord?: ((run: string) => boolean) | null): CrossingFit | null;
|
|
19
|
+
/**
|
|
20
|
+
* Whether every diagonal run a laying's new tiles would stand in is taken by
|
|
21
|
+
* `isWord`: each walked from its top end down, with the word's own letters on
|
|
22
|
+
* its new squares. Only a run through a new tile can change, and two new
|
|
23
|
+
* tiles of one word never share a diagonal (they share a row or a column), so
|
|
24
|
+
* each is read on its own. A run of two, a corner touch, is never read.
|
|
25
|
+
*/
|
|
26
|
+
export declare function diagonalsRead(letterAt: (row: number, col: number) => string, inside: (row: number, col: number) => boolean, word: string, fit: CrossingFit, isWord: (run: string) => boolean): boolean;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE A WORD CAN CROSS A CROSSWORD WITHOUT SPOILING IT: the one rule the
|
|
3
|
+
* generator lays its bags by (`generate.ts`) and the computer player lays its
|
|
4
|
+
* own table by (`computerPlay.ts`), written once.
|
|
5
|
+
*
|
|
6
|
+
* A word laid across or down through a tile already down, with nothing at
|
|
7
|
+
* either end of it and no new tile touching anything at its sides, makes one
|
|
8
|
+
* new run — the word itself — and leaves every other run as it was. So on a
|
|
9
|
+
* grid that was sound, the grid after it is sound too: that is what lets the
|
|
10
|
+
* generator promise every bag can be finished, and what keeps the computer
|
|
11
|
+
* from ever leaving a misspelt table.
|
|
12
|
+
*
|
|
13
|
+
* With DIAGONALS (`diagonalWord`), a new tile may also make or lengthen a
|
|
14
|
+
* diagonal run of three or more (`DIAGONAL_RUN_LEAST`), which is then read
|
|
15
|
+
* too; a word is laid only where each such run is a word, so a sound grid
|
|
16
|
+
* stays sound under that rule as well.
|
|
17
|
+
*
|
|
18
|
+
* Pure, and blind to what the squares are kept in: the caller answers what
|
|
19
|
+
* letter stands at a square (`""` for none) and whether a square may be used
|
|
20
|
+
* at all, so the generator's fixed laying square and the computer's table
|
|
21
|
+
* with no edges ask the same question.
|
|
22
|
+
*/
|
|
23
|
+
import { DIAGONAL_RUN_LEAST } from "./grid.js";
|
|
24
|
+
/**
|
|
25
|
+
* Where `word` stands with its letter `at` on the tile at `anchor`, running
|
|
26
|
+
* across or down, or null where it cannot: a square outside what may be used,
|
|
27
|
+
* a tile at either end, a different letter in its way, a new tile with a
|
|
28
|
+
* neighbour at its side, or no new tile at all — and, given `diagonalWord`
|
|
29
|
+
* (a game with Diagonals), a new tile standing in a diagonal run of three or
|
|
30
|
+
* more that `diagonalWord` does not take (`diagonalsRead`).
|
|
31
|
+
*/
|
|
32
|
+
export function crossingFit(letterAt, inside, word, anchor, at, across, diagonalWord = null) {
|
|
33
|
+
const dr = across ? 0 : 1;
|
|
34
|
+
const dc = across ? 1 : 0;
|
|
35
|
+
const row = anchor.row - at * dr;
|
|
36
|
+
const col = anchor.col - at * dc;
|
|
37
|
+
const length = word.length;
|
|
38
|
+
if (!inside(row, col) || !inside(row + (length - 1) * dr, col + (length - 1) * dc))
|
|
39
|
+
return null;
|
|
40
|
+
const taken = (r, c) => inside(r, c) && letterAt(r, c) !== "";
|
|
41
|
+
if (taken(row - dr, col - dc) || taken(row + length * dr, col + length * dc))
|
|
42
|
+
return null;
|
|
43
|
+
const fresh = [];
|
|
44
|
+
for (let k = 0; k < length; k += 1) {
|
|
45
|
+
const r = row + k * dr;
|
|
46
|
+
const c = col + k * dc;
|
|
47
|
+
const there = letterAt(r, c);
|
|
48
|
+
if (there !== "") {
|
|
49
|
+
if (there !== word[k])
|
|
50
|
+
return null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Its sides: above and below a word across, left and right of one down.
|
|
54
|
+
if (taken(r - dc, c - dr) || taken(r + dc, c + dr))
|
|
55
|
+
return null;
|
|
56
|
+
fresh.push({ row: r, col: c });
|
|
57
|
+
}
|
|
58
|
+
if (fresh.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
if (diagonalWord !== null && !diagonalsRead(letterAt, inside, word, { first: { row, col }, fresh }, diagonalWord))
|
|
61
|
+
return null;
|
|
62
|
+
return { first: { row, col }, fresh };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether every diagonal run a laying's new tiles would stand in is taken by
|
|
66
|
+
* `isWord`: each walked from its top end down, with the word's own letters on
|
|
67
|
+
* its new squares. Only a run through a new tile can change, and two new
|
|
68
|
+
* tiles of one word never share a diagonal (they share a row or a column), so
|
|
69
|
+
* each is read on its own. A run of two, a corner touch, is never read.
|
|
70
|
+
*/
|
|
71
|
+
export function diagonalsRead(letterAt, inside, word, fit, isWord) {
|
|
72
|
+
const freshAt = new Map(fit.fresh.map((square) => [`${square.row},${square.col}`, word[(square.row - fit.first.row) + (square.col - fit.first.col)]]));
|
|
73
|
+
const at = (row, col) => (inside(row, col) ? letterAt(row, col) || (freshAt.get(`${row},${col}`) ?? "") : "");
|
|
74
|
+
for (const { row, col } of fit.fresh) {
|
|
75
|
+
for (const lean of [1, -1]) {
|
|
76
|
+
let top = 0;
|
|
77
|
+
while (at(row - top - 1, col - (top + 1) * lean) !== "")
|
|
78
|
+
top += 1;
|
|
79
|
+
let run = "";
|
|
80
|
+
for (let step = -top; at(row + step, col + step * lean) !== ""; step += 1)
|
|
81
|
+
run += at(row + step, col + step * lean);
|
|
82
|
+
if (run.length >= DIAGONAL_RUN_LEAST && !isWord(run))
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
package/dist/play.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type GridVerdict, type Tiles } from "./grid.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A KUMIMOJI BEING PLAYED: the bag, the tiles traded back into it, how many
|
|
4
|
+
* have been taken out, the tiles on the table and the hand. Every move is a
|
|
5
|
+
* function that returns a new state and leaves the one it was given alone,
|
|
6
|
+
* the way the engine's moves are. Nothing here knows how the table is being
|
|
7
|
+
* looked at — zoom and pan are the view's (`tableView.ts`), never the game's.
|
|
8
|
+
*
|
|
9
|
+
* THE BAG IS A LINE, not a heap. Its tiles come out in the order the seed dealt
|
|
10
|
+
* them (`givens`), and a tile traded back joins the END of the line, so the
|
|
11
|
+
* tiles still to come are always `bag + returned` from `taken` on. That keeps
|
|
12
|
+
* a game the same for everybody at a seed, trades and all, and lets a kept
|
|
13
|
+
* game be checked: what is held — hand and table — is exactly what was taken,
|
|
14
|
+
* less what was given back.
|
|
15
|
+
*/
|
|
16
|
+
export type TilePlay = {
|
|
17
|
+
/** The tiles in the order they come out: the puzzle's givens. */
|
|
18
|
+
bag: string;
|
|
19
|
+
/** Tiles traded back, in order; they come out after the bag. */
|
|
20
|
+
returned: string;
|
|
21
|
+
/** How many tiles have been taken from `bag + returned`. */
|
|
22
|
+
taken: number;
|
|
23
|
+
/** The tiles on the table, by square ("row,col"). */
|
|
24
|
+
tiles: Tiles;
|
|
25
|
+
/** The hand, in the order the player keeps it. */
|
|
26
|
+
hand: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
/** The opening hand: the first `handSize` tiles of the bag, and an empty table. */
|
|
29
|
+
export declare function deal(bag: string, handSize: number): TilePlay;
|
|
30
|
+
/** How many tiles are still in the bag. */
|
|
31
|
+
export declare function tilesLeft(play: TilePlay): number;
|
|
32
|
+
/** A tile from the hand onto an empty square. */
|
|
33
|
+
export declare function placeFromHand(play: TilePlay, handAt: number, square: string): TilePlay;
|
|
34
|
+
/** Give a hand tile its chosen reading, without changing which physical tile it is. */
|
|
35
|
+
export declare function assignHandTile(play: TilePlay, handAt: number, face: string): TilePlay;
|
|
36
|
+
/** Give a tile on the table its chosen reading, without changing its square. */
|
|
37
|
+
export declare function assignTableTile(play: TilePlay, square: string, face: string): TilePlay;
|
|
38
|
+
/** A tile on the table to another square: to an empty one it moves, onto a tile the two change places. */
|
|
39
|
+
export declare function moveOnTable(play: TilePlay, from: string, to: string): TilePlay;
|
|
40
|
+
/** A tile from the hand onto a square that holds one: they change places, the table's tile going to the hand where the other was. */
|
|
41
|
+
export declare function swapWithHand(play: TilePlay, handAt: number, square: string): TilePlay;
|
|
42
|
+
/** A tile on the table back to the end of the hand. */
|
|
43
|
+
export declare function liftToHand(play: TilePlay, square: string): TilePlay;
|
|
44
|
+
/** Every tile on the table back to the hand. */
|
|
45
|
+
export declare function liftAll(play: TilePlay): TilePlay;
|
|
46
|
+
/**
|
|
47
|
+
* The hand in order (John, 2026-09-28: "it gets sorted for letters
|
|
48
|
+
* alphabetically"): English A to Z, Japanese あいうえお, which is the order
|
|
49
|
+
* of the kana's tile codes, and the wilds last as they came.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sortHand(play: TilePlay): TilePlay;
|
|
52
|
+
/** Whether Draw may be pressed: the hand used, the grid sound, and a tile left to draw. */
|
|
53
|
+
export declare function mayDraw(play: TilePlay, verdict: GridVerdict): boolean;
|
|
54
|
+
/** The next tile or tiles out of the bag into the hand. */
|
|
55
|
+
export declare function draw(play: TilePlay, count?: number): TilePlay;
|
|
56
|
+
/** Whether a trade may be made: the bag holds as many as a trade takes. */
|
|
57
|
+
export declare function mayTrade(play: TilePlay): boolean;
|
|
58
|
+
/** One tile from the hand to the bottom of the bag, and the next three out of it. */
|
|
59
|
+
export declare function trade(play: TilePlay, handAt: number): TilePlay;
|
|
60
|
+
/** Whether the game is over: the bag empty, the hand used, and the grid sound. */
|
|
61
|
+
export declare function isFinished(play: TilePlay, verdict: GridVerdict): boolean;
|
|
62
|
+
export declare function encodeTileProgress(play: TilePlay): string;
|
|
63
|
+
/** The parts of a kept game, or null for a string that is not one: its shape only, with no bag to check it against. */
|
|
64
|
+
export declare function readTileProgress(code: string): {
|
|
65
|
+
taken: number;
|
|
66
|
+
returned: string;
|
|
67
|
+
hand: string[];
|
|
68
|
+
tiles: Map<string, string>;
|
|
69
|
+
} | null;
|
|
70
|
+
/**
|
|
71
|
+
* A kept game opened again on its own bag, or null where it does not belong
|
|
72
|
+
* to it: every tile held — hand and table — must be one taken from the bag
|
|
73
|
+
* and not given back. Null opens the game fresh rather than on a grid that
|
|
74
|
+
* never came out of this bag.
|
|
75
|
+
*/
|
|
76
|
+
export declare function decodeTileProgress(code: string, bag: string, language?: "english" | "japanese"): TilePlay | null;
|