packing-bingo 0.2.0 → 0.4.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,38 +32,109 @@ 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
- 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",
125
+ tag: "",
126
+ subtitle: "",
57
127
  packed: "\u305D\u308D\u3063\u305F",
58
128
  bingo: "BINGO",
59
129
  score: "SCORE",
60
130
  combo: (n) => `${n} COMBO`,
61
131
  comboPop: (n) => `${n} COMBO!`,
62
132
  reset: "\u30EA\u30BB\u30C3\u30C8",
133
+ shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
63
134
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
64
135
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
65
136
  stamp: "OK",
137
+ free: "FREE",
66
138
  reachBadge: "\u30EA\u30FC\u30C1",
67
139
  reachPop: "\u30EA\u30FC\u30C1!",
68
140
  bannerSingle: "BINGO!",
@@ -70,14 +142,15 @@ var ja = {
70
142
  bannerTriple: "\u30C8\u30EA\u30D7\u30EB\u30D3\u30F3\u30B4!",
71
143
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
72
144
  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`,
145
+ statusStart: () => "",
74
146
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
75
- 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`,
147
+ statusReach: () => "",
76
148
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
77
149
  perfectTag: "\u{1F3C6} PERFECT",
78
150
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
79
151
  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",
152
+ perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
153
+ \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
81
154
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
82
155
  statScore: "SCORE",
83
156
  statCombo: "MAX COMBO",
@@ -90,17 +163,19 @@ var ja = {
90
163
  };
91
164
  var en = {
92
165
  title: "Packing Bingo",
93
- tag: "School trip",
94
- subtitle: "Tap to stamp. Rows, columns and diagonals all count!",
166
+ tag: "",
167
+ subtitle: "",
95
168
  packed: "PACKED",
96
169
  bingo: "BINGO",
97
170
  score: "SCORE",
98
171
  combo: (n) => `${n} COMBO`,
99
172
  comboPop: (n) => `${n} COMBO!`,
100
173
  reset: "Reset",
174
+ shuffle: "\u{1F500} Shuffle",
101
175
  soundOn: "\u{1F50A} Sound on",
102
176
  soundOff: "\u{1F507} Muted",
103
177
  stamp: "OK",
178
+ free: "FREE",
104
179
  reachBadge: "REACH",
105
180
  reachPop: "Reach!",
106
181
  bannerSingle: "BINGO!",
@@ -108,14 +183,15 @@ var en = {
108
183
  bannerTriple: "TRIPLE BINGO!",
109
184
  bannerNth: (n) => `Line ${n}!`,
110
185
  bannerMulti: (n) => `${n} lines at once!`,
111
- statusStart: (left) => `${left} to go. Finish a line first!`,
186
+ statusStart: () => "",
112
187
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
113
- statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
188
+ statusReach: () => "",
114
189
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
115
190
  perfectTag: "\u{1F3C6} PERFECT",
116
191
  perfectStamp: "ALL\nSET",
117
192
  perfectTitle: "Perfect!",
118
- perfectBody: "All 16 packed!\nHave a great trip!",
193
+ perfectBody: (n) => `All ${n} packed!
194
+ Have a great trip!`,
119
195
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
120
196
  statScore: "SCORE",
121
197
  statCombo: "MAX COMBO",
@@ -215,6 +291,11 @@ function createSound(isMuted) {
215
291
  },
216
292
  tick: () => tone(1760, 0, 0.03, "square", 0.03),
217
293
  note: (k, at = 0) => tone(step(k), at, 0.12, "square", 0.05),
294
+ shuffle() {
295
+ for (let i = 0; i < 7; i++) noise(i * 0.042, 0.05, 0.16, 1600 + i * 260);
296
+ tone(hz(7), 0.34, 0.12, "triangle", 0.14);
297
+ tone(hz(12), 0.42, 0.2, "triangle", 0.14);
298
+ },
218
299
  close() {
219
300
  if (ac) void ac.close().catch(() => {
220
301
  });
@@ -229,7 +310,7 @@ function resolveStorageKey(options) {
229
310
  if (options.controlled) return null;
230
311
  if (options.storageKey !== void 0) return options.storageKey || null;
231
312
  if (options.demo) return null;
232
- return itemsKey(options.items ?? DEFAULT_ITEMS);
313
+ return itemsKey(layoutItems(options));
233
314
  }
234
315
  function itemsKey(items) {
235
316
  const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
@@ -242,24 +323,24 @@ function itemsKey(items) {
242
323
  }
243
324
 
244
325
  // src/styles.ts
245
- var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
246
326
  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%}
327
+ .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
328
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
249
329
  .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
330
  .pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
251
331
  @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
332
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
253
333
  @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}
334
+ .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
255
335
  .pkb-tags{display:flex;gap:8px;align-items:center}
256
336
  .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}
337
+ .pkb-kicker:empty{display:none}
257
338
  .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}
258
339
  @keyframes pkb-rb{to{background-position:-200% 0}}
259
340
  .pkb button{font-family:var(--pk-body)}
260
341
  .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
342
  .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}
343
+ .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
263
344
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
264
345
  .pkb-score{display:flex;gap:10px}
265
346
  .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 +359,18 @@ var CSS = `
278
359
  .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
279
360
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
280
361
  .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)}
362
+ .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
363
+ .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}
364
+ .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
284
365
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
285
366
  .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}
367
+ .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
368
+ .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}
369
+ .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
370
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
290
371
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
372
+ .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
373
+ .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
374
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
292
375
  .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
376
  @keyframes pkb-glint{0%{background-position:200% 0}30%,100%{background-position:-100% 0}}
@@ -304,12 +387,17 @@ var CSS = `
304
387
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
305
388
  .pkb-board.pkb-all .pkb-lines{opacity:0}
306
389
  .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}
390
+ .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:calc(13px*var(--pk-d))}
391
+ .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
309
392
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
310
393
  @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}
312
- .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}
394
+ .pkb-foot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
395
+ .pkb-foot>span{flex:1 1 14em}
396
+ .pkb-foot>span:empty{flex-basis:0}
397
+ .pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
398
+ .pkb-reset,.pkb-shuf{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}
399
+ .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
400
+ .pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
313
401
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
314
402
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
315
403
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -323,7 +411,7 @@ var CSS = `
323
411
  .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
412
  .pkb-float.pkb-pt{font-size:22px;color:var(--pk-paper)}
325
413
  .pkb-float.pkb-cmb{font-size:17px}
326
- .pkb-float.pkb-big{font-size:clamp(20px,6cqi,34px);color:#FFE066}
414
+ .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
327
415
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
328
416
  @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
417
  .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 +422,7 @@ var CSS = `
334
422
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
335
423
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
336
424
  .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}
425
+ .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
426
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
339
427
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
340
428
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -344,32 +432,21 @@ var CSS = `
344
432
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
345
433
  .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
434
  .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}}
435
+ @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}}
436
+ @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}}
437
+ @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
438
  @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
439
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
350
440
  `;
351
441
 
352
442
  // 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];
443
+ var LINES = bingoLines(4);
366
444
  var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"];
367
445
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
368
446
  var COMBO_MS = 2500;
369
447
  var MUTE_KEY = "packing-bingo-muted";
448
+ var ORDER_PREFIX = "packing-bingo-order:";
370
449
  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
450
  function readStore(key) {
374
451
  try {
375
452
  return localStorage.getItem(key);
@@ -383,30 +460,32 @@ function writeStore(key, value) {
383
460
  } catch {
384
461
  }
385
462
  }
386
- function ensureAssets(fonts) {
387
- if (!document.getElementById("pkb-styles")) {
388
- const s = document.createElement("style");
389
- s.id = "pkb-styles";
390
- s.textContent = CSS;
391
- document.head.appendChild(s);
392
- }
393
- if (fonts && !document.getElementById("pkb-fonts")) {
394
- const l = document.createElement("link");
395
- l.id = "pkb-fonts";
396
- l.rel = "stylesheet";
397
- l.href = FONT_URL;
398
- document.head.appendChild(l);
399
- }
463
+ var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
464
+ var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
465
+ var FLY_MS = 560;
466
+ function ensureStyles() {
467
+ if (document.getElementById("pkb-styles")) return;
468
+ const s = document.createElement("style");
469
+ s.id = "pkb-styles";
470
+ s.textContent = CSS;
471
+ document.head.appendChild(s);
400
472
  }
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("");
473
+ function template(L, M, { demo, shuffle, vibrate }) {
474
+ const n = L.size;
475
+ const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
476
+ const tiles = Array.from({ length: n * n }, (_, c) => {
477
+ const gd = `--pkb-gd:${glintDelay(c, n)}`;
478
+ if (c === L.center)
479
+ return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
480
+ const it = L.items[L.itemCell.indexOf(c)];
481
+ return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}">${input}<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>`;
482
+ }).join("");
483
+ 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
484
  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">
485
+ return `<div class="pkb" style="${scale}"><div class="pkb-board">
407
486
  <div class="pkb-head"><div>
408
487
  <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>
488
+ <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
410
489
  <div class="pkb-side"><div class="pkb-score">
411
490
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
412
491
  <div class="pkb-pill" data-r="bp"><small>${esc(M.bingo)}</small><b data-r="bng"></b></div>
@@ -414,24 +493,44 @@ function template(items, M, demo) {
414
493
  <div class="pkb-combo" data-r="combo"><b data-r="cmbN"></b><span class="pkb-bar"><i data-r="cbar"></i></span></div></div></div>
415
494
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
416
495
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
417
- <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>
496
+ <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><div class="pkb-acts">${shuffle ? `<button type="button" class="pkb-shuf" data-r="shuf">${esc(M.shuffle)}</button>` : ""}<button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div></div>
418
497
  <div class="pkb-flash" data-r="flash"></div>
419
498
  <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>
499
+ <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
500
  <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
501
  <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
423
502
  <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
424
503
  }
504
+ var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
425
505
  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}).`);
506
+ const L = resolveLayout(options);
507
+ const { items, cellLines } = L;
508
+ const n = L.size, N = items.length;
509
+ const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
510
+ const identity = () => Array.from({ length: N }, (_, s) => s);
511
+ const isIdentity = (o) => o.every((v, s) => v === s);
512
+ const validOrder = (o) => Array.isArray(o) && o.length === N && o.every((v) => Number.isInteger(v) && v >= 0 && v < N) && new Set(o).size === N;
513
+ let order = identity();
514
+ const slotOf = identity();
515
+ let lines = L.lines;
516
+ const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
517
+ const demoSlots = L.itemCell.flatMap((c, s) => demoCells.includes(c) ? [s] : []);
518
+ const demoStart = () => {
519
+ const on = normalize(void 0);
520
+ demoSlots.forEach((s) => on[order[s]] = true);
521
+ return on;
522
+ };
523
+ const far = (c) => Math.floor(c / n) + c % n;
524
+ const dScale = Math.min(1, 4 / n);
428
525
  const M = { ...ja, ...options.messages };
429
526
  if (options.title != null) M.title = options.title;
430
527
  if (options.tag != null) M.tag = options.tag;
431
528
  if (options.subtitle != null) M.subtitle = options.subtitle;
432
529
  const controlled = !!options.controlled;
433
530
  const storageKey = resolveStorageKey(options);
531
+ const orderKey = storageKey && ORDER_PREFIX + storageKey;
434
532
  const demo = !!options.demo;
533
+ const vibrate = options.vibrate !== false;
435
534
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
436
535
  let dead = false;
437
536
  const timers = /* @__PURE__ */ new Set();
@@ -444,8 +543,8 @@ function createPackingBingo(container, options = {}) {
444
543
  };
445
544
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
446
545
  let gen = 0;
447
- ensureAssets(options.fonts !== false);
448
- container.innerHTML = template(items, M, demo);
546
+ ensureStyles();
547
+ container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
449
548
  const $ = (r) => {
450
549
  const el = container.querySelector(`[data-r="${r}"]`);
451
550
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -457,7 +556,9 @@ function createPackingBingo(container, options = {}) {
457
556
  const svg = container.querySelector(".pkb-lines");
458
557
  const cv = container.querySelector(".pkb-fx");
459
558
  const ctx = cv.getContext("2d");
460
- const tiles = Array.from(grid.children);
559
+ const cellTiles = Array.from(grid.children);
560
+ const tiles = L.itemCell.map((c) => cellTiles[c]);
561
+ const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
461
562
  const ins = tiles.map((t) => t.querySelector("input"));
462
563
  const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
463
564
  let checked = normalize(void 0);
@@ -492,14 +593,14 @@ function createPackingBingo(container, options = {}) {
492
593
  cv.width = Math.round(BW * dpr);
493
594
  cv.height = Math.round(BH * dpr);
494
595
  };
495
- const piece = (x, y, vx, vy, g, s, life, shape) => ({
596
+ const piece = (x, y, vx, vy, g, s, life, shape2) => ({
496
597
  x,
497
598
  y,
498
599
  vx,
499
600
  vy,
500
601
  g,
501
602
  life,
502
- shape,
603
+ shape: shape2,
503
604
  w: (5 + Math.random() * 6) * s,
504
605
  h: (3 + Math.random() * 5) * s,
505
606
  r: Math.random() * 6,
@@ -509,9 +610,9 @@ function createPackingBingo(container, options = {}) {
509
610
  const go = () => {
510
611
  if (!raf && ctx) raf = requestAnimationFrame(tick);
511
612
  };
512
- function burst(x, y, n, o = {}) {
613
+ function burst(x, y, n2, o = {}) {
513
614
  if (calm) return;
514
- for (let i = 0; i < n; i++) {
615
+ for (let i = 0; i < n2; i++) {
515
616
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
516
617
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
517
618
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
@@ -602,11 +703,11 @@ function createPackingBingo(container, options = {}) {
602
703
  void el.offsetWidth;
603
704
  el.classList.add(cls);
604
705
  };
605
- const hop = (i, d) => {
706
+ const hop = (t, d) => {
606
707
  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");
708
+ t.classList.remove("pkb-squash");
709
+ t.style.setProperty("--pkb-d", d + "ms");
710
+ replay(t, "pkb-hop");
610
711
  };
611
712
  grid.addEventListener("animationend", (e) => {
612
713
  const t = e.target;
@@ -620,6 +721,7 @@ function createPackingBingo(container, options = {}) {
620
721
  r.addEventListener("animationend", () => r.remove());
621
722
  };
622
723
  const buzz = (pattern) => {
724
+ if (!vibrate) return;
623
725
  const ua = navigator.userActivation;
624
726
  if (ua && !ua.hasBeenActive) return;
625
727
  try {
@@ -654,8 +756,8 @@ function createPackingBingo(container, options = {}) {
654
756
  st(t0);
655
757
  if (!calm) replay($("sp"), "pkb-bump");
656
758
  }
657
- const showGain = (n, x, y, cls = "pt") => {
658
- floatText(`+${n}`, x, y, cls);
759
+ const showGain = (pts, x, y, cls = "pt") => {
760
+ floatText(`+${pts}`, x, y, cls);
659
761
  countScore();
660
762
  };
661
763
  function countUp(el, a, b, d, fmt, ticks = false) {
@@ -675,15 +777,19 @@ function createPackingBingo(container, options = {}) {
675
777
  };
676
778
  st(t0);
677
779
  }
678
- const ctr = (i) => {
679
- const t = tiles[i];
780
+ const ctr = (t) => {
680
781
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
681
782
  };
783
+ let landing = 0;
682
784
  function drawLines(fresh = []) {
785
+ if (landing) {
786
+ svg.innerHTML = "";
787
+ return;
788
+ }
683
789
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
684
790
  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;
791
+ const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
792
+ 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
793
  const at = `x1="${a.x - ex}" y1="${a.y - ey}" x2="${b.x + ex}" y2="${b.y + ey}" pathLength="1"`;
688
794
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
689
795
  }).join("");
@@ -694,6 +800,7 @@ function createPackingBingo(container, options = {}) {
694
800
  combo = 0;
695
801
  $("combo").classList.remove("pkb-on");
696
802
  sfx.off();
803
+ buzz(8);
697
804
  return;
698
805
  }
699
806
  const now = performance.now();
@@ -716,7 +823,7 @@ function createPackingBingo(container, options = {}) {
716
823
  $("cmbN").textContent = M.combo(combo);
717
824
  replay($("combo"), "pkb-on");
718
825
  }
719
- buzz(12);
826
+ buzz(18);
720
827
  }
721
828
  $("cbar").addEventListener("animationend", () => {
722
829
  $("combo").classList.remove("pkb-on");
@@ -730,29 +837,29 @@ function createPackingBingo(container, options = {}) {
730
837
  later(() => sfx.reach(), 150);
731
838
  }
732
839
  function bingo(fresh, pts) {
733
- const n = fresh.length;
734
- const seq = [...new Set(fresh.flatMap((k) => LINES[k]))];
840
+ const nl = fresh.length;
841
+ const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
735
842
  const step = Math.min(70, 300 / seq.length);
736
843
  seq.forEach(
737
- (i, j) => later(() => {
738
- hop(i, 0);
739
- const p = rel(tiles[i]);
844
+ (t, j) => later(() => {
845
+ hop(t, 0);
846
+ const p = rel(t);
740
847
  burst(p.x, p.y, 14, { speed: 5, g: 0.15, life: 30, size: 0.7, star: 0.8 });
741
848
  sfx.note(j + 3);
742
849
  }, j * step)
743
850
  );
744
851
  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);
852
+ $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
853
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
747
854
  replay($("banner"), "pkb-show");
748
855
  if (!calm) {
749
856
  replay($("flash"), "pkb-go");
750
857
  replay(board, "pkb-punch");
751
858
  }
752
859
  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]);
860
+ burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
861
+ sfx.bingo(nl);
862
+ buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
756
863
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
757
864
  }, seq.length * step + 60);
758
865
  }
@@ -760,9 +867,10 @@ function createPackingBingo(container, options = {}) {
760
867
  const bonus = gotP ? 0 : 1e3;
761
868
  gotP = true;
762
869
  score += bonus;
763
- tiles.forEach((_, i) => hop(i, ((i >> 2) + (i & 3)) * 60));
870
+ cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
764
871
  sfx.perfect();
765
872
  if (!calm) replay($("flash"), "pkb-go");
873
+ $("pbody").textContent = perfectText(M, N);
766
874
  $("sScore").textContent = "0";
767
875
  $("sCombo").textContent = "\xD70";
768
876
  $("sBingo").textContent = "0";
@@ -809,30 +917,32 @@ function createPackingBingo(container, options = {}) {
809
917
  $("combo").classList.remove("pkb-on");
810
918
  perfectEl.classList.remove("pkb-show");
811
919
  }
920
+ const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
812
921
  function render(src, quiet) {
813
922
  const events = [];
814
923
  const on = checked, c = on.filter(Boolean).length;
815
- cur = LINES.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
924
+ cur = completeLines();
816
925
  const fresh = cur.filter((k) => !prevLines.has(k));
817
926
  if ([...prevLines].some((k) => !cur.includes(k))) {
818
927
  cancelFx();
819
- if (c < 16) perfectEl.classList.remove("pkb-show");
928
+ if (c < N) perfectEl.classList.remove("pkb-show");
820
929
  }
821
- const hit = new Set(cur.flatMap((k) => LINES[k]));
930
+ const hit = new Set(cur.flatMap((k) => cellLines[k]));
822
931
  const reach = /* @__PURE__ */ new Set();
823
- for (const l of LINES) {
932
+ for (const l of lines) {
824
933
  const m = l.filter((i) => !on[i]);
825
934
  if (m.length === 1) reach.add(m[0]);
826
935
  }
827
936
  tiles.forEach((t, i) => {
828
937
  t.classList.toggle("pkb-on", on[i]);
829
- t.classList.toggle("pkb-line", hit.has(i));
938
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
830
939
  t.classList.toggle("pkb-reach", reach.has(i));
831
940
  });
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`;
941
+ freeTile?.classList.toggle("pkb-line", hit.has(L.center));
942
+ board.classList.toggle("pkb-all", c === N);
943
+ kick.classList.toggle("pkb-gold", c === N);
944
+ kick.textContent = c === N ? M.perfectTag : M.tag;
945
+ $("cnt").textContent = `${c}/${N}`;
836
946
  $("bng").textContent = String(cur.length);
837
947
  const bp = $("bp");
838
948
  if (cur.length > lastN) {
@@ -843,13 +953,13 @@ function createPackingBingo(container, options = {}) {
843
953
  lastN = cur.length;
844
954
  drawLines(fresh);
845
955
  if (src !== null) tapFx(src, on[src]);
846
- const newL = fresh.filter((k) => !gotL.has(k));
847
- newL.forEach((k) => gotL.add(k));
956
+ const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
957
+ newL.forEach((key) => gotL.add(key));
848
958
  const linePts = 100 * newL.length * newL.length;
849
959
  score += linePts;
850
960
  if (quiet) {
851
961
  if (linePts) countScore();
852
- } else if (c === 16 && !prevAll) perfect(events);
962
+ } else if (c === N && !prevAll) perfect(events);
853
963
  else if (fresh.length) {
854
964
  bingo(fresh, linePts);
855
965
  const e = { newLines: fresh, totalLines: cur.length, score };
@@ -860,9 +970,9 @@ function createPackingBingo(container, options = {}) {
860
970
  }
861
971
  prevLines = new Set(cur);
862
972
  prevReach = reach;
863
- prevAll = c === 16;
973
+ prevAll = c === N;
864
974
  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);
975
+ $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
866
976
  return events;
867
977
  }
868
978
  const flush = (events) => {
@@ -889,8 +999,9 @@ function createPackingBingo(container, options = {}) {
889
999
  got.clear();
890
1000
  gotL.clear();
891
1001
  checked.forEach((v, i) => v && got.add(i));
892
- LINES.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
893
- prevLines = new Set(gotL);
1002
+ const done = completeLines();
1003
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1004
+ prevLines = new Set(done);
894
1005
  prevReach = /* @__PURE__ */ new Set();
895
1006
  prevAll = checked.every(Boolean);
896
1007
  gotP = prevAll;
@@ -913,6 +1024,77 @@ function createPackingBingo(container, options = {}) {
913
1024
  options.onChange?.(checked.slice());
914
1025
  flush(events);
915
1026
  }
1027
+ function place(next) {
1028
+ order = next;
1029
+ order.forEach((i, s) => {
1030
+ slotOf[i] = s;
1031
+ cellTiles[L.itemCell[s]] = tiles[i];
1032
+ });
1033
+ lines = L.lines.map((l) => l.map((s) => order[s]));
1034
+ const focused = document.activeElement;
1035
+ cellTiles.forEach((t, c) => {
1036
+ t.style.setProperty("--pkb-gd", glintDelay(c, n));
1037
+ grid.appendChild(t);
1038
+ });
1039
+ if (focused instanceof HTMLElement && grid.contains(focused) && document.activeElement !== focused)
1040
+ focused.focus({ preventScroll: true });
1041
+ }
1042
+ const saveOrder = () => {
1043
+ if (!orderKey) return;
1044
+ writeStore(orderKey, JSON.stringify(order));
1045
+ };
1046
+ let flights = [];
1047
+ function arrange(next) {
1048
+ if (!checked.every(Boolean)) cancelFx();
1049
+ const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1050
+ flights.forEach((a) => a.cancel());
1051
+ flights = [];
1052
+ place(next);
1053
+ lines.forEach((l) => {
1054
+ if (l.every((i) => got.has(i))) gotL.add(lineKey(l));
1055
+ });
1056
+ const done = completeLines();
1057
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1058
+ prevLines = new Set(done);
1059
+ render(null, true);
1060
+ saveOrder();
1061
+ if (!from || typeof Element.prototype.animate !== "function") return;
1062
+ tiles.forEach((t, i) => {
1063
+ const a = from[i], b = t.getBoundingClientRect();
1064
+ const dx = a.left - b.left, dy = a.top - b.top;
1065
+ if (Math.abs(dx) + Math.abs(dy) < 1) return;
1066
+ flights.push(
1067
+ t.animate([{ transform: `translate(${dx}px,${dy}px)` }, { transform: "none" }], {
1068
+ duration: FLY_MS - 140,
1069
+ delay: Math.random() * 140,
1070
+ easing: "cubic-bezier(.3,1.25,.5,1)",
1071
+ fill: "backwards"
1072
+ })
1073
+ );
1074
+ });
1075
+ clearTimeout(landing);
1076
+ landing = window.setTimeout(() => {
1077
+ landing = 0;
1078
+ flights = [];
1079
+ if (!dead) drawLines(cur);
1080
+ }, FLY_MS);
1081
+ svg.innerHTML = "";
1082
+ }
1083
+ function shuffleCard() {
1084
+ let next = order.slice();
1085
+ do {
1086
+ for (let s = N - 1; s > 0; s--) {
1087
+ const r = Math.floor(Math.random() * (s + 1));
1088
+ [next[s], next[r]] = [next[r], next[s]];
1089
+ }
1090
+ } while (next.every((v, s) => v === order[s]));
1091
+ arrange(next);
1092
+ sfx.shuffle();
1093
+ buzz([10, 40, 10, 40, 10, 40, 24]);
1094
+ }
1095
+ const unshuffle = () => {
1096
+ if (!isIdentity(order)) arrange(identity());
1097
+ };
916
1098
  grid.addEventListener("change", (e) => {
917
1099
  const i = ins.indexOf(e.target);
918
1100
  if (i < 0) return;
@@ -920,7 +1102,11 @@ function createPackingBingo(container, options = {}) {
920
1102
  next[i] = !next[i];
921
1103
  request(next, i);
922
1104
  });
923
- $("reset").addEventListener("click", () => request(normalize(void 0), null, { reset: true }));
1105
+ container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1106
+ $("reset").addEventListener("click", () => {
1107
+ unshuffle();
1108
+ request(normalize(void 0), null, { reset: true });
1109
+ });
924
1110
  $("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
925
1111
  perfectEl.addEventListener("click", (e) => {
926
1112
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
@@ -949,18 +1135,31 @@ function createPackingBingo(container, options = {}) {
949
1135
  }
950
1136
  };
951
1137
  $("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();
1138
+ const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1139
+ const pick = checked.every(Boolean) ? [...lines[0]] : open();
954
1140
  void run(pick.map((i) => ({ i, delay: 320 })));
955
1141
  });
956
1142
  $("dP").addEventListener("click", () => {
957
- const miss = checked.every(Boolean) ? tiles.map((_, i) => i) : tiles.map((_, i) => i).filter((i) => !checked[i]);
1143
+ const all = items.map((_, i) => i);
1144
+ const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
958
1145
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
959
1146
  });
960
1147
  $("dR").addEventListener("click", () => {
961
- if (!busy) request(demoStart(), null, { reset: true });
1148
+ if (busy) return;
1149
+ unshuffle();
1150
+ request(demoStart(), null, { reset: true });
962
1151
  });
963
1152
  }
1153
+ let startOrder = null;
1154
+ if (orderKey) {
1155
+ try {
1156
+ const saved = JSON.parse(readStore(orderKey) ?? "null");
1157
+ if (validOrder(saved)) startOrder = saved;
1158
+ } catch {
1159
+ }
1160
+ }
1161
+ if (!startOrder && validOrder(options.defaultOrder)) startOrder = options.defaultOrder.slice();
1162
+ if (startOrder && !isIdentity(startOrder)) place(startOrder);
964
1163
  let initial = null;
965
1164
  if (storageKey) {
966
1165
  try {
@@ -982,29 +1181,45 @@ function createPackingBingo(container, options = {}) {
982
1181
  return {
983
1182
  setChecked(next, opts = {}) {
984
1183
  if (dead) return;
985
- const n = normalize(next);
986
- const changed = n.flatMap((v, i) => v !== checked[i] ? [i] : []);
1184
+ const n2 = normalize(next);
1185
+ const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
987
1186
  if (!changed.length) return;
988
1187
  if (!opts.animate || changed.length !== 1) {
989
- baseline(n);
1188
+ baseline(n2);
990
1189
  return;
991
1190
  }
992
- flush(apply(n, changed[0], false));
1191
+ flush(apply(n2, changed[0], false));
993
1192
  },
994
1193
  reset() {
995
- if (!dead) request(normalize(void 0), null, { reset: true });
1194
+ if (dead) return;
1195
+ unshuffle();
1196
+ request(normalize(void 0), null, { reset: true });
1197
+ },
1198
+ shuffle() {
1199
+ if (!dead) shuffleCard();
996
1200
  },
997
1201
  refresh() {
998
1202
  if (!dead) render(null, true);
999
1203
  },
1000
1204
  getState() {
1001
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean) };
1205
+ return {
1206
+ checked: checked.slice(),
1207
+ lines: cur.slice(),
1208
+ score,
1209
+ maxCombo,
1210
+ complete: checked.every(Boolean),
1211
+ size: n,
1212
+ free: L.free,
1213
+ order: order.slice()
1214
+ };
1002
1215
  },
1003
1216
  destroy() {
1004
1217
  if (dead) return;
1005
1218
  dead = true;
1006
1219
  timers.forEach((id) => clearTimeout(id));
1007
1220
  timers.clear();
1221
+ clearTimeout(landing);
1222
+ flights.forEach((a) => a.cancel());
1008
1223
  cancelAnimationFrame(raf);
1009
1224
  cancelAnimationFrame(tw);
1010
1225
  ro?.disconnect();
@@ -1017,6 +1232,7 @@ function createPackingBingo(container, options = {}) {
1017
1232
  0 && (module.exports = {
1018
1233
  DEFAULT_ITEMS,
1019
1234
  LINES,
1235
+ bingoLines,
1020
1236
  createPackingBingo,
1021
1237
  en,
1022
1238
  ja