packing-bingo 0.1.1 → 0.3.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.
@@ -2,29 +2,98 @@
2
2
 
3
3
  // src/items.ts
4
4
  var DEFAULT_ITEMS = [
5
- { emoji: "\u{1F4D6}", label: "\u65C5\u306E\u3057\u304A\u308A" },
6
- { emoji: "\u{1FAAA}", label: "\u751F\u5F92\u624B\u5E33" },
7
- { emoji: "\u{1F3E5}", label: "\u4FDD\u967A\u8A3C\u30B3\u30D4\u30FC" },
8
- { emoji: "\u{1F45B}", label: "\u304A\u3053\u3065\u304B\u3044" },
9
- { emoji: "\u{1F455}", label: "\u7740\u66FF\u3048" },
10
- { emoji: "\u{1F319}", label: "\u30D1\u30B8\u30E3\u30DE" },
11
- { emoji: "\u{1F9E5}", label: "\u4E0A\u7740" },
12
- { emoji: "\u{1FAA5}", label: "\u6D17\u9762\u7528\u5177" },
13
- { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
14
- { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1\u30FB\u30C6\u30A3\u30C3\u30B7\u30E5" },
5
+ { emoji: "\u{1F45B}", label: "\u8CA1\u5E03" },
6
+ { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB" },
7
+ { emoji: "\u{1F50B}", label: "\u30E2\u30D0\u30A4\u30EB\u200B\u5145\u96FB\u5668" },
8
+ { emoji: "\u{1F4CB}", label: "\u5065\u5EB7\u8ABF\u67FB\u7968" },
9
+ { emoji: "\u{1FAAA}", label: "\u30DE\u30A4\u30CA\u30AB\u30FC\u30C9\u200B\u307E\u305F\u306F\u4FDD\u967A\u8A3C" },
10
+ { emoji: "\u{1F4D6}", label: "\u3057\u304A\u308A" },
15
11
  { emoji: "\u{1F637}", label: "\u30DE\u30B9\u30AF" },
16
- { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC\u30FB\u9154\u3044\u6B62\u3081" },
17
- { emoji: "\u2602\uFE0F", label: "\u6298\u308A\u305F\u305F\u307F\u5098" },
18
- { emoji: "\u270F\uFE0F", label: "\u7B46\u8A18\u7528\u5177" },
12
+ { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1" },
13
+ { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC" },
19
14
  { emoji: "\u{1F6CD}\uFE0F", label: "\u30D3\u30CB\u30FC\u30EB\u888B" },
20
- { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB\u30FB\u5145\u96FB\u5668" }
15
+ { emoji: "\u{1F9E4}", label: "\u8ECD\u624B\uFF08\u5B66\u6821\u652F\u7D66\uFF09" },
16
+ { emoji: "\u{1F526}", label: "\u30D8\u30C3\u30C9\u30E9\u30A4\u30C8" },
17
+ { emoji: "\u{1F455}", label: "\u30B8\u30E3\u30FC\u30B8" },
18
+ { emoji: "\u2614", label: "\u96E8\u5177" },
19
+ { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
20
+ { emoji: "\u{1F9F4}", label: "\u65E5\u713C\u3051\u6B62\u3081" }
21
21
  ];
22
22
 
23
+ // src/layout.ts
24
+ var MIN_SIZE = 2;
25
+ var MAX_SIZE = 10;
26
+ var fail = (msg) => {
27
+ throw new Error(`[packing-bingo] ${msg}`);
28
+ };
29
+ function layoutItems(options) {
30
+ if (options.items) return options.items;
31
+ const n = options.size;
32
+ if (n == null || !Number.isInteger(n) || n < MIN_SIZE) return DEFAULT_ITEMS;
33
+ return DEFAULT_ITEMS.slice(0, n * n - (options.free ? 1 : 0));
34
+ }
35
+ function checkSize(size, free) {
36
+ if (!Number.isInteger(size) || size < MIN_SIZE || size > MAX_SIZE)
37
+ fail(`"size" must be a whole number from ${MIN_SIZE} to ${MAX_SIZE} (got ${size}).`);
38
+ if (free && size % 2 === 0) fail(`"free" needs an odd size such as 3 or 5, so that the card has a center (got ${size}).`);
39
+ }
40
+ function shape(n, free) {
41
+ const seq = (f) => Array.from({ length: n }, (_, k) => f(k));
42
+ const cellLines = [
43
+ ...seq((r) => seq((c) => r * n + c)),
44
+ // rows
45
+ ...seq((c) => seq((r) => r * n + c)),
46
+ // columns
47
+ seq((k) => k * (n + 1)),
48
+ // top left to bottom right
49
+ seq((k) => (k + 1) * (n - 1))
50
+ // top right to bottom left
51
+ ];
52
+ const center = free ? (n * n - 1) / 2 : -1;
53
+ const toItem = (c) => free && c > center ? c - 1 : c;
54
+ return { center, cellLines, lines: cellLines.map((l) => l.filter((c) => c !== center).map(toItem)) };
55
+ }
56
+ function resolveLayout(options) {
57
+ const items = layoutItems(options);
58
+ const free = !!options.free;
59
+ let size = options.size;
60
+ if (size == null) {
61
+ const len = items.length;
62
+ const sideOf = (cells) => {
63
+ const r = Math.round(Math.sqrt(cells));
64
+ return r * r === cells && r >= MIN_SIZE && r <= MAX_SIZE ? r : 0;
65
+ };
66
+ size = sideOf(len + (free ? 1 : 0));
67
+ if (!size) {
68
+ const other = sideOf(len + (free ? 0 : 1));
69
+ const hint = free ? other ? `; ${len} items fill a ${other}\xD7${other} card without "free"` : "" : other % 2 ? `, or set "free" for a ${other}\xD7${other} card with a FREE center` : "";
70
+ fail(
71
+ free ? `With "free", "items" must have one less than an odd square number of entries (8, 24, 48, 80)${hint} (got ${len}).` : `"items" must have a square number of entries (4, 9, 16, 25, \u2026, 100)${hint} (got ${len}).`
72
+ );
73
+ }
74
+ }
75
+ checkSize(size, free);
76
+ const want = size * size - (free ? 1 : 0);
77
+ if (items.length !== want) {
78
+ const card = `a ${size}\xD7${size} card${free ? " with a FREE center" : ""}`;
79
+ fail(
80
+ options.items ? `"items" must have exactly ${want} entries for ${card} (got ${items.length}).` : `A ${card.slice(2)} needs ${want} items, but DEFAULT_ITEMS has only ${DEFAULT_ITEMS.length}. Pass "items".`
81
+ );
82
+ }
83
+ const { center, cellLines, lines } = shape(size, free);
84
+ const itemCell = items.map((_, i) => free && i >= center ? i + 1 : i);
85
+ return { size, free, items, itemCell, center, cellLines, lines };
86
+ }
87
+ function bingoLines(size, free = false) {
88
+ checkSize(size, free);
89
+ return shape(size, free).lines;
90
+ }
91
+
23
92
  // src/messages.ts
24
93
  var ja = {
25
94
  title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
26
95
  tag: "\u4FEE\u5B66\u65C5\u884C",
27
- subtitle: "\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30F3\u30D7\u3002\u30BF\u30C6\u30FB\u30E8\u30B3\u30FB\u30CA\u30CA\u30E1\u3067\u30D3\u30F3\u30B4\uFF01",
96
+ subtitle: "",
28
97
  packed: "\u305D\u308D\u3063\u305F",
29
98
  bingo: "BINGO",
30
99
  score: "SCORE",
@@ -34,6 +103,7 @@ var ja = {
34
103
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
35
104
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
36
105
  stamp: "OK",
106
+ free: "FREE",
37
107
  reachBadge: "\u30EA\u30FC\u30C1",
38
108
  reachPop: "\u30EA\u30FC\u30C1!",
39
109
  bannerSingle: "BINGO!",
@@ -41,14 +111,15 @@ var ja = {
41
111
  bannerTriple: "\u30C8\u30EA\u30D7\u30EB\u30D3\u30F3\u30B4!",
42
112
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
43
113
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
44
- statusStart: (left) => `\u306E\u3053\u308A ${left} \u3053\u3002\u307E\u305A\u306F1\u30E9\u30A4\u30F3\u305D\u308D\u3048\u3088\u3046`,
114
+ statusStart: () => "",
45
115
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
46
116
  statusReach: (labels) => `\u30EA\u30FC\u30C1\uFF01 \u3042\u3068${labels.slice(0, 2).map((l) => `\u300C${l}\u300D`).join("\u304B")}${labels.length > 2 ? "\u306A\u3069" : ""}\u3067\u30D3\u30F3\u30B4`,
47
117
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
48
118
  perfectTag: "\u{1F3C6} PERFECT",
49
119
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
50
120
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
51
- perfectBody: "16\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01\n\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01",
121
+ perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
122
+ \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
52
123
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
53
124
  statScore: "SCORE",
54
125
  statCombo: "MAX COMBO",
@@ -62,7 +133,7 @@ var ja = {
62
133
  var en = {
63
134
  title: "Packing Bingo",
64
135
  tag: "School trip",
65
- subtitle: "Tap to stamp. Rows, columns and diagonals all count!",
136
+ subtitle: "",
66
137
  packed: "PACKED",
67
138
  bingo: "BINGO",
68
139
  score: "SCORE",
@@ -72,6 +143,7 @@ var en = {
72
143
  soundOn: "\u{1F50A} Sound on",
73
144
  soundOff: "\u{1F507} Muted",
74
145
  stamp: "OK",
146
+ free: "FREE",
75
147
  reachBadge: "REACH",
76
148
  reachPop: "Reach!",
77
149
  bannerSingle: "BINGO!",
@@ -79,14 +151,15 @@ var en = {
79
151
  bannerTriple: "TRIPLE BINGO!",
80
152
  bannerNth: (n) => `Line ${n}!`,
81
153
  bannerMulti: (n) => `${n} lines at once!`,
82
- statusStart: (left) => `${left} to go. Finish a line first!`,
154
+ statusStart: () => "",
83
155
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
84
156
  statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
85
157
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
86
158
  perfectTag: "\u{1F3C6} PERFECT",
87
159
  perfectStamp: "ALL\nSET",
88
160
  perfectTitle: "Perfect!",
89
- perfectBody: "All 16 packed!\nHave a great trip!",
161
+ perfectBody: (n) => `All ${n} packed!
162
+ Have a great trip!`,
90
163
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
91
164
  statScore: "SCORE",
92
165
  statCombo: "MAX COMBO",
@@ -195,17 +268,34 @@ function createSound(isMuted) {
195
268
  };
196
269
  }
197
270
 
271
+ // src/storage-key.ts
272
+ function resolveStorageKey(options) {
273
+ if (options.controlled) return null;
274
+ if (options.storageKey !== void 0) return options.storageKey || null;
275
+ if (options.demo) return null;
276
+ return itemsKey(layoutItems(options));
277
+ }
278
+ function itemsKey(items) {
279
+ const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
280
+ let h = 2166136261;
281
+ for (let i = 0; i < text.length; i++) {
282
+ h ^= text.charCodeAt(i);
283
+ h = Math.imul(h, 16777619);
284
+ }
285
+ return `packing-bingo:${(h >>> 0).toString(36)}`;
286
+ }
287
+
198
288
  // src/styles.ts
199
289
  var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
200
290
  var CSS = `
201
- .pkb{--pk-sun:var(--pkb-sun,#FFD54F);--pk-ink:var(--pkb-ink,#2A1F14);--pk-paper:var(--pkb-paper,#FFFDF6);--pk-red:var(--pkb-red,#E8473B);--pk-mint:var(--pkb-mint,#BDEBD2);--pk-display:"Dela Gothic One",var(--pkb-font-fallback,sans-serif);--pk-body:"M PLUS Rounded 1c","Hiragino Maru Gothic ProN",var(--pkb-font-fallback,sans-serif);container-type:inline-size;isolation:isolate;width:100%}
291
+ .pkb{--pk-sun:var(--pkb-sun,#FFD54F);--pk-ink:var(--pkb-ink,#2A1F14);--pk-paper:var(--pkb-paper,#FFFDF6);--pk-red:var(--pkb-red,#E8473B);--pk-mint:var(--pkb-mint,#BDEBD2);--pk-display:"Dela Gothic One",var(--pkb-font-fallback,sans-serif);--pk-body:"M PLUS Rounded 1c","Hiragino Maru Gothic ProN",var(--pkb-font-fallback,sans-serif);--pk-n:4;--pk-s:1;--pk-d:1;container-type:inline-size;isolation:isolate;width:100%}
202
292
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
203
293
  .pkb-board{position:relative;overflow:hidden;max-width:720px;margin:0 auto;padding:22px 26px 20px;border-radius:24px;background-color:var(--pk-sun);background-image:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px);background-size:16px 16px;border:2.5px solid var(--pk-ink);color:var(--pk-ink);font-family:var(--pk-body);line-height:1.4;text-align:left}
204
294
  .pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
205
295
  @keyframes pkb-shake{10%,90%{transform:translate(-1px,1px)}20%,80%{transform:translate(3px,-2px)}30%,50%,70%{transform:translate(-6px,2px)}40%,60%{transform:translate(6px,-1px)}}
206
296
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
207
297
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
208
- .pkb-head{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}
298
+ .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
209
299
  .pkb-tags{display:flex;gap:8px;align-items:center}
210
300
  .pkb-kicker{display:inline-block;background:var(--pk-ink);color:var(--pk-sun);font-weight:800;font-size:11px;letter-spacing:.14em;padding:4px 10px;border-radius:999px}
211
301
  .pkb-kicker.pkb-gold{color:#fff;background:linear-gradient(90deg,#E8473B,#FF9F1C,#1FA971,#2F6FED,#E8473B);background-size:200% 100%;animation:pkb-rb 2s linear infinite}
@@ -213,7 +303,7 @@ var CSS = `
213
303
  .pkb button{font-family:var(--pk-body)}
214
304
  .pkb-snd{appearance:none;border:2px solid var(--pk-ink);background:var(--pk-paper);border-radius:999px;padding:2px 9px;font-weight:800;font-size:11px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
215
305
  .pkb-title{font-family:var(--pk-display);font-weight:400;font-size:34px;line-height:1.15;margin-top:8px;letter-spacing:.02em;color:inherit}
216
- .pkb-sub{font-weight:800;font-size:13px;margin-top:4px}
306
+ .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
217
307
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
218
308
  .pkb-score{display:flex;gap:10px}
219
309
  .pkb-pill{min-width:78px;background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:14px;padding:6px 10px 5px;text-align:center;box-shadow:3px 3px 0 var(--pk-ink)}
@@ -232,16 +322,18 @@ var CSS = `
232
322
  .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
233
323
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
234
324
  .pkb-gw{position:relative}
235
- .pkb-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
236
- .pkb-tile{position:relative;aspect-ratio:1/.82;background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:16px;box-shadow:4px 4px 0 var(--pk-ink);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;transition:transform .1s,box-shadow .1s,background-color .25s}
237
- .pkb-tile:active{transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
325
+ .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
326
+ .pkb-tile{position:relative;min-width:0;place-self:stretch;aspect-ratio:1/.82;background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:calc(16px*var(--pk-d));box-shadow:4px 4px 0 var(--pk-ink);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:calc(6px*var(--pk-d));cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;transition:transform .1s,box-shadow .1s,background-color .25s}
327
+ .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
238
328
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
239
329
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
240
- .pkb-emo{font-size:32px;line-height:1;transition:opacity .2s}
241
- .pkb-lbl{font-weight:800;font-size:13px;line-height:1.25;text-align:center;padding:0 6px;transition:opacity .2s;word-break:keep-all;overflow-wrap:anywhere}
242
- .pkb-stamp{position:absolute;left:50%;top:50%;width:64%;aspect-ratio:1;border-radius:50%;border:3px solid var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-paper),inset 0 0 0 4px var(--pk-red);color:var(--pk-red);display:grid;place-items:center;font-family:var(--pk-display);font-size:20px;letter-spacing:.04em;opacity:0;transform:translate(-50%,-50%) rotate(-16deg) scale(1.7);transition:transform .25s cubic-bezier(.3,1.5,.5,1),opacity .15s;pointer-events:none}
330
+ .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
331
+ .pkb-lbl{font-weight:800;font-size:max(9px,calc(13px*var(--pk-s)));line-height:1.25;text-align:center;padding:0 calc(6px*var(--pk-d));transition:opacity .2s;word-break:keep-all;overflow-wrap:anywhere}
332
+ .pkb-stamp{position:absolute;left:50%;top:50%;width:64%;aspect-ratio:1;border-radius:50%;border:3px solid var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-paper),inset 0 0 0 4px var(--pk-red);color:var(--pk-red);display:grid;place-items:center;font-family:var(--pk-display);font-size:max(9px,calc(20px*var(--pk-s)));letter-spacing:.04em;opacity:0;transform:translate(-50%,-50%) rotate(-16deg) scale(1.7);transition:transform .25s cubic-bezier(.3,1.5,.5,1),opacity .15s;pointer-events:none}
243
333
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
244
334
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
335
+ .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
336
+ .pkb-tile.pkb-free .pkb-stamp{width:72%;opacity:1;transition:none;font-size:17cqi;letter-spacing:.02em;line-height:1;text-align:center;background:var(--pk-red);color:var(--pk-paper);border-color:var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-red),inset 0 0 0 4px var(--pk-paper)}
245
337
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
246
338
  .pkb-tile.pkb-line::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:linear-gradient(110deg,transparent 30%,rgba(255,255,255,.9) 45%,transparent 60%) no-repeat;background-size:250% 100%;background-position:200% 0;animation:pkb-glint 3.2s ease-in-out var(--pkb-gd,0s) infinite}
247
339
  @keyframes pkb-glint{0%{background-position:200% 0}30%,100%{background-position:-100% 0}}
@@ -258,11 +350,11 @@ var CSS = `
258
350
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
259
351
  .pkb-board.pkb-all .pkb-lines{opacity:0}
260
352
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
261
- .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:13}
262
- .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:6}
353
+ .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:calc(13px*var(--pk-d))}
354
+ .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
263
355
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
264
356
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
265
- .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px}
357
+ .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
266
358
  .pkb-reset{appearance:none;flex:none;border:2px solid var(--pk-ink);background:transparent;border-radius:999px;padding:5px 12px;font-weight:800;font-size:12px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
267
359
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
268
360
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
@@ -277,7 +369,7 @@ var CSS = `
277
369
  .pkb-float{position:absolute;z-index:11;pointer-events:none;white-space:nowrap;font-family:var(--pk-display);font-weight:400;line-height:1;-webkit-text-stroke:4px var(--pk-ink);paint-order:stroke fill;transform:translate(-50%,-50%);animation:pkb-rise .95s cubic-bezier(.2,.9,.3,1) both}
278
370
  .pkb-float.pkb-pt{font-size:22px;color:var(--pk-paper)}
279
371
  .pkb-float.pkb-cmb{font-size:17px}
280
- .pkb-float.pkb-big{font-size:clamp(20px,6cqi,34px);color:#FFE066}
372
+ .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
281
373
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
282
374
  @keyframes pkb-rise{0%{opacity:0;transform:translate(-50%,-20%) scale(.4)}18%{opacity:1;transform:translate(-50%,-60%) scale(1.2)}30%{transform:translate(-50%,-70%) scale(1)}75%{opacity:1}100%{opacity:0;transform:translate(-50%,-190%) scale(1)}}
283
375
  .pkb-perfect{position:absolute;inset:0;z-index:8;display:grid;place-items:center;padding:20px;background:rgba(42,31,20,.5);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s}
@@ -288,7 +380,7 @@ var CSS = `
288
380
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
289
381
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
290
382
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
291
- .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line}
383
+ .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line;word-break:auto-phrase}
292
384
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
293
385
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
294
386
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -298,32 +390,20 @@ var CSS = `
298
390
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
299
391
  .pkb-demo{max-width:720px;margin:12px auto 4px;display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-family:var(--pk-body);font-size:12px;color:#666}
300
392
  .pkb-demo button{appearance:none;border:1px solid #ccc;background:#f3f3f3;color:#222;border-radius:999px;padding:5px 12px;font-size:12px;cursor:pointer}
301
- @container (max-width:560px){.pkb-board{padding:18px 14px 16px}.pkb-head{flex-direction:column;align-items:stretch;gap:12px}.pkb-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.pkb-pill,.pkb-pill.pkb-sc{min-width:0;padding:5px 4px 4px}.pkb-pill b{font-size:19px}.pkb-title{font-size:26px}.pkb-grid{gap:8px}.pkb-tile{border-radius:12px;box-shadow:3px 3px 0 var(--pk-ink);gap:3px;aspect-ratio:1/1}.pkb-emo{font-size:24px}.pkb-lbl{font-size:10.5px;padding:0 2px}.pkb-stamp{font-size:14px}.pkb-tile.pkb-reach::after{font-size:9px;padding:2px 5px;top:-9px;right:-5px}.pkb-lines .pkb-u{stroke-width:10}.pkb-lines .pkb-o{stroke-width:5}.pkb-banner::before{width:380px;height:380px;margin:-190px 0 0 -190px}.pkb-banner span{font-size:12px}.pkb-medal{width:96px;height:96px;font-size:20px}.pkb-card{padding:20px 14px 16px}.pkb-ptitle{font-size:24px}.pkb-stats b{font-size:17px}.pkb-float.pkb-pt{font-size:17px}.pkb-float.pkb-cmb{font-size:13px}}
393
+ @container (max-width:560px){.pkb-board{padding:18px 14px 16px}.pkb-head{flex-direction:column;align-items:stretch;gap:12px}.pkb-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.pkb-pill,.pkb-pill.pkb-sc{min-width:0;padding:5px 4px 4px}.pkb-pill b{font-size:19px}.pkb-title{font-size:26px}.pkb-grid{gap:max(3px,calc(8px*var(--pk-d)))}.pkb-tile{border-radius:calc(12px*var(--pk-d));box-shadow:3px 3px 0 var(--pk-ink);gap:calc(3px*var(--pk-d));aspect-ratio:1/1}.pkb-emo{font-size:max(13px,calc(24px*var(--pk-s)))}.pkb-lbl{font-size:max(8.5px,calc(10.5px*var(--pk-s)));padding:0 calc(2px*var(--pk-d))}.pkb-stamp{font-size:max(9px,calc(14px*var(--pk-s)))}.pkb-tile.pkb-reach::after{font-size:9px;padding:2px 5px;top:-9px;right:-5px}.pkb-lines .pkb-u{stroke-width:calc(10px*var(--pk-d))}.pkb-lines .pkb-o{stroke-width:calc(5px*var(--pk-d))}.pkb-banner::before{width:380px;height:380px;margin:-190px 0 0 -190px}.pkb-banner span{font-size:12px}.pkb-medal{width:96px;height:96px;font-size:20px}.pkb-card{padding:20px 14px 16px}.pkb-ptitle{font-size:24px}.pkb-stats b{font-size:17px}.pkb-float.pkb-pt{font-size:17px}.pkb-float.pkb-cmb{font-size:13px}}
394
+ @container (max-width:400px){.pkb-board{padding:16px 10px 14px}.pkb-grid{gap:max(3px,calc(6px*var(--pk-d)))}.pkb-lbl{font-size:max(8.5px,calc(10px*var(--pk-s)));padding:0 calc(1px*var(--pk-d))}.pkb-emo{font-size:max(12px,calc(22px*var(--pk-s)))}.pkb-stamp{font-size:max(9px,calc(13px*var(--pk-s)))}.pkb-foot{margin-top:14px;font-size:12px}.pkb-perfect{padding:12px}.pkb-stats{gap:6px}.pkb-stats>div{padding:6px 2px 5px}.pkb-stats small{letter-spacing:.02em}}
395
+ @container (max-width:320px){.pkb-board{padding-inline:8px}.pkb-grid{gap:max(3px,calc(5px*var(--pk-d)))}.pkb-lbl{font-size:max(8.5px,calc(9.5px*var(--pk-s)));padding:0}}
302
396
  @media (prefers-reduced-motion:reduce){.pkb-board.pkb-shake,.pkb-board.pkb-punch,.pkb-flash.pkb-go,.pkb-tile.pkb-hop,.pkb-tile.pkb-squash,.pkb-tile.pkb-reach,.pkb-tile.pkb-line::before,.pkb-pill.pkb-hot,.pkb-bump,.pkb-tile.pkb-reach::after,.pkb-banner::before,.pkb-lines .pkb-draw line,.pkb-kicker.pkb-gold{animation:none}.pkb-banner.pkb-show{animation:pkb-fade 1.8s forwards}.pkb-float{animation:pkb-fade 1s both}.pkb-perfect.pkb-show .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg)}.pkb-card{transition:none}}
303
397
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
304
398
  `;
305
399
 
306
400
  // src/core.ts
307
- var LINES = [
308
- [0, 1, 2, 3],
309
- [4, 5, 6, 7],
310
- [8, 9, 10, 11],
311
- [12, 13, 14, 15],
312
- [0, 4, 8, 12],
313
- [1, 5, 9, 13],
314
- [2, 6, 10, 14],
315
- [3, 7, 11, 15],
316
- [0, 5, 10, 15],
317
- [3, 6, 9, 12]
318
- ];
319
- var DEMO_START = [0, 1, 2, 3, 5, 10];
401
+ var LINES = bingoLines(4);
320
402
  var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"];
321
403
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
322
404
  var COMBO_MS = 2500;
323
405
  var MUTE_KEY = "packing-bingo-muted";
324
406
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
325
- var normalize = (v) => Array.from({ length: 16 }, (_, i) => !!v?.[i]);
326
- var demoStart = () => Array.from({ length: 16 }, (_, i) => DEMO_START.includes(i));
327
407
  function readStore(key) {
328
408
  try {
329
409
  return localStorage.getItem(key);
@@ -352,15 +432,21 @@ function ensureAssets(fonts) {
352
432
  document.head.appendChild(l);
353
433
  }
354
434
  }
355
- function template(items, M, demo) {
356
- const tiles = items.map(
357
- (it, i) => `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="--pkb-gd:${((i >> 2) + (i & 3)) * 0.12}s"><input type="checkbox"><span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`
358
- ).join("");
435
+ function template(L, M, demo) {
436
+ const n = L.size;
437
+ const tiles = Array.from({ length: n * n }, (_, c) => {
438
+ const gd = `--pkb-gd:${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
439
+ if (c === L.center)
440
+ return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
441
+ const it = L.items[L.itemCell.indexOf(c)];
442
+ return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}"><input type="checkbox"><span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`;
443
+ }).join("");
444
+ const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
359
445
  const demoBar = demo ? `<div class="pkb-demo"><span>${esc(M.demoLabel)}</span><button type="button" data-r="dB">${esc(M.demoBingo)}</button><button type="button" data-r="dP">${esc(M.demoPerfect)}</button><button type="button" data-r="dR">${esc(M.demoRestore)}</button></div>` : "";
360
- return `<div class="pkb"><div class="pkb-board">
446
+ return `<div class="pkb" style="${scale}"><div class="pkb-board">
361
447
  <div class="pkb-head"><div>
362
448
  <div class="pkb-tags"><span class="pkb-kicker" data-r="kick">${esc(M.tag)}</span><button type="button" class="pkb-snd" data-r="snd"></button></div>
363
- <h2 class="pkb-title">${esc(M.title)}</h2><p class="pkb-sub">${esc(M.subtitle)}</p></div>
449
+ <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
364
450
  <div class="pkb-side"><div class="pkb-score">
365
451
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
366
452
  <div class="pkb-pill" data-r="bp"><small>${esc(M.bingo)}</small><b data-r="bng"></b></div>
@@ -371,20 +457,28 @@ function template(items, M, demo) {
371
457
  <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div>
372
458
  <div class="pkb-flash" data-r="flash"></div>
373
459
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
374
- <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody">${esc(M.perfectBody)}</p>
460
+ <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody" data-r="pbody">${esc(perfectText(M, L.items.length))}</p>
375
461
  <div class="pkb-stats"><div><small>${esc(M.statScore)}</small><b data-r="sScore">0</b></div><div><small>${esc(M.statCombo)}</small><b data-r="sCombo">\xD70</b></div><div><small>${esc(M.statLines)}</small><b data-r="sBingo">0</b></div></div>
376
462
  <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
377
463
  <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
378
464
  }
465
+ var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
379
466
  function createPackingBingo(container, options = {}) {
380
- const items = options.items ?? DEFAULT_ITEMS;
381
- if (items.length !== 16) throw new Error(`[packing-bingo] "items" must have exactly 16 entries (got ${items.length}).`);
467
+ const L = resolveLayout(options);
468
+ const { items, lines, cellLines } = L;
469
+ const n = L.size, N = items.length;
470
+ const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
471
+ const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
472
+ const demoItems = new Set(L.itemCell.flatMap((c, i) => demoCells.includes(c) ? [i] : []));
473
+ const demoStart = () => Array.from({ length: N }, (_, i) => demoItems.has(i));
474
+ const far = (c) => Math.floor(c / n) + c % n;
475
+ const dScale = Math.min(1, 4 / n);
382
476
  const M = { ...ja, ...options.messages };
383
477
  if (options.title != null) M.title = options.title;
384
478
  if (options.tag != null) M.tag = options.tag;
385
479
  if (options.subtitle != null) M.subtitle = options.subtitle;
386
480
  const controlled = !!options.controlled;
387
- const storageKey = controlled ? null : options.storageKey ?? null;
481
+ const storageKey = resolveStorageKey(options);
388
482
  const demo = !!options.demo;
389
483
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
390
484
  let dead = false;
@@ -399,7 +493,7 @@ function createPackingBingo(container, options = {}) {
399
493
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
400
494
  let gen = 0;
401
495
  ensureAssets(options.fonts !== false);
402
- container.innerHTML = template(items, M, demo);
496
+ container.innerHTML = template(L, M, demo);
403
497
  const $ = (r) => {
404
498
  const el = container.querySelector(`[data-r="${r}"]`);
405
499
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -411,7 +505,9 @@ function createPackingBingo(container, options = {}) {
411
505
  const svg = container.querySelector(".pkb-lines");
412
506
  const cv = container.querySelector(".pkb-fx");
413
507
  const ctx = cv.getContext("2d");
414
- const tiles = Array.from(grid.children);
508
+ const cellTiles = Array.from(grid.children);
509
+ const tiles = L.itemCell.map((c) => cellTiles[c]);
510
+ const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
415
511
  const ins = tiles.map((t) => t.querySelector("input"));
416
512
  const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
417
513
  let checked = normalize(void 0);
@@ -446,14 +542,14 @@ function createPackingBingo(container, options = {}) {
446
542
  cv.width = Math.round(BW * dpr);
447
543
  cv.height = Math.round(BH * dpr);
448
544
  };
449
- const piece = (x, y, vx, vy, g, s, life, shape) => ({
545
+ const piece = (x, y, vx, vy, g, s, life, shape2) => ({
450
546
  x,
451
547
  y,
452
548
  vx,
453
549
  vy,
454
550
  g,
455
551
  life,
456
- shape,
552
+ shape: shape2,
457
553
  w: (5 + Math.random() * 6) * s,
458
554
  h: (3 + Math.random() * 5) * s,
459
555
  r: Math.random() * 6,
@@ -463,9 +559,9 @@ function createPackingBingo(container, options = {}) {
463
559
  const go = () => {
464
560
  if (!raf && ctx) raf = requestAnimationFrame(tick);
465
561
  };
466
- function burst(x, y, n, o = {}) {
562
+ function burst(x, y, n2, o = {}) {
467
563
  if (calm) return;
468
- for (let i = 0; i < n; i++) {
564
+ for (let i = 0; i < n2; i++) {
469
565
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
470
566
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
471
567
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
@@ -556,11 +652,11 @@ function createPackingBingo(container, options = {}) {
556
652
  void el.offsetWidth;
557
653
  el.classList.add(cls);
558
654
  };
559
- const hop = (i, d) => {
655
+ const hop = (t, d) => {
560
656
  if (calm) return;
561
- tiles[i].classList.remove("pkb-squash");
562
- tiles[i].style.setProperty("--pkb-d", d + "ms");
563
- replay(tiles[i], "pkb-hop");
657
+ t.classList.remove("pkb-squash");
658
+ t.style.setProperty("--pkb-d", d + "ms");
659
+ replay(t, "pkb-hop");
564
660
  };
565
661
  grid.addEventListener("animationend", (e) => {
566
662
  const t = e.target;
@@ -608,8 +704,8 @@ function createPackingBingo(container, options = {}) {
608
704
  st(t0);
609
705
  if (!calm) replay($("sp"), "pkb-bump");
610
706
  }
611
- const showGain = (n, x, y, cls = "pt") => {
612
- floatText(`+${n}`, x, y, cls);
707
+ const showGain = (pts, x, y, cls = "pt") => {
708
+ floatText(`+${pts}`, x, y, cls);
613
709
  countScore();
614
710
  };
615
711
  function countUp(el, a, b, d, fmt, ticks = false) {
@@ -629,15 +725,14 @@ function createPackingBingo(container, options = {}) {
629
725
  };
630
726
  st(t0);
631
727
  }
632
- const ctr = (i) => {
633
- const t = tiles[i];
728
+ const ctr = (t) => {
634
729
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
635
730
  };
636
731
  function drawLines(fresh = []) {
637
732
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
638
733
  svg.innerHTML = cur.map((k) => {
639
- const a = ctr(LINES[k][0]), b = ctr(LINES[k][3]);
640
- const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1, ex = dx / d * 26, ey = dy / d * 26;
734
+ const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
735
+ const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1, r = 26 * dScale, ex = dx / d * r, ey = dy / d * r;
641
736
  const at = `x1="${a.x - ex}" y1="${a.y - ey}" x2="${b.x + ex}" y2="${b.y + ey}" pathLength="1"`;
642
737
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
643
738
  }).join("");
@@ -684,29 +779,29 @@ function createPackingBingo(container, options = {}) {
684
779
  later(() => sfx.reach(), 150);
685
780
  }
686
781
  function bingo(fresh, pts) {
687
- const n = fresh.length;
688
- const seq = [...new Set(fresh.flatMap((k) => LINES[k]))];
782
+ const nl = fresh.length;
783
+ const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
689
784
  const step = Math.min(70, 300 / seq.length);
690
785
  seq.forEach(
691
- (i, j) => later(() => {
692
- hop(i, 0);
693
- const p = rel(tiles[i]);
786
+ (t, j) => later(() => {
787
+ hop(t, 0);
788
+ const p = rel(t);
694
789
  burst(p.x, p.y, 14, { speed: 5, g: 0.15, life: 30, size: 0.7, star: 0.8 });
695
790
  sfx.note(j + 3);
696
791
  }, j * step)
697
792
  );
698
793
  later(() => {
699
- $("bT").textContent = n >= 3 ? M.bannerTriple : n === 2 ? M.bannerDouble : M.bannerSingle;
700
- $("bS").textContent = n > 1 ? M.bannerMulti(n) : M.bannerNth(cur.length);
794
+ $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
795
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
701
796
  replay($("banner"), "pkb-show");
702
797
  if (!calm) {
703
798
  replay($("flash"), "pkb-go");
704
799
  replay(board, "pkb-punch");
705
800
  }
706
801
  const c = rel(gw);
707
- burst(c.x, c.y, n > 1 ? 180 : 110, { speed: n > 1 ? 16 : 13, star: 0.3 });
708
- sfx.bingo(n);
709
- buzz(n > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
802
+ burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
803
+ sfx.bingo(nl);
804
+ buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
710
805
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
711
806
  }, seq.length * step + 60);
712
807
  }
@@ -714,9 +809,10 @@ function createPackingBingo(container, options = {}) {
714
809
  const bonus = gotP ? 0 : 1e3;
715
810
  gotP = true;
716
811
  score += bonus;
717
- tiles.forEach((_, i) => hop(i, ((i >> 2) + (i & 3)) * 60));
812
+ cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
718
813
  sfx.perfect();
719
814
  if (!calm) replay($("flash"), "pkb-go");
815
+ $("pbody").textContent = perfectText(M, N);
720
816
  $("sScore").textContent = "0";
721
817
  $("sCombo").textContent = "\xD70";
722
818
  $("sBingo").textContent = "0";
@@ -766,27 +862,28 @@ function createPackingBingo(container, options = {}) {
766
862
  function render(src, quiet) {
767
863
  const events = [];
768
864
  const on = checked, c = on.filter(Boolean).length;
769
- cur = LINES.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
865
+ cur = lines.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
770
866
  const fresh = cur.filter((k) => !prevLines.has(k));
771
867
  if ([...prevLines].some((k) => !cur.includes(k))) {
772
868
  cancelFx();
773
- if (c < 16) perfectEl.classList.remove("pkb-show");
869
+ if (c < N) perfectEl.classList.remove("pkb-show");
774
870
  }
775
- const hit = new Set(cur.flatMap((k) => LINES[k]));
871
+ const hit = new Set(cur.flatMap((k) => cellLines[k]));
776
872
  const reach = /* @__PURE__ */ new Set();
777
- for (const l of LINES) {
873
+ for (const l of lines) {
778
874
  const m = l.filter((i) => !on[i]);
779
875
  if (m.length === 1) reach.add(m[0]);
780
876
  }
781
877
  tiles.forEach((t, i) => {
782
878
  t.classList.toggle("pkb-on", on[i]);
783
- t.classList.toggle("pkb-line", hit.has(i));
879
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
784
880
  t.classList.toggle("pkb-reach", reach.has(i));
785
881
  });
786
- board.classList.toggle("pkb-all", c === 16);
787
- kick.classList.toggle("pkb-gold", c === 16);
788
- kick.textContent = c === 16 ? M.perfectTag : M.tag;
789
- $("cnt").textContent = `${c}/16`;
882
+ freeTile?.classList.toggle("pkb-line", hit.has(L.center));
883
+ board.classList.toggle("pkb-all", c === N);
884
+ kick.classList.toggle("pkb-gold", c === N);
885
+ kick.textContent = c === N ? M.perfectTag : M.tag;
886
+ $("cnt").textContent = `${c}/${N}`;
790
887
  $("bng").textContent = String(cur.length);
791
888
  const bp = $("bp");
792
889
  if (cur.length > lastN) {
@@ -803,7 +900,7 @@ function createPackingBingo(container, options = {}) {
803
900
  score += linePts;
804
901
  if (quiet) {
805
902
  if (linePts) countScore();
806
- } else if (c === 16 && !prevAll) perfect(events);
903
+ } else if (c === N && !prevAll) perfect(events);
807
904
  else if (fresh.length) {
808
905
  bingo(fresh, linePts);
809
906
  const e = { newLines: fresh, totalLines: cur.length, score };
@@ -814,9 +911,9 @@ function createPackingBingo(container, options = {}) {
814
911
  }
815
912
  prevLines = new Set(cur);
816
913
  prevReach = reach;
817
- prevAll = c === 16;
914
+ prevAll = c === N;
818
915
  const labels = [...reach].map((i) => items[i].label);
819
- $("msg").textContent = c === 16 ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, 16 - c) : M.statusStart(16 - c);
916
+ $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
820
917
  return events;
821
918
  }
822
919
  const flush = (events) => {
@@ -843,7 +940,7 @@ function createPackingBingo(container, options = {}) {
843
940
  got.clear();
844
941
  gotL.clear();
845
942
  checked.forEach((v, i) => v && got.add(i));
846
- LINES.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
943
+ lines.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
847
944
  prevLines = new Set(gotL);
848
945
  prevReach = /* @__PURE__ */ new Set();
849
946
  prevAll = checked.every(Boolean);
@@ -903,12 +1000,13 @@ function createPackingBingo(container, options = {}) {
903
1000
  }
904
1001
  };
905
1002
  $("dB").addEventListener("click", () => {
906
- const open = () => LINES.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
907
- const pick = checked.every(Boolean) ? [...LINES[0]] : open();
1003
+ const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1004
+ const pick = checked.every(Boolean) ? [...lines[0]] : open();
908
1005
  void run(pick.map((i) => ({ i, delay: 320 })));
909
1006
  });
910
1007
  $("dP").addEventListener("click", () => {
911
- const miss = checked.every(Boolean) ? tiles.map((_, i) => i) : tiles.map((_, i) => i).filter((i) => !checked[i]);
1008
+ const all = items.map((_, i) => i);
1009
+ const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
912
1010
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
913
1011
  });
914
1012
  $("dR").addEventListener("click", () => {
@@ -936,14 +1034,14 @@ function createPackingBingo(container, options = {}) {
936
1034
  return {
937
1035
  setChecked(next, opts = {}) {
938
1036
  if (dead) return;
939
- const n = normalize(next);
940
- const changed = n.flatMap((v, i) => v !== checked[i] ? [i] : []);
1037
+ const n2 = normalize(next);
1038
+ const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
941
1039
  if (!changed.length) return;
942
1040
  if (!opts.animate || changed.length !== 1) {
943
- baseline(n);
1041
+ baseline(n2);
944
1042
  return;
945
1043
  }
946
- flush(apply(n, changed[0], false));
1044
+ flush(apply(n2, changed[0], false));
947
1045
  },
948
1046
  reset() {
949
1047
  if (!dead) request(normalize(void 0), null, { reset: true });
@@ -952,7 +1050,7 @@ function createPackingBingo(container, options = {}) {
952
1050
  if (!dead) render(null, true);
953
1051
  },
954
1052
  getState() {
955
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean) };
1053
+ return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean), size: n, free: L.free };
956
1054
  },
957
1055
  destroy() {
958
1056
  if (dead) return;
@@ -970,8 +1068,10 @@ function createPackingBingo(container, options = {}) {
970
1068
 
971
1069
  export {
972
1070
  DEFAULT_ITEMS,
1071
+ bingoLines,
973
1072
  ja,
974
1073
  en,
1074
+ resolveStorageKey,
975
1075
  LINES,
976
1076
  createPackingBingo
977
1077
  };