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/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  DEFAULT_ITEMS: () => DEFAULT_ITEMS,
25
25
  LINES: () => LINES,
26
26
  PackingBingo: () => PackingBingo,
27
+ bingoLines: () => bingoLines,
27
28
  createPackingBingo: () => createPackingBingo,
28
29
  en: () => en,
29
30
  ja: () => ja
@@ -35,29 +36,98 @@ var import_react = require("react");
35
36
 
36
37
  // src/items.ts
37
38
  var DEFAULT_ITEMS = [
38
- { emoji: "\u{1F4D6}", label: "\u65C5\u306E\u3057\u304A\u308A" },
39
- { emoji: "\u{1FAAA}", label: "\u751F\u5F92\u624B\u5E33" },
40
- { emoji: "\u{1F3E5}", label: "\u4FDD\u967A\u8A3C\u30B3\u30D4\u30FC" },
41
- { emoji: "\u{1F45B}", label: "\u304A\u3053\u3065\u304B\u3044" },
42
- { emoji: "\u{1F455}", label: "\u7740\u66FF\u3048" },
43
- { emoji: "\u{1F319}", label: "\u30D1\u30B8\u30E3\u30DE" },
44
- { emoji: "\u{1F9E5}", label: "\u4E0A\u7740" },
45
- { emoji: "\u{1FAA5}", label: "\u6D17\u9762\u7528\u5177" },
46
- { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
47
- { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1\u30FB\u30C6\u30A3\u30C3\u30B7\u30E5" },
39
+ { emoji: "\u{1F45B}", label: "\u8CA1\u5E03" },
40
+ { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB" },
41
+ { emoji: "\u{1F50B}", label: "\u30E2\u30D0\u30A4\u30EB\u200B\u5145\u96FB\u5668" },
42
+ { emoji: "\u{1F4CB}", label: "\u5065\u5EB7\u8ABF\u67FB\u7968" },
43
+ { emoji: "\u{1FAAA}", label: "\u30DE\u30A4\u30CA\u30AB\u30FC\u30C9\u200B\u307E\u305F\u306F\u4FDD\u967A\u8A3C" },
44
+ { emoji: "\u{1F4D6}", label: "\u3057\u304A\u308A" },
48
45
  { emoji: "\u{1F637}", label: "\u30DE\u30B9\u30AF" },
49
- { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC\u30FB\u9154\u3044\u6B62\u3081" },
50
- { emoji: "\u2602\uFE0F", label: "\u6298\u308A\u305F\u305F\u307F\u5098" },
51
- { emoji: "\u270F\uFE0F", label: "\u7B46\u8A18\u7528\u5177" },
46
+ { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1" },
47
+ { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC" },
52
48
  { emoji: "\u{1F6CD}\uFE0F", label: "\u30D3\u30CB\u30FC\u30EB\u888B" },
53
- { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB\u30FB\u5145\u96FB\u5668" }
49
+ { emoji: "\u{1F9E4}", label: "\u8ECD\u624B\uFF08\u5B66\u6821\u652F\u7D66\uFF09" },
50
+ { emoji: "\u{1F526}", label: "\u30D8\u30C3\u30C9\u30E9\u30A4\u30C8" },
51
+ { emoji: "\u{1F455}", label: "\u30B8\u30E3\u30FC\u30B8" },
52
+ { emoji: "\u2614", label: "\u96E8\u5177" },
53
+ { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
54
+ { emoji: "\u{1F9F4}", label: "\u65E5\u713C\u3051\u6B62\u3081" }
54
55
  ];
55
56
 
57
+ // src/layout.ts
58
+ var MIN_SIZE = 2;
59
+ var MAX_SIZE = 10;
60
+ var fail = (msg) => {
61
+ throw new Error(`[packing-bingo] ${msg}`);
62
+ };
63
+ function layoutItems(options) {
64
+ if (options.items) return options.items;
65
+ const n = options.size;
66
+ if (n == null || !Number.isInteger(n) || n < MIN_SIZE) return DEFAULT_ITEMS;
67
+ return DEFAULT_ITEMS.slice(0, n * n - (options.free ? 1 : 0));
68
+ }
69
+ function checkSize(size, free) {
70
+ if (!Number.isInteger(size) || size < MIN_SIZE || size > MAX_SIZE)
71
+ fail(`"size" must be a whole number from ${MIN_SIZE} to ${MAX_SIZE} (got ${size}).`);
72
+ 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}).`);
73
+ }
74
+ function shape(n, free) {
75
+ const seq = (f) => Array.from({ length: n }, (_, k) => f(k));
76
+ const cellLines = [
77
+ ...seq((r) => seq((c) => r * n + c)),
78
+ // rows
79
+ ...seq((c) => seq((r) => r * n + c)),
80
+ // columns
81
+ seq((k) => k * (n + 1)),
82
+ // top left to bottom right
83
+ seq((k) => (k + 1) * (n - 1))
84
+ // top right to bottom left
85
+ ];
86
+ const center = free ? (n * n - 1) / 2 : -1;
87
+ const toItem = (c) => free && c > center ? c - 1 : c;
88
+ return { center, cellLines, lines: cellLines.map((l) => l.filter((c) => c !== center).map(toItem)) };
89
+ }
90
+ function resolveLayout(options) {
91
+ const items = layoutItems(options);
92
+ const free = !!options.free;
93
+ let size = options.size;
94
+ if (size == null) {
95
+ const len = items.length;
96
+ const sideOf = (cells) => {
97
+ const r = Math.round(Math.sqrt(cells));
98
+ return r * r === cells && r >= MIN_SIZE && r <= MAX_SIZE ? r : 0;
99
+ };
100
+ size = sideOf(len + (free ? 1 : 0));
101
+ if (!size) {
102
+ const other = sideOf(len + (free ? 0 : 1));
103
+ 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` : "";
104
+ fail(
105
+ 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}).`
106
+ );
107
+ }
108
+ }
109
+ checkSize(size, free);
110
+ const want = size * size - (free ? 1 : 0);
111
+ if (items.length !== want) {
112
+ const card = `a ${size}\xD7${size} card${free ? " with a FREE center" : ""}`;
113
+ fail(
114
+ 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".`
115
+ );
116
+ }
117
+ const { center, cellLines, lines } = shape(size, free);
118
+ const itemCell = items.map((_, i) => free && i >= center ? i + 1 : i);
119
+ return { size, free, items, itemCell, center, cellLines, lines };
120
+ }
121
+ function bingoLines(size, free = false) {
122
+ checkSize(size, free);
123
+ return shape(size, free).lines;
124
+ }
125
+
56
126
  // src/messages.ts
57
127
  var ja = {
58
128
  title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
59
129
  tag: "\u4FEE\u5B66\u65C5\u884C",
60
- subtitle: "\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30F3\u30D7\u3002\u30BF\u30C6\u30FB\u30E8\u30B3\u30FB\u30CA\u30CA\u30E1\u3067\u30D3\u30F3\u30B4\uFF01",
130
+ subtitle: "",
61
131
  packed: "\u305D\u308D\u3063\u305F",
62
132
  bingo: "BINGO",
63
133
  score: "SCORE",
@@ -67,6 +137,7 @@ var ja = {
67
137
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
68
138
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
69
139
  stamp: "OK",
140
+ free: "FREE",
70
141
  reachBadge: "\u30EA\u30FC\u30C1",
71
142
  reachPop: "\u30EA\u30FC\u30C1!",
72
143
  bannerSingle: "BINGO!",
@@ -74,14 +145,15 @@ var ja = {
74
145
  bannerTriple: "\u30C8\u30EA\u30D7\u30EB\u30D3\u30F3\u30B4!",
75
146
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
76
147
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
77
- statusStart: (left) => `\u306E\u3053\u308A ${left} \u3053\u3002\u307E\u305A\u306F1\u30E9\u30A4\u30F3\u305D\u308D\u3048\u3088\u3046`,
148
+ statusStart: () => "",
78
149
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
79
150
  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`,
80
151
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
81
152
  perfectTag: "\u{1F3C6} PERFECT",
82
153
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
83
154
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
84
- 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",
155
+ perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
156
+ \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
85
157
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
86
158
  statScore: "SCORE",
87
159
  statCombo: "MAX COMBO",
@@ -95,7 +167,7 @@ var ja = {
95
167
  var en = {
96
168
  title: "Packing Bingo",
97
169
  tag: "School trip",
98
- subtitle: "Tap to stamp. Rows, columns and diagonals all count!",
170
+ subtitle: "",
99
171
  packed: "PACKED",
100
172
  bingo: "BINGO",
101
173
  score: "SCORE",
@@ -105,6 +177,7 @@ var en = {
105
177
  soundOn: "\u{1F50A} Sound on",
106
178
  soundOff: "\u{1F507} Muted",
107
179
  stamp: "OK",
180
+ free: "FREE",
108
181
  reachBadge: "REACH",
109
182
  reachPop: "Reach!",
110
183
  bannerSingle: "BINGO!",
@@ -112,14 +185,15 @@ var en = {
112
185
  bannerTriple: "TRIPLE BINGO!",
113
186
  bannerNth: (n) => `Line ${n}!`,
114
187
  bannerMulti: (n) => `${n} lines at once!`,
115
- statusStart: (left) => `${left} to go. Finish a line first!`,
188
+ statusStart: () => "",
116
189
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
117
190
  statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
118
191
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
119
192
  perfectTag: "\u{1F3C6} PERFECT",
120
193
  perfectStamp: "ALL\nSET",
121
194
  perfectTitle: "Perfect!",
122
- perfectBody: "All 16 packed!\nHave a great trip!",
195
+ perfectBody: (n) => `All ${n} packed!
196
+ Have a great trip!`,
123
197
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
124
198
  statScore: "SCORE",
125
199
  statCombo: "MAX COMBO",
@@ -233,7 +307,7 @@ function resolveStorageKey(options) {
233
307
  if (options.controlled) return null;
234
308
  if (options.storageKey !== void 0) return options.storageKey || null;
235
309
  if (options.demo) return null;
236
- return itemsKey(options.items ?? DEFAULT_ITEMS);
310
+ return itemsKey(layoutItems(options));
237
311
  }
238
312
  function itemsKey(items) {
239
313
  const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
@@ -248,14 +322,14 @@ function itemsKey(items) {
248
322
  // src/styles.ts
249
323
  var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
250
324
  var CSS = `
251
- .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%}
325
+ .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%}
252
326
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
253
327
  .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}
254
328
  .pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
255
329
  @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)}}
256
330
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
257
331
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
258
- .pkb-head{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}
332
+ .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
259
333
  .pkb-tags{display:flex;gap:8px;align-items:center}
260
334
  .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}
261
335
  .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}
@@ -263,7 +337,7 @@ var CSS = `
263
337
  .pkb button{font-family:var(--pk-body)}
264
338
  .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}
265
339
  .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}
266
- .pkb-sub{font-weight:800;font-size:13px;margin-top:4px}
340
+ .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
267
341
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
268
342
  .pkb-score{display:flex;gap:10px}
269
343
  .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)}
@@ -282,16 +356,18 @@ var CSS = `
282
356
  .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
283
357
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
284
358
  .pkb-gw{position:relative}
285
- .pkb-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
286
- .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}
287
- .pkb-tile:active{transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
359
+ .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
360
+ .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}
361
+ .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
288
362
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
289
363
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
290
- .pkb-emo{font-size:32px;line-height:1;transition:opacity .2s}
291
- .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}
292
- .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}
364
+ .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
365
+ .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}
366
+ .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}
293
367
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
294
368
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
369
+ .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
370
+ .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)}
295
371
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
296
372
  .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}
297
373
  @keyframes pkb-glint{0%{background-position:200% 0}30%,100%{background-position:-100% 0}}
@@ -308,11 +384,11 @@ var CSS = `
308
384
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
309
385
  .pkb-board.pkb-all .pkb-lines{opacity:0}
310
386
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
311
- .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:13}
312
- .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:6}
387
+ .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:calc(13px*var(--pk-d))}
388
+ .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
313
389
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
314
390
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
315
- .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px}
391
+ .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}
316
392
  .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}
317
393
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
318
394
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
@@ -327,7 +403,7 @@ var CSS = `
327
403
  .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}
328
404
  .pkb-float.pkb-pt{font-size:22px;color:var(--pk-paper)}
329
405
  .pkb-float.pkb-cmb{font-size:17px}
330
- .pkb-float.pkb-big{font-size:clamp(20px,6cqi,34px);color:#FFE066}
406
+ .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
331
407
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
332
408
  @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)}}
333
409
  .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}
@@ -338,7 +414,7 @@ var CSS = `
338
414
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
339
415
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
340
416
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
341
- .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line}
417
+ .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}
342
418
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
343
419
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
344
420
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -348,32 +424,20 @@ var CSS = `
348
424
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
349
425
  .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}
350
426
  .pkb-demo button{appearance:none;border:1px solid #ccc;background:#f3f3f3;color:#222;border-radius:999px;padding:5px 12px;font-size:12px;cursor:pointer}
351
- @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}}
427
+ @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}}
428
+ @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}}
429
+ @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}}
352
430
  @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}}
353
431
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
354
432
  `;
355
433
 
356
434
  // src/core.ts
357
- var LINES = [
358
- [0, 1, 2, 3],
359
- [4, 5, 6, 7],
360
- [8, 9, 10, 11],
361
- [12, 13, 14, 15],
362
- [0, 4, 8, 12],
363
- [1, 5, 9, 13],
364
- [2, 6, 10, 14],
365
- [3, 7, 11, 15],
366
- [0, 5, 10, 15],
367
- [3, 6, 9, 12]
368
- ];
369
- var DEMO_START = [0, 1, 2, 3, 5, 10];
435
+ var LINES = bingoLines(4);
370
436
  var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"];
371
437
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
372
438
  var COMBO_MS = 2500;
373
439
  var MUTE_KEY = "packing-bingo-muted";
374
440
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
375
- var normalize = (v) => Array.from({ length: 16 }, (_, i) => !!v?.[i]);
376
- var demoStart = () => Array.from({ length: 16 }, (_, i) => DEMO_START.includes(i));
377
441
  function readStore(key) {
378
442
  try {
379
443
  return localStorage.getItem(key);
@@ -402,15 +466,21 @@ function ensureAssets(fonts) {
402
466
  document.head.appendChild(l);
403
467
  }
404
468
  }
405
- function template(items, M, demo) {
406
- const tiles = items.map(
407
- (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>`
408
- ).join("");
469
+ function template(L, M, demo) {
470
+ const n = L.size;
471
+ const tiles = Array.from({ length: n * n }, (_, c) => {
472
+ const gd = `--pkb-gd:${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
473
+ if (c === L.center)
474
+ return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
475
+ const it = L.items[L.itemCell.indexOf(c)];
476
+ 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>`;
477
+ }).join("");
478
+ const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
409
479
  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>` : "";
410
- return `<div class="pkb"><div class="pkb-board">
480
+ return `<div class="pkb" style="${scale}"><div class="pkb-board">
411
481
  <div class="pkb-head"><div>
412
482
  <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>
413
- <h2 class="pkb-title">${esc(M.title)}</h2><p class="pkb-sub">${esc(M.subtitle)}</p></div>
483
+ <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
414
484
  <div class="pkb-side"><div class="pkb-score">
415
485
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
416
486
  <div class="pkb-pill" data-r="bp"><small>${esc(M.bingo)}</small><b data-r="bng"></b></div>
@@ -421,14 +491,22 @@ function template(items, M, demo) {
421
491
  <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>
422
492
  <div class="pkb-flash" data-r="flash"></div>
423
493
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
424
- <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody">${esc(M.perfectBody)}</p>
494
+ <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>
425
495
  <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>
426
496
  <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
427
497
  <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
428
498
  }
499
+ var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
429
500
  function createPackingBingo(container, options = {}) {
430
- const items = options.items ?? DEFAULT_ITEMS;
431
- if (items.length !== 16) throw new Error(`[packing-bingo] "items" must have exactly 16 entries (got ${items.length}).`);
501
+ const L = resolveLayout(options);
502
+ const { items, lines, cellLines } = L;
503
+ const n = L.size, N = items.length;
504
+ const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
505
+ const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
506
+ const demoItems = new Set(L.itemCell.flatMap((c, i) => demoCells.includes(c) ? [i] : []));
507
+ const demoStart = () => Array.from({ length: N }, (_, i) => demoItems.has(i));
508
+ const far = (c) => Math.floor(c / n) + c % n;
509
+ const dScale = Math.min(1, 4 / n);
432
510
  const M = { ...ja, ...options.messages };
433
511
  if (options.title != null) M.title = options.title;
434
512
  if (options.tag != null) M.tag = options.tag;
@@ -449,7 +527,7 @@ function createPackingBingo(container, options = {}) {
449
527
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
450
528
  let gen = 0;
451
529
  ensureAssets(options.fonts !== false);
452
- container.innerHTML = template(items, M, demo);
530
+ container.innerHTML = template(L, M, demo);
453
531
  const $ = (r) => {
454
532
  const el = container.querySelector(`[data-r="${r}"]`);
455
533
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -461,7 +539,9 @@ function createPackingBingo(container, options = {}) {
461
539
  const svg = container.querySelector(".pkb-lines");
462
540
  const cv = container.querySelector(".pkb-fx");
463
541
  const ctx = cv.getContext("2d");
464
- const tiles = Array.from(grid.children);
542
+ const cellTiles = Array.from(grid.children);
543
+ const tiles = L.itemCell.map((c) => cellTiles[c]);
544
+ const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
465
545
  const ins = tiles.map((t) => t.querySelector("input"));
466
546
  const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
467
547
  let checked = normalize(void 0);
@@ -496,14 +576,14 @@ function createPackingBingo(container, options = {}) {
496
576
  cv.width = Math.round(BW * dpr);
497
577
  cv.height = Math.round(BH * dpr);
498
578
  };
499
- const piece = (x, y, vx, vy, g, s, life, shape) => ({
579
+ const piece = (x, y, vx, vy, g, s, life, shape2) => ({
500
580
  x,
501
581
  y,
502
582
  vx,
503
583
  vy,
504
584
  g,
505
585
  life,
506
- shape,
586
+ shape: shape2,
507
587
  w: (5 + Math.random() * 6) * s,
508
588
  h: (3 + Math.random() * 5) * s,
509
589
  r: Math.random() * 6,
@@ -513,9 +593,9 @@ function createPackingBingo(container, options = {}) {
513
593
  const go = () => {
514
594
  if (!raf && ctx) raf = requestAnimationFrame(tick);
515
595
  };
516
- function burst(x, y, n, o = {}) {
596
+ function burst(x, y, n2, o = {}) {
517
597
  if (calm) return;
518
- for (let i = 0; i < n; i++) {
598
+ for (let i = 0; i < n2; i++) {
519
599
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
520
600
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
521
601
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
@@ -606,11 +686,11 @@ function createPackingBingo(container, options = {}) {
606
686
  void el.offsetWidth;
607
687
  el.classList.add(cls);
608
688
  };
609
- const hop = (i, d) => {
689
+ const hop = (t, d) => {
610
690
  if (calm) return;
611
- tiles[i].classList.remove("pkb-squash");
612
- tiles[i].style.setProperty("--pkb-d", d + "ms");
613
- replay(tiles[i], "pkb-hop");
691
+ t.classList.remove("pkb-squash");
692
+ t.style.setProperty("--pkb-d", d + "ms");
693
+ replay(t, "pkb-hop");
614
694
  };
615
695
  grid.addEventListener("animationend", (e) => {
616
696
  const t = e.target;
@@ -658,8 +738,8 @@ function createPackingBingo(container, options = {}) {
658
738
  st(t0);
659
739
  if (!calm) replay($("sp"), "pkb-bump");
660
740
  }
661
- const showGain = (n, x, y, cls = "pt") => {
662
- floatText(`+${n}`, x, y, cls);
741
+ const showGain = (pts, x, y, cls = "pt") => {
742
+ floatText(`+${pts}`, x, y, cls);
663
743
  countScore();
664
744
  };
665
745
  function countUp(el, a, b, d, fmt, ticks = false) {
@@ -679,15 +759,14 @@ function createPackingBingo(container, options = {}) {
679
759
  };
680
760
  st(t0);
681
761
  }
682
- const ctr = (i) => {
683
- const t = tiles[i];
762
+ const ctr = (t) => {
684
763
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
685
764
  };
686
765
  function drawLines(fresh = []) {
687
766
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
688
767
  svg.innerHTML = cur.map((k) => {
689
- const a = ctr(LINES[k][0]), b = ctr(LINES[k][3]);
690
- 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;
768
+ const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
769
+ 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;
691
770
  const at = `x1="${a.x - ex}" y1="${a.y - ey}" x2="${b.x + ex}" y2="${b.y + ey}" pathLength="1"`;
692
771
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
693
772
  }).join("");
@@ -734,29 +813,29 @@ function createPackingBingo(container, options = {}) {
734
813
  later(() => sfx.reach(), 150);
735
814
  }
736
815
  function bingo(fresh, pts) {
737
- const n = fresh.length;
738
- const seq = [...new Set(fresh.flatMap((k) => LINES[k]))];
816
+ const nl = fresh.length;
817
+ const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
739
818
  const step = Math.min(70, 300 / seq.length);
740
819
  seq.forEach(
741
- (i, j) => later(() => {
742
- hop(i, 0);
743
- const p = rel(tiles[i]);
820
+ (t, j) => later(() => {
821
+ hop(t, 0);
822
+ const p = rel(t);
744
823
  burst(p.x, p.y, 14, { speed: 5, g: 0.15, life: 30, size: 0.7, star: 0.8 });
745
824
  sfx.note(j + 3);
746
825
  }, j * step)
747
826
  );
748
827
  later(() => {
749
- $("bT").textContent = n >= 3 ? M.bannerTriple : n === 2 ? M.bannerDouble : M.bannerSingle;
750
- $("bS").textContent = n > 1 ? M.bannerMulti(n) : M.bannerNth(cur.length);
828
+ $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
829
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
751
830
  replay($("banner"), "pkb-show");
752
831
  if (!calm) {
753
832
  replay($("flash"), "pkb-go");
754
833
  replay(board, "pkb-punch");
755
834
  }
756
835
  const c = rel(gw);
757
- burst(c.x, c.y, n > 1 ? 180 : 110, { speed: n > 1 ? 16 : 13, star: 0.3 });
758
- sfx.bingo(n);
759
- buzz(n > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
836
+ burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
837
+ sfx.bingo(nl);
838
+ buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
760
839
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
761
840
  }, seq.length * step + 60);
762
841
  }
@@ -764,9 +843,10 @@ function createPackingBingo(container, options = {}) {
764
843
  const bonus = gotP ? 0 : 1e3;
765
844
  gotP = true;
766
845
  score += bonus;
767
- tiles.forEach((_, i) => hop(i, ((i >> 2) + (i & 3)) * 60));
846
+ cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
768
847
  sfx.perfect();
769
848
  if (!calm) replay($("flash"), "pkb-go");
849
+ $("pbody").textContent = perfectText(M, N);
770
850
  $("sScore").textContent = "0";
771
851
  $("sCombo").textContent = "\xD70";
772
852
  $("sBingo").textContent = "0";
@@ -816,27 +896,28 @@ function createPackingBingo(container, options = {}) {
816
896
  function render(src, quiet) {
817
897
  const events = [];
818
898
  const on = checked, c = on.filter(Boolean).length;
819
- cur = LINES.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
899
+ cur = lines.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
820
900
  const fresh = cur.filter((k) => !prevLines.has(k));
821
901
  if ([...prevLines].some((k) => !cur.includes(k))) {
822
902
  cancelFx();
823
- if (c < 16) perfectEl.classList.remove("pkb-show");
903
+ if (c < N) perfectEl.classList.remove("pkb-show");
824
904
  }
825
- const hit = new Set(cur.flatMap((k) => LINES[k]));
905
+ const hit = new Set(cur.flatMap((k) => cellLines[k]));
826
906
  const reach = /* @__PURE__ */ new Set();
827
- for (const l of LINES) {
907
+ for (const l of lines) {
828
908
  const m = l.filter((i) => !on[i]);
829
909
  if (m.length === 1) reach.add(m[0]);
830
910
  }
831
911
  tiles.forEach((t, i) => {
832
912
  t.classList.toggle("pkb-on", on[i]);
833
- t.classList.toggle("pkb-line", hit.has(i));
913
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
834
914
  t.classList.toggle("pkb-reach", reach.has(i));
835
915
  });
836
- board.classList.toggle("pkb-all", c === 16);
837
- kick.classList.toggle("pkb-gold", c === 16);
838
- kick.textContent = c === 16 ? M.perfectTag : M.tag;
839
- $("cnt").textContent = `${c}/16`;
916
+ freeTile?.classList.toggle("pkb-line", hit.has(L.center));
917
+ board.classList.toggle("pkb-all", c === N);
918
+ kick.classList.toggle("pkb-gold", c === N);
919
+ kick.textContent = c === N ? M.perfectTag : M.tag;
920
+ $("cnt").textContent = `${c}/${N}`;
840
921
  $("bng").textContent = String(cur.length);
841
922
  const bp = $("bp");
842
923
  if (cur.length > lastN) {
@@ -853,7 +934,7 @@ function createPackingBingo(container, options = {}) {
853
934
  score += linePts;
854
935
  if (quiet) {
855
936
  if (linePts) countScore();
856
- } else if (c === 16 && !prevAll) perfect(events);
937
+ } else if (c === N && !prevAll) perfect(events);
857
938
  else if (fresh.length) {
858
939
  bingo(fresh, linePts);
859
940
  const e = { newLines: fresh, totalLines: cur.length, score };
@@ -864,9 +945,9 @@ function createPackingBingo(container, options = {}) {
864
945
  }
865
946
  prevLines = new Set(cur);
866
947
  prevReach = reach;
867
- prevAll = c === 16;
948
+ prevAll = c === N;
868
949
  const labels = [...reach].map((i) => items[i].label);
869
- $("msg").textContent = c === 16 ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, 16 - c) : M.statusStart(16 - c);
950
+ $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
870
951
  return events;
871
952
  }
872
953
  const flush = (events) => {
@@ -893,7 +974,7 @@ function createPackingBingo(container, options = {}) {
893
974
  got.clear();
894
975
  gotL.clear();
895
976
  checked.forEach((v, i) => v && got.add(i));
896
- LINES.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
977
+ lines.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
897
978
  prevLines = new Set(gotL);
898
979
  prevReach = /* @__PURE__ */ new Set();
899
980
  prevAll = checked.every(Boolean);
@@ -953,12 +1034,13 @@ function createPackingBingo(container, options = {}) {
953
1034
  }
954
1035
  };
955
1036
  $("dB").addEventListener("click", () => {
956
- const open = () => LINES.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
957
- const pick = checked.every(Boolean) ? [...LINES[0]] : open();
1037
+ const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1038
+ const pick = checked.every(Boolean) ? [...lines[0]] : open();
958
1039
  void run(pick.map((i) => ({ i, delay: 320 })));
959
1040
  });
960
1041
  $("dP").addEventListener("click", () => {
961
- const miss = checked.every(Boolean) ? tiles.map((_, i) => i) : tiles.map((_, i) => i).filter((i) => !checked[i]);
1042
+ const all = items.map((_, i) => i);
1043
+ const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
962
1044
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
963
1045
  });
964
1046
  $("dR").addEventListener("click", () => {
@@ -986,14 +1068,14 @@ function createPackingBingo(container, options = {}) {
986
1068
  return {
987
1069
  setChecked(next, opts = {}) {
988
1070
  if (dead) return;
989
- const n = normalize(next);
990
- const changed = n.flatMap((v, i) => v !== checked[i] ? [i] : []);
1071
+ const n2 = normalize(next);
1072
+ const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
991
1073
  if (!changed.length) return;
992
1074
  if (!opts.animate || changed.length !== 1) {
993
- baseline(n);
1075
+ baseline(n2);
994
1076
  return;
995
1077
  }
996
- flush(apply(n, changed[0], false));
1078
+ flush(apply(n2, changed[0], false));
997
1079
  },
998
1080
  reset() {
999
1081
  if (!dead) request(normalize(void 0), null, { reset: true });
@@ -1002,7 +1084,7 @@ function createPackingBingo(container, options = {}) {
1002
1084
  if (!dead) render(null, true);
1003
1085
  },
1004
1086
  getState() {
1005
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean) };
1087
+ return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean), size: n, free: L.free };
1006
1088
  },
1007
1089
  destroy() {
1008
1090
  if (dead) return;
@@ -1085,6 +1167,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1085
1167
  DEFAULT_ITEMS,
1086
1168
  LINES,
1087
1169
  PackingBingo,
1170
+ bingoLines,
1088
1171
  createPackingBingo,
1089
1172
  en,
1090
1173
  ja