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.
package/dist/core.cjs CHANGED
@@ -23,6 +23,7 @@ var core_exports = {};
23
23
  __export(core_exports, {
24
24
  DEFAULT_ITEMS: () => DEFAULT_ITEMS,
25
25
  LINES: () => LINES,
26
+ bingoLines: () => bingoLines,
26
27
  createPackingBingo: () => createPackingBingo,
27
28
  en: () => en,
28
29
  ja: () => ja
@@ -31,29 +32,98 @@ module.exports = __toCommonJS(core_exports);
31
32
 
32
33
  // src/items.ts
33
34
  var DEFAULT_ITEMS = [
34
- { emoji: "\u{1F4D6}", label: "\u65C5\u306E\u3057\u304A\u308A" },
35
- { emoji: "\u{1FAAA}", label: "\u751F\u5F92\u624B\u5E33" },
36
- { emoji: "\u{1F3E5}", label: "\u4FDD\u967A\u8A3C\u30B3\u30D4\u30FC" },
37
- { emoji: "\u{1F45B}", label: "\u304A\u3053\u3065\u304B\u3044" },
38
- { emoji: "\u{1F455}", label: "\u7740\u66FF\u3048" },
39
- { emoji: "\u{1F319}", label: "\u30D1\u30B8\u30E3\u30DE" },
40
- { emoji: "\u{1F9E5}", label: "\u4E0A\u7740" },
41
- { emoji: "\u{1FAA5}", label: "\u6D17\u9762\u7528\u5177" },
42
- { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
43
- { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1\u30FB\u30C6\u30A3\u30C3\u30B7\u30E5" },
35
+ { emoji: "\u{1F45B}", label: "\u8CA1\u5E03" },
36
+ { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB" },
37
+ { emoji: "\u{1F50B}", label: "\u30E2\u30D0\u30A4\u30EB\u200B\u5145\u96FB\u5668" },
38
+ { emoji: "\u{1F4CB}", label: "\u5065\u5EB7\u8ABF\u67FB\u7968" },
39
+ { emoji: "\u{1FAAA}", label: "\u30DE\u30A4\u30CA\u30AB\u30FC\u30C9\u200B\u307E\u305F\u306F\u4FDD\u967A\u8A3C" },
40
+ { emoji: "\u{1F4D6}", label: "\u3057\u304A\u308A" },
44
41
  { emoji: "\u{1F637}", label: "\u30DE\u30B9\u30AF" },
45
- { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC\u30FB\u9154\u3044\u6B62\u3081" },
46
- { emoji: "\u2602\uFE0F", label: "\u6298\u308A\u305F\u305F\u307F\u5098" },
47
- { emoji: "\u270F\uFE0F", label: "\u7B46\u8A18\u7528\u5177" },
42
+ { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1" },
43
+ { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC" },
48
44
  { emoji: "\u{1F6CD}\uFE0F", label: "\u30D3\u30CB\u30FC\u30EB\u888B" },
49
- { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB\u30FB\u5145\u96FB\u5668" }
45
+ { emoji: "\u{1F9E4}", label: "\u8ECD\u624B\uFF08\u5B66\u6821\u652F\u7D66\uFF09" },
46
+ { emoji: "\u{1F526}", label: "\u30D8\u30C3\u30C9\u30E9\u30A4\u30C8" },
47
+ { emoji: "\u{1F455}", label: "\u30B8\u30E3\u30FC\u30B8" },
48
+ { emoji: "\u2614", label: "\u96E8\u5177" },
49
+ { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
50
+ { emoji: "\u{1F9F4}", label: "\u65E5\u713C\u3051\u6B62\u3081" }
50
51
  ];
51
52
 
53
+ // src/layout.ts
54
+ var MIN_SIZE = 2;
55
+ var MAX_SIZE = 10;
56
+ var fail = (msg) => {
57
+ throw new Error(`[packing-bingo] ${msg}`);
58
+ };
59
+ function layoutItems(options) {
60
+ if (options.items) return options.items;
61
+ const n = options.size;
62
+ if (n == null || !Number.isInteger(n) || n < MIN_SIZE) return DEFAULT_ITEMS;
63
+ return DEFAULT_ITEMS.slice(0, n * n - (options.free ? 1 : 0));
64
+ }
65
+ function checkSize(size, free) {
66
+ if (!Number.isInteger(size) || size < MIN_SIZE || size > MAX_SIZE)
67
+ fail(`"size" must be a whole number from ${MIN_SIZE} to ${MAX_SIZE} (got ${size}).`);
68
+ 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}).`);
69
+ }
70
+ function shape(n, free) {
71
+ const seq = (f) => Array.from({ length: n }, (_, k) => f(k));
72
+ const cellLines = [
73
+ ...seq((r) => seq((c) => r * n + c)),
74
+ // rows
75
+ ...seq((c) => seq((r) => r * n + c)),
76
+ // columns
77
+ seq((k) => k * (n + 1)),
78
+ // top left to bottom right
79
+ seq((k) => (k + 1) * (n - 1))
80
+ // top right to bottom left
81
+ ];
82
+ const center = free ? (n * n - 1) / 2 : -1;
83
+ const toItem = (c) => free && c > center ? c - 1 : c;
84
+ return { center, cellLines, lines: cellLines.map((l) => l.filter((c) => c !== center).map(toItem)) };
85
+ }
86
+ function resolveLayout(options) {
87
+ const items = layoutItems(options);
88
+ const free = !!options.free;
89
+ let size = options.size;
90
+ if (size == null) {
91
+ const len = items.length;
92
+ const sideOf = (cells) => {
93
+ const r = Math.round(Math.sqrt(cells));
94
+ return r * r === cells && r >= MIN_SIZE && r <= MAX_SIZE ? r : 0;
95
+ };
96
+ size = sideOf(len + (free ? 1 : 0));
97
+ if (!size) {
98
+ const other = sideOf(len + (free ? 0 : 1));
99
+ 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` : "";
100
+ fail(
101
+ 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}).`
102
+ );
103
+ }
104
+ }
105
+ checkSize(size, free);
106
+ const want = size * size - (free ? 1 : 0);
107
+ if (items.length !== want) {
108
+ const card = `a ${size}\xD7${size} card${free ? " with a FREE center" : ""}`;
109
+ fail(
110
+ 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".`
111
+ );
112
+ }
113
+ const { center, cellLines, lines } = shape(size, free);
114
+ const itemCell = items.map((_, i) => free && i >= center ? i + 1 : i);
115
+ return { size, free, items, itemCell, center, cellLines, lines };
116
+ }
117
+ function bingoLines(size, free = false) {
118
+ checkSize(size, free);
119
+ return shape(size, free).lines;
120
+ }
121
+
52
122
  // src/messages.ts
53
123
  var ja = {
54
124
  title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
55
125
  tag: "\u4FEE\u5B66\u65C5\u884C",
56
- subtitle: "\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30F3\u30D7\u3002\u30BF\u30C6\u30FB\u30E8\u30B3\u30FB\u30CA\u30CA\u30E1\u3067\u30D3\u30F3\u30B4\uFF01",
126
+ subtitle: "",
57
127
  packed: "\u305D\u308D\u3063\u305F",
58
128
  bingo: "BINGO",
59
129
  score: "SCORE",
@@ -63,6 +133,7 @@ var ja = {
63
133
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
64
134
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
65
135
  stamp: "OK",
136
+ free: "FREE",
66
137
  reachBadge: "\u30EA\u30FC\u30C1",
67
138
  reachPop: "\u30EA\u30FC\u30C1!",
68
139
  bannerSingle: "BINGO!",
@@ -70,14 +141,15 @@ var ja = {
70
141
  bannerTriple: "\u30C8\u30EA\u30D7\u30EB\u30D3\u30F3\u30B4!",
71
142
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
72
143
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
73
- statusStart: (left) => `\u306E\u3053\u308A ${left} \u3053\u3002\u307E\u305A\u306F1\u30E9\u30A4\u30F3\u305D\u308D\u3048\u3088\u3046`,
144
+ statusStart: () => "",
74
145
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
75
146
  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`,
76
147
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
77
148
  perfectTag: "\u{1F3C6} PERFECT",
78
149
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
79
150
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
80
- 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",
151
+ perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
152
+ \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
81
153
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
82
154
  statScore: "SCORE",
83
155
  statCombo: "MAX COMBO",
@@ -91,7 +163,7 @@ var ja = {
91
163
  var en = {
92
164
  title: "Packing Bingo",
93
165
  tag: "School trip",
94
- subtitle: "Tap to stamp. Rows, columns and diagonals all count!",
166
+ subtitle: "",
95
167
  packed: "PACKED",
96
168
  bingo: "BINGO",
97
169
  score: "SCORE",
@@ -101,6 +173,7 @@ var en = {
101
173
  soundOn: "\u{1F50A} Sound on",
102
174
  soundOff: "\u{1F507} Muted",
103
175
  stamp: "OK",
176
+ free: "FREE",
104
177
  reachBadge: "REACH",
105
178
  reachPop: "Reach!",
106
179
  bannerSingle: "BINGO!",
@@ -108,14 +181,15 @@ var en = {
108
181
  bannerTriple: "TRIPLE BINGO!",
109
182
  bannerNth: (n) => `Line ${n}!`,
110
183
  bannerMulti: (n) => `${n} lines at once!`,
111
- statusStart: (left) => `${left} to go. Finish a line first!`,
184
+ statusStart: () => "",
112
185
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
113
186
  statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
114
187
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
115
188
  perfectTag: "\u{1F3C6} PERFECT",
116
189
  perfectStamp: "ALL\nSET",
117
190
  perfectTitle: "Perfect!",
118
- perfectBody: "All 16 packed!\nHave a great trip!",
191
+ perfectBody: (n) => `All ${n} packed!
192
+ Have a great trip!`,
119
193
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
120
194
  statScore: "SCORE",
121
195
  statCombo: "MAX COMBO",
@@ -224,17 +298,34 @@ function createSound(isMuted) {
224
298
  };
225
299
  }
226
300
 
301
+ // src/storage-key.ts
302
+ function resolveStorageKey(options) {
303
+ if (options.controlled) return null;
304
+ if (options.storageKey !== void 0) return options.storageKey || null;
305
+ if (options.demo) return null;
306
+ return itemsKey(layoutItems(options));
307
+ }
308
+ function itemsKey(items) {
309
+ const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
310
+ let h = 2166136261;
311
+ for (let i = 0; i < text.length; i++) {
312
+ h ^= text.charCodeAt(i);
313
+ h = Math.imul(h, 16777619);
314
+ }
315
+ return `packing-bingo:${(h >>> 0).toString(36)}`;
316
+ }
317
+
227
318
  // src/styles.ts
228
319
  var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
229
320
  var CSS = `
230
- .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%}
321
+ .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%}
231
322
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
232
323
  .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}
233
324
  .pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
234
325
  @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)}}
235
326
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
236
327
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
237
- .pkb-head{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}
328
+ .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
238
329
  .pkb-tags{display:flex;gap:8px;align-items:center}
239
330
  .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}
240
331
  .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}
