packing-bingo 0.2.0 → 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",
@@ -229,7 +303,7 @@ function resolveStorageKey(options) {
229
303
  if (options.controlled) return null;
230
304
  if (options.storageKey !== void 0) return options.storageKey || null;
231
305
  if (options.demo) return null;
232
- return itemsKey(options.items ?? DEFAULT_ITEMS);
306
+ return itemsKey(layoutItems(options));
233
307
  }
234
308
  function itemsKey(items) {
235
309
  const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
@@ -244,14 +318,14 @@ function itemsKey(items) {
244
318
  // src/styles.ts
245
319
  var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
246
320
  var CSS = `
247
- .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%}
248
322
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
249
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}
250
324
  .pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
251
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)}}
252
326
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
253
327
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
254
- .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}
255
329
  .pkb-tags{display:flex;gap:8px;align-items:center}
256
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}
257
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}
@@ -259,7 +333,7 @@ var CSS = `
259
333
  .pkb button{font-family:var(--pk-body)}
260
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}
261
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}
262
- .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}
263
337
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
264
338
  .pkb-score{display:flex;gap:10px}
265
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)}
@@ -278,16 +352,18 @@ var CSS = `
278
352
  .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
279
353
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
280
354
  .pkb-gw{position:relative}
281
- .pkb-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
282
- .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}
283
- .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)}
284
358
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
285
359
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
286
- .pkb-emo{font-size:32px;line-height:1;transition:opacity .2s}
287
- .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}
288
- .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}
289
363
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
290
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)}
291
367
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
292
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}
293
369
  @keyframes pkb-glint{0%{background-position:200% 0}30%,100%{background-position:-100% 0}}
@@ -304,11 +380,11 @@ var CSS = `
304
380
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
305
381
  .pkb-board.pkb-all .pkb-lines{opacity:0}
306
382
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
307
- .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:13}
308
- .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))}
309
385
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
310
386
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
311
- .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}
312
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}
313
389
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
314
390
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
@@ -323,7 +399,7 @@ var CSS = `
323
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}
324
400
  .pkb-float.pkb-pt{font-size:22px;color:var(--pk-paper)}
325
401
  .pkb-float.pkb-cmb{font-size:17px}
326
- .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}
327
403
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
328
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)}}
329
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}
@@ -334,7 +410,7 @@ var CSS = `
334
410
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
335
411
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
336
412
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
337
- .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}
338
414
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
339
415
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
340
416
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -344,32 +420,20 @@ var CSS = `
344
420
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
345
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}
346
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}
347
- @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}}
348
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}}
349
427
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
350
428
  `;
351
429
 
352
430
  // src/core.ts
353
- var LINES = [
354
- [0, 1, 2, 3],
355
- [4, 5, 6, 7],
356
- [8, 9, 10, 11],
357
- [12, 13, 14, 15],
358
- [0, 4, 8, 12],
359
- [1, 5, 9, 13],
360
- [2, 6, 10, 14],
361
- [3, 7, 11, 15],
362
- [0, 5, 10, 15],
363
- [3, 6, 9, 12]
364
- ];
365
- var DEMO_START = [0, 1, 2, 3, 5, 10];
431
+ var LINES = bingoLines(4);
366
432
  var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"];
367
433
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
368
434
  var COMBO_MS = 2500;
369
435
  var MUTE_KEY = "packing-bingo-muted";
370
436
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
371
- var normalize = (v) => Array.from({ length: 16 }, (_, i) => !!v?.[i]);
372
- var demoStart = () => Array.from({ length: 16 }, (_, i) => DEMO_START.includes(i));
373
437
  function readStore(key) {
374
438
  try {
375
439
  return localStorage.getItem(key);
@@ -398,15 +462,21 @@ function ensureAssets(fonts) {
398
462
  document.head.appendChild(l);
399
463
  }
400
464
  }
401
- function template(items, M, demo) {
402
- const tiles = items.map(
403
- (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>`
404
- ).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)}`;
405
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>` : "";
406
- return `<div class="pkb"><div class="pkb-board">
476
+ return `<div class="pkb" style="${scale}"><div class="pkb-board">
407
477
  <div class="pkb-head"><div>
408
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>
409
- <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>
410
480
  <div class="pkb-side"><div class="pkb-score">
411
481
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
412
482
  <div class="pkb-pill" data-r="bp"><small>${esc(M.bingo)}</small><b data-r="bng"></b></div>
@@ -417,14 +487,22 @@ function template(items, M, demo) {
417
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>
418
488
  <div class="pkb-flash" data-r="flash"></div>
419
489
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
420
- <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>
421
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>
422
492
  <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
423
493
  <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
424
494
  }
495
+ var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
425
496
  function createPackingBingo(container, options = {}) {
426
- const items = options.items ?? DEFAULT_ITEMS;
427
- 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);
428
506
  const M = { ...ja, ...options.messages };
429
507
  if (options.title != null) M.title = options.title;
430
508
  if (options.tag != null) M.tag = options.tag;
@@ -445,7 +523,7 @@ function createPackingBingo(container, options = {}) {
445
523
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
446
524
  let gen = 0;
447
525
  ensureAssets(options.fonts !== false);
448
- container.innerHTML = template(items, M, demo);
526
+ container.innerHTML = template(L, M, demo);
449
527
  const $ = (r) => {
450
528
  const el = container.querySelector(`[data-r="${r}"]`);
451
529
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -457,7 +535,9 @@ function createPackingBingo(container, options = {}) {
457
535
  const svg = container.querySelector(".pkb-lines");
458
536
  const cv = container.querySelector(".pkb-fx");
459
537
  const ctx = cv.getContext("2d");
460
- 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;
461
541
  const ins = tiles.map((t) => t.querySelector("input"));
462
542
  const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
463
543
  let checked = normalize(void 0);
@@ -492,14 +572,14 @@ function createPackingBingo(container, options = {}) {
492
572
  cv.width = Math.round(BW * dpr);
493
573
  cv.height = Math.round(BH * dpr);
494
574
  };
495
- const piece = (x, y, vx, vy, g, s, life, shape) => ({
575
+ const piece = (x, y, vx, vy, g, s, life, shape2) => ({
496
576
  x,
497
577
  y,
498
578
  vx,
499
579
  vy,
500
580
  g,
501
581
  life,
502
- shape,
582
+ shape: shape2,
503
583
  w: (5 + Math.random() * 6) * s,
504
584
  h: (3 + Math.random() * 5) * s,
505
585
  r: Math.random() * 6,
@@ -509,9 +589,9 @@ function createPackingBingo(container, options = {}) {
509
589
  const go = () => {
510
590
  if (!raf && ctx) raf = requestAnimationFrame(tick);
511
591
  };
512
- function burst(x, y, n, o = {}) {
592
+ function burst(x, y, n2, o = {}) {
513
593
  if (calm) return;
514
- for (let i = 0; i < n; i++) {
594
+ for (let i = 0; i < n2; i++) {
515
595
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
516
596
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
517
597
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
@@ -602,11 +682,11 @@ function createPackingBingo(container, options = {}) {
602
682
  void el.offsetWidth;
603
683
  el.classList.add(cls);
604
684
  };
605
- const hop = (i, d) => {
685
+ const hop = (t, d) => {
606
686
  if (calm) return;
607
- tiles[i].classList.remove("pkb-squash");
608
- tiles[i].style.setProperty("--pkb-d", d + "ms");
609
- replay(tiles[i], "pkb-hop");
687
+ t.classList.remove("pkb-squash");
688
+ t.style.setProperty("--pkb-d", d + "ms");
689
+ replay(t, "pkb-hop");
610
690
  };
611
691
  grid.addEventListener("animationend", (e) => {
612
692
  const t = e.target;
@@ -654,8 +734,8 @@ function createPackingBingo(container, options = {}) {
654
734
  st(t0);
655
735
  if (!calm) replay($("sp"), "pkb-bump");
656
736
  }
657
- const showGain = (n, x, y, cls = "pt") => {
658
- floatText(`+${n}`, x, y, cls);
737
+ const showGain = (pts, x, y, cls = "pt") => {
738
+ floatText(`+${pts}`, x, y, cls);
659
739
  countScore();
660
740
  };
661
741
  function countUp(el, a, b, d, fmt, ticks = false) {
@@ -675,15 +755,14 @@ function createPackingBingo(container, options = {}) {
675
755
  };
676
756
  st(t0);
677
757
  }
678
- const ctr = (i) => {
679
- const t = tiles[i];
758
+ const ctr = (t) => {
680
759
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
681
760
  };
682
761
  function drawLines(fresh = []) {
683
762
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
684
763
  svg.innerHTML = cur.map((k) => {
685
- const a = ctr(LINES[k][0]), b = ctr(LINES[k][3]);
686
- 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;
687
766
  const at = `x1="${a.x - ex}" y1="${a.y - ey}" x2="${b.x + ex}" y2="${b.y + ey}" pathLength="1"`;
688
767
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
689
768
  }).join("");
@@ -730,29 +809,29 @@ function createPackingBingo(container, options = {}) {
730
809
  later(() => sfx.reach(), 150);
731
810
  }
732
811
  function bingo(fresh, pts) {
733
- const n = fresh.length;
734
- 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]);
735
814
  const step = Math.min(70, 300 / seq.length);
736
815
  seq.forEach(
737
- (i, j) => later(() => {
738
- hop(i, 0);
739
- const p = rel(tiles[i]);
816
+ (t, j) => later(() => {
817
+ hop(t, 0);
818
+ const p = rel(t);
740
819
  burst(p.x, p.y, 14, { speed: 5, g: 0.15, life: 30, size: 0.7, star: 0.8 });
741
820
  sfx.note(j + 3);
742
821
  }, j * step)
743
822
  );
744
823
  later(() => {
745
- $("bT").textContent = n >= 3 ? M.bannerTriple : n === 2 ? M.bannerDouble : M.bannerSingle;
746
- $("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);
747
826
  replay($("banner"), "pkb-show");
748
827
  if (!calm) {
749
828
  replay($("flash"), "pkb-go");
750
829
  replay(board, "pkb-punch");
751
830
  }
752
831
  const c = rel(gw);
753
- burst(c.x, c.y, n > 1 ? 180 : 110, { speed: n > 1 ? 16 : 13, star: 0.3 });
754
- sfx.bingo(n);
755
- 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]);
756
835
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
757
836
  }, seq.length * step + 60);
758
837
  }
@@ -760,9 +839,10 @@ function createPackingBingo(container, options = {}) {
760
839
  const bonus = gotP ? 0 : 1e3;
761
840
  gotP = true;
762
841
  score += bonus;
763
- 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)));
764
843
  sfx.perfect();
765
844
  if (!calm) replay($("flash"), "pkb-go");
845
+ $("pbody").textContent = perfectText(M, N);
766
846
  $("sScore").textContent = "0";
767
847
  $("sCombo").textContent = "\xD70";
768
848
  $("sBingo").textContent = "0";
@@ -812,27 +892,28 @@ function createPackingBingo(container, options = {}) {
812
892
  function render(src, quiet) {
813
893
  const events = [];
814
894
  const on = checked, c = on.filter(Boolean).length;
815
- cur = LINES.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
895
+ cur = lines.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
816
896
  const fresh = cur.filter((k) => !prevLines.has(k));
817
897
  if ([...prevLines].some((k) => !cur.includes(k))) {
818
898
  cancelFx();
819
- if (c < 16) perfectEl.classList.remove("pkb-show");
899
+ if (c < N) perfectEl.classList.remove("pkb-show");
820
900
  }
821
- const hit = new Set(cur.flatMap((k) => LINES[k]));
901
+ const hit = new Set(cur.flatMap((k) => cellLines[k]));
822
902
  const reach = /* @__PURE__ */ new Set();
823
- for (const l of LINES) {
903
+ for (const l of lines) {
824
904
  const m = l.filter((i) => !on[i]);
825
905
  if (m.length === 1) reach.add(m[0]);
826
906
  }
827
907
  tiles.forEach((t, i) => {
828
908
  t.classList.toggle("pkb-on", on[i]);
829
- t.classList.toggle("pkb-line", hit.has(i));
909
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
830
910
  t.classList.toggle("pkb-reach", reach.has(i));
831
911
  });
832
- board.classList.toggle("pkb-all", c === 16);
833
- kick.classList.toggle("pkb-gold", c === 16);
834
- kick.textContent = c === 16 ? M.perfectTag : M.tag;
835
- $("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}`;
836
917
  $("bng").textContent = String(cur.length);
837
918
  const bp = $("bp");
838
919
  if (cur.length > lastN) {
@@ -849,7 +930,7 @@ function createPackingBingo(container, options = {}) {
849
930
  score += linePts;
850
931
  if (quiet) {
851
932
  if (linePts) countScore();
852
- } else if (c === 16 && !prevAll) perfect(events);
933
+ } else if (c === N && !prevAll) perfect(events);
853
934
  else if (fresh.length) {
854
935
  bingo(fresh, linePts);
855
936
  const e = { newLines: fresh, totalLines: cur.length, score };
@@ -860,9 +941,9 @@ function createPackingBingo(container, options = {}) {
860
941
  }
861
942
  prevLines = new Set(cur);
862
943
  prevReach = reach;
863
- prevAll = c === 16;
944
+ prevAll = c === N;
864
945
  const labels = [...reach].map((i) => items[i].label);
865
- $("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);
866
947
  return events;
867
948
  }
868
949
  const flush = (events) => {
@@ -889,7 +970,7 @@ function createPackingBingo(container, options = {}) {
889
970
  got.clear();
890
971
  gotL.clear();
891
972
  checked.forEach((v, i) => v && got.add(i));
892
- 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));
893
974
  prevLines = new Set(gotL);
894
975
  prevReach = /* @__PURE__ */ new Set();
895
976
  prevAll = checked.every(Boolean);
@@ -949,12 +1030,13 @@ function createPackingBingo(container, options = {}) {
949
1030
  }
950
1031
  };
951
1032
  $("dB").addEventListener("click", () => {
952
- const open = () => LINES.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
953
- 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();
954
1035
  void run(pick.map((i) => ({ i, delay: 320 })));
955
1036
  });
956
1037
  $("dP").addEventListener("click", () => {
957
- 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]);
958
1040
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
959
1041
  });
960
1042
  $("dR").addEventListener("click", () => {
@@ -982,14 +1064,14 @@ function createPackingBingo(container, options = {}) {
982
1064
  return {
983
1065
  setChecked(next, opts = {}) {
984
1066
  if (dead) return;
985
- const n = normalize(next);
986
- 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] : []);
987
1069
  if (!changed.length) return;
988
1070
  if (!opts.animate || changed.length !== 1) {
989
- baseline(n);
1071
+ baseline(n2);
990
1072
  return;
991
1073
  }
992
- flush(apply(n, changed[0], false));
1074
+ flush(apply(n2, changed[0], false));
993
1075
  },
994
1076
  reset() {
995
1077
  if (!dead) request(normalize(void 0), null, { reset: true });
@@ -998,7 +1080,7 @@ function createPackingBingo(container, options = {}) {
998
1080
  if (!dead) render(null, true);
999
1081
  },
1000
1082
  getState() {
1001
- 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 };
1002
1084
  },
1003
1085
  destroy() {
1004
1086
  if (dead) return;
@@ -1017,6 +1099,7 @@ function createPackingBingo(container, options = {}) {
1017
1099
  0 && (module.exports = {
1018
1100
  DEFAULT_ITEMS,
1019
1101
  LINES,
1102
+ bingoLines,
1020
1103
  createPackingBingo,
1021
1104
  en,
1022
1105
  ja