-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
7495 lines (6899 loc) · 211 KB
/
Copy pathapp.js
File metadata and controls
7495 lines (6899 loc) · 211 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// NOTE: Puzzle is only considered "solved" through user entry,
// not via Reveal helpers.
'use strict';
// Session 8: keep a reference to the currently loaded puzzle UI + state
let CURRENT = null;
let MOBILE_INPUT = null;
// Mobile focus lock: used to avoid stealing focus back while user interacts with controls
let MOBILE_FOCUS_LOCK_UNTIL = 0;
function isTouchLikely() {
// Touch-capable detection that keeps desktop stable, but does NOT break tablets in landscape.
const maxTouchPoints = navigator.maxTouchPoints || 0;
const coarse = window.matchMedia?.('(pointer: coarse)')?.matches ?? false;
const noHover = window.matchMedia?.('(hover: none)')?.matches ?? false;
// Basic touch signal.
const touchCapable = maxTouchPoints > 0 || coarse || noHover;
// “Probably a real touch device” (tablets/phones), even in landscape:
// - coarse pointer or no-hover is a strong mobile/tablet signal
// - OR a smaller screen (helps catch edge cases)
const probablyTabletOrPhone =
coarse ||
noHover ||
(window.matchMedia?.('(max-width: 1100px)')?.matches ?? false);
return touchCapable && probablyTabletOrPhone;
}
function getMobileInput() {
if (MOBILE_INPUT) return MOBILE_INPUT;
MOBILE_INPUT = document.getElementById('mobileInput');
return MOBILE_INPUT;
}
function focusMobileInput() {
const input = $('mobileInput');
if (!input) return;
// Touch only
if (!isTouchLikely()) return;
const x = window.scrollX;
const y = window.scrollY;
try {
input.focus({ preventScroll: true });
} catch {
input.focus();
}
// Put caret at end so typing feels normal
try {
const v = input.value || '';
input.setSelectionRange(v.length, v.length);
} catch {
// Some browsers may not allow this in all cases, safe to ignore.
}
// Do not jump the page around when keyboard opens
requestAnimationFrame(() => window.scrollTo(x, y));
}
function setMobileTypingVisual(isTyping) {
// Only matters on touch devices
if (!isTouchLikely()) return;
document.body.classList.toggle('mobile-typing', Boolean(isTyping));
}
function scrollActiveCellIntoView() {
const activeCell = document.querySelector('.cell.active');
if (!activeCell) return;
// Give the keyboard a moment to appear before scrolling
requestAnimationFrame(() => {
activeCell.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
});
}
function suppressMobileRefocus(ms = 900) {
MOBILE_FOCUS_LOCK_UNTIL = Date.now() + ms;
}
function isInteractiveControl(el) {
if (!el) return false;
const tag = (el.tagName || '').toUpperCase();
if (
tag === 'SELECT' ||
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
tag === 'BUTTON' ||
tag === 'A'
) {
return true;
}
return Boolean(el.closest && el.closest('#configForm'));
}
function shouldAutoRefocusMobileInput() {
if (!CURRENT) return false;
if (!isTouchLikely()) return false;
// If we recently interacted with controls, do NOT steal focus back yet.
if (Date.now() < MOBILE_FOCUS_LOCK_UNTIL) return false;
const ae = document.activeElement;
// If a real control is focused (or we’re inside config), leave it alone.
if (isInteractiveControl(ae)) return false;
return true;
}
function $(id) {
return document.getElementById(id);
}
// -----------------------------
// Theme
// -----------------------------
const THEME_STORAGE_KEY = 'crswrd_theme';
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
function getStoredTheme() {
try {
const v = localStorage.getItem(THEME_STORAGE_KEY);
return v === 'light' || v === 'dark' ? v : null;
} catch {
return null;
}
}
function setStoredTheme(theme) {
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// If storage is blocked, we still allow toggling for this session.
}
}
function updateThemeToggleLabel(theme) {
const btn = $('themeToggleBtn');
if (!btn) return;
const isDark = theme === 'dark';
// Show the CURRENT state (what you're in right now)
btn.textContent = isDark ? '🌙 Dark' : '☀️ Light';
// Keep aria-pressed tied to "dark mode is on"
btn.setAttribute('aria-pressed', String(isDark));
// Tooltip still explains the action
btn.title = `Switch to ${isDark ? 'Light' : 'Dark'} mode`;
}
function applyTheme(theme) {
// Use data-theme so CSS can switch tokens cleanly
document.documentElement.setAttribute('data-theme', theme);
// Keep the header button synced (icon + current theme name)
updateThemeToggleLabel(theme);
}
function initTheme() {
const stored = getStoredTheme();
const initial = stored || getSystemTheme();
applyTheme(initial);
updateThemeToggleLabel(initial);
// If user has NOT explicitly chosen a theme, follow system changes.
if (!stored) {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener?.('change', () => {
if (!getStoredTheme()) applyTheme(getSystemTheme());
});
}
const btn = $('themeToggleBtn');
if (!btn) return;
btn.addEventListener('click', () => {
const current = document.documentElement.dataset.theme || getSystemTheme();
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
setStoredTheme(next);
});
}
// -----------------------------
// Preferences (Pack / Tone / Grid size)
// -----------------------------
const PREFS_STORAGE_KEY = 'crswrd_prefs_v1';
/**
* Safe read of stored preferences.
* Returns null if blocked/unavailable/corrupt.
*/
function getStoredPrefs() {
try {
const raw = localStorage.getItem(PREFS_STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : null;
} catch {
return null;
}
}
/**
* Safe write of preferences. Silently no-ops if storage is blocked.
*/
function setStoredPrefs(prefs) {
try {
localStorage.setItem(PREFS_STORAGE_KEY, JSON.stringify(prefs));
} catch {
// Storage might be blocked (private mode, strict settings, etc).
// App still works without persistence.
}
}
/**
* Only set a <select> value if that option exists (prevents stale values).
*/
function setSelectIfValid(selectId, value) {
const el = $(selectId);
if (!el || typeof value !== 'string' || !value) return;
const hasOption = Array.from(el.options).some((o) => o.value === value);
if (hasOption) el.value = value;
}
/**
* Read the current UI selections we care about for persistence.
*/
function readPrefsFromUI() {
return {
difficulty: $('difficulty')?.value || 'medium',
pack: $('pack')?.value || 'general',
tone: $('tone')?.value || 'serious',
timeLength: $('timeLength')?.value || 'medium',
};
}
/**
* Apply stored preferences to the UI (if present and valid).
*/
function applyStoredPrefsToUI() {
const prefs = getStoredPrefs();
if (!prefs) return;
setSelectIfValid('difficulty', prefs.difficulty);
setSelectIfValid('pack', prefs.pack);
setSelectIfValid('tone', prefs.tone);
setSelectIfValid('timeLength', prefs.timeLength);
}
/**
* Save current UI selections immediately.
*/
function persistPrefsFromUI() {
setStoredPrefs(readPrefsFromUI());
}
/**
* Wire dropdown changes so updates are persisted as soon as the user changes them.
*/
function wirePrefsPersistence() {
const ids = ['difficulty', 'pack', 'tone', 'timeLength'];
ids.forEach((id) => {
const el = $(id);
if (!el) return;
el.addEventListener('change', persistPrefsFromUI);
});
}
function readConfig() {
return {
difficulty: $('difficulty').value,
pack: $('pack').value,
tone: $('tone').value,
timeLength: $('timeLength').value,
};
}
function logConfig(config) {
console.group('CRSWRD | Generate Puzzle (Phase 1)');
console.table(config);
console.groupEnd();
}
function setStatus(message) {
const el = document.getElementById('status');
if (el) el.textContent = message;
}
function collapseConfigPanel(shouldCollapse) {
const panel = document.querySelector('.panel');
const toggleBtn = $('toggleConfigBtn');
if (!panel || !toggleBtn) return;
// Hide the panel itself
panel.classList.toggle('is-collapsed', shouldCollapse);
// Tell CSS to collapse the whole layout column too (prevents “ghost” space)
document.body.classList.toggle('config-collapsed', shouldCollapse);
// Accessibility state
toggleBtn.setAttribute('aria-expanded', String(!shouldCollapse));
}
function init() {
initTheme();
// Mark touch-first devices so CSS can enable the keyboard dock.
document.body.classList.toggle('has-touch', isTouchLikely());
// Keep touch mode correct on rotate / resize (tablets love to change their mind)
window.addEventListener('resize', () => {
document.body.classList.toggle('has-touch', isTouchLikely());
});
const form = $('configForm');
if (!form) {
console.error('CRSWRD: configForm not found. Check index.html IDs.');
return;
}
// 1) Restore saved preferences before we generate anything.
applyStoredPrefsToUI();
// 2) Persist any changes as the user tweaks dropdowns.
wirePrefsPersistence();
// Mobile: allow settings controls (dropdowns) to open without us stealing focus back.
form.addEventListener('pointerdown', () => suppressMobileRefocus(1200), true);
form.addEventListener('focusin', () => suppressMobileRefocus(1200), true);
// Settings toggle should be wired once (not on every submit)
const toggleBtn = $('toggleConfigBtn');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const panel = document.querySelector('.panel');
const isCollapsed = panel?.classList.contains('is-collapsed') ?? false;
collapseConfigPanel(!isCollapsed);
setStatus(isCollapsed ? 'Settings opened.' : 'Settings hidden.');
});
}
form.addEventListener('submit', (e) => {
e.preventDefault();
const config = readConfig();
logConfig(config);
// Save selections even if the user didn't touch the dropdowns this time.
persistPrefsFromUI();
// Phase 3: Load puzzle from selected pack (data-driven)
initCrosswordFromSelections();
setStatus('Puzzle ready.');
const stageText = document.querySelector('.stage-text');
if (stageText) {
stageText.textContent = `Puzzle generated. Grid size: ${config.timeLength}.`;
}
// Touch devices (phone + tablet, including landscape): collapse after generate
if (isTouchLikely()) {
collapseConfigPanel(true);
setStatus('Settings captured. Panel collapsed for gameplay.');
}
});
// Keyboard sanity: Ctrl/Command + Enter submits, Escape clears focus
document.addEventListener('keydown', (e) => {
const isSubmitCombo = (e.ctrlKey || e.metaKey) && e.key === 'Enter';
if (isSubmitCombo) {
e.preventDefault();
form.requestSubmit();
return;
}
// Escape clears focus (useful if a dropdown gets “stuck” in focus
if (e.key === 'Escape') {
const active = document.activeElement;
if (active && typeof active.blur === 'function') {
active.blur();
setStatus('Focus cleared.');
}
}
});
initCrosswordFromSelections();
wireCheckButtons();
wireMobileKeyboard();
wireKeyboardLauncher(); // makes the "Keyboard" bar button actually work
console.info('CRSWRD: Phase 2 UI loaded (static crossword, no generation).');
}
/**
* Packs (data-driven puzzles)
*
* Schema (per pack):
* PACKS[packId] = {
* id: string,
* name: string,
* puzzles: [
* {
* id: string,
* title: string,
* grid: string[], // each string is a row, '#' = block
* clues: {
* serious: { across: Record<startKey, string>, down: Record<startKey, string> },
* funny: { across: Record<startKey, string>, down: Record<startKey, string> }
* }
* }
* ]
* }
*
* Note: startKey uses the Phase 2 convention: "r{row}c{col}" at the START of an entry.
*/
const PACKS = {
general: {
id: 'general',
name: 'General',
// Word bank used by the generator (Across-only for now).
// Each item can carry tone variants for clues.
wordBank: [
// ── 3-letter glue + fallback coverage
{
word: 'ERA',
serious: 'Long period of time',
funny: 'History’s chapter label',
},
{ word: 'ORE', serious: 'Mined material', funny: 'Rock with goals' },
{
word: 'EMU',
serious: 'Flightless bird',
funny: 'Bird that chose legs over wings',
},
{
word: 'ADO',
serious: 'Commotion or fuss',
funny: 'Noise with no payoff',
},
{
word: 'EKE',
serious: '___ out a living',
funny: 'What you might say when you see a mouse',
},
{
word: 'AGE',
serious: 'How old someone is',
funny: 'Years since you had energy',
},
{
word: 'ASP',
serious: 'Venomous snake',
funny: 'Spicy rope with fangs',
},
{ word: 'ICY', serious: 'Slippery or cold', funny: 'Cold with attitude' },
{
word: 'SPA',
serious: 'Place for a massage',
funny: 'Relaxation headquarters',
},
{ word: 'USE', serious: 'To employ', funny: 'Put it to work' },
{ word: 'ODD', serious: 'Not even', funny: 'Math’s rebel' },
{ word: 'OLD', serious: 'Not new', funny: 'Vintage, but tired' },
{ word: 'ANY', serious: 'Whichever one', funny: 'Dealer’s choice' },
{ word: 'OWN', serious: 'To possess', funny: 'Mine, officially' },
{
word: 'AXE',
serious: 'Chopping tool',
funny: 'Tree’s least favorite thing',
},
{
word: 'CAT',
serious: 'Feline friend',
funny: 'Small boss with whiskers',
},
{
word: 'DOG',
serious: 'Canine companion',
funny: 'Professional tail wag',
},
{ word: 'TAR', serious: 'Sticky black substance', funny: 'Road glue' },
{ word: 'RAT', serious: 'Common rodent', funny: 'Tiny schemer' },
{
word: 'OWL',
serious: 'Nocturnal bird of prey',
funny: 'Night shift bird',
},
{ word: 'SUN', serious: 'Daytime star', funny: 'Sky flashlight' },
{ word: 'MOON', serious: 'Night sky body', funny: 'Earth’s night lamp' },
{ word: 'SKY', serious: 'What’s above', funny: 'Cloud parking lot' },
{ word: 'SEA', serious: 'Saltwater body', funny: 'Big splash zone' },
{ word: 'ICE', serious: 'Frozen water', funny: 'Slippery trouble' },
{ word: 'FIR', serious: 'Evergreen tree', funny: 'Holiday scent source' },
{ word: 'TOR', serious: 'Rocky hill', funny: 'Nature’s speed bump' },
{
word: 'DAR',
serious: 'Give (Spanish)',
funny: 'To hand it over, en español',
},
{ word: 'GOT', serious: 'Received', funny: 'Ended up with' },
// ── 4-letter sweet spot
{
word: 'AREA',
serious: 'Space or region',
funny: 'A patch of somewhere',
},
{ word: 'ECHO', serious: 'Repeated sound', funny: 'Sound’s copy-paste' },
{
word: 'ALOE',
serious: 'Soothing plant',
funny: 'Nature’s first aid gel',
},
{
word: 'OPAL',
serious: 'Iridescent gem',
funny: 'Rock with a light show',
},
{ word: 'IDEA', serious: 'A thought', funny: 'Brain spark' },
{
word: 'ICON',
serious: 'Symbol on a screen',
funny: 'Small picture, big job',
},
{ word: 'EXIT', serious: 'Way out', funny: 'The escape hatch' },
{ word: 'OMIT', serious: 'To leave out', funny: 'Delete with manners' },
{
word: 'ITEM',
serious: 'Entry on a list',
funny: 'One thing in the pile',
},
{ word: 'ONCE', serious: 'Formerly', funny: 'Back when' },
{ word: 'EASY', serious: 'Not difficult', funny: 'Low-stress mode' },
{ word: 'ABLE', serious: 'Having the power', funny: 'Can do' },
{ word: 'ARID', serious: 'Very dry', funny: 'Moisture-free zone' },
{ word: 'ETCH', serious: 'To engrave', funny: 'Scratch with purpose' },
{
word: 'UNIT',
serious: 'Single part',
funny: 'One piece of the puzzle',
},
{ word: 'SOLO', serious: 'By oneself', funny: 'Party of one' },
{ word: 'ORAL', serious: 'Spoken aloud', funny: 'Said out loud' },
{ word: 'OPEN', serious: 'Not closed', funny: 'Unlocked vibes' },
{
word: 'USER',
serious: 'One on a computer',
funny: 'The person clicking everything',
},
{ word: 'EDGE', serious: 'The brink', funny: 'Where things get spicy' },
// ── 5-letter connectors
{ word: 'ADIEU', serious: 'French goodbye', funny: 'Fancy “see ya”' },
{
word: 'OCEAN',
serious: 'The deep blue',
funny: 'Planet’s splash zone',
},
{
word: 'ALERT',
serious: 'On one’s toes',
funny: 'Eyes open, coffee engaged',
},
{
word: 'ASIDE',
serious: 'Stage whisper',
funny: 'Quick off-to-the-side note',
},
{
word: 'EVENT',
serious: 'Happening',
funny: 'Thing that interrupts your plans',
},
{ word: 'USAGE', serious: 'Way of using', funny: 'How it gets used up' },
{ word: 'IMAGE', serious: 'Picture', funny: 'Visual proof' },
{ word: 'ALTER', serious: 'To change', funny: 'Switch it up' },
{ word: 'OUTER', serious: 'External', funny: 'On the outside' },
{
word: 'INPUT',
serious: 'Computer data',
funny: 'What you feed the machine',
},
{
word: 'EXTRA',
serious: 'More than needed',
funny: 'Bonus for no reason',
},
{ word: 'ABOUT', serious: 'Regarding', funny: 'Topic: this thing' },
{ word: 'BASIC', serious: 'Fundamental', funny: 'The starter pack' },
{ word: 'CLEAR', serious: 'Transparent', funny: 'See-through truth' },
{ word: 'DAILY', serious: 'Every day', funny: 'On repeat' },
{
word: 'FOCUS',
serious: 'Center of attention',
funny: 'Brain spotlight',
},
{ word: 'GIANT', serious: 'Huge', funny: 'Big enough to notice' },
{ word: 'HAPPY', serious: 'Joyful', funny: 'Mood: up' },
{ word: 'INNER', serious: 'Inside', funny: 'Core zone' },
{ word: 'LEVEL', serious: 'Flat', funny: 'Even-steven' },
// ── 6-letter utility
{ word: 'ACTION', serious: 'Movement', funny: 'Do the thing' },
{
word: 'ADVICE',
serious: 'Guidance',
funny: 'Free opinions, fresh today',
},
{ word: 'COMMON', serious: 'Ordinary', funny: 'The default setting' },
{
word: 'DETAIL',
serious: 'Specific point',
funny: 'The part people argue about',
},
{
word: 'ENERGY',
serious: 'Vigor',
funny: 'The thing coffee pretends to be',
},
{ word: 'FUTURE', serious: 'Time to come', funny: 'Not here yet' },
{
word: 'GROWTH',
serious: 'Increase',
funny: 'Getting bigger on purpose',
},
{ word: 'IMPACT', serious: 'Effect', funny: 'The “that mattered” part' },
{
word: 'METHOD',
serious: 'Way of doing',
funny: 'Steps that (sometimes) work',
},
{ word: 'NATURE', serious: 'The outdoors', funny: 'Where bugs live' },
{ word: 'OBJECT', serious: 'Item', funny: 'Thing with a job' },
{ word: 'RESULT', serious: 'Outcome', funny: 'What you get at the end' },
{ word: 'SIMPLE', serious: 'Easy', funny: 'No drama required' },
{
word: 'UNIQUE',
serious: 'One of a kind',
funny: 'No duplicates allowed',
},
{
word: 'AGENDA',
serious: 'List of things to do',
funny: 'Plans that will be ignored',
},
{ word: 'AMOUNT', serious: 'Quantity', funny: 'How much we’re talking' },
// ── 7+ anchors
{ word: 'EXAMPLE', serious: 'Instance', funny: 'Proof by showing' },
{
word: 'GENERAL',
serious: 'Not specific',
funny: 'Covers a lot of ground',
},
{
word: 'HISTORY',
serious: 'The past',
funny: 'Everything that already happened',
},
{
word: 'JOURNEY',
serious: 'A long trip',
funny: 'The scenic route of life',
},
{
word: 'LIBRARY',
serious: 'Place for books',
funny: 'Quiet building full of stories',
},
{
word: 'MESSAGE',
serious: 'Communication',
funny: 'Text with a mission',
},
{
word: 'OPINION',
serious: 'Personal view',
funny: 'Belief with volume',
},
{
word: 'PATTERN',
serious: 'Regular design',
funny: 'The repeat that gives it away',
},
{
word: 'SCIENCE',
serious: 'Study of nature',
funny: 'Testing your assumptions',
},
{
word: 'SERVICE',
serious: 'Help provided',
funny: 'Someone doing the thing for you',
},
{ word: 'THOUGHT', serious: 'Mental product', funny: 'Brain note' },
{ word: 'UNKNOWN', serious: 'Not identified', funny: 'Mystery status' },
{ word: 'VARIETY', serious: 'A mix', funny: 'Not the same thing again' },
{ word: 'WEATHER', serious: 'Climate state', funny: 'Small talk fuel' },
{ word: 'BALANCE', serious: 'Stability', funny: 'Not falling over' },
],
puzzles: [
{
id: 'gen-001',
title: 'Tiny Animals',
difficulty: 'easy',
grid: ['CAT#DOG', 'A#O#A#O', 'TAR#RAT', '###A###', 'OWL#EMU'],
clues: {
serious: {
across: {
r0c0: 'Feline friend',
r0c4: 'Canine companion',
r2c0: 'Sticky black substance',
r2c4: 'Common rodent',
r4c0: 'Nocturnal bird of prey',
r4c4: 'Australian bird (short name)',
},
down: {
r0c0: 'A loud reaction to prices',
r0c1: 'Second letter of the alphabet',
r0c2: 'A warm drink starter (tea, for example)',
r0c4: 'Not a cat (in this tiny grid)',
r0c5: 'Round vowel',
r0c6: 'Another round vowel',
},
},
funny: {
across: {
r0c0: 'Soft roommate who judges you',
r0c4: 'Walks you, not the other way around',
r2c0: 'What your shoes find on fresh asphalt',
r2c4: 'Kitchen gremlin with whiskers',
r4c0: 'Feathered glare machine',
r4c4: 'Bird that looks like it has opinions',
},
down: {
r0c0: 'Noise you make when rent increases',
r0c1: 'B’s quieter cousin',
r0c2: 'Hot drink entry point',
r0c4: 'Natural enemy of your clean floor',
r0c5: 'O, but make it dramatic',
r0c6: 'O again, because why not',
},
},
},
},
{
id: 'gen-002',
title: 'Tiny Words',
difficulty: 'medium',
grid: ['SUN#MOON', 'A#I#E#A', 'SKY#SEA', '###O###', 'ICE#FIR'],
clues: {
serious: {
across: {
r0c0: 'Daytime star',
r0c4: 'Nighttime companion',
r2c0: 'What clouds live in',
r2c4: 'Large body of saltwater',
r4c0: 'Frozen water',
r4c4: 'Type of tree',
},
down: {
r0c0: 'Opposite of night',
r0c1: 'First vowel',
r0c2: 'Ninth letter',
},
},
funny: {
across: {
r0c0: 'The thing that wakes you up',
r0c4: 'Romantic rock in the sky',
r2c0: 'Cloud apartment',
r2c4: 'Big salty splash zone',
r4c0: 'Slippery regret',
r4c4: 'Tree that smells like Christmas',
},
down: {
r0c0: 'When alarms happen',
r0c1: 'A, but louder',
r0c2: 'I, standing alone',
},
},
},
},
],
},
movies: {
id: 'movies',
name: 'Movies',
wordBank: [
// 3-letter glue
{
word: 'SET',
serious: 'Where filming happens',
funny: 'Workplace with fake walls',
},
{
word: 'CAM',
serious: 'Camera, short form',
funny: 'The eye that never blinks',
},
{
word: 'CUT',
serious: 'Director’s shout',
funny: 'Everyone freeze instantly',
},
{
word: 'BIO',
serious: 'Life story film',
funny: 'Wikipedia with a budget',
},
{ word: 'ACT', serious: 'A movie segment', funny: 'Drama in chunks' },
{
word: 'CUE',
serious: 'Actor’s signal',
funny: 'Your turn, don’t mess up',
},
{ word: 'POP', serious: '___corn', funny: 'Snack sound effect' },
{ word: 'ACE', serious: 'Top performer', funny: 'Best at pretending' },
{ word: 'HIT', serious: 'Box office success', funny: 'Money printer' },
{
word: 'FIN',
serious: 'The end, in French',
funny: 'Fancy way to stop',
},
{
word: 'SPY',
serious: 'Secret agent type',
funny: 'Trust issues with gadgets',
},
{ word: 'RED', serious: 'Carpet color', funny: 'Celebrity runway' },
{
word: 'DUB',
serious: 'Voice replacement',
funny: 'Mouth doesn’t match',
},
{ word: 'FAN', serious: 'Movie buff', funny: 'Claps during credits' },
{
word: 'ART',
serious: 'Creative craft',
funny: 'When it works, it’s magic',
},
{ word: 'MAP', serious: 'A guide', funny: 'Directions for your brain' },
// 4-letter sweet spot
{ word: 'PLOT', serious: 'Storyline', funny: 'Reason explosions happen' },
{ word: 'STAR', serious: 'Leading actor', funny: 'Paid to be noticed' },
{
word: 'CAST',
serious: 'Film ensemble',
funny: 'Group chat with trailers',
},
{ word: 'ROLE', serious: 'Actor’s part', funny: 'Your fake job' },
{
word: 'FILM',
serious: 'The medium',
funny: 'Moving pictures, officially',
},
{
word: 'LENS',
serious: 'Camera glass',
funny: 'Very expensive eyeball',
},
{ word: 'EPIC', serious: 'Grand-scale film', funny: 'Long and loud' },
{
word: 'NOIR',
serious: 'Dark film genre',
funny: 'Dark looks, moody vibes',
},
{
word: 'CREW',
serious: 'Film workers',
funny: 'People who actually do things',
},
{
word: 'PROP',
serious: 'On-set object',
funny: 'Fake thing with real rules',
},
{ word: 'BOOM', serious: 'Sound mic', funny: 'Pole of awkwardness' },
{ word: 'TAKE', serious: 'Filmed attempt', funny: 'Try number many' },
{
word: 'RAVE',
serious: 'Great review',
funny: 'Critic enthusiasm spike',
},
{
word: 'ICON',
serious: 'Legendary star',
funny: 'Famous enough to be a noun',
},
{
word: 'EDIT',
serious: 'Post-production cut',
funny: 'Fix it later button',
},
{
word: 'HERO',
serious: 'Protagonist',
funny: 'Bad decisions, good music',
},
{
word: 'FLOP',
serious: 'Box office failure',
funny: 'Budget goes poof',
},
{
word: 'CULT',
serious: '___ classic',
funny: 'Beloved by a devoted few',
},
{
word: 'SLOT',
serious: 'Screening time',
funny: 'Calendar square of hope',
},
// 5-letter connectors
{ word: 'OSCAR', serious: 'Film award', funny: 'Gold man of judgment' },
{
word: 'SCENE',
serious: 'Single sequence',
funny: 'One chunk of drama',
},
{
word: 'GENRE',
serious: 'Type of film',
funny: 'Shelf label for vibes',
},
{ word: 'DRAMA', serious: 'Serious movie', funny: 'Feelings turned up' },
{ word: 'SHORT', serious: 'Brief film', funny: 'Snack-sized cinema' },
{
word: 'CAMEO',
serious: 'Brief appearance',
funny: 'Celebrity jump-scare',
},
{
word: 'INDIE',
serious: 'Independent film',
funny: 'Low budget, high feelings',
},
{ word: 'STUNT', serious: 'Action feat', funny: 'Insurance paperwork' },
{ word: 'SCRIPT', serious: 'Written lines', funny: 'Plan before chaos' },
{
word: 'AWARD',
serious: 'Trophy',
funny: 'Shelf decoration with opinions',
},
{
word: 'EXTRA',
serious: 'Background actor',
funny: 'Professional walker-by',
},
{
word: 'SCORE',
serious: 'Film music',
funny: 'Emotional remote control',
},
{
word: 'AUDIO',
serious: 'Film sound',
funny: 'The part you notice when it’s bad',
},
{ word: 'MOVIE', serious: 'The flick', funny: 'Two hours of commitment' },
{ word: 'GUILD', serious: 'Actors’ union', funny: 'Rules with lawyers' },
{ word: 'THEME', serious: 'Main idea', funny: 'The point, allegedly' },
{ word: 'TITLE', serious: 'The name', funny: 'The hook before watching' },
{ word: 'HORROR', serious: 'Scary genre', funny: 'Lights on required' },
{
word: 'INTRO',
serious: 'Opening section',
funny: 'Where vibes are set',