@@ -242,7 +333,7 @@ var CSS = `
242
333
  .pkb button{font-family:var(--pk-body)}
243
334
  .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}
244
335
  .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}
245
- .pkb-sub{font-weight:800;font-size:13px;margin-top:4px}
336
+ .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
246
337
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
247
338
  .pkb-score{display:flex;gap:10px}
248
339
  .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)}
@@ -261,16 +352,18 @@ var CSS = `
261
352
  .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
262
353
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
263
354
  .pkb-gw{position:relative}
264
- .pkb-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
265
- .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}
266
- .pkb-tile:active{transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
355
+ .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
356
+ .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}
357
+ .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
267
358
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
268
359
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
269
- .pkb-emo{font-size:32px;line-height:1;transition:opacity .2s}
270
- .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}
271
- .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}
360
+ .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
361
+ .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}
362
+ .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}
272
363
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
273
364
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
365
+ .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
366
+ .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)}
274
367
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
275
368
  .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}
276
369
  @keyframes pkb-glint{0%{background-position:200% 0}30%,100%{background-position:-100% 0}}
@@ -287,11 +380,11 @@ var CSS = `
287
380
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
288
381
  .pkb-board.pkb-all .pkb-lines{opacity:0}
289
382
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
290
- .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:13}
291
- .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:6}
383
+ .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:calc(13px*var(--pk-d))}
384
+ .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
292
385
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
293
386
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
294
- .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px}
387
+ .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}
295
388
  .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}
296
389
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
297
390
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
@@ -306,7 +399,7 @@ var CSS = `
306
399
  .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}
307
400
  .pkb-float.pkb-pt{font-size:22px;color:var(--pk-paper)}
308
401
  .pkb-float.pkb-cmb{font-size:17px}
309
- .pkb-float.pkb-big{font-size:clamp(20px,6cqi,34px);color:#FFE066}
402
+ .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
310
403
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
311
404
  @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)}}
312
405
  .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}
@@ -317,7 +410,7 @@ var CSS = `
317
410
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
318
411
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
319
412
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
320
- .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line}
413
+ .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}
321
414
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
322
415
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
323
416
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -327,32 +420,20 @@ var CSS = `
327
420
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
328
421
  .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}
329
422
  .pkb-demo button{appearance:none;border:1px solid #ccc;background:#f3f3f3;color:#222;border-radius:999px;padding:5px 12px;font-size:12px;cursor:pointer}
330
- @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}}
423
+ @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}}
424
+ @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}}
425
+ @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}}
331
426
  @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}}
332
427
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
333
428
  `;
334
429
 
335
430
  // src/core.ts
336
- var LINES = [
337
- [0, 1, 2, 3],
338
- [4, 5, 6, 7],
339
- [8, 9, 10, 11],
340
- [12, 13, 14, 15],
341
- [0, 4, 8, 12],
342
- [1, 5, 9, 13],
343
- [2, 6, 10, 14],
344
- [3, 7, 11, 15],
345
- [0, 5, 10, 15],
346
- [3, 6, 9, 12]
347
- ];
348
- var DEMO_START = [0, 1, 2, 3, 5, 10];
431
+ var LINES = bingoLines(4);
349
432
  var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"];
350
433
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
351
434
  var COMBO_MS = 2500;
352
435
  var MUTE_KEY = "packing-bingo-muted";
353
436
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
354
- var normalize = (v) => Array.from({ length: 16 }, (_, i) => !!v?.[i]);
355
- var demoStart = () => Array.from({ length: 16 }, (_, i) => DEMO_START.includes(i));
356
437
  function readStore(key) {
357
438
  try {
358
439
  return localStorage.getItem(key);
@@ -381,15 +462,21 @@ function ensureAssets(fonts) {
381
462
  document.head.appendChild(l);
382
463
  }
383
464
  }
384
- function template(items, M, demo) {
385
- const tiles = items.map(
386
- (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>`
387
- ).join("");
465
+ function template(L, M, demo) {
466
+ const n = L.size;
467
+ const tiles = Array.from({ length: n * n }, (_, c) => {
468
+ const gd = `--pkb-gd:${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
469
+ if (c === L.center)
470
+ return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
471
+ const it = L.items[L.itemCell.indexOf(c)];
472
+ 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>`;
473
+ }).join("");
474
+ const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
388
475
  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>` : "";
389
- return `<div class="pkb"><div class="pkb-board">
476
+ return `<div class="pkb" style="${scale}"><div class="pkb-board">
390
477
  <div class="pkb-head"><div>
391
478
  <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>
392
- <h2 class="pkb-title">${esc(M.title)}</h2><p class="pkb-sub">${esc(M.subtitle)}</p></div>
479
+ <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
393
480
  <div class="pkb-side"><div class="pkb-score">
394
481
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
395
482
  <div class="pkb-pill" data-r="bp"><small>${esc(M.bingo)}</small><b data-r="bng"></b></div>
@@ -400,20 +487,28 @@ function template(items, M, demo) {
400
487
  <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>
401
488
  <div class="pkb-flash" data-r="flash"></div>
402
489
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
403
- <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody">${esc(M.perfectBody)}</p>
490
+ <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>
404
491
  <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>
405
492
  <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
406
493
  <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
407
494
  }
495
+ var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
408
496
  function createPackingBingo(container, options = {}) {
409
- const items = options.items ?? DEFAULT_ITEMS;
410
- if (items.length !== 16) throw new Error(`[packing-bingo] "items" must have exactly 16 entries (got ${items.length}).`);
497
+ const L = resolveLayout(options);
498
+ const { items, lines, cellLines } = L;
499
+ const n = L.size, N = items.length;
500
+ const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
501
+ const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
502
+ const demoItems = new Set(L.itemCell.flatMap((c, i) => demoCells.includes(c) ? [i] : []));
503
+ const demoStart = () => Array.from({ length: N }, (_, i) => demoItems.has(i));
504
+ const far = (c) => Math.floor(c / n) + c % n;
505
+ const dScale = Math.min(1, 4 / n);
411
506
  const M = { ...ja, ...options.messages };
412
507
  if (options.title != null) M.title = options.title;
413
508
  if (options.tag != null) M.tag = options.tag;
414
509
  if (options.subtitle != null) M.subtitle = options.subtitle;
415
510
  const controlled = !!options.controlled;
416
- const storageKey = controlled ? null : options.storageKey ?? null;
511
+ const storageKey = resolveStorageKey(options);
417
512
  const demo = !!options.demo;
418
513
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
419
514
  let dead = false;
@@ -428,7 +523,7 @@ function createPackingBingo(container, options = {}) {
428
523
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
429
524
  let gen = 0;
430
525
  ensureAssets(options.fonts !== false);
431
- container.innerHTML = template(items, M, demo);
526
+ container.innerHTML = template(L, M, demo);
432
527
  const $ = (r) => {
433
528
  const el = container.querySelector(`[data-r="${r}"]`);
434
529
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -440,7 +535,9 @@ function createPackingBingo(container, options = {}) {
440
535
  const svg = container.querySelector(".pkb-lines");
441
536
  const cv = container.querySelector(".pkb-fx");
442
537
  const ctx = cv.getContext("2d");
443
- const tiles = Array.from(grid.children);
538
+ const cellTiles = Array.from(grid.children);
539
+ const tiles = L.itemCell.map((c) => cellTiles[c]);
540
+ const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
444
541
  const ins = tiles.map((t) => t.querySelector("input"));
445
542
  const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
446
543
  let checked = normalize(void 0);
@@ -475,14 +572,14 @@ function createPackingBingo(container, options = {}) {
475
572
  cv.width = Math.round(BW * dpr);
476
573
  cv.height = Math.round(BH * dpr);
477
574
  };
478
- const piece = (x, y, vx, vy, g, s, life, shape) => ({
575
+ const piece = (x, y, vx, vy, g, s, life, shape2) => ({
479
576
  x,
480
577
  y,
481
578
  vx,
482
579
  vy,
483
580
  g,
484
581
  life,
485
- shape,
582
+ shape: shape2,
486
583
  w: (5 + Math.random() * 6) * s,
487
584
  h: (3 + Math.random() * 5) * s,
488
585
  r: Math.random() * 6,
@@ -492,9 +589,9 @@ function createPackingBingo(container, options = {}) {
492
589
  const go = () => {
493
590
  if (!raf && ctx) raf = requestAnimationFrame(tick);
494
591
  };
495
- function burst(x, y, n, o = {}) {
592
+ function burst(x, y, n2, o = {}) {
496
593
  if (calm) return;
497
- for (let i = 0; i < n; i++) {
594
+ for (let i = 0; i < n2; i++) {
498
595
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
499
596
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
500
597
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
@@ -585,11 +682,11 @@ function createPackingBingo(container, options = {}) {
585
682
  void el.offsetWidth;
586
683
  el.classList.add(cls);
587
684
  };
588
- const hop = (i, d) => {
685
+ const hop = (t, d) => {
589
686
  if (calm) return;
590
- tiles[i].classList.remove("pkb-squash");
591
- tiles[i].style.setProperty("--pkb-d", d + "ms");
592
- replay(tiles[i], "pkb-hop");
687
+ t.classList.remove("pkb-squash");
688
+ t.style.setProperty("--pkb-d", d + "ms");
689
+ replay(t, "pkb-hop");
593
690
  };
594
691
  grid.addEventListener("animationend", (e) => {
595
692
  const t = e.target;
@@ -637,8 +734,8 @@ function createPackingBingo(container, options = {}) {
637
734
  st(t0);
638
735
  if (!calm) replay($("sp"), "pkb-bump");
639
736
  }
640
- const showGain = (n, x, y, cls = "pt") => {
641
- floatText(`+${n}`, x, y, cls);
737
+ const showGain = (pts, x, y, cls = "pt") => {
738
+ floatText(`+${pts}`, x, y, cls);
642
739
  countScore();
643
740
  };
644
741
  function countUp(el, a, b, d, fmt, ticks = false) {
@@ -658,15 +755,14 @@ function createPackingBingo(container, options = {}) {
658
755
  };
659
756
  st(t0);
660
757
  }
661
- const ctr = (i) => {
662
- const t = tiles[i];
758
+ const ctr = (t) => {
663
759
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
664
760
  };
665
761
  function drawLines(fresh = []) {
666
762
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
667
763
  svg.innerHTML = cur.map((k) => {
668
- const a = ctr(LINES[k][0]), b = ctr(LINES[k][3]);
669
- 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;
764
+ const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
765
+ 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;
670
766
  const at = `x1="${a.x - ex}" y1="${a.y - ey}" x2="${b.x + ex}" y2="${b.y + ey}" pathLength="1"`;
671
767
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
672
768
  }).join("");
@@ -713,29 +809,29 @@ function createPackingBingo(container, options = {}) {
713
809
  later(() => sfx.reach(), 150);
714
810
  }
715
811
  function bingo(fresh, pts) {
716
- const n = fresh.length;
717
- const seq = [...new Set(fresh.flatMap((k) => LINES[k]))];
812
+ const nl = fresh.length;
813
+ const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
718
814
  const step = Math.min(70, 300 / seq.length);
719
815
  seq.forEach(
720
- (i, j) => later(() => {
721
- hop(i, 0);
722
- const p = rel(tiles[i]);
816
+ (t, j) => later(() => {
817
+ hop(t, 0);
818
+ const p = rel(t);
723
819
  burst(p.x, p.y, 14, { speed: 5, g: 0.15, life: 30, size: 0.7, star: 0.8 });
724
820
  sfx.note(j + 3);
725
821
  }, j * step)
726
822
  );
727
823
  later(() => {
728
- $("bT").textContent = n >= 3 ? M.bannerTriple : n === 2 ? M.bannerDouble : M.bannerSingle;
729
- $("bS").textContent = n > 1 ? M.bannerMulti(n) : M.bannerNth(cur.length);
824
+ $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
825
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
730
826
  replay($("banner"), "pkb-show");
731
827
  if (!calm) {
732
828
  replay($("flash"), "pkb-go");
733
829
  replay(board, "pkb-punch");
734
830
  }
735
831
  const c = rel(gw);
736
- burst(c.x, c.y, n > 1 ? 180 : 110, { speed: n > 1 ? 16 : 13, star: 0.3 });
737
- sfx.bingo(n);
738
- buzz(n > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
832
+ burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
833
+ sfx.bingo(nl);
834
+ buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
739
835
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
740
836
  }, seq.length * step + 60);
741
837
  }
@@ -743,9 +839,10 @@ function createPackingBingo(container, options = {}) {
743
839
  const bonus = gotP ? 0 : 1e3;
744
840
  gotP = true;
745
841
  score += bonus;
746
- tiles.forEach((_, i) => hop(i, ((i >> 2) + (i & 3)) * 60));
842
+ cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
747
843
  sfx.perfect();
748
844
  if (!calm) replay($("flash"), "pkb-go");
845
+ $("pbody").textContent = perfectText(M, N);
749
846
  $("sScore").textContent = "0";
750
847
  $("sCombo").textContent = "\xD70";
751
848
  $("sBingo").textContent = "0";
@@ -795,27 +892,28 @@ function createPackingBingo(container, options = {}) {
795
892
  function render(src, quiet) {
796
893
  const events = [];
797
894
  const on = checked, c = on.filter(Boolean).length;
798
- cur = LINES.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
895
+ cur = lines.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
799
896
  const fresh = cur.filter((k) => !prevLines.has(k));
800
897
  if ([...prevLines].some((k) => !cur.includes(k))) {
801
898
  cancelFx();
802
- if (c < 16) perfectEl.classList.remove("pkb-show");
899
+ if (c < N) perfectEl.classList.remove("pkb-show");
803
900
  }
804
- const hit = new Set(cur.flatMap((k) => LINES[k]));
901
+ const hit = new Set(cur.flatMap((k) => cellLines[k]));
805
902
  const reach = /* @__PURE__ */ new Set();
806
- for (const l of LINES) {
903
+ for (const l of lines) {
807
904
  const m = l.filter((i) => !on[i]);
808
905
  if (m.length === 1) reach.add(m[0]);
809
906
  }
810
907
  tiles.forEach((t, i) => {
811
908
  t.classList.toggle("pkb-on", on[i]);
812
- t.classList.toggle("pkb-line", hit.has(i));
909
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
813
910
  t.classList.toggle("pkb-reach", reach.has(i));
814
911
  });
815
- board.classList.toggle("pkb-all", c === 16);
816
- kick.classList.toggle("pkb-gold", c === 16);
817
- kick.textContent = c === 16 ? M.perfectTag : M.tag;
818
- $("cnt").textContent = `${c}/16`;
912
+ freeTile?.classList.toggle("pkb-line", hit.has(L.center));
913
+ board.classList.toggle("pkb-all", c === N);
914
+ kick.classList.toggle("pkb-gold", c === N);
915
+ kick.textContent = c === N ? M.perfectTag : M.tag;
916
+ $("cnt").textContent = `${c}/${N}`;
819
917
  $("bng").textContent = String(cur.length);
820
918
  const bp = $("bp");
821
919
  if (cur.length > lastN) {
@@ -832,7 +930,7 @@ function createPackingBingo(container, options = {}) {
832
930
  score += linePts;
833
931
  if (quiet) {
834
932
  if (linePts) countScore();
835
- } else if (c === 16 && !prevAll) perfect(events);
933
+ } else if (c === N && !prevAll) perfect(events);
836
934
  else if (fresh.length) {
837
935
  bingo(fresh, linePts);
838
936
  const e = { newLines: fresh, totalLines: cur.length, score };
@@ -843,9 +941,9 @@ function createPackingBingo(container, options = {}) {
843
941
  }
844
942
  prevLines = new Set(cur);
845
943
  prevReach = reach;
846
- prevAll = c === 16;
944
+ prevAll = c === N;
847
945
  const labels = [...reach].map((i) => items[i].label);
848
- $("msg").textContent = c === 16 ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, 16 - c) : M.statusStart(16 - c);
946
+ $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
849
947
  return events;
850
948
  }
851
949
  const flush = (events) => {
@@ -872,7 +970,7 @@ function createPackingBingo(container, options = {}) {
872
970
  got.clear();
873
971
  gotL.clear();
874
972
  checked.forEach((v, i) => v && got.add(i));
875
- LINES.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
973
+ lines.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
876
974
  prevLines = new Set(gotL);
877
975
  prevReach = /* @__PURE__ */ new Set();
878
976
  prevAll = checked.every(Boolean);
@@ -932,12 +1030,13 @@ function createPackingBingo(container, options = {}) {
932
1030
  }
933
1031
  };
934
1032
  $("dB").addEventListener("click", () => {
935
- const open = () => LINES.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
936
- const pick = checked.every(Boolean) ? [...LINES[0]] : open();
1033
+ const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1034
+ const pick = checked.every(Boolean) ? [...lines[0]] : open();
937
1035
  void run(pick.map((i) => ({ i, delay: 320 })));
938
1036
  });
939
1037
  $("dP").addEventListener("click", () => {
940
- const miss = checked.every(Boolean) ? tiles.map((_, i) => i) : tiles.map((_, i) => i).filter((i) => !checked[i]);
1038
+ const all = items.map((_, i) => i);
1039
+ const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
941
1040
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
942
1041
  });
943
1042
  $("dR").addEventListener("click", () => {
@@ -965,14 +1064,14 @@ function createPackingBingo(container, options = {}) {
965
1064
  return {
966
1065
  setChecked(next, opts = {}) {
967
1066
  if (dead) return;
968
- const n = normalize(next);
969
- const changed = n.flatMap((v, i) => v !== checked[i] ? [i] : []);
1067
+ const n2 = normalize(next);
1068
+ const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
970
1069
  if (!changed.length) return;
971
1070
  if (!opts.animate || changed.length !== 1) {
972
- baseline(n);
1071
+ baseline(n2);
973
1072
  return;
974
1073
  }
975
- flush(apply(n, changed[0], false));
1074
+ flush(apply(n2, changed[0], false));
976
1075
  },
977
1076
  reset() {
978
1077
  if (!dead) request(normalize(void 0), null, { reset: true });
@@ -981,7 +1080,7 @@ function createPackingBingo(container, options = {}) {
981
1080
  if (!dead) render(null, true);
982
1081
  },
983
1082
  getState() {
984
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean) };
1083
+ return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean), size: n, free: L.free };
985
1084
  },
986
1085
  destroy() {
987
1086
  if (dead) return;
@@ -1000,6 +1099,7 @@ function createPackingBingo(container, options = {}) {
1000
1099
  0 && (module.exports = {
1001
1100
  DEFAULT_ITEMS,
1002
1101
  LINES,
1102
+ bingoLines,
1003
1103
  createPackingBingo,
1004
1104
  en,
1005
1105
  ja