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/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  DEFAULT_ITEMS: () => DEFAULT_ITEMS,
25
25
  LINES: () => LINES,
26
26
  PackingBingo: () => PackingBingo,
27
+ bingoLines: () => bingoLines,
27
28
  createPackingBingo: () => createPackingBingo,
28
29
  en: () => en,
29
30
  ja: () => ja
@@ -35,38 +36,109 @@ var import_react = require("react");
35
36
 
36
37
  // src/items.ts
37
38
  var DEFAULT_ITEMS = [
38
- { emoji: "\u{1F4D6}", label: "\u65C5\u306E\u3057\u304A\u308A" },
39
- { emoji: "\u{1FAAA}", label: "\u751F\u5F92\u624B\u5E33" },
40
- { emoji: "\u{1F3E5}", label: "\u4FDD\u967A\u8A3C\u30B3\u30D4\u30FC" },
41
- { emoji: "\u{1F45B}", label: "\u304A\u3053\u3065\u304B\u3044" },
42
- { emoji: "\u{1F455}", label: "\u7740\u66FF\u3048" },
43
- { emoji: "\u{1F319}", label: "\u30D1\u30B8\u30E3\u30DE" },
44
- { emoji: "\u{1F9E5}", label: "\u4E0A\u7740" },
45
- { emoji: "\u{1FAA5}", label: "\u6D17\u9762\u7528\u5177" },
46
- { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
47
- { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1\u30FB\u30C6\u30A3\u30C3\u30B7\u30E5" },
39
+ { emoji: "\u{1F45B}", label: "\u8CA1\u5E03" },
40
+ { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB" },
41
+ { emoji: "\u{1F50B}", label: "\u30E2\u30D0\u30A4\u30EB\u200B\u5145\u96FB\u5668" },
42
+ { emoji: "\u{1F4CB}", label: "\u5065\u5EB7\u8ABF\u67FB\u7968" },
43
+ { emoji: "\u{1FAAA}", label: "\u30DE\u30A4\u30CA\u30AB\u30FC\u30C9\u200B\u307E\u305F\u306F\u4FDD\u967A\u8A3C" },
44
+ { emoji: "\u{1F4D6}", label: "\u3057\u304A\u308A" },
48
45
  { emoji: "\u{1F637}", label: "\u30DE\u30B9\u30AF" },
49
- { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC\u30FB\u9154\u3044\u6B62\u3081" },
50
- { emoji: "\u2602\uFE0F", label: "\u6298\u308A\u305F\u305F\u307F\u5098" },
51
- { emoji: "\u270F\uFE0F", label: "\u7B46\u8A18\u7528\u5177" },
46
+ { emoji: "\u{1F927}", label: "\u30CF\u30F3\u30AB\u30C1" },
47
+ { emoji: "\u{1F48A}", label: "\u5E38\u5099\u85AC" },
52
48
  { emoji: "\u{1F6CD}\uFE0F", label: "\u30D3\u30CB\u30FC\u30EB\u888B" },
53
- { emoji: "\u{1F4F1}", label: "\u30B9\u30DE\u30DB\u30FB\u5145\u96FB\u5668" }
49
+ { emoji: "\u{1F9E4}", label: "\u8ECD\u624B\uFF08\u5B66\u6821\u652F\u7D66\uFF09" },
50
+ { emoji: "\u{1F526}", label: "\u30D8\u30C3\u30C9\u30E9\u30A4\u30C8" },
51
+ { emoji: "\u{1F455}", label: "\u30B8\u30E3\u30FC\u30B8" },
52
+ { emoji: "\u2614", label: "\u96E8\u5177" },
53
+ { emoji: "\u{1F6C1}", label: "\u30BF\u30AA\u30EB" },
54
+ { emoji: "\u{1F9F4}", label: "\u65E5\u713C\u3051\u6B62\u3081" }
54
55
  ];
55
56
 
57
+ // src/layout.ts
58
+ var MIN_SIZE = 2;
59
+ var MAX_SIZE = 10;
60
+ var fail = (msg) => {
61
+ throw new Error(`[packing-bingo] ${msg}`);
62
+ };
63
+ function layoutItems(options) {
64
+ if (options.items) return options.items;
65
+ const n = options.size;
66
+ if (n == null || !Number.isInteger(n) || n < MIN_SIZE) return DEFAULT_ITEMS;
67
+ return DEFAULT_ITEMS.slice(0, n * n - (options.free ? 1 : 0));
68
+ }
69
+ function checkSize(size, free) {
70
+ if (!Number.isInteger(size) || size < MIN_SIZE || size > MAX_SIZE)
71
+ fail(`"size" must be a whole number from ${MIN_SIZE} to ${MAX_SIZE} (got ${size}).`);
72
+ if (free && size % 2 === 0) fail(`"free" needs an odd size such as 3 or 5, so that the card has a center (got ${size}).`);
73
+ }
74
+ function shape(n, free) {
75
+ const seq = (f) => Array.from({ length: n }, (_, k) => f(k));
76
+ const cellLines = [
77
+ ...seq((r) => seq((c) => r * n + c)),
78
+ // rows
79
+ ...seq((c) => seq((r) => r * n + c)),
80
+ // columns
81
+ seq((k) => k * (n + 1)),
82
+ // top left to bottom right
83
+ seq((k) => (k + 1) * (n - 1))
84
+ // top right to bottom left
85
+ ];
86
+ const center = free ? (n * n - 1) / 2 : -1;
87
+ const toItem = (c) => free && c > center ? c - 1 : c;
88
+ return { center, cellLines, lines: cellLines.map((l) => l.filter((c) => c !== center).map(toItem)) };
89
+ }
90
+ function resolveLayout(options) {
91
+ const items = layoutItems(options);
92
+ const free = !!options.free;
93
+ let size = options.size;
94
+ if (size == null) {
95
+ const len = items.length;
96
+ const sideOf = (cells) => {
97
+ const r = Math.round(Math.sqrt(cells));
98
+ return r * r === cells && r >= MIN_SIZE && r <= MAX_SIZE ? r : 0;
99
+ };
100
+ size = sideOf(len + (free ? 1 : 0));
101
+ if (!size) {
102
+ const other = sideOf(len + (free ? 0 : 1));
103
+ const hint = free ? other ? `; ${len} items fill a ${other}\xD7${other} card without "free"` : "" : other % 2 ? `, or set "free" for a ${other}\xD7${other} card with a FREE center` : "";
104
+ fail(
105
+ free ? `With "free", "items" must have one less than an odd square number of entries (8, 24, 48, 80)${hint} (got ${len}).` : `"items" must have a square number of entries (4, 9, 16, 25, \u2026, 100)${hint} (got ${len}).`
106
+ );
107
+ }
108
+ }
109
+ checkSize(size, free);
110
+ const want = size * size - (free ? 1 : 0);
111
+ if (items.length !== want) {
112
+ const card = `a ${size}\xD7${size} card${free ? " with a FREE center" : ""}`;
113
+ fail(
114
+ options.items ? `"items" must have exactly ${want} entries for ${card} (got ${items.length}).` : `A ${card.slice(2)} needs ${want} items, but DEFAULT_ITEMS has only ${DEFAULT_ITEMS.length}. Pass "items".`
115
+ );
116
+ }
117
+ const { center, cellLines, lines } = shape(size, free);
118
+ const itemCell = items.map((_, i) => free && i >= center ? i + 1 : i);
119
+ return { size, free, items, itemCell, center, cellLines, lines };
120
+ }
121
+ function bingoLines(size, free = false) {
122
+ checkSize(size, free);
123
+ return shape(size, free).lines;
124
+ }
125
+
56
126
  // src/messages.ts
57
127
  var ja = {
58
128
  title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
59
- tag: "\u4FEE\u5B66\u65C5\u884C",
60
- subtitle: "\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30F3\u30D7\u3002\u30BF\u30C6\u30FB\u30E8\u30B3\u30FB\u30CA\u30CA\u30E1\u3067\u30D3\u30F3\u30B4\uFF01",
129
+ tag: "",
130
+ subtitle: "",
61
131
  packed: "\u305D\u308D\u3063\u305F",
62
132
  bingo: "BINGO",
63
133
  score: "SCORE",
64
134
  combo: (n) => `${n} COMBO`,
65
135
  comboPop: (n) => `${n} COMBO!`,
66
136
  reset: "\u30EA\u30BB\u30C3\u30C8",
137
+ shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
67
138
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
68
139
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
69
140
  stamp: "OK",
141
+ free: "FREE",
70
142
  reachBadge: "\u30EA\u30FC\u30C1",
71
143
  reachPop: "\u30EA\u30FC\u30C1!",
72
144
  bannerSingle: "BINGO!",
@@ -74,14 +146,15 @@ var ja = {
74
146
  bannerTriple: "\u30C8\u30EA\u30D7\u30EB\u30D3\u30F3\u30B4!",
75
147
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
76
148
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
77
- statusStart: (left) => `\u306E\u3053\u308A ${left} \u3053\u3002\u307E\u305A\u306F1\u30E9\u30A4\u30F3\u305D\u308D\u3048\u3088\u3046`,
149
+ statusStart: () => "",
78
150
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
79
- 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`,
151
+ statusReach: () => "",
80
152
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
81
153
  perfectTag: "\u{1F3C6} PERFECT",
82
154
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
83
155
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
84
- perfectBody: "16\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01\n\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01",
156
+ perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
157
+ \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
85
158
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
86
159
  statScore: "SCORE",
87
160
  statCombo: "MAX COMBO",
@@ -94,17 +167,19 @@ var ja = {
94
167
  };
95
168
  var en = {
96
169
  title: "Packing Bingo",
97
- tag: "School trip",
98
- subtitle: "Tap to stamp. Rows, columns and diagonals all count!",
170
+ tag: "",
171
+ subtitle: "",
99
172
  packed: "PACKED",
100
173
  bingo: "BINGO",
101
174
  score: "SCORE",
102
175
  combo: (n) => `${n} COMBO`,
103
176
  comboPop: (n) => `${n} COMBO!`,
104
177
  reset: "Reset",
178
+ shuffle: "\u{1F500} Shuffle",
105
179
  soundOn: "\u{1F50A} Sound on",
106
180
  soundOff: "\u{1F507} Muted",
107
181
  stamp: "OK",
182
+ free: "FREE",
108
183
  reachBadge: "REACH",
109
184
  reachPop: "Reach!",
110
185
  bannerSingle: "BINGO!",
@@ -112,14 +187,15 @@ var en = {
112
187
  bannerTriple: "TRIPLE BINGO!",
113
188
  bannerNth: (n) => `Line ${n}!`,
114
189
  bannerMulti: (n) => `${n} lines at once!`,
115
- statusStart: (left) => `${left} to go. Finish a line first!`,
190
+ statusStart: () => "",
116
191
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
117
- statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
192
+ statusReach: () => "",
118
193
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
119
194
  perfectTag: "\u{1F3C6} PERFECT",
120
195
  perfectStamp: "ALL\nSET",
121
196
  perfectTitle: "Perfect!",
122
- perfectBody: "All 16 packed!\nHave a great trip!",
197
+ perfectBody: (n) => `All ${n} packed!
198
+ Have a great trip!`,
123
199
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
124
200
  statScore: "SCORE",
125
201
  statCombo: "MAX COMBO",
@@ -219,6 +295,11 @@ function createSound(isMuted) {
219
295
  },
220
296
  tick: () => tone(1760, 0, 0.03, "square", 0.03),
221
297
  note: (k, at = 0) => tone(step(k), at, 0.12, "square", 0.05),
298
+ shuffle() {
299
+ for (let i = 0; i < 7; i++) noise(i * 0.042, 0.05, 0.16, 1600 + i * 260);
300
+ tone(hz(7), 0.34, 0.12, "triangle", 0.14);
301
+ tone(hz(12), 0.42, 0.2, "triangle", 0.14);
302
+ },
222
303
  close() {
223
304
  if (ac) void ac.close().catch(() => {
224
305
  });
@@ -233,7 +314,7 @@ function resolveStorageKey(options) {
233
314
  if (options.controlled) return null;
234
315
  if (options.storageKey !== void 0) return options.storageKey || null;
235
316
  if (options.demo) return null;
236
- return itemsKey(options.items ?? DEFAULT_ITEMS);
317
+ return itemsKey(layoutItems(options));
237
318
  }
238
319
  function itemsKey(items) {
239
320
  const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
@@ -246,24 +327,24 @@ function itemsKey(items) {
246
327
  }
247
328
 
248
329
  // src/styles.ts
249
- var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
250
330
  var CSS = `
251
- .pkb{--pk-sun:var(--pkb-sun,#FFD54F);--pk-ink:var(--pkb-ink,#2A1F14);--pk-paper:var(--pkb-paper,#FFFDF6);--pk-red:var(--pkb-red,#E8473B);--pk-mint:var(--pkb-mint,#BDEBD2);--pk-display:"Dela Gothic One",var(--pkb-font-fallback,sans-serif);--pk-body:"M PLUS Rounded 1c","Hiragino Maru Gothic ProN",var(--pkb-font-fallback,sans-serif);container-type:inline-size;isolation:isolate;width:100%}
331
+ .pkb{--pk-sun:var(--pkb-sun,#FFD54F);--pk-ink:var(--pkb-ink,#2A1F14);--pk-paper:var(--pkb-paper,#FFFDF6);--pk-red:var(--pkb-red,#E8473B);--pk-mint:var(--pkb-mint,#BDEBD2);--pk-display:"Dela Gothic One",var(--pkb-font-fallback,sans-serif);--pk-body:"M PLUS Rounded 1c","Hiragino Maru Gothic ProN",var(--pkb-font-fallback,sans-serif);--pk-n:4;--pk-s:1;--pk-d:1;container-type:inline-size;isolation:isolate;width:100%}
252
332
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
253
333
  .pkb-board{position:relative;overflow:hidden;max-width:720px;margin:0 auto;padding:22px 26px 20px;border-radius:24px;background-color:var(--pk-sun);background-image:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px);background-size:16px 16px;border:2.5px solid var(--pk-ink);color:var(--pk-ink);font-family:var(--pk-body);line-height:1.4;text-align:left}
254
334
  .pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
255
335
  @keyframes pkb-shake{10%,90%{transform:translate(-1px,1px)}20%,80%{transform:translate(3px,-2px)}30%,50%,70%{transform:translate(-6px,2px)}40%,60%{transform:translate(6px,-1px)}}
256
336
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
257
337
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
258
- .pkb-head{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}
338
+ .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
259
339
  .pkb-tags{display:flex;gap:8px;align-items:center}
260
340
  .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}
341
+ .pkb-kicker:empty{display:none}
261
342
  .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}
262
343
  @keyframes pkb-rb{to{background-position:-200% 0}}
263
344
  .pkb button{font-family:var(--pk-body)}
264
345
  .pkb-snd{appearance:none;border:2px solid var(--pk-ink);background:var(--pk-paper);border-radius:999px;padding:2px 9px;font-weight:800;font-size:11px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
265
346
  .pkb-title{font-family:var(--pk-display);font-weight:400;font-size:34px;line-height:1.15;margin-top:8px;letter-spacing:.02em;color:inherit}
266
- .pkb-sub{font-weight:800;font-size:13px;margin-top:4px}
347
+ .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
267
348
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
268
349
  .pkb-score{display:flex;gap:10px}
269
350
  .pkb-pill{min-width:78px;background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:14px;padding:6px 10px 5px;text-align:center;box-shadow:3px 3px 0 var(--pk-ink)}
@@ -282,16 +363,18 @@ var CSS = `
282
363
  .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
283
364
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
284
365
  .pkb-gw{position:relative}
285
- .pkb-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
286
- .pkb-tile{position:relative;aspect-ratio:1/.82;background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:16px;box-shadow:4px 4px 0 var(--pk-ink);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;transition:transform .1s,box-shadow .1s,background-color .25s}
287
- .pkb-tile:active{transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
366
+ .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
367
+ .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}
368
+ .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
288
369
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
289
370
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
290
- .pkb-emo{font-size:32px;line-height:1;transition:opacity .2s}
291
- .pkb-lbl{font-weight:800;font-size:13px;line-height:1.25;text-align:center;padding:0 6px;transition:opacity .2s;word-break:keep-all;overflow-wrap:anywhere}
292
- .pkb-stamp{position:absolute;left:50%;top:50%;width:64%;aspect-ratio:1;border-radius:50%;border:3px solid var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-paper),inset 0 0 0 4px var(--pk-red);color:var(--pk-red);display:grid;place-items:center;font-family:var(--pk-display);font-size:20px;letter-spacing:.04em;opacity:0;transform:translate(-50%,-50%) rotate(-16deg) scale(1.7);transition:transform .25s cubic-bezier(.3,1.5,.5,1),opacity .15s;pointer-events:none}
371
+ .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
372
+ .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}
373
+ .pkb-stamp{position:absolute;left:50%;top:50%;width:64%;aspect-ratio:1;border-radius:50%;border:3px solid var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-paper),inset 0 0 0 4px var(--pk-red);color:var(--pk-red);display:grid;place-items:center;font-family:var(--pk-display);font-size:max(9px,calc(20px*var(--pk-s)));letter-spacing:.04em;opacity:0;transform:translate(-50%,-50%) rotate(-16deg) scale(1.7);transition:transform .25s cubic-bezier(.3,1.5,.5,1),opacity .15s;pointer-events:none}
293
374
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
294
375
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
376
+ .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
377
+ .pkb-tile.pkb-free .pkb-stamp{width:72%;opacity:1;transition:none;font-size:17cqi;letter-spacing:.02em;line-height:1;text-align:center;background:var(--pk-red);color:var(--pk-paper);border-color:var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-red),inset 0 0 0 4px var(--pk-paper)}
295
378
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
296
379
  .pkb-tile.pkb-line::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:linear-gradient(110deg,transparent 30%,rgba(255,255,255,.9) 45%,transparent 60%) no-repeat;background-size:250% 100%;background-position:200% 0;animation:pkb-glint 3.2s ease-in-out var(--pkb-gd,0s) infinite}
297
380
  @keyframes pkb-glint{0%{background-position:200% 0}30%,100%{background-position:-100% 0}}
@@ -308,12 +391,17 @@ var CSS = `
308
391
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
309
392
  .pkb-board.pkb-all .pkb-lines{opacity:0}
310
393
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
311
- .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:13}
312
- .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:6}
394
+ .pkb-lines .pkb-u{stroke:var(--pk-paper);stroke-width:calc(13px*var(--pk-d))}
395
+ .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
313
396
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
314
397
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
315
- .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px}
316
- .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}
398
+ .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}
399
+ .pkb-foot>span{flex:1 1 14em}
400
+ .pkb-foot>span:empty{flex-basis:0}
401
+ .pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
402
+ .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}
403
+ .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
404
+ .pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
317
405
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
318
406
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
319
407
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -327,7 +415,7 @@ var CSS = `
327
415
  .pkb-float{position:absolute;z-index:11;pointer-events:none;white-space:nowrap;font-family:var(--pk-display);font-weight:400;line-height:1;-webkit-text-stroke:4px var(--pk-ink);paint-order:stroke fill;transform:translate(-50%,-50%);animation:pkb-rise .95s cubic-bezier(.2,.9,.3,1) both}
328
416
  .pkb-float.pkb-pt{font-size:22px;color:var(--pk-paper)}
329
417
  .pkb-float.pkb-cmb{font-size:17px}
330
- .pkb-float.pkb-big{font-size:clamp(20px,6cqi,34px);color:#FFE066}
418
+ .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
331
419
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
332
420
  @keyframes pkb-rise{0%{opacity:0;transform:translate(-50%,-20%) scale(.4)}18%{opacity:1;transform:translate(-50%,-60%) scale(1.2)}30%{transform:translate(-50%,-70%) scale(1)}75%{opacity:1}100%{opacity:0;transform:translate(-50%,-190%) scale(1)}}
333
421
  .pkb-perfect{position:absolute;inset:0;z-index:8;display:grid;place-items:center;padding:20px;background:rgba(42,31,20,.5);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s}
@@ -338,7 +426,7 @@ var CSS = `
338
426
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
339
427
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
340
428
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
341
- .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line}
429
+ .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line;word-break:auto-phrase}
342
430
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
343
431
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
344
432
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -348,32 +436,21 @@ var CSS = `
348
436
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
349
437
  .pkb-demo{max-width:720px;margin:12px auto 4px;display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-family:var(--pk-body);font-size:12px;color:#666}
350
438
  .pkb-demo button{appearance:none;border:1px solid #ccc;background:#f3f3f3;color:#222;border-radius:999px;padding:5px 12px;font-size:12px;cursor:pointer}
351
- @container (max-width:560px){.pkb-board{padding:18px 14px 16px}.pkb-head{flex-direction:column;align-items:stretch;gap:12px}.pkb-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.pkb-pill,.pkb-pill.pkb-sc{min-width:0;padding:5px 4px 4px}.pkb-pill b{font-size:19px}.pkb-title{font-size:26px}.pkb-grid{gap:8px}.pkb-tile{border-radius:12px;box-shadow:3px 3px 0 var(--pk-ink);gap:3px;aspect-ratio:1/1}.pkb-emo{font-size:24px}.pkb-lbl{font-size:10.5px;padding:0 2px}.pkb-stamp{font-size:14px}.pkb-tile.pkb-reach::after{font-size:9px;padding:2px 5px;top:-9px;right:-5px}.pkb-lines .pkb-u{stroke-width:10}.pkb-lines .pkb-o{stroke-width:5}.pkb-banner::before{width:380px;height:380px;margin:-190px 0 0 -190px}.pkb-banner span{font-size:12px}.pkb-medal{width:96px;height:96px;font-size:20px}.pkb-card{padding:20px 14px 16px}.pkb-ptitle{font-size:24px}.pkb-stats b{font-size:17px}.pkb-float.pkb-pt{font-size:17px}.pkb-float.pkb-cmb{font-size:13px}}
439
+ @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}}
440
+ @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}}
441
+ @container (max-width:320px){.pkb-board{padding-inline:8px}.pkb-grid{gap:max(3px,calc(5px*var(--pk-d)))}.pkb-lbl{font-size:max(8.5px,calc(9.5px*var(--pk-s)));padding:0}}
352
442
  @media (prefers-reduced-motion:reduce){.pkb-board.pkb-shake,.pkb-board.pkb-punch,.pkb-flash.pkb-go,.pkb-tile.pkb-hop,.pkb-tile.pkb-squash,.pkb-tile.pkb-reach,.pkb-tile.pkb-line::before,.pkb-pill.pkb-hot,.pkb-bump,.pkb-tile.pkb-reach::after,.pkb-banner::before,.pkb-lines .pkb-draw line,.pkb-kicker.pkb-gold{animation:none}.pkb-banner.pkb-show{animation:pkb-fade 1.8s forwards}.pkb-float{animation:pkb-fade 1s both}.pkb-perfect.pkb-show .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg)}.pkb-card{transition:none}}
353
443
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
354
444
  `;
355
445
 
356
446
  // src/core.ts
357
- var LINES = [
358
- [0, 1, 2, 3],
359
- [4, 5, 6, 7],
360
- [8, 9, 10, 11],
361
- [12, 13, 14, 15],
362
- [0, 4, 8, 12],
363
- [1, 5, 9, 13],
364
- [2, 6, 10, 14],
365
- [3, 7, 11, 15],
366
- [0, 5, 10, 15],
367
- [3, 6, 9, 12]
368
- ];
369
- var DEMO_START = [0, 1, 2, 3, 5, 10];
447
+ var LINES = bingoLines(4);
370
448
  var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"];
371
449
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
372
450
  var COMBO_MS = 2500;
373
451
  var MUTE_KEY = "packing-bingo-muted";
452
+ var ORDER_PREFIX = "packing-bingo-order:";
374
453
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
375
- var normalize = (v) => Array.from({ length: 16 }, (_, i) => !!v?.[i]);
376
- var demoStart = () => Array.from({ length: 16 }, (_, i) => DEMO_START.includes(i));
377
454
  function readStore(key) {
378
455
  try {
379
456
  return localStorage.getItem(key);
@@ -387,30 +464,32 @@ function writeStore(key, value) {
387
464
  } catch {
388
465
  }
389
466
  }
390
- function ensureAssets(fonts) {
391
- if (!document.getElementById("pkb-styles")) {
392
- const s = document.createElement("style");
393
- s.id = "pkb-styles";
394
- s.textContent = CSS;
395
- document.head.appendChild(s);
396
- }
397
- if (fonts && !document.getElementById("pkb-fonts")) {
398
- const l = document.createElement("link");
399
- l.id = "pkb-fonts";
400
- l.rel = "stylesheet";
401
- l.href = FONT_URL;
402
- document.head.appendChild(l);
403
- }
467
+ var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
468
+ var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
469
+ var FLY_MS = 560;
470
+ function ensureStyles() {
471
+ if (document.getElementById("pkb-styles")) return;
472
+ const s = document.createElement("style");
473
+ s.id = "pkb-styles";
474
+ s.textContent = CSS;
475
+ document.head.appendChild(s);
404
476
  }
405
- function template(items, M, demo) {
406
- const tiles = items.map(
407
- (it, i) => `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="--pkb-gd:${((i >> 2) + (i & 3)) * 0.12}s"><input type="checkbox"><span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`
408
- ).join("");
477
+ function template(L, M, { demo, shuffle, vibrate }) {
478
+ const n = L.size;
479
+ const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
480
+ const tiles = Array.from({ length: n * n }, (_, c) => {
481
+ const gd = `--pkb-gd:${glintDelay(c, n)}`;
482
+ if (c === L.center)
483
+ return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
484
+ const it = L.items[L.itemCell.indexOf(c)];
485
+ 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>`;
486
+ }).join("");
487
+ const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
409
488
  const demoBar = demo ? `<div class="pkb-demo"><span>${esc(M.demoLabel)}</span><button type="button" data-r="dB">${esc(M.demoBingo)}</button><button type="button" data-r="dP">${esc(M.demoPerfect)}</button><button type="button" data-r="dR">${esc(M.demoRestore)}</button></div>` : "";
410
- return `<div class="pkb"><div class="pkb-board">
489
+ return `<div class="pkb" style="${scale}"><div class="pkb-board">
411
490
  <div class="pkb-head"><div>
412
491
  <div class="pkb-tags"><span class="pkb-kicker" data-r="kick">${esc(M.tag)}</span><button type="button" class="pkb-snd" data-r="snd"></button></div>
413
- <h2 class="pkb-title">${esc(M.title)}</h2><p class="pkb-sub">${esc(M.subtitle)}</p></div>
492
+ <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
414
493
  <div class="pkb-side"><div class="pkb-score">
415
494
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
416
495
  <div class="pkb-pill" data-r="bp"><small>${esc(M.bingo)}</small><b data-r="bng"></b></div>
@@ -418,24 +497,44 @@ function template(items, M, demo) {
418
497
  <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>
419
498
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
420
499
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
421
- <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>
500
+ <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>
422
501
  <div class="pkb-flash" data-r="flash"></div>
423
502
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
424
- <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody">${esc(M.perfectBody)}</p>
503
+ <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody" data-r="pbody">${esc(perfectText(M, L.items.length))}</p>
425
504
  <div class="pkb-stats"><div><small>${esc(M.statScore)}</small><b data-r="sScore">0</b></div><div><small>${esc(M.statCombo)}</small><b data-r="sCombo">\xD70</b></div><div><small>${esc(M.statLines)}</small><b data-r="sBingo">0</b></div></div>
426
505
  <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
427
506
  <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
428
507
  }
508
+ var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
429
509
  function createPackingBingo(container, options = {}) {
430
- const items = options.items ?? DEFAULT_ITEMS;
431
- if (items.length !== 16) throw new Error(`[packing-bingo] "items" must have exactly 16 entries (got ${items.length}).`);
510
+ const L = resolveLayout(options);
511
+ const { items, cellLines } = L;
512
+ const n = L.size, N = items.length;
513
+ const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
514
+ const identity = () => Array.from({ length: N }, (_, s) => s);
515
+ const isIdentity = (o) => o.every((v, s) => v === s);
516
+ const validOrder = (o) => Array.isArray(o) && o.length === N && o.every((v) => Number.isInteger(v) && v >= 0 && v < N) && new Set(o).size === N;
517
+ let order = identity();
518
+ const slotOf = identity();
519
+ let lines = L.lines;
520
+ const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
521
+ const demoSlots = L.itemCell.flatMap((c, s) => demoCells.includes(c) ? [s] : []);
522
+ const demoStart = () => {
523
+ const on = normalize(void 0);
524
+ demoSlots.forEach((s) => on[order[s]] = true);
525
+ return on;
526
+ };
527
+ const far = (c) => Math.floor(c / n) + c % n;
528
+ const dScale = Math.min(1, 4 / n);
432
529
  const M = { ...ja, ...options.messages };
433
530
  if (options.title != null) M.title = options.title;
434
531
  if (options.tag != null) M.tag = options.tag;
435
532
  if (options.subtitle != null) M.subtitle = options.subtitle;
436
533
  const controlled = !!options.controlled;
437
534
  const storageKey = resolveStorageKey(options);
535
+ const orderKey = storageKey && ORDER_PREFIX + storageKey;
438
536
  const demo = !!options.demo;
537
+ const vibrate = options.vibrate !== false;
439
538
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
440
539
  let dead = false;
441
540
  const timers = /* @__PURE__ */ new Set();
@@ -448,8 +547,8 @@ function createPackingBingo(container, options = {}) {
448
547
  };
449
548
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
450
549
  let gen = 0;
451
- ensureAssets(options.fonts !== false);
452
- container.innerHTML = template(items, M, demo);
550
+ ensureStyles();
551
+ container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
453
552
  const $ = (r) => {
454
553
  const el = container.querySelector(`[data-r="${r}"]`);
455
554
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -461,7 +560,9 @@ function createPackingBingo(container, options = {}) {
461
560
  const svg = container.querySelector(".pkb-lines");
462
561
  const cv = container.querySelector(".pkb-fx");
463
562
  const ctx = cv.getContext("2d");
464
- const tiles = Array.from(grid.children);
563
+ const cellTiles = Array.from(grid.children);
564
+ const tiles = L.itemCell.map((c) => cellTiles[c]);
565
+ const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
465
566
  const ins = tiles.map((t) => t.querySelector("input"));
466
567
  const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
467
568
  let checked = normalize(void 0);
@@ -496,14 +597,14 @@ function createPackingBingo(container, options = {}) {
496
597
  cv.width = Math.round(BW * dpr);
497
598
  cv.height = Math.round(BH * dpr);
498
599
  };
499
- const piece = (x, y, vx, vy, g, s, life, shape) => ({
600
+ const piece = (x, y, vx, vy, g, s, life, shape2) => ({
500
601
  x,
501
602
  y,
502
603
  vx,
503
604
  vy,
504
605
  g,
505
606
  life,
506
- shape,
607
+ shape: shape2,
507
608
  w: (5 + Math.random() * 6) * s,
508
609
  h: (3 + Math.random() * 5) * s,
509
610
  r: Math.random() * 6,
@@ -513,9 +614,9 @@ function createPackingBingo(container, options = {}) {
513
614
  const go = () => {
514
615
  if (!raf && ctx) raf = requestAnimationFrame(tick);
515
616
  };
516
- function burst(x, y, n, o = {}) {
617
+ function burst(x, y, n2, o = {}) {
517
618
  if (calm) return;
518
- for (let i = 0; i < n; i++) {
619
+ for (let i = 0; i < n2; i++) {
519
620
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
520
621
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
521
622
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
@@ -606,11 +707,11 @@ function createPackingBingo(container, options = {}) {
606
707
  void el.offsetWidth;
607
708
  el.classList.add(cls);
608
709
  };
609
- const hop = (i, d) => {
710
+ const hop = (t, d) => {
610
711
  if (calm) return;
611
- tiles[i].classList.remove("pkb-squash");
612
- tiles[i].style.setProperty("--pkb-d", d + "ms");
613
- replay(tiles[i], "pkb-hop");
712
+ t.classList.remove("pkb-squash");
713
+ t.style.setProperty("--pkb-d", d + "ms");
714
+ replay(t, "pkb-hop");
614
715
  };
615
716
  grid.addEventListener("animationend", (e) => {
616
717
  const t = e.target;
@@ -624,6 +725,7 @@ function createPackingBingo(container, options = {}) {
624
725
  r.addEventListener("animationend", () => r.remove());
625
726
  };
626
727
  const buzz = (pattern) => {
728
+ if (!vibrate) return;
627
729
  const ua = navigator.userActivation;
628
730
  if (ua && !ua.hasBeenActive) return;
629
731
  try {
@@ -658,8 +760,8 @@ function createPackingBingo(container, options = {}) {
658
760
  st(t0);
659
761
  if (!calm) replay($("sp"), "pkb-bump");
660
762
  }
661
- const showGain = (n, x, y, cls = "pt") => {
662
- floatText(`+${n}`, x, y, cls);
763
+ const showGain = (pts, x, y, cls = "pt") => {
764
+ floatText(`+${pts}`, x, y, cls);
663
765
  countScore();
664
766
  };
665
767
  function countUp(el, a, b, d, fmt, ticks = false) {
@@ -679,15 +781,19 @@ function createPackingBingo(container, options = {}) {
679
781
  };
680
782
  st(t0);
681
783
  }
682
- const ctr = (i) => {
683
- const t = tiles[i];
784
+ const ctr = (t) => {
684
785
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
685
786
  };
787
+ let landing = 0;
686
788
  function drawLines(fresh = []) {
789
+ if (landing) {
790
+ svg.innerHTML = "";
791
+ return;
792
+ }
687
793
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
688
794
  svg.innerHTML = cur.map((k) => {
689
- const a = ctr(LINES[k][0]), b = ctr(LINES[k][3]);
690
- const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1, ex = dx / d * 26, ey = dy / d * 26;
795
+ const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
796
+ const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1, r = 26 * dScale, ex = dx / d * r, ey = dy / d * r;
691
797
  const at = `x1="${a.x - ex}" y1="${a.y - ey}" x2="${b.x + ex}" y2="${b.y + ey}" pathLength="1"`;
692
798
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
693
799
  }).join("");
@@ -698,6 +804,7 @@ function createPackingBingo(container, options = {}) {
698
804
  combo = 0;
699
805
  $("combo").classList.remove("pkb-on");
700
806
  sfx.off();
807
+ buzz(8);
701
808
  return;
702
809
  }
703
810
  const now = performance.now();
@@ -720,7 +827,7 @@ function createPackingBingo(container, options = {}) {
720
827
  $("cmbN").textContent = M.combo(combo);
721
828
  replay($("combo"), "pkb-on");
722
829
  }
723
- buzz(12);
830
+ buzz(18);
724
831
  }
725
832
  $("cbar").addEventListener("animationend", () => {
726
833
  $("combo").classList.remove("pkb-on");
@@ -734,29 +841,29 @@ function createPackingBingo(container, options = {}) {
734
841
  later(() => sfx.reach(), 150);
735
842
  }
736
843
  function bingo(fresh, pts) {
737
- const n = fresh.length;
738
- const seq = [...new Set(fresh.flatMap((k) => LINES[k]))];
844
+ const nl = fresh.length;
845
+ const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
739
846
  const step = Math.min(70, 300 / seq.length);
740
847
  seq.forEach(
741
- (i, j) => later(() => {
742
- hop(i, 0);
743
- const p = rel(tiles[i]);
848
+ (t, j) => later(() => {
849
+ hop(t, 0);
850
+ const p = rel(t);
744
851
  burst(p.x, p.y, 14, { speed: 5, g: 0.15, life: 30, size: 0.7, star: 0.8 });
745
852
  sfx.note(j + 3);
746
853
  }, j * step)
747
854
  );
748
855
  later(() => {
749
- $("bT").textContent = n >= 3 ? M.bannerTriple : n === 2 ? M.bannerDouble : M.bannerSingle;
750
- $("bS").textContent = n > 1 ? M.bannerMulti(n) : M.bannerNth(cur.length);
856
+ $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
857
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
751
858
  replay($("banner"), "pkb-show");
752
859
  if (!calm) {
753
860
  replay($("flash"), "pkb-go");
754
861
  replay(board, "pkb-punch");
755
862
  }
756
863
  const c = rel(gw);
757
- burst(c.x, c.y, n > 1 ? 180 : 110, { speed: n > 1 ? 16 : 13, star: 0.3 });
758
- sfx.bingo(n);
759
- buzz(n > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
864
+ burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
865
+ sfx.bingo(nl);
866
+ buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
760
867
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
761
868
  }, seq.length * step + 60);
762
869
  }
@@ -764,9 +871,10 @@ function createPackingBingo(container, options = {}) {
764
871
  const bonus = gotP ? 0 : 1e3;
765
872
  gotP = true;
766
873
  score += bonus;
767
- tiles.forEach((_, i) => hop(i, ((i >> 2) + (i & 3)) * 60));
874
+ cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
768
875
  sfx.perfect();
769
876
  if (!calm) replay($("flash"), "pkb-go");
877
+ $("pbody").textContent = perfectText(M, N);
770
878
  $("sScore").textContent = "0";
771
879
  $("sCombo").textContent = "\xD70";
772
880
  $("sBingo").textContent = "0";
@@ -813,30 +921,32 @@ function createPackingBingo(container, options = {}) {
813
921
  $("combo").classList.remove("pkb-on");
814
922
  perfectEl.classList.remove("pkb-show");
815
923
  }
924
+ const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
816
925
  function render(src, quiet) {
817
926
  const events = [];
818
927
  const on = checked, c = on.filter(Boolean).length;
819
- cur = LINES.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
928
+ cur = completeLines();
820
929
  const fresh = cur.filter((k) => !prevLines.has(k));
821
930
  if ([...prevLines].some((k) => !cur.includes(k))) {
822
931
  cancelFx();
823
- if (c < 16) perfectEl.classList.remove("pkb-show");
932
+ if (c < N) perfectEl.classList.remove("pkb-show");
824
933
  }
825
- const hit = new Set(cur.flatMap((k) => LINES[k]));
934
+ const hit = new Set(cur.flatMap((k) => cellLines[k]));
826
935
  const reach = /* @__PURE__ */ new Set();
827
- for (const l of LINES) {
936
+ for (const l of lines) {
828
937
  const m = l.filter((i) => !on[i]);
829
938
  if (m.length === 1) reach.add(m[0]);
830
939
  }
831
940
  tiles.forEach((t, i) => {
832
941
  t.classList.toggle("pkb-on", on[i]);
833
- t.classList.toggle("pkb-line", hit.has(i));
942
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
834
943
  t.classList.toggle("pkb-reach", reach.has(i));
835
944
  });
836
- board.classList.toggle("pkb-all", c === 16);
837
- kick.classList.toggle("pkb-gold", c === 16);
838
- kick.textContent = c === 16 ? M.perfectTag : M.tag;
839
- $("cnt").textContent = `${c}/16`;
945
+ freeTile?.classList.toggle("pkb-line", hit.has(L.center));
946
+ board.classList.toggle("pkb-all", c === N);
947
+ kick.classList.toggle("pkb-gold", c === N);
948
+ kick.textContent = c === N ? M.perfectTag : M.tag;
949
+ $("cnt").textContent = `${c}/${N}`;
840
950
  $("bng").textContent = String(cur.length);
841
951
  const bp = $("bp");
842
952
  if (cur.length > lastN) {
@@ -847,13 +957,13 @@ function createPackingBingo(container, options = {}) {
847
957
  lastN = cur.length;
848
958
  drawLines(fresh);
849
959
  if (src !== null) tapFx(src, on[src]);
850
- const newL = fresh.filter((k) => !gotL.has(k));
851
- newL.forEach((k) => gotL.add(k));
960
+ const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
961
+ newL.forEach((key) => gotL.add(key));
852
962
  const linePts = 100 * newL.length * newL.length;
853
963
  score += linePts;
854
964
  if (quiet) {
855
965
  if (linePts) countScore();
856
- } else if (c === 16 && !prevAll) perfect(events);
966
+ } else if (c === N && !prevAll) perfect(events);
857
967
  else if (fresh.length) {
858
968
  bingo(fresh, linePts);
859
969
  const e = { newLines: fresh, totalLines: cur.length, score };
@@ -864,9 +974,9 @@ function createPackingBingo(container, options = {}) {
864
974
  }
865
975
  prevLines = new Set(cur);
866
976
  prevReach = reach;
867
- prevAll = c === 16;
977
+ prevAll = c === N;
868
978
  const labels = [...reach].map((i) => items[i].label);
869
- $("msg").textContent = c === 16 ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, 16 - c) : M.statusStart(16 - c);
979
+ $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
870
980
  return events;
871
981
  }
872
982
  const flush = (events) => {
@@ -893,8 +1003,9 @@ function createPackingBingo(container, options = {}) {
893
1003
  got.clear();
894
1004
  gotL.clear();
895
1005
  checked.forEach((v, i) => v && got.add(i));
896
- LINES.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
897
- prevLines = new Set(gotL);
1006
+ const done = completeLines();
1007
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1008
+ prevLines = new Set(done);
898
1009
  prevReach = /* @__PURE__ */ new Set();
899
1010
  prevAll = checked.every(Boolean);
900
1011
  gotP = prevAll;
@@ -917,6 +1028,77 @@ function createPackingBingo(container, options = {}) {
917
1028
  options.onChange?.(checked.slice());
918
1029
  flush(events);
919
1030
  }
1031
+ function place(next) {
1032
+ order = next;
1033
+ order.forEach((i, s) => {
1034
+ slotOf[i] = s;
1035
+ cellTiles[L.itemCell[s]] = tiles[i];
1036
+ });
1037
+ lines = L.lines.map((l) => l.map((s) => order[s]));
1038
+ const focused = document.activeElement;
1039
+ cellTiles.forEach((t, c) => {
1040
+ t.style.setProperty("--pkb-gd", glintDelay(c, n));
1041
+ grid.appendChild(t);
1042
+ });
1043
+ if (focused instanceof HTMLElement && grid.contains(focused) && document.activeElement !== focused)
1044
+ focused.focus({ preventScroll: true });
1045
+ }
1046
+ const saveOrder = () => {
1047
+ if (!orderKey) return;
1048
+ writeStore(orderKey, JSON.stringify(order));
1049
+ };
1050
+ let flights = [];
1051
+ function arrange(next) {
1052
+ if (!checked.every(Boolean)) cancelFx();
1053
+ const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1054
+ flights.forEach((a) => a.cancel());
1055
+ flights = [];
1056
+ place(next);
1057
+ lines.forEach((l) => {
1058
+ if (l.every((i) => got.has(i))) gotL.add(lineKey(l));
1059
+ });
1060
+ const done = completeLines();
1061
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1062
+ prevLines = new Set(done);
1063
+ render(null, true);
1064
+ saveOrder();
1065
+ if (!from || typeof Element.prototype.animate !== "function") return;
1066
+ tiles.forEach((t, i) => {
1067
+ const a = from[i], b = t.getBoundingClientRect();
1068
+ const dx = a.left - b.left, dy = a.top - b.top;
1069
+ if (Math.abs(dx) + Math.abs(dy) < 1) return;
1070
+ flights.push(
1071
+ t.animate([{ transform: `translate(${dx}px,${dy}px)` }, { transform: "none" }], {
1072
+ duration: FLY_MS - 140,
1073
+ delay: Math.random() * 140,
1074
+ easing: "cubic-bezier(.3,1.25,.5,1)",
1075
+ fill: "backwards"
1076
+ })
1077
+ );
1078
+ });
1079
+ clearTimeout(landing);
1080
+ landing = window.setTimeout(() => {
1081
+ landing = 0;
1082
+ flights = [];
1083
+ if (!dead) drawLines(cur);
1084
+ }, FLY_MS);
1085
+ svg.innerHTML = "";
1086
+ }
1087
+ function shuffleCard() {
1088
+ let next = order.slice();
1089
+ do {
1090
+ for (let s = N - 1; s > 0; s--) {
1091
+ const r = Math.floor(Math.random() * (s + 1));
1092
+ [next[s], next[r]] = [next[r], next[s]];
1093
+ }
1094
+ } while (next.every((v, s) => v === order[s]));
1095
+ arrange(next);
1096
+ sfx.shuffle();
1097
+ buzz([10, 40, 10, 40, 10, 40, 24]);
1098
+ }
1099
+ const unshuffle = () => {
1100
+ if (!isIdentity(order)) arrange(identity());
1101
+ };
920
1102
  grid.addEventListener("change", (e) => {
921
1103
  const i = ins.indexOf(e.target);
922
1104
  if (i < 0) return;
@@ -924,7 +1106,11 @@ function createPackingBingo(container, options = {}) {
924
1106
  next[i] = !next[i];
925
1107
  request(next, i);
926
1108
  });
927
- $("reset").addEventListener("click", () => request(normalize(void 0), null, { reset: true }));
1109
+ container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1110
+ $("reset").addEventListener("click", () => {
1111
+ unshuffle();
1112
+ request(normalize(void 0), null, { reset: true });
1113
+ });
928
1114
  $("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
929
1115
  perfectEl.addEventListener("click", (e) => {
930
1116
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
@@ -953,18 +1139,31 @@ function createPackingBingo(container, options = {}) {
953
1139
  }
954
1140
  };
955
1141
  $("dB").addEventListener("click", () => {
956
- const open = () => LINES.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
957
- const pick = checked.every(Boolean) ? [...LINES[0]] : open();
1142
+ const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1143
+ const pick = checked.every(Boolean) ? [...lines[0]] : open();
958
1144
  void run(pick.map((i) => ({ i, delay: 320 })));
959
1145
  });
960
1146
  $("dP").addEventListener("click", () => {
961
- const miss = checked.every(Boolean) ? tiles.map((_, i) => i) : tiles.map((_, i) => i).filter((i) => !checked[i]);
1147
+ const all = items.map((_, i) => i);
1148
+ const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
962
1149
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
963
1150
  });
964
1151
  $("dR").addEventListener("click", () => {
965
- if (!busy) request(demoStart(), null, { reset: true });
1152
+ if (busy) return;
1153
+ unshuffle();
1154
+ request(demoStart(), null, { reset: true });
966
1155
  });
967
1156
  }
1157
+ let startOrder = null;
1158
+ if (orderKey) {
1159
+ try {
1160
+ const saved = JSON.parse(readStore(orderKey) ?? "null");
1161
+ if (validOrder(saved)) startOrder = saved;
1162
+ } catch {
1163
+ }
1164
+ }
1165
+ if (!startOrder && validOrder(options.defaultOrder)) startOrder = options.defaultOrder.slice();
1166
+ if (startOrder && !isIdentity(startOrder)) place(startOrder);
968
1167
  let initial = null;
969
1168
  if (storageKey) {
970
1169
  try {
@@ -986,29 +1185,45 @@ function createPackingBingo(container, options = {}) {
986
1185
  return {
987
1186
  setChecked(next, opts = {}) {
988
1187
  if (dead) return;
989
- const n = normalize(next);
990
- const changed = n.flatMap((v, i) => v !== checked[i] ? [i] : []);
1188
+ const n2 = normalize(next);
1189
+ const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
991
1190
  if (!changed.length) return;
992
1191
  if (!opts.animate || changed.length !== 1) {
993
- baseline(n);
1192
+ baseline(n2);
994
1193
  return;
995
1194
  }
996
- flush(apply(n, changed[0], false));
1195
+ flush(apply(n2, changed[0], false));
997
1196
  },
998
1197
  reset() {
999
- if (!dead) request(normalize(void 0), null, { reset: true });
1198
+ if (dead) return;
1199
+ unshuffle();
1200
+ request(normalize(void 0), null, { reset: true });
1201
+ },
1202
+ shuffle() {
1203
+ if (!dead) shuffleCard();
1000
1204
  },
1001
1205
  refresh() {
1002
1206
  if (!dead) render(null, true);
1003
1207
  },
1004
1208
  getState() {
1005
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean) };
1209
+ return {
1210
+ checked: checked.slice(),
1211
+ lines: cur.slice(),
1212
+ score,
1213
+ maxCombo,
1214
+ complete: checked.every(Boolean),
1215
+ size: n,
1216
+ free: L.free,
1217
+ order: order.slice()
1218
+ };
1006
1219
  },
1007
1220
  destroy() {
1008
1221
  if (dead) return;
1009
1222
  dead = true;
1010
1223
  timers.forEach((id) => clearTimeout(id));
1011
1224
  timers.clear();
1225
+ clearTimeout(landing);
1226
+ flights.forEach((a) => a.cancel());
1012
1227
  cancelAnimationFrame(raf);
1013
1228
  cancelAnimationFrame(tw);
1014
1229
  ro?.disconnect();
@@ -1021,7 +1236,7 @@ function createPackingBingo(container, options = {}) {
1021
1236
  // src/react.tsx
1022
1237
  var import_jsx_runtime = require("react/jsx-runtime");
1023
1238
  var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, ref) {
1024
- const { checked, defaultChecked, className, style, onChange, onBingo, onPerfect, messages, ...config } = props;
1239
+ const { checked, defaultChecked, defaultOrder, className, style, onChange, onBingo, onPerfect, messages, fonts: _fonts, ...config } = props;
1025
1240
  const host = (0, import_react.useRef)(null);
1026
1241
  const inst = (0, import_react.useRef)(null);
1027
1242
  const callbacks = (0, import_react.useRef)({ onChange, onBingo, onPerfect });
@@ -1033,6 +1248,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1033
1248
  const storageKey = resolveStorageKey({ ...config, controlled });
1034
1249
  const latestKey = (0, import_react.useRef)(storageKey);
1035
1250
  if (controlled) latest.current = checked;
1251
+ const latestOrder = (0, import_react.useRef)(defaultOrder);
1036
1252
  const messageShape = messages ? Object.fromEntries(Object.entries(messages).map(([k, v]) => [k, typeof v === "function" ? "\u0192" : v])) : null;
1037
1253
  const key = JSON.stringify({ ...config, messageShape, controlled });
1038
1254
  (0, import_react.useEffect)(() => {
@@ -1048,6 +1264,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1048
1264
  messages: liveMessages,
1049
1265
  controlled,
1050
1266
  defaultChecked: controlled || latestKey.current === storageKey ? latest.current : defaultChecked,
1267
+ defaultOrder: latestKey.current === storageKey ? latestOrder.current : defaultOrder,
1051
1268
  onChange: (next) => {
1052
1269
  if (!controlled) latest.current = next;
1053
1270
  callbacks.current.onChange?.(next);
@@ -1057,7 +1274,9 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1057
1274
  });
1058
1275
  inst.current = instance;
1059
1276
  return () => {
1060
- if (!controlled) latest.current = instance.getState().checked;
1277
+ const state = instance.getState();
1278
+ if (!controlled) latest.current = state.checked;
1279
+ latestOrder.current = state.order;
1061
1280
  latestKey.current = storageKey;
1062
1281
  instance.destroy();
1063
1282
  if (inst.current === instance) inst.current = null;
@@ -1073,6 +1292,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1073
1292
  ref,
1074
1293
  () => ({
1075
1294
  reset: () => inst.current?.reset(),
1295
+ shuffle: () => inst.current?.shuffle(),
1076
1296
  setChecked: (next) => inst.current?.setChecked(next, { animate: true }),
1077
1297
  getState: () => inst.current?.getState() ?? null
1078
1298
  }),
@@ -1085,6 +1305,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1085
1305
  DEFAULT_ITEMS,
1086
1306
  LINES,
1087
1307
  PackingBingo,
1308
+ bingoLines,
1088
1309
  createPackingBingo,
1089
1310
  en,
1090
1311
  ja