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