packing-bingo 0.3.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.
@@ -92,7 +92,7 @@ function bingoLines(size, free = false) {
92
92
  // src/messages.ts
93
93
  var ja = {
94
94
  title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
95
- tag: "\u4FEE\u5B66\u65C5\u884C",
95
+ tag: "",
96
96
  subtitle: "",
97
97
  packed: "\u305D\u308D\u3063\u305F",
98
98
  bingo: "BINGO",
@@ -100,6 +100,7 @@ var ja = {
100
100
  combo: (n) => `${n} COMBO`,
101
101
  comboPop: (n) => `${n} COMBO!`,
102
102
  reset: "\u30EA\u30BB\u30C3\u30C8",
103
+ shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
103
104
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
104
105
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
105
106
  stamp: "OK",
@@ -113,7 +114,7 @@ var ja = {
113
114
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
114
115
  statusStart: () => "",
115
116
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
116
- statusReach: (labels) => `\u30EA\u30FC\u30C1\uFF01 \u3042\u3068${labels.slice(0, 2).map((l) => `\u300C${l}\u300D`).join("\u304B")}${labels.length > 2 ? "\u306A\u3069" : ""}\u3067\u30D3\u30F3\u30B4`,
117
+ statusReach: () => "",
117
118
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
118
119
  perfectTag: "\u{1F3C6} PERFECT",
119
120
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
@@ -132,7 +133,7 @@ var ja = {
132
133
  };
133
134
  var en = {
134
135
  title: "Packing Bingo",
135
- tag: "School trip",
136
+ tag: "",
136
137
  subtitle: "",
137
138
  packed: "PACKED",
138
139
  bingo: "BINGO",
@@ -140,6 +141,7 @@ var en = {
140
141
  combo: (n) => `${n} COMBO`,
141
142
  comboPop: (n) => `${n} COMBO!`,
142
143
  reset: "Reset",
144
+ shuffle: "\u{1F500} Shuffle",
143
145
  soundOn: "\u{1F50A} Sound on",
144
146
  soundOff: "\u{1F507} Muted",
145
147
  stamp: "OK",
@@ -153,7 +155,7 @@ var en = {
153
155
  bannerMulti: (n) => `${n} lines at once!`,
154
156
  statusStart: () => "",
155
157
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
156
- statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
158
+ statusReach: () => "",
157
159
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
158
160
  perfectTag: "\u{1F3C6} PERFECT",
159
161
  perfectStamp: "ALL\nSET",
@@ -259,6 +261,11 @@ function createSound(isMuted) {
259
261
  },
260
262
  tick: () => tone(1760, 0, 0.03, "square", 0.03),
261
263
  note: (k, at = 0) => tone(step(k), at, 0.12, "square", 0.05),
264
+ shuffle() {
265
+ for (let i = 0; i < 7; i++) noise(i * 0.042, 0.05, 0.16, 1600 + i * 260);
266
+ tone(hz(7), 0.34, 0.12, "triangle", 0.14);
267
+ tone(hz(12), 0.42, 0.2, "triangle", 0.14);
268
+ },
262
269
  close() {
263
270
  if (ac) void ac.close().catch(() => {
264
271
  });
@@ -286,7 +293,6 @@ function itemsKey(items) {
286
293
  }
287
294
 
288
295
  // src/styles.ts
289
- var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
290
296
  var CSS = `
291
297
  .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%}
292
298
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
@@ -298,6 +304,7 @@ var CSS = `
298
304
  .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
299
305
  .pkb-tags{display:flex;gap:8px;align-items:center}
300
306
  .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}
307
+ .pkb-kicker:empty{display:none}
301
308
  .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}
302
309
  @keyframes pkb-rb{to{background-position:-200% 0}}
303
310
  .pkb button{font-family:var(--pk-body)}
@@ -354,8 +361,13 @@ var CSS = `
354
361
  .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
355
362
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
356
363
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
357
- .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
358
- .pkb-reset{appearance:none;flex:none;border:2px solid var(--pk-ink);background:transparent;border-radius:999px;padding:5px 12px;font-weight:800;font-size:12px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
364
+ .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}
365
+ .pkb-foot>span{flex:1 1 14em}
366
+ .pkb-foot>span:empty{flex-basis:0}
367
+ .pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
368
+ .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}
369
+ .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
370
+ .pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
359
371
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
360
372
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
361
373
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -403,6 +415,7 @@ var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"
403
415
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
404
416
  var COMBO_MS = 2500;
405
417
  var MUTE_KEY = "packing-bingo-muted";
418
+ var ORDER_PREFIX = "packing-bingo-order:";
406
419
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
407
420
  function readStore(key) {
408
421
  try {
@@ -417,29 +430,25 @@ function writeStore(key, value) {
417
430
  } catch {
418
431
  }
419
432
  }
420
- function ensureAssets(fonts) {
421
- if (!document.getElementById("pkb-styles")) {
422
- const s = document.createElement("style");
423
- s.id = "pkb-styles";
424
- s.textContent = CSS;
425
- document.head.appendChild(s);
426
- }
427
- if (fonts && !document.getElementById("pkb-fonts")) {
428
- const l = document.createElement("link");
429
- l.id = "pkb-fonts";
430
- l.rel = "stylesheet";
431
- l.href = FONT_URL;
432
- document.head.appendChild(l);
433
- }
433
+ var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
434
+ var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
435
+ var FLY_MS = 560;
436
+ function ensureStyles() {
437
+ if (document.getElementById("pkb-styles")) return;
438
+ const s = document.createElement("style");
439
+ s.id = "pkb-styles";
440
+ s.textContent = CSS;
441
+ document.head.appendChild(s);
434
442
  }
435
- function template(L, M, demo) {
443
+ function template(L, M, { demo, shuffle, vibrate }) {
436
444
  const n = L.size;
445
+ const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
437
446
  const tiles = Array.from({ length: n * n }, (_, c) => {
438
- const gd = `--pkb-gd:${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
447
+ const gd = `--pkb-gd:${glintDelay(c, n)}`;
439
448
  if (c === L.center)
440
449
  return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
441
450
  const it = L.items[L.itemCell.indexOf(c)];
442
- return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}"><input type="checkbox"><span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`;
451
+ 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>`;
443
452
  }).join("");
444
453
  const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
445
454
  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>` : "";
@@ -454,7 +463,7 @@ function template(L, M, demo) {
454
463
  <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>
455
464
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
456
465
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
457
- <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div>
466
+ <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>
458
467
  <div class="pkb-flash" data-r="flash"></div>
459
468
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
460
469
  <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>
@@ -465,12 +474,22 @@ function template(L, M, demo) {
465
474
  var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
466
475
  function createPackingBingo(container, options = {}) {
467
476
  const L = resolveLayout(options);
468
- const { items, lines, cellLines } = L;
477
+ const { items, cellLines } = L;
469
478
  const n = L.size, N = items.length;
470
479
  const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
480
+ const identity = () => Array.from({ length: N }, (_, s) => s);
481
+ const isIdentity = (o) => o.every((v, s) => v === s);
482
+ const validOrder = (o) => Array.isArray(o) && o.length === N && o.every((v) => Number.isInteger(v) && v >= 0 && v < N) && new Set(o).size === N;
483
+ let order = identity();
484
+ const slotOf = identity();
485
+ let lines = L.lines;
471
486
  const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
472
- const demoItems = new Set(L.itemCell.flatMap((c, i) => demoCells.includes(c) ? [i] : []));
473
- const demoStart = () => Array.from({ length: N }, (_, i) => demoItems.has(i));
487
+ const demoSlots = L.itemCell.flatMap((c, s) => demoCells.includes(c) ? [s] : []);
488
+ const demoStart = () => {
489
+ const on = normalize(void 0);
490
+ demoSlots.forEach((s) => on[order[s]] = true);
491
+ return on;
492
+ };
474
493
  const far = (c) => Math.floor(c / n) + c % n;
475
494
  const dScale = Math.min(1, 4 / n);
476
495
  const M = { ...ja, ...options.messages };
@@ -479,7 +498,9 @@ function createPackingBingo(container, options = {}) {
479
498
  if (options.subtitle != null) M.subtitle = options.subtitle;
480
499
  const controlled = !!options.controlled;
481
500
  const storageKey = resolveStorageKey(options);
501
+ const orderKey = storageKey && ORDER_PREFIX + storageKey;
482
502
  const demo = !!options.demo;
503
+ const vibrate = options.vibrate !== false;
483
504
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
484
505
  let dead = false;
485
506
  const timers = /* @__PURE__ */ new Set();
@@ -492,8 +513,8 @@ function createPackingBingo(container, options = {}) {
492
513
  };
493
514
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
494
515
  let gen = 0;
495
- ensureAssets(options.fonts !== false);
496
- container.innerHTML = template(L, M, demo);
516
+ ensureStyles();
517
+ container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
497
518
  const $ = (r) => {
498
519
  const el = container.querySelector(`[data-r="${r}"]`);
499
520
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -670,6 +691,7 @@ function createPackingBingo(container, options = {}) {
670
691
  r.addEventListener("animationend", () => r.remove());
671
692
  };
672
693
  const buzz = (pattern) => {
694
+ if (!vibrate) return;
673
695
  const ua = navigator.userActivation;
674
696
  if (ua && !ua.hasBeenActive) return;
675
697
  try {
@@ -728,7 +750,12 @@ function createPackingBingo(container, options = {}) {
728
750
  const ctr = (t) => {
729
751
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
730
752
  };
753
+ let landing = 0;
731
754
  function drawLines(fresh = []) {
755
+ if (landing) {
756
+ svg.innerHTML = "";
757
+ return;
758
+ }
732
759
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
733
760
  svg.innerHTML = cur.map((k) => {
734
761
  const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
@@ -743,6 +770,7 @@ function createPackingBingo(container, options = {}) {
743
770
  combo = 0;
744
771
  $("combo").classList.remove("pkb-on");
745
772
  sfx.off();
773
+ buzz(8);
746
774
  return;
747
775
  }
748
776
  const now = performance.now();
@@ -765,7 +793,7 @@ function createPackingBingo(container, options = {}) {
765
793
  $("cmbN").textContent = M.combo(combo);
766
794
  replay($("combo"), "pkb-on");
767
795
  }
768
- buzz(12);
796
+ buzz(18);
769
797
  }
770
798
  $("cbar").addEventListener("animationend", () => {
771
799
  $("combo").classList.remove("pkb-on");
@@ -859,10 +887,11 @@ function createPackingBingo(container, options = {}) {
859
887
  $("combo").classList.remove("pkb-on");
860
888
  perfectEl.classList.remove("pkb-show");
861
889
  }
890
+ const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
862
891
  function render(src, quiet) {
863
892
  const events = [];
864
893
  const on = checked, c = on.filter(Boolean).length;
865
- cur = lines.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
894
+ cur = completeLines();
866
895
  const fresh = cur.filter((k) => !prevLines.has(k));
867
896
  if ([...prevLines].some((k) => !cur.includes(k))) {
868
897
  cancelFx();
@@ -876,7 +905,7 @@ function createPackingBingo(container, options = {}) {
876
905
  }
877
906
  tiles.forEach((t, i) => {
878
907
  t.classList.toggle("pkb-on", on[i]);
879
- t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
908
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
880
909
  t.classList.toggle("pkb-reach", reach.has(i));
881
910
  });
882
911
  freeTile?.classList.toggle("pkb-line", hit.has(L.center));
@@ -894,8 +923,8 @@ function createPackingBingo(container, options = {}) {
894
923
  lastN = cur.length;
895
924
  drawLines(fresh);
896
925
  if (src !== null) tapFx(src, on[src]);
897
- const newL = fresh.filter((k) => !gotL.has(k));
898
- newL.forEach((k) => gotL.add(k));
926
+ const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
927
+ newL.forEach((key) => gotL.add(key));
899
928
  const linePts = 100 * newL.length * newL.length;
900
929
  score += linePts;
901
930
  if (quiet) {
@@ -940,8 +969,9 @@ function createPackingBingo(container, options = {}) {
940
969
  got.clear();
941
970
  gotL.clear();
942
971
  checked.forEach((v, i) => v && got.add(i));
943
- lines.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
944
- prevLines = new Set(gotL);
972
+ const done = completeLines();
973
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
974
+ prevLines = new Set(done);
945
975
  prevReach = /* @__PURE__ */ new Set();
946
976
  prevAll = checked.every(Boolean);
947
977
  gotP = prevAll;
@@ -964,6 +994,77 @@ function createPackingBingo(container, options = {}) {
964
994
  options.onChange?.(checked.slice());
965
995
  flush(events);
966
996
  }
997
+ function place(next) {
998
+ order = next;
999
+ order.forEach((i, s) => {
1000
+ slotOf[i] = s;
1001
+ cellTiles[L.itemCell[s]] = tiles[i];
1002
+ });
1003
+ lines = L.lines.map((l) => l.map((s) => order[s]));
1004
+ const focused = document.activeElement;
1005
+ cellTiles.forEach((t, c) => {
1006
+ t.style.setProperty("--pkb-gd", glintDelay(c, n));
1007
+ grid.appendChild(t);
1008
+ });
1009
+ if (focused instanceof HTMLElement && grid.contains(focused) && document.activeElement !== focused)
1010
+ focused.focus({ preventScroll: true });
1011
+ }
1012
+ const saveOrder = () => {
1013
+ if (!orderKey) return;
1014
+ writeStore(orderKey, JSON.stringify(order));
1015
+ };
1016
+ let flights = [];
1017
+ function arrange(next) {
1018
+ if (!checked.every(Boolean)) cancelFx();
1019
+ const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1020
+ flights.forEach((a) => a.cancel());
1021
+ flights = [];
1022
+ place(next);
1023
+ lines.forEach((l) => {
1024
+ if (l.every((i) => got.has(i))) gotL.add(lineKey(l));
1025
+ });
1026
+ const done = completeLines();
1027
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1028
+ prevLines = new Set(done);
1029
+ render(null, true);
1030
+ saveOrder();
1031
+ if (!from || typeof Element.prototype.animate !== "function") return;
1032
+ tiles.forEach((t, i) => {
1033
+ const a = from[i], b = t.getBoundingClientRect();
1034
+ const dx = a.left - b.left, dy = a.top - b.top;
1035
+ if (Math.abs(dx) + Math.abs(dy) < 1) return;
1036
+ flights.push(
1037
+ t.animate([{ transform: `translate(${dx}px,${dy}px)` }, { transform: "none" }], {
1038
+ duration: FLY_MS - 140,
1039
+ delay: Math.random() * 140,
1040
+ easing: "cubic-bezier(.3,1.25,.5,1)",
1041
+ fill: "backwards"
1042
+ })
1043
+ );
1044
+ });
1045
+ clearTimeout(landing);
1046
+ landing = window.setTimeout(() => {
1047
+ landing = 0;
1048
+ flights = [];
1049
+ if (!dead) drawLines(cur);
1050
+ }, FLY_MS);
1051
+ svg.innerHTML = "";
1052
+ }
1053
+ function shuffleCard() {
1054
+ let next = order.slice();
1055
+ do {
1056
+ for (let s = N - 1; s > 0; s--) {
1057
+ const r = Math.floor(Math.random() * (s + 1));
1058
+ [next[s], next[r]] = [next[r], next[s]];
1059
+ }
1060
+ } while (next.every((v, s) => v === order[s]));
1061
+ arrange(next);
1062
+ sfx.shuffle();
1063
+ buzz([10, 40, 10, 40, 10, 40, 24]);
1064
+ }
1065
+ const unshuffle = () => {
1066
+ if (!isIdentity(order)) arrange(identity());
1067
+ };
967
1068
  grid.addEventListener("change", (e) => {
968
1069
  const i = ins.indexOf(e.target);
969
1070
  if (i < 0) return;
@@ -971,7 +1072,11 @@ function createPackingBingo(container, options = {}) {
971
1072
  next[i] = !next[i];
972
1073
  request(next, i);
973
1074
  });
974
- $("reset").addEventListener("click", () => request(normalize(void 0), null, { reset: true }));
1075
+ container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1076
+ $("reset").addEventListener("click", () => {
1077
+ unshuffle();
1078
+ request(normalize(void 0), null, { reset: true });
1079
+ });
975
1080
  $("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
976
1081
  perfectEl.addEventListener("click", (e) => {
977
1082
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
@@ -1010,9 +1115,21 @@ function createPackingBingo(container, options = {}) {
1010
1115
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
1011
1116
  });
1012
1117
  $("dR").addEventListener("click", () => {
1013
- if (!busy) request(demoStart(), null, { reset: true });
1118
+ if (busy) return;
1119
+ unshuffle();
1120
+ request(demoStart(), null, { reset: true });
1014
1121
  });
1015
1122
  }
1123
+ let startOrder = null;
1124
+ if (orderKey) {
1125
+ try {
1126
+ const saved = JSON.parse(readStore(orderKey) ?? "null");
1127
+ if (validOrder(saved)) startOrder = saved;
1128
+ } catch {
1129
+ }
1130
+ }
1131
+ if (!startOrder && validOrder(options.defaultOrder)) startOrder = options.defaultOrder.slice();
1132
+ if (startOrder && !isIdentity(startOrder)) place(startOrder);
1016
1133
  let initial = null;
1017
1134
  if (storageKey) {
1018
1135
  try {
@@ -1044,19 +1161,35 @@ function createPackingBingo(container, options = {}) {
1044
1161
  flush(apply(n2, changed[0], false));
1045
1162
  },
1046
1163
  reset() {
1047
- if (!dead) request(normalize(void 0), null, { reset: true });
1164
+ if (dead) return;
1165
+ unshuffle();
1166
+ request(normalize(void 0), null, { reset: true });
1167
+ },
1168
+ shuffle() {
1169
+ if (!dead) shuffleCard();
1048
1170
  },
1049
1171
  refresh() {
1050
1172
  if (!dead) render(null, true);
1051
1173
  },
1052
1174
  getState() {
1053
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean), size: n, free: L.free };
1175
+ return {
1176
+ checked: checked.slice(),
1177
+ lines: cur.slice(),
1178
+ score,
1179
+ maxCombo,
1180
+ complete: checked.every(Boolean),
1181
+ size: n,
1182
+ free: L.free,
1183
+ order: order.slice()
1184
+ };
1054
1185
  },
1055
1186
  destroy() {
1056
1187
  if (dead) return;
1057
1188
  dead = true;
1058
1189
  timers.forEach((id) => clearTimeout(id));
1059
1190
  timers.clear();
1191
+ clearTimeout(landing);
1192
+ flights.forEach((a) => a.cancel());
1060
1193
  cancelAnimationFrame(raf);
1061
1194
  cancelAnimationFrame(tw);
1062
1195
  ro?.disconnect();