diff --git a/assets/images/game-icons.png b/assets/images/game-icons.png index e9a3b9d..3d219da 100644 Binary files a/assets/images/game-icons.png and b/assets/images/game-icons.png differ diff --git a/assets/images/game-icons.psd b/assets/images/game-icons.psd index a4e6444..5e5645a 100644 Binary files a/assets/images/game-icons.psd and b/assets/images/game-icons.psd differ diff --git a/assets/music/track19.mp3 b/assets/music/track19.mp3 new file mode 100644 index 0000000..8435a5f Binary files /dev/null and b/assets/music/track19.mp3 differ diff --git a/assets/music/track20.mp3 b/assets/music/track20.mp3 new file mode 100644 index 0000000..8ae50c7 Binary files /dev/null and b/assets/music/track20.mp3 differ diff --git a/assets/speech/convo/michael-terry.mp3 b/assets/speech/convo/michael-terry.mp3 new file mode 100644 index 0000000..8613d47 Binary files /dev/null and b/assets/speech/convo/michael-terry.mp3 differ diff --git a/assets/speech/convo/michael-victor.mp3 b/assets/speech/convo/michael-victor.mp3 new file mode 100644 index 0000000..6d8f4ce Binary files /dev/null and b/assets/speech/convo/michael-victor.mp3 differ diff --git a/data/jumble-riddles.json b/data/jumble-riddles.json new file mode 100644 index 0000000..b93fc8e --- /dev/null +++ b/data/jumble-riddles.json @@ -0,0 +1,83 @@ +{ + "riddles": [ + { "id": "golfer-pants", "setup": "Q: Why did the golfer bring two pairs of pants?\nA: In case he got a ___.", "answer": "HOLE IN ONE" }, + { "id": "bicycle-tired", "setup": "Q: Why couldn't the bicycle stand up by itself?\nA: It was ___.", "answer": "TWO TIRED" }, + { "id": "baker-rich", "setup": "Q: Why did the baker stop making bread?\nA: He didn't have enough ___.", "answer": "DOUGH NUTS" }, + { "id": "scarecrow-award", "setup": "Q: Why did the scarecrow win an award?\nA: He was outstanding in his ___.", "answer": "PUMPKIN FIELD" }, + { "id": "skeleton-party", "setup": "Q: Why don't skeletons go to parties?\nA: They have no ___ to dance with.", "answer": "BODY" }, + { "id": "clock-hungry", "setup": "Q: Why was the clock always hungry?\nA: It kept going back for ___.", "answer": "SECONDS" }, + { "id": "spider-computer", "setup": "Q: Why did the spider buy a computer?\nA: To build a ___.", "answer": "WEBSITE" }, + { "id": "book-astronaut", "setup": "Q: Why did the astronaut break up with his girlfriend?\nA: He needed ___.", "answer": "SPACE" }, + { "id": "fish-piano", "setup": "Q: Why don't fish play the piano?\nA: You can't tuna ___.", "answer": "FISH" }, + { "id": "banana-doctor", "setup": "Q: Why did the banana go to the doctor?\nA: It wasn't ___.", "answer": "PEELING WELL" }, + { "id": "ghost-elevator", "setup": "Q: Why do ghosts ride elevators?\nA: To lift their ___.", "answer": "SPIRITS" }, + { "id": "cow-farmer", "setup": "Q: Why did the farmer win an award?\nA: He was out ___ his field.", "answer": "STANDING IN" }, + { "id": "egg-comedian", "setup": "Q: Why did the egg refuse to tell jokes?\nA: It might ___.", "answer": "CRACK UP" }, + { "id": "snowman-carrot", "setup": "Q: Why was the snowman looking through the carrots?\nA: He was ___.", "answer": "PICKING HIS NOSE" }, + { "id": "cat-piano", "setup": "Q: Why did the cat sit on the computer?\nA: To keep an eye on the ___.", "answer": "MOUSE" }, + { "id": "toast-jam", "setup": "Q: Why did the toast go to therapy?\nA: It kept getting into a ___.", "answer": "JAM" }, + { "id": "elephant-suitcase", "setup": "Q: Why did the elephant pack a trunk?\nA: For his ___.", "answer": "VACATION" }, + { "id": "chef-race", "setup": "Q: Why did the chef quit the race?\nA: He couldn't ___ the pace.", "answer": "KETCHUP TO" }, + { "id": "farmer-tractor", "setup": "Q: Why did the farmer name his rooster Robinson?\nA: Because it woke up at the ___.", "answer": "CRACK OF DAWN" }, + { "id": "owl-homework", "setup": "Q: Why did the owl get invited to every party?\nA: Everybody thought he was a ___.", "answer": "HOOT" }, + { "id": "carpenter-nails", "setup": "Q: Why did the carpenter cry?\nA: He couldn't ___ his mistake.", "answer": "NAIL DOWN" }, + { "id": "detective-cheese", "setup": "Q: Why did the detective suspect the cheese?\nA: It looked pretty ___.", "answer": "GRATED" }, + { "id": "tomato-race", "setup": "Q: Why did the tomato turn red?\nA: It saw the salad ___.", "answer": "DRESSING" }, + { "id": "rabbit-haircut", "setup": "Q: Why did the rabbit need a haircut?\nA: It had too many bad ___ days.", "answer": "HARE" }, + { "id": "penguin-fridge", "setup": "Q: Why does a penguin carry a fish in its beak?\nA: Because it forgot its ___.", "answer": "LUNCHBOX" }, + { "id": "mountain-cold", "setup": "Q: Why did the mountain climber bring a blanket?\nA: The peak was ___.", "answer": "FREEZING COLD" }, + { "id": "vampire-dentist", "setup": "Q: Why did the vampire visit the dentist?\nA: He had terrible ___.", "answer": "BAD BREATH" }, + { "id": "artist-jail", "setup": "Q: Why was the painter arrested?\nA: He was framed for ___.", "answer": "ASSAULT" }, + { "id": "wizard-school", "setup": "Q: Why did the wizard fail his exam?\nA: He didn't ___ enough.", "answer": "SPELL WELL" }, + { "id": "pirate-map", "setup": "Q: Why couldn't the pirate find treasure?\nA: He couldn't read the ___.", "answer": "TREASURE MAP" }, + { "id": "robot-vacation", "setup": "Q: Why did the robot go on vacation?\nA: It needed to ___.", "answer": "RECHARGE" }, + { "id": "bee-wedding", "setup": "Q: Why did the bee get married?\nA: It found its ___.", "answer": "HONEY" }, + { "id": "dinosaur-firework", "setup": "Q: Why don't dinosaurs talk?\nA: Because they're ___.", "answer": "EXTINCT" }, + { "id": "gardener-race", "setup": "Q: Why did the gardener plant a race track?\nA: To grow ___ potatoes.", "answer": "SPEEDY" }, + { "id": "teacher-blackboard", "setup": "Q: Why did the teacher wear sunglasses?\nA: Her students were too ___.", "answer": "BRIGHT" }, + { "id": "mummy-vacation", "setup": "Q: Why wouldn't the mummy take a vacation?\nA: He was too ___ to relax.", "answer": "WOUND UP" }, + { "id": "octopus-armor", "setup": "Q: Why did the octopus win the boxing match?\nA: It was well ___.", "answer": "ARMED" }, + { "id": "cloud-underwear", "setup": "Q: Why did the cloud wear a raincoat?\nA: It thought it might ___.", "answer": "SPRINKLE" }, + { "id": "lion-barber", "setup": "Q: Why did the lion visit the barber?\nA: His ___ was too shaggy.", "answer": "MANE" }, + { "id": "computer-cold", "setup": "Q: Why did the computer catch a cold?\nA: It left its ___ open.", "answer": "WINDOWS" }, + { "id": "cookie-doctor", "setup": "Q: Why did the cookie visit the doctor?\nA: It was feeling ___.", "answer": "CRUMBY" }, + { "id": "surgeon-bakery", "setup": "Q: Why did the surgeon open a bakery?\nA: He kneaded the ___.", "answer": "DOUGH" }, + { "id": "farmer-eggs", "setup": "Q: Why did the farmer separate the chickens?\nA: To settle a ___.", "answer": "FOWL DISPUTE" }, + { "id": "magician-cards", "setup": "Q: Why did the magician's card trick fail?\nA: He lost his ___.", "answer": "CONFIDENCE" }, + { "id": "snail-highway", "setup": "Q: Why did the snail avoid the highway?\nA: It feared the ___.", "answer": "FAST LANE" }, + { "id": "astronomer-diet", "setup": "Q: Why did the astronomer start a diet?\nA: To watch her ___.", "answer": "WAISTLINE" }, + { "id": "candle-argument", "setup": "Q: Why did the candles stop fighting?\nA: They agreed to ___.", "answer": "MAKE PEACE" }, + { "id": "clockmaker-late", "setup": "Q: Why was the clockmaker always late?\nA: He kept losing ___.", "answer": "TRACK OF TIME" }, + { "id": "onion-crying", "setup": "Q: Why did nobody want to fight the onion?\nA: It always made them ___.", "answer": "TEARFUL" }, + { "id": "beekeeper-raise", "setup": "Q: Why did the beekeeper get a raise?\nA: He knew how to ___ the hive.", "answer": "MANAGE" }, + { "id": "sailor-map", "setup": "Q: Why did the sailor carry seaweed?\nA: In case he got ___.", "answer": "SEASICK" }, + { "id": "dragon-diet", "setup": "Q: Why did the dragon go on a diet?\nA: He had too much ___.", "answer": "FIRE POWER" }, + { "id": "author-block", "setup": "Q: Why did the author sit in the freezer?\nA: To beat his writer's ___.", "answer": "BLOCK" }, + { "id": "farmer-scarecrow", "setup": "Q: Why did the scarecrow get promoted?\nA: He was outstanding at ___.", "answer": "CROW CONTROL" }, + { "id": "gym-teacher", "setup": "Q: Why did the gym teacher get locked out?\nA: He lost his ___.", "answer": "KEY CARD" }, + { "id": "plumber-secret", "setup": "Q: Why did the plumber keep a secret?\nA: It was under ___.", "answer": "WRAPS" }, + { "id": "beaver-carpenter", "setup": "Q: Why did the beaver quit carpentry?\nA: It was ___ with the deadlines.", "answer": "DAMMED" }, + { "id": "painter-argument", "setup": "Q: Why did the painters stop arguing?\nA: They decided to ___.", "answer": "PATCH THINGS UP" }, + { "id": "lumberjack-love", "setup": "Q: Why did the lumberjack fall in love?\nA: She was a total ___.", "answer": "KNOCKOUT" }, + { "id": "farmer-corn", "setup": "Q: Why did the farmer win the lottery?\nA: He had ___.", "answer": "AMAZING LUCK" }, + { "id": "computer-virus", "setup": "Q: Why did the computer go to the doctor?\nA: It caught a ___.", "answer": "COMPUTER VIRUS" }, + { "id": "astronaut-breakup", "setup": "Q: Why did the astronaut break up with the alien?\nA: There was no ___.", "answer": "ATMOSPHERE" }, + { "id": "broom-late", "setup": "Q: Why was the broom late to work?\nA: It ___.", "answer": "OVERSLEPT" }, + { "id": "calendar-busy", "setup": "Q: Why did the calendar seem so important?\nA: It was full of ___.", "answer": "APPOINTMENTS" }, + { "id": "boxer-clock", "setup": "Q: Why did the boxer's watch stop working?\nA: It got ___.", "answer": "KNOCKED OUT" }, + { "id": "electrician-marriage", "setup": "Q: Why did the electrician have a happy marriage?\nA: They had great ___.", "answer": "CONNECTIONS" }, + { "id": "karate-choir", "setup": "Q: Why did the karate champion join the choir?\nA: He had excellent ___.", "answer": "COORDINATION" }, + { "id": "leopard-hide", "setup": "Q: Why couldn't the leopard hide from the zookeeper?\nA: It always got ___.", "answer": "RECOGNIZED" }, + { "id": "calendar-therapy", "setup": "Q: Why did the calendar need therapy?\nA: Too many ___.", "answer": "COMMITMENTS" }, + { "id": "mechanic-therapist", "setup": "Q: Why did the mechanic become a therapist?\nA: He was great with ___.", "answer": "RELATIONSHIPS" }, + { "id": "photographer-calm", "setup": "Q: Why did the photographer stay so calm?\nA: He kept it all in ___.", "answer": "PERSPECTIVE" }, + { "id": "marathoner-map", "setup": "Q: Why did the marathoner carry a map?\nA: To avoid getting ___.", "answer": "SIDETRACKED" }, + { "id": "chess-jacket", "setup": "Q: Why did the chess player bring a jacket?\nA: In case of a ___.", "answer": "CHECKMATE" }, + { "id": "librarian-race", "setup": "Q: Why did the librarian win the race?\nA: She knew all the ___.", "answer": "SHORTCUTS" }, + { "id": "sculptor-humble", "setup": "Q: Why did the sculptor stay humble?\nA: He kept everything in ___.", "answer": "PROPORTION" }, + { "id": "coach-promotion", "setup": "Q: Why did the coach get a promotion?\nA: Excellent team ___.", "answer": "MANAGEMENT" }, + { "id": "tailor-detective", "setup": "Q: Why did the tailor make a great detective?\nA: He noticed every ___.", "answer": "LOOSE THREAD" }, + { "id": "pilot-promotion", "setup": "Q: Why did the pilot get promoted so quickly?\nA: Outstanding ___.", "answer": "NAVIGATION" }, + { "id": "historian-puzzles", "setup": "Q: Why did the historian love puzzles?\nA: Everything finally made ___.", "answer": "PERFECT SENSE" } + ] +} diff --git a/data/jumble.json b/data/jumble.json new file mode 100644 index 0000000..f23878c --- /dev/null +++ b/data/jumble.json @@ -0,0 +1,8159 @@ +{ + "generatedAt": "2026-07-01T01:00:33.086Z", + "seed": 1246577228, + "count": 58, + "puzzles": [ + { + "level": 1, + "riddleId": "detective-cheese", + "riddle": "Q: Why did the detective suspect the cheese?\nA: It looked pretty ___.", + "words": [ + { + "solution": "SCAR", + "scrambled": [ + { + "char": "S" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "BITS", + "scrambled": [ + { + "char": "I" + }, + { + "char": "B" + }, + { + "char": "T" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + false, + true, + false + ] + }, + { + "solution": "GIVE", + "scrambled": [ + { + "char": "G" + }, + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "V" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "SAID", + "scrambled": [ + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "D" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "G" + }, + { + "char": "R" + }, + { + "char": "T" + }, + { + "char": "D" + }, + { + "char": "A" + }, + { + "char": "E" + } + ], + "bonusAnswer": "GRATED" + }, + { + "level": 2, + "riddleId": "gardener-race", + "riddle": "Q: Why did the gardener plant a race track?\nA: To grow ___ potatoes.", + "words": [ + { + "solution": "POPE", + "scrambled": [ + { + "char": "O" + }, + { + "char": "E" + }, + { + "char": "P" + }, + { + "char": "P" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "SPEC", + "scrambled": [ + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "C" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "READ", + "scrambled": [ + { + "char": "A" + }, + { + "char": "D" + }, + { + "char": "R" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "PRAY", + "scrambled": [ + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "Y" + }, + { + "char": "P" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "Y" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "D" + }, + { + "char": "P" + }, + { + "char": "E" + } + ], + "bonusAnswer": "SPEEDY" + }, + { + "level": 3, + "riddleId": "teacher-blackboard", + "riddle": "Q: Why did the teacher wear sunglasses?\nA: Her students were too ___.", + "words": [ + { + "solution": "GOAT", + "scrambled": [ + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "G" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "GRIM", + "scrambled": [ + { + "char": "I" + }, + { + "char": "M" + }, + { + "char": "R" + }, + { + "char": "G" + } + ], + "circledSlots": [ + false, + true, + false, + false + ] + }, + { + "solution": "FIST", + "scrambled": [ + { + "char": "S" + }, + { + "char": "T" + }, + { + "char": "I" + }, + { + "char": "F" + } + ], + "circledSlots": [ + false, + true, + false, + false + ] + }, + { + "solution": "BOTH", + "scrambled": [ + { + "char": "B" + }, + { + "char": "H" + }, + { + "char": "O" + }, + { + "char": "T" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "H" + }, + { + "char": "T" + }, + { + "char": "B" + }, + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "R" + } + ], + "bonusAnswer": "BRIGHT" + }, + { + "level": 4, + "riddleId": "cookie-doctor", + "riddle": "Q: Why did the cookie visit the doctor?\nA: It was feeling ___.", + "words": [ + { + "solution": "RAGE", + "scrambled": [ + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "G" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + false, + false + ] + }, + { + "solution": "BABY", + "scrambled": [ + { + "char": "Y" + }, + { + "char": "B" + }, + { + "char": "A" + }, + { + "char": "B" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "TUCK", + "scrambled": [ + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "U" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "NORM", + "scrambled": [ + { + "char": "R" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "M" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "Y" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "U" + }, + { + "char": "B" + }, + { + "char": "M" + } + ], + "bonusAnswer": "CRUMBY" + }, + { + "level": 5, + "riddleId": "beekeeper-raise", + "riddle": "Q: Why did the beekeeper get a raise?\nA: He knew how to ___ the hive.", + "words": [ + { + "solution": "GAIN", + "scrambled": [ + { + "char": "A" + }, + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "N" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "SOMA", + "scrambled": [ + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "O" + }, + { + "char": "M" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "FILE", + "scrambled": [ + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "F" + }, + { + "char": "L" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "BEAM", + "scrambled": [ + { + "char": "A" + }, + { + "char": "M" + }, + { + "char": "B" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "A" + }, + { + "char": "G" + }, + { + "char": "M" + } + ], + "bonusAnswer": "MANAGE" + }, + { + "level": 6, + "riddleId": "beaver-carpenter", + "riddle": "Q: Why did the beaver quit carpentry?\nA: It was ___ with the deadlines.", + "words": [ + { + "solution": "DEAL", + "scrambled": [ + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "WARM", + "scrambled": [ + { + "char": "W" + }, + { + "char": "M" + }, + { + "char": "R" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "MAID", + "scrambled": [ + { + "char": "I" + }, + { + "char": "M" + }, + { + "char": "D" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + false, + false + ] + }, + { + "solution": "SIDE", + "scrambled": [ + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "D" + }, + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "A" + }, + { + "char": "M" + }, + { + "char": "M" + } + ], + "bonusAnswer": "DAMMED" + }, + { + "level": 7, + "riddleId": "clock-hungry", + "riddle": "Q: Why was the clock always hungry?\nA: It kept going back for ___.", + "words": [ + { + "solution": "MODS", + "scrambled": [ + { + "char": "D" + }, + { + "char": "M" + }, + { + "char": "O" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "PANS", + "scrambled": [ + { + "char": "A" + }, + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "FUEL", + "scrambled": [ + { + "char": "U" + }, + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "F" + } + ], + "circledSlots": [ + false, + false, + true, + false + ] + }, + { + "solution": "ROCK", + "scrambled": [ + { + "char": "R" + }, + { + "char": "O" + }, + { + "char": "K" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "D" + }, + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "S" + }, + { + "char": "O" + }, + { + "char": "N" + } + ], + "bonusAnswer": "SECONDS" + }, + { + "level": 8, + "riddleId": "spider-computer", + "riddle": "Q: Why did the spider buy a computer?\nA: To build a ___.", + "words": [ + { + "solution": "TAKE", + "scrambled": [ + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "E" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "BETA", + "scrambled": [ + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "B" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "BIND", + "scrambled": [ + { + "char": "B" + }, + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "COWS", + "scrambled": [ + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "W" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "T" + }, + { + "char": "W" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "B" + }, + { + "char": "E" + }, + { + "char": "I" + } + ], + "bonusAnswer": "WEBSITE" + }, + { + "level": 9, + "riddleId": "ghost-elevator", + "riddle": "Q: Why do ghosts ride elevators?\nA: To lift their ___.", + "words": [ + { + "solution": "ASKS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "K" + }, + { + "char": "A" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + }, + { + "solution": "RIBS", + "scrambled": [ + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "B" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "TITS", + "scrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "I" + }, + { + "char": "T" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "PECK", + "scrambled": [ + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "E" + }, + { + "char": "P" + } + ], + "circledSlots": [ + true, + false, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "P" + }, + { + "char": "I" + }, + { + "char": "S" + } + ], + "bonusAnswer": "SPIRITS" + }, + { + "level": 10, + "riddleId": "egg-comedian", + "riddle": "Q: Why did the egg refuse to tell jokes?\nA: It might ___.", + "words": [ + { + "solution": "COMP", + "scrambled": [ + { + "char": "P" + }, + { + "char": "O" + }, + { + "char": "C" + }, + { + "char": "M" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "ROOM", + "scrambled": [ + { + "char": "O" + }, + { + "char": "O" + }, + { + "char": "M" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + false, + false + ] + }, + { + "solution": "HAUL", + "scrambled": [ + { + "char": "U" + }, + { + "char": "A" + }, + { + "char": "H" + }, + { + "char": "L" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "MOCK", + "scrambled": [ + { + "char": "M" + }, + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "K" + }, + { + "char": "A" + }, + { + "char": "P" + }, + { + "char": "C" + }, + { + "char": "C" + }, + { + "char": "R" + }, + { + "char": "U" + } + ], + "bonusAnswer": "CRACK UP" + }, + { + "level": 11, + "riddleId": "artist-jail", + "riddle": "Q: Why was the painter arrested?\nA: He was framed for ___.", + "words": [ + { + "solution": "BASS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "B" + }, + { + "char": "A" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "TUNA", + "scrambled": [ + { + "char": "A" + }, + { + "char": "U" + }, + { + "char": "N" + }, + { + "char": "T" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + }, + { + "solution": "WILL", + "scrambled": [ + { + "char": "I" + }, + { + "char": "L" + }, + { + "char": "L" + }, + { + "char": "W" + } + ], + "circledSlots": [ + false, + false, + true, + false + ] + }, + { + "solution": "PAST", + "scrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "P" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "A" + }, + { + "char": "S" + }, + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "L" + }, + { + "char": "S" + }, + { + "char": "U" + } + ], + "bonusAnswer": "ASSAULT" + }, + { + "level": 12, + "riddleId": "dinosaur-firework", + "riddle": "Q: Why don't dinosaurs talk?\nA: Because they're ___.", + "words": [ + { + "solution": "TENS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "N" + }, + { + "char": "T" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "INCH", + "scrambled": [ + { + "char": "H" + }, + { + "char": "C" + }, + { + "char": "I" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + false, + true, + false + ] + }, + { + "solution": "NEXT", + "scrambled": [ + { + "char": "T" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "X" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "HINT", + "scrambled": [ + { + "char": "T" + }, + { + "char": "N" + }, + { + "char": "H" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "T" + }, + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "X" + }, + { + "char": "I" + }, + { + "char": "N" + } + ], + "bonusAnswer": "EXTINCT" + }, + { + "level": 13, + "riddleId": "mummy-vacation", + "riddle": "Q: Why wouldn't the mummy take a vacation?\nA: He was too ___ to relax.", + "words": [ + { + "solution": "SOUR", + "scrambled": [ + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "O" + }, + { + "char": "U" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "PADS", + "scrambled": [ + { + "char": "P" + }, + { + "char": "A" + }, + { + "char": "S" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "PONY", + "scrambled": [ + { + "char": "P" + }, + { + "char": "N" + }, + { + "char": "Y" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + false, + true, + false + ] + }, + { + "solution": "SQUAW", + "scrambled": [ + { + "char": "W" + }, + { + "char": "U" + }, + { + "char": "Q" + }, + { + "char": "S" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "U" + }, + { + "char": "O" + }, + { + "char": "D" + }, + { + "char": "W" + }, + { + "char": "U" + }, + { + "char": "P" + }, + { + "char": "N" + } + ], + "bonusAnswer": "WOUND UP" + }, + { + "level": 14, + "riddleId": "computer-cold", + "riddle": "Q: Why did the computer catch a cold?\nA: It left its ___ open.", + "words": [ + { + "solution": "WINS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "N" + }, + { + "char": "W" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "OWLS", + "scrambled": [ + { + "char": "O" + }, + { + "char": "L" + }, + { + "char": "S" + }, + { + "char": "W" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + }, + { + "solution": "DIVA", + "scrambled": [ + { + "char": "V" + }, + { + "char": "I" + }, + { + "char": "A" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + false, + false, + false + ] + }, + { + "solution": "BOWL", + "scrambled": [ + { + "char": "B" + }, + { + "char": "O" + }, + { + "char": "L" + }, + { + "char": "W" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "D" + }, + { + "char": "S" + }, + { + "char": "W" + }, + { + "char": "W" + }, + { + "char": "O" + }, + { + "char": "I" + }, + { + "char": "N" + } + ], + "bonusAnswer": "WINDOWS" + }, + { + "level": 15, + "riddleId": "onion-crying", + "riddle": "Q: Why did nobody want to fight the onion?\nA: It always made them ___.", + "words": [ + { + "solution": "DUFF", + "scrambled": [ + { + "char": "U" + }, + { + "char": "F" + }, + { + "char": "F" + }, + { + "char": "D" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "DUDE", + "scrambled": [ + { + "char": "E" + }, + { + "char": "U" + }, + { + "char": "D" + }, + { + "char": "D" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + }, + { + "solution": "YARN", + "scrambled": [ + { + "char": "A" + }, + { + "char": "Y" + }, + { + "char": "N" + }, + { + "char": "R" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "LATE", + "scrambled": [ + { + "char": "L" + }, + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "A" + }, + { + "char": "F" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "U" + }, + { + "char": "T" + }, + { + "char": "L" + } + ], + "bonusAnswer": "TEARFUL" + }, + { + "level": 16, + "riddleId": "sailor-map", + "riddle": "Q: Why did the sailor carry seaweed?\nA: In case he got ___.", + "words": [ + { + "solution": "PITS", + "scrambled": [ + { + "char": "I" + }, + { + "char": "T" + }, + { + "char": "P" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + }, + { + "solution": "FINE", + "scrambled": [ + { + "char": "F" + }, + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + false, + false, + true + ] + }, + { + "solution": "PICS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "C" + }, + { + "char": "P" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "PEAK", + "scrambled": [ + { + "char": "K" + }, + { + "char": "E" + }, + { + "char": "P" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "S" + }, + { + "char": "K" + }, + { + "char": "S" + }, + { + "char": "I" + }, + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "A" + } + ], + "bonusAnswer": "SEASICK" + }, + { + "level": 17, + "riddleId": "gym-teacher", + "riddle": "Q: Why did the gym teacher get locked out?\nA: He lost his ___.", + "words": [ + { + "solution": "DRUG", + "scrambled": [ + { + "char": "U" + }, + { + "char": "G" + }, + { + "char": "R" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "NECK", + "scrambled": [ + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "N" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + }, + { + "solution": "CYAN", + "scrambled": [ + { + "char": "Y" + }, + { + "char": "C" + }, + { + "char": "N" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "BRAS", + "scrambled": [ + { + "char": "B" + }, + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "C" + }, + { + "char": "Y" + }, + { + "char": "K" + }, + { + "char": "A" + } + ], + "bonusAnswer": "KEY CARD" + }, + { + "level": 18, + "riddleId": "bicycle-tired", + "riddle": "Q: Why couldn't the bicycle stand up by itself?\nA: It was ___.", + "words": [ + { + "solution": "DARE", + "scrambled": [ + { + "char": "D" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "SLOW", + "scrambled": [ + { + "char": "L" + }, + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "W" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "BUTT", + "scrambled": [ + { + "char": "T" + }, + { + "char": "T" + }, + { + "char": "U" + }, + { + "char": "B" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "LIEU", + "scrambled": [ + { + "char": "L" + }, + { + "char": "U" + }, + { + "char": "I" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "I" + }, + { + "char": "T" + }, + { + "char": "W" + }, + { + "char": "D" + }, + { + "char": "R" + }, + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "E" + } + ], + "bonusAnswer": "TWO TIRED" + }, + { + "level": 19, + "riddleId": "elephant-suitcase", + "riddle": "Q: Why did the elephant pack a trunk?\nA: For his ___.", + "words": [ + { + "solution": "GAVE", + "scrambled": [ + { + "char": "A" + }, + { + "char": "V" + }, + { + "char": "G" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "AUTO", + "scrambled": [ + { + "char": "A" + }, + { + "char": "U" + }, + { + "char": "O" + }, + { + "char": "T" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "INCH", + "scrambled": [ + { + "char": "I" + }, + { + "char": "C" + }, + { + "char": "N" + }, + { + "char": "H" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "BIAS", + "scrambled": [ + { + "char": "I" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "B" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "O" + }, + { + "char": "C" + }, + { + "char": "A" + }, + { + "char": "I" + }, + { + "char": "A" + }, + { + "char": "N" + }, + { + "char": "V" + }, + { + "char": "T" + } + ], + "bonusAnswer": "VACATION" + }, + { + "level": 20, + "riddleId": "carpenter-nails", + "riddle": "Q: Why did the carpenter cry?\nA: He couldn't ___ his mistake.", + "words": [ + { + "solution": "RAID", + "scrambled": [ + { + "char": "R" + }, + { + "char": "I" + }, + { + "char": "D" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "LOWS", + "scrambled": [ + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "W" + }, + { + "char": "L" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "LUNG", + "scrambled": [ + { + "char": "N" + }, + { + "char": "G" + }, + { + "char": "U" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "SEND", + "scrambled": [ + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "D" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "L" + }, + { + "char": "I" + }, + { + "char": "A" + }, + { + "char": "D" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "W" + }, + { + "char": "N" + } + ], + "bonusAnswer": "NAIL DOWN" + }, + { + "level": 21, + "riddleId": "tomato-race", + "riddle": "Q: Why did the tomato turn red?\nA: It saw the salad ___.", + "words": [ + { + "solution": "GONE", + "scrambled": [ + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "G" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "LIST", + "scrambled": [ + { + "char": "S" + }, + { + "char": "L" + }, + { + "char": "T" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "PIES", + "scrambled": [ + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "P" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "REDS", + "scrambled": [ + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "S" + }, + { + "char": "G" + }, + { + "char": "N" + }, + { + "char": "S" + } + ], + "bonusAnswer": "DRESSING" + }, + { + "level": 22, + "riddleId": "penguin-fridge", + "riddle": "Q: Why does a penguin carry a fish in its beak?\nA: Because it forgot its ___.", + "words": [ + { + "solution": "THUS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "T" + }, + { + "char": "U" + }, + { + "char": "H" + } + ], + "circledSlots": [ + false, + true, + true, + false + ] + }, + { + "solution": "NICK", + "scrambled": [ + { + "char": "I" + }, + { + "char": "K" + }, + { + "char": "N" + }, + { + "char": "C" + } + ], + "circledSlots": [ + true, + false, + true, + false + ] + }, + { + "solution": "BOWL", + "scrambled": [ + { + "char": "O" + }, + { + "char": "B" + }, + { + "char": "L" + }, + { + "char": "W" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "FLUX", + "scrambled": [ + { + "char": "L" + }, + { + "char": "U" + }, + { + "char": "X" + }, + { + "char": "F" + } + ], + "circledSlots": [ + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "X" + }, + { + "char": "L" + }, + { + "char": "B" + }, + { + "char": "H" + }, + { + "char": "C" + }, + { + "char": "U" + }, + { + "char": "N" + }, + { + "char": "O" + } + ], + "bonusAnswer": "LUNCHBOX" + }, + { + "level": 23, + "riddleId": "robot-vacation", + "riddle": "Q: Why did the robot go on vacation?\nA: It needed to ___.", + "words": [ + { + "solution": "BEAR", + "scrambled": [ + { + "char": "B" + }, + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "GIVE", + "scrambled": [ + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "V" + }, + { + "char": "G" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "SPEC", + "scrambled": [ + { + "char": "P" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + false, + true, + true + ] + }, + { + "solution": "RUSH", + "scrambled": [ + { + "char": "S" + }, + { + "char": "U" + }, + { + "char": "H" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "H" + }, + { + "char": "G" + } + ], + "bonusAnswer": "RECHARGE" + }, + { + "level": 24, + "riddleId": "cloud-underwear", + "riddle": "Q: Why did the cloud wear a raincoat?\nA: It thought it might ___.", + "words": [ + { + "solution": "LURE", + "scrambled": [ + { + "char": "U" + }, + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "PITS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "P" + }, + { + "char": "T" + }, + { + "char": "I" + } + ], + "circledSlots": [ + true, + true, + false, + false + ] + }, + { + "solution": "NETS", + "scrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "N" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + }, + { + "solution": "RICK", + "scrambled": [ + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "R" + }, + { + "char": "I" + } + ], + "circledSlots": [ + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "L" + }, + { + "char": "P" + }, + { + "char": "E" + }, + { + "char": "K" + } + ], + "bonusAnswer": "SPRINKLE" + }, + { + "level": 25, + "riddleId": "snail-highway", + "riddle": "Q: Why did the snail avoid the highway?\nA: It feared the ___.", + "words": [ + { + "solution": "ALTER", + "scrambled": [ + { + "char": "T" + }, + { + "char": "L" + }, + { + "char": "A" + }, + { + "char": "R" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "LENSE", + "scrambled": [ + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "S" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "FALLS", + "scrambled": [ + { + "char": "A" + }, + { + "char": "L" + }, + { + "char": "S" + }, + { + "char": "F" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false + ] + }, + { + "solution": "FAINT", + "scrambled": [ + { + "char": "I" + }, + { + "char": "T" + }, + { + "char": "F" + }, + { + "char": "N" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "N" + }, + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "F" + }, + { + "char": "A" + }, + { + "char": "L" + } + ], + "bonusAnswer": "FAST LANE" + }, + { + "level": 26, + "riddleId": "lumberjack-love", + "riddle": "Q: Why did the lumberjack fall in love?\nA: She was a total ___.", + "words": [ + { + "solution": "HAIKU", + "scrambled": [ + { + "char": "I" + }, + { + "char": "U" + }, + { + "char": "H" + }, + { + "char": "K" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + false, + true, + true + ] + }, + { + "solution": "KRONA", + "scrambled": [ + { + "char": "R" + }, + { + "char": "O" + }, + { + "char": "A" + }, + { + "char": "N" + }, + { + "char": "K" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false + ] + }, + { + "solution": "FUTON", + "scrambled": [ + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "F" + }, + { + "char": "N" + }, + { + "char": "U" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + }, + { + "solution": "NICOL", + "scrambled": [ + { + "char": "I" + }, + { + "char": "O" + }, + { + "char": "L" + }, + { + "char": "C" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "U" + }, + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "K" + }, + { + "char": "C" + } + ], + "bonusAnswer": "KNOCKOUT" + }, + { + "level": 27, + "riddleId": "golfer-pants", + "riddle": "Q: Why did the golfer bring two pairs of pants?\nA: In case he got a ___.", + "words": [ + { + "solution": "VOTER", + "scrambled": [ + { + "char": "R" + }, + { + "char": "T" + }, + { + "char": "E" + }, + { + "char": "V" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false + ] + }, + { + "solution": "LANCE", + "scrambled": [ + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + false, + true, + false, + true + ] + }, + { + "solution": "HELLO", + "scrambled": [ + { + "char": "L" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "H" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + }, + { + "solution": "MINAS", + "scrambled": [ + { + "char": "N" + }, + { + "char": "M" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "N" + }, + { + "char": "H" + }, + { + "char": "O" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "L" + }, + { + "char": "O" + } + ], + "bonusAnswer": "HOLE IN ONE" + }, + { + "level": 28, + "riddleId": "baker-rich", + "riddle": "Q: Why did the baker stop making bread?\nA: He didn't have enough ___.", + "words": [ + { + "solution": "ROUGH", + "scrambled": [ + { + "char": "R" + }, + { + "char": "U" + }, + { + "char": "G" + }, + { + "char": "O" + }, + { + "char": "H" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false + ] + }, + { + "solution": "BOUND", + "scrambled": [ + { + "char": "U" + }, + { + "char": "D" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "B" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false + ] + }, + { + "solution": "HUNTS", + "scrambled": [ + { + "char": "H" + }, + { + "char": "S" + }, + { + "char": "N" + }, + { + "char": "T" + }, + { + "char": "U" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true + ] + }, + { + "solution": "MODUS", + "scrambled": [ + { + "char": "D" + }, + { + "char": "U" + }, + { + "char": "S" + }, + { + "char": "M" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "D" + }, + { + "char": "S" + }, + { + "char": "H" + }, + { + "char": "U" + }, + { + "char": "T" + }, + { + "char": "G" + }, + { + "char": "U" + } + ], + "bonusAnswer": "DOUGH NUTS" + }, + { + "level": 29, + "riddleId": "chef-race", + "riddle": "Q: Why did the chef quit the race?\nA: He couldn't ___ the pace.", + "words": [ + { + "solution": "CHAIR", + "scrambled": [ + { + "char": "A" + }, + { + "char": "I" + }, + { + "char": "C" + }, + { + "char": "R" + }, + { + "char": "H" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false + ] + }, + { + "solution": "AORTA", + "scrambled": [ + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "R" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false + ] + }, + { + "solution": "PULSE", + "scrambled": [ + { + "char": "L" + }, + { + "char": "U" + }, + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + true, + false, + false, + true + ] + }, + { + "solution": "STALK", + "scrambled": [ + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "A" + }, + { + "char": "L" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "H" + }, + { + "char": "K" + }, + { + "char": "P" + }, + { + "char": "C" + }, + { + "char": "T" + }, + { + "char": "T" + }, + { + "char": "U" + }, + { + "char": "O" + }, + { + "char": "E" + } + ], + "bonusAnswer": "KETCHUP TO" + }, + { + "level": 30, + "riddleId": "vampire-dentist", + "riddle": "Q: Why did the vampire visit the dentist?\nA: He had terrible ___.", + "words": [ + { + "solution": "BABEL", + "scrambled": [ + { + "char": "L" + }, + { + "char": "B" + }, + { + "char": "E" + }, + { + "char": "B" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false + ] + }, + { + "solution": "BAKER", + "scrambled": [ + { + "char": "B" + }, + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "K" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + false, + false, + true + ] + }, + { + "solution": "DOUGH", + "scrambled": [ + { + "char": "G" + }, + { + "char": "U" + }, + { + "char": "H" + }, + { + "char": "O" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + }, + { + "solution": "STARS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "R" + }, + { + "char": "T" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "H" + }, + { + "char": "B" + }, + { + "char": "A" + }, + { + "char": "A" + }, + { + "char": "T" + }, + { + "char": "D" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "B" + } + ], + "bonusAnswer": "BAD BREATH" + }, + { + "level": 31, + "riddleId": "wizard-school", + "riddle": "Q: Why did the wizard fail his exam?\nA: He didn't ___ enough.", + "words": [ + { + "solution": "MARES", + "scrambled": [ + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "M" + }, + { + "char": "R" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + false, + true, + true + ] + }, + { + "solution": "WHIRL", + "scrambled": [ + { + "char": "R" + }, + { + "char": "W" + }, + { + "char": "H" + }, + { + "char": "I" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + }, + { + "solution": "SPELL", + "scrambled": [ + { + "char": "P" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "L" + } + ], + "circledSlots": [ + false, + true, + false, + true, + true + ] + }, + { + "solution": "LEGIT", + "scrambled": [ + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "P" + }, + { + "char": "L" + }, + { + "char": "L" + }, + { + "char": "S" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "W" + }, + { + "char": "L" + } + ], + "bonusAnswer": "SPELL WELL" + }, + { + "level": 32, + "riddleId": "astronomer-diet", + "riddle": "Q: Why did the astronomer start a diet?\nA: To watch her ___.", + "words": [ + { + "solution": "TELEX", + "scrambled": [ + { + "char": "X" + }, + { + "char": "T" + }, + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false + ] + }, + { + "solution": "ARIEL", + "scrambled": [ + { + "char": "A" + }, + { + "char": "I" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false + ] + }, + { + "solution": "TWINE", + "scrambled": [ + { + "char": "T" + }, + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "W" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false + ] + }, + { + "solution": "HEIST", + "scrambled": [ + { + "char": "H" + }, + { + "char": "I" + }, + { + "char": "T" + }, + { + "char": "E" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "W" + }, + { + "char": "I" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "N" + }, + { + "char": "T" + }, + { + "char": "L" + } + ], + "bonusAnswer": "WAISTLINE" + }, + { + "level": 33, + "riddleId": "candle-argument", + "riddle": "Q: Why did the candles stop fighting?\nA: They agreed to ___.", + "words": [ + { + "solution": "TACIT", + "scrambled": [ + { + "char": "I" + }, + { + "char": "A" + }, + { + "char": "T" + }, + { + "char": "T" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + }, + { + "solution": "BEIGE", + "scrambled": [ + { + "char": "I" + }, + { + "char": "B" + }, + { + "char": "G" + }, + { + "char": "E" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + false, + false, + true + ] + }, + { + "solution": "PARKS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "P" + }, + { + "char": "R" + }, + { + "char": "K" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false + ] + }, + { + "solution": "MILLE", + "scrambled": [ + { + "char": "M" + }, + { + "char": "I" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "P" + }, + { + "char": "A" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "K" + }, + { + "char": "E" + }, + { + "char": "C" + }, + { + "char": "M" + }, + { + "char": "E" + } + ], + "bonusAnswer": "MAKE PEACE" + }, + { + "level": 34, + "riddleId": "dragon-diet", + "riddle": "Q: Why did the dragon go on a diet?\nA: He had too much ___.", + "words": [ + { + "solution": "NORMS", + "scrambled": [ + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "S" + }, + { + "char": "R" + }, + { + "char": "M" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + }, + { + "solution": "FLAWS", + "scrambled": [ + { + "char": "L" + }, + { + "char": "A" + }, + { + "char": "S" + }, + { + "char": "W" + }, + { + "char": "F" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "PAYEE", + "scrambled": [ + { + "char": "A" + }, + { + "char": "Y" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "P" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true + ] + }, + { + "solution": "FERMI", + "scrambled": [ + { + "char": "F" + }, + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "M" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "R" + }, + { + "char": "W" + }, + { + "char": "O" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "F" + }, + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "P" + } + ], + "bonusAnswer": "FIRE POWER" + }, + { + "level": 35, + "riddleId": "broom-late", + "riddle": "Q: Why was the broom late to work?\nA: It ___.", + "words": [ + { + "solution": "TROVE", + "scrambled": [ + { + "char": "T" + }, + { + "char": "R" + }, + { + "char": "O" + }, + { + "char": "E" + }, + { + "char": "V" + } + ], + "circledSlots": [ + false, + false, + true, + true, + true + ] + }, + { + "solution": "PARTS", + "scrambled": [ + { + "char": "R" + }, + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "T" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false + ] + }, + { + "solution": "TOWEL", + "scrambled": [ + { + "char": "E" + }, + { + "char": "W" + }, + { + "char": "O" + }, + { + "char": "L" + }, + { + "char": "T" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + }, + { + "solution": "SCALE", + "scrambled": [ + { + "char": "L" + }, + { + "char": "S" + }, + { + "char": "A" + }, + { + "char": "C" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "O" + }, + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "L" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "E" + }, + { + "char": "V" + } + ], + "bonusAnswer": "OVERSLEPT" + }, + { + "level": 36, + "riddleId": "chess-jacket", + "riddle": "Q: Why did the chess player bring a jacket?\nA: In case of a ___.", + "words": [ + { + "solution": "HEADS", + "scrambled": [ + { + "char": "H" + }, + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "A" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + true, + true, + false, + false + ] + }, + { + "solution": "MURKY", + "scrambled": [ + { + "char": "K" + }, + { + "char": "M" + }, + { + "char": "R" + }, + { + "char": "U" + }, + { + "char": "Y" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "CREWS", + "scrambled": [ + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "C" + }, + { + "char": "R" + }, + { + "char": "W" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false + ] + }, + { + "solution": "OPTIC", + "scrambled": [ + { + "char": "C" + }, + { + "char": "T" + }, + { + "char": "I" + }, + { + "char": "P" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "H" + }, + { + "char": "M" + }, + { + "char": "A" + }, + { + "char": "C" + }, + { + "char": "E" + } + ], + "bonusAnswer": "CHECKMATE" + }, + { + "level": 37, + "riddleId": "librarian-race", + "riddle": "Q: Why did the librarian win the race?\nA: She knew all the ___.", + "words": [ + { + "solution": "HAWKS", + "scrambled": [ + { + "char": "H" + }, + { + "char": "W" + }, + { + "char": "A" + }, + { + "char": "S" + }, + { + "char": "K" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true + ] + }, + { + "solution": "NOUNS", + "scrambled": [ + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "N" + }, + { + "char": "U" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + }, + { + "solution": "TRACK", + "scrambled": [ + { + "char": "R" + }, + { + "char": "K" + }, + { + "char": "T" + }, + { + "char": "A" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false + ] + }, + { + "solution": "TOOTH", + "scrambled": [ + { + "char": "H" + }, + { + "char": "T" + }, + { + "char": "T" + }, + { + "char": "O" + }, + { + "char": "O" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "S" + }, + { + "char": "U" + }, + { + "char": "T" + }, + { + "char": "R" + }, + { + "char": "H" + }, + { + "char": "C" + }, + { + "char": "S" + }, + { + "char": "O" + }, + { + "char": "T" + } + ], + "bonusAnswer": "SHORTCUTS" + }, + { + "level": 38, + "riddleId": "cow-farmer", + "riddle": "Q: Why did the farmer win an award?\nA: He was out ___ his field.", + "words": [ + { + "solution": "VISTA", + "scrambled": [ + { + "char": "I" + }, + { + "char": "V" + }, + { + "char": "A" + }, + { + "char": "T" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false + ] + }, + { + "solution": "PAINS", + "scrambled": [ + { + "char": "I" + }, + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "S" + }, + { + "char": "P" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false + ] + }, + { + "solution": "RENTS", + "scrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "R" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + }, + { + "solution": "DYING", + "scrambled": [ + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "Y" + }, + { + "char": "D" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "N" + }, + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "D" + }, + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "I" + }, + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "G" + } + ], + "bonusAnswer": "STANDING IN" + }, + { + "level": 39, + "riddleId": "magician-cards", + "riddle": "Q: Why did the magician's card trick fail?\nA: He lost his ___.", + "words": [ + { + "solution": "INERT", + "scrambled": [ + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "T" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + }, + { + "solution": "COEDS", + "scrambled": [ + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "D" + }, + { + "char": "C" + }, + { + "char": "S" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false + ] + }, + { + "solution": "FECES", + "scrambled": [ + { + "char": "C" + }, + { + "char": "F" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "S" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false + ] + }, + { + "solution": "PINTO", + "scrambled": [ + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "T" + }, + { + "char": "P" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "F" + }, + { + "char": "I" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "C" + }, + { + "char": "C" + }, + { + "char": "D" + }, + { + "char": "E" + } + ], + "bonusAnswer": "CONFIDENCE" + }, + { + "level": 40, + "riddleId": "astronaut-breakup", + "riddle": "Q: Why did the astronaut break up with the alien?\nA: There was no ___.", + "words": [ + { + "solution": "CHEMO", + "scrambled": [ + { + "char": "O" + }, + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "H" + }, + { + "char": "C" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false + ] + }, + { + "solution": "GNATS", + "scrambled": [ + { + "char": "A" + }, + { + "char": "G" + }, + { + "char": "S" + }, + { + "char": "T" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + false, + false, + true, + true + ] + }, + { + "solution": "POLAR", + "scrambled": [ + { + "char": "R" + }, + { + "char": "P" + }, + { + "char": "O" + }, + { + "char": "A" + }, + { + "char": "L" + } + ], + "circledSlots": [ + false, + true, + false, + true, + true + ] + }, + { + "solution": "ROPES", + "scrambled": [ + { + "char": "O" + }, + { + "char": "E" + }, + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "R" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "A" + }, + { + "char": "P" + }, + { + "char": "H" + }, + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "R" + } + ], + "bonusAnswer": "ATMOSPHERE" + }, + { + "level": 41, + "riddleId": "boxer-clock", + "riddle": "Q: Why did the boxer's watch stop working?\nA: It got ___.", + "words": [ + { + "solution": "TAKER", + "scrambled": [ + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false + ] + }, + { + "solution": "LENDS", + "scrambled": [ + { + "char": "N" + }, + { + "char": "D" + }, + { + "char": "L" + }, + { + "char": "S" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false + ] + }, + { + "solution": "FUCKS", + "scrambled": [ + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "S" + }, + { + "char": "U" + }, + { + "char": "F" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + }, + { + "solution": "KROON", + "scrambled": [ + { + "char": "O" + }, + { + "char": "R" + }, + { + "char": "N" + }, + { + "char": "K" + }, + { + "char": "O" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "D" + }, + { + "char": "U" + }, + { + "char": "O" + }, + { + "char": "O" + }, + { + "char": "K" + }, + { + "char": "K" + }, + { + "char": "C" + } + ], + "bonusAnswer": "KNOCKED OUT" + }, + { + "level": 42, + "riddleId": "leopard-hide", + "riddle": "Q: Why couldn't the leopard hide from the zookeeper?\nA: It always got ___.", + "words": [ + { + "solution": "CHARD", + "scrambled": [ + { + "char": "H" + }, + { + "char": "D" + }, + { + "char": "C" + }, + { + "char": "A" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "ANGEL", + "scrambled": [ + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "G" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false + ] + }, + { + "solution": "GLIDE", + "scrambled": [ + { + "char": "I" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "G" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false + ] + }, + { + "solution": "MEZZO", + "scrambled": [ + { + "char": "E" + }, + { + "char": "Z" + }, + { + "char": "M" + }, + { + "char": "Z" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + true, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "C" + }, + { + "char": "Z" + } + ], + "bonusAnswer": "RECOGNIZED" + }, + { + "level": 43, + "riddleId": "sculptor-humble", + "riddle": "Q: Why did the sculptor stay humble?\nA: He kept everything in ___.", + "words": [ + { + "solution": "MONDO", + "scrambled": [ + { + "char": "D" + }, + { + "char": "O" + }, + { + "char": "M" + }, + { + "char": "N" + }, + { + "char": "O" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + }, + { + "solution": "PERRY", + "scrambled": [ + { + "char": "E" + }, + { + "char": "Y" + }, + { + "char": "R" + }, + { + "char": "P" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false + ] + }, + { + "solution": "IONIC", + "scrambled": [ + { + "char": "N" + }, + { + "char": "C" + }, + { + "char": "I" + }, + { + "char": "O" + }, + { + "char": "I" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false + ] + }, + { + "solution": "OPTIC", + "scrambled": [ + { + "char": "P" + }, + { + "char": "T" + }, + { + "char": "O" + }, + { + "char": "I" + }, + { + "char": "C" + } + ], + "circledSlots": [ + true, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "I" + }, + { + "char": "N" + }, + { + "char": "P" + }, + { + "char": "O" + }, + { + "char": "P" + }, + { + "char": "T" + }, + { + "char": "O" + }, + { + "char": "O" + }, + { + "char": "R" + }, + { + "char": "R" + } + ], + "bonusAnswer": "PROPORTION" + }, + { + "level": 44, + "riddleId": "coach-promotion", + "riddle": "Q: Why did the coach get a promotion?\nA: Excellent team ___.", + "words": [ + { + "solution": "GENET", + "scrambled": [ + { + "char": "N" + }, + { + "char": "G" + }, + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + true, + false, + false, + true + ] + }, + { + "solution": "NINJA", + "scrambled": [ + { + "char": "I" + }, + { + "char": "A" + }, + { + "char": "J" + }, + { + "char": "N" + }, + { + "char": "N" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false + ] + }, + { + "solution": "MODEM", + "scrambled": [ + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "M" + }, + { + "char": "O" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "MADAM", + "scrambled": [ + { + "char": "M" + }, + { + "char": "A" + }, + { + "char": "A" + }, + { + "char": "D" + }, + { + "char": "M" + } + ], + "circledSlots": [ + false, + true, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "A" + }, + { + "char": "G" + }, + { + "char": "T" + }, + { + "char": "N" + }, + { + "char": "M" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "A" + } + ], + "bonusAnswer": "MANAGEMENT" + }, + { + "level": 45, + "riddleId": "pilot-promotion", + "riddle": "Q: Why did the pilot get promoted so quickly?\nA: Outstanding ___.", + "words": [ + { + "solution": "GNOME", + "scrambled": [ + { + "char": "M" + }, + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "G" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false + ] + }, + { + "solution": "IONIC", + "scrambled": [ + { + "char": "O" + }, + { + "char": "I" + }, + { + "char": "I" + }, + { + "char": "C" + }, + { + "char": "N" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false + ] + }, + { + "solution": "NAVEL", + "scrambled": [ + { + "char": "A" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "V" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false + ] + }, + { + "solution": "ENACT", + "scrambled": [ + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "A" + }, + { + "char": "T" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + true, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "T" + }, + { + "char": "V" + }, + { + "char": "O" + }, + { + "char": "I" + }, + { + "char": "A" + }, + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "N" + } + ], + "bonusAnswer": "NAVIGATION" + }, + { + "level": 46, + "riddleId": "banana-doctor", + "riddle": "Q: Why did the banana go to the doctor?\nA: It wasn't ___.", + "words": [ + { + "solution": "WIPES", + "scrambled": [ + { + "char": "W" + }, + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "P" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false + ] + }, + { + "solution": "LABEL", + "scrambled": [ + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "L" + }, + { + "char": "B" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true + ] + }, + { + "solution": "GERMS", + "scrambled": [ + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "G" + }, + { + "char": "M" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false + ] + }, + { + "solution": "CLING", + "scrambled": [ + { + "char": "C" + }, + { + "char": "N" + }, + { + "char": "L" + }, + { + "char": "I" + }, + { + "char": "G" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "G" + }, + { + "char": "W" + }, + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "P" + }, + { + "char": "L" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "L" + } + ], + "bonusAnswer": "PEELING WELL" + }, + { + "level": 47, + "riddleId": "farmer-tractor", + "riddle": "Q: Why did the farmer name his rooster Robinson?\nA: Because it woke up at the ___.", + "words": [ + { + "solution": "WACKY", + "scrambled": [ + { + "char": "K" + }, + { + "char": "C" + }, + { + "char": "Y" + }, + { + "char": "W" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + true, + true, + false, + false + ] + }, + { + "solution": "CHOIR", + "scrambled": [ + { + "char": "H" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "O" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true + ] + }, + { + "solution": "CAGED", + "scrambled": [ + { + "char": "C" + }, + { + "char": "G" + }, + { + "char": "A" + }, + { + "char": "D" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + true, + false, + false, + true + ] + }, + { + "solution": "FLANK", + "scrambled": [ + { + "char": "L" + }, + { + "char": "F" + }, + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "K" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "A" + }, + { + "char": "D" + }, + { + "char": "C" + }, + { + "char": "A" + }, + { + "char": "K" + }, + { + "char": "O" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "W" + }, + { + "char": "N" + }, + { + "char": "F" + } + ], + "bonusAnswer": "CRACK OF DAWN" + }, + { + "level": 48, + "riddleId": "pirate-map", + "riddle": "Q: Why couldn't the pirate find treasure?\nA: He couldn't read the ___.", + "words": [ + { + "solution": "GAUGE", + "scrambled": [ + { + "char": "G" + }, + { + "char": "U" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "G" + } + ], + "circledSlots": [ + false, + true, + true, + false, + true + ] + }, + { + "solution": "DRAPE", + "scrambled": [ + { + "char": "D" + }, + { + "char": "P" + }, + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + false, + true, + true, + true + ] + }, + { + "solution": "SCARE", + "scrambled": [ + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "C" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false + ] + }, + { + "solution": "TRAMS", + "scrambled": [ + { + "char": "T" + }, + { + "char": "S" + }, + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "M" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "R" + }, + { + "char": "U" + }, + { + "char": "A" + }, + { + "char": "A" + }, + { + "char": "P" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "T" + }, + { + "char": "M" + } + ], + "bonusAnswer": "TREASURE MAP" + }, + { + "level": 49, + "riddleId": "farmer-eggs", + "riddle": "Q: Why did the farmer separate the chickens?\nA: To settle a ___.", + "words": [ + { + "solution": "UNTIED", + "scrambled": [ + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "D" + }, + { + "char": "I" + }, + { + "char": "U" + }, + { + "char": "N" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false, + false + ] + }, + { + "solution": "POISED", + "scrambled": [ + { + "char": "I" + }, + { + "char": "O" + }, + { + "char": "P" + }, + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "S" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + true + ] + }, + { + "solution": "BLOKES", + "scrambled": [ + { + "char": "E" + }, + { + "char": "B" + }, + { + "char": "L" + }, + { + "char": "K" + }, + { + "char": "O" + }, + { + "char": "S" + } + ], + "circledSlots": [ + false, + true, + true, + false, + true, + false + ] + }, + { + "solution": "INFLOW", + "scrambled": [ + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "W" + }, + { + "char": "F" + }, + { + "char": "O" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + false, + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "D" + }, + { + "char": "P" + }, + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "T" + }, + { + "char": "W" + }, + { + "char": "I" + }, + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "U" + }, + { + "char": "F" + } + ], + "bonusAnswer": "FOWL DISPUTE" + }, + { + "level": 50, + "riddleId": "clockmaker-late", + "riddle": "Q: Why was the clockmaker always late?\nA: He kept losing ___.", + "words": [ + { + "solution": "FUTURE", + "scrambled": [ + { + "char": "T" + }, + { + "char": "F" + }, + { + "char": "R" + }, + { + "char": "U" + }, + { + "char": "E" + }, + { + "char": "U" + } + ], + "circledSlots": [ + true, + false, + true, + false, + true, + false + ] + }, + { + "solution": "AMOUNT", + "scrambled": [ + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "U" + }, + { + "char": "A" + }, + { + "char": "T" + }, + { + "char": "M" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false, + false + ] + }, + { + "solution": "COSINE", + "scrambled": [ + { + "char": "I" + }, + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "C" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false, + false + ] + }, + { + "solution": "SKETCH", + "scrambled": [ + { + "char": "C" + }, + { + "char": "S" + }, + { + "char": "H" + }, + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "I" + }, + { + "char": "O" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "K" + }, + { + "char": "T" + }, + { + "char": "T" + }, + { + "char": "C" + }, + { + "char": "F" + }, + { + "char": "M" + } + ], + "bonusAnswer": "TRACK OF TIME" + }, + { + "level": 51, + "riddleId": "farmer-scarecrow", + "riddle": "Q: Why did the scarecrow get promoted?\nA: He was outstanding at ___.", + "words": [ + { + "solution": "CRAWLS", + "scrambled": [ + { + "char": "L" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "W" + }, + { + "char": "S" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false, + false + ] + }, + { + "solution": "CALICO", + "scrambled": [ + { + "char": "O" + }, + { + "char": "A" + }, + { + "char": "C" + }, + { + "char": "L" + }, + { + "char": "C" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + false, + true, + false, + true, + false + ] + }, + { + "solution": "TOMATO", + "scrambled": [ + { + "char": "A" + }, + { + "char": "M" + }, + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "T" + }, + { + "char": "O" + } + ], + "circledSlots": [ + true, + true, + false, + false, + false, + true + ] + }, + { + "solution": "ROUNDS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "U" + }, + { + "char": "D" + }, + { + "char": "N" + }, + { + "char": "R" + }, + { + "char": "O" + } + ], + "circledSlots": [ + true, + true, + false, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "L" + }, + { + "char": "O" + }, + { + "char": "C" + }, + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "T" + }, + { + "char": "R" + }, + { + "char": "R" + }, + { + "char": "O" + }, + { + "char": "C" + }, + { + "char": "W" + } + ], + "bonusAnswer": "CROW CONTROL" + }, + { + "level": 52, + "riddleId": "farmer-corn", + "riddle": "Q: Why did the farmer win the lottery?\nA: He had ___.", + "words": [ + { + "solution": "ECZEMA", + "scrambled": [ + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "A" + }, + { + "char": "C" + }, + { + "char": "Z" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + true, + false, + true, + false + ] + }, + { + "solution": "LINENS", + "scrambled": [ + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "S" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + true, + false, + false, + true, + false + ] + }, + { + "solution": "PARKAS", + "scrambled": [ + { + "char": "A" + }, + { + "char": "R" + }, + { + "char": "K" + }, + { + "char": "P" + }, + { + "char": "S" + }, + { + "char": "A" + } + ], + "circledSlots": [ + false, + true, + false, + false, + true, + false + ] + }, + { + "solution": "TUGRIK", + "scrambled": [ + { + "char": "G" + }, + { + "char": "R" + }, + { + "char": "U" + }, + { + "char": "T" + }, + { + "char": "I" + }, + { + "char": "K" + } + ], + "circledSlots": [ + false, + true, + true, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "A" + }, + { + "char": "I" + }, + { + "char": "C" + }, + { + "char": "Z" + }, + { + "char": "U" + }, + { + "char": "G" + }, + { + "char": "K" + }, + { + "char": "L" + }, + { + "char": "M" + }, + { + "char": "N" + }, + { + "char": "A" + } + ], + "bonusAnswer": "AMAZING LUCK" + }, + { + "level": 53, + "riddleId": "electrician-marriage", + "riddle": "Q: Why did the electrician have a happy marriage?\nA: They had great ___.", + "words": [ + { + "solution": "CRINGE", + "scrambled": [ + { + "char": "N" + }, + { + "char": "R" + }, + { + "char": "I" + }, + { + "char": "G" + }, + { + "char": "C" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false, + true + ] + }, + { + "solution": "OILERS", + "scrambled": [ + { + "char": "R" + }, + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "L" + } + ], + "circledSlots": [ + true, + false, + false, + false, + false, + true + ] + }, + { + "solution": "INTROS", + "scrambled": [ + { + "char": "I" + }, + { + "char": "S" + }, + { + "char": "R" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "T" + } + ], + "circledSlots": [ + false, + true, + true, + false, + true, + false + ] + }, + { + "solution": "CANCEL", + "scrambled": [ + { + "char": "N" + }, + { + "char": "C" + }, + { + "char": "L" + }, + { + "char": "C" + }, + { + "char": "A" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "T" + }, + { + "char": "I" + }, + { + "char": "N" + }, + { + "char": "O" + }, + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "N" + }, + { + "char": "N" + }, + { + "char": "S" + }, + { + "char": "C" + }, + { + "char": "O" + } + ], + "bonusAnswer": "CONNECTIONS" + }, + { + "level": 54, + "riddleId": "calendar-therapy", + "riddle": "Q: Why did the calendar need therapy?\nA: Too many ___.", + "words": [ + { + "solution": "MEMOIR", + "scrambled": [ + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "M" + } + ], + "circledSlots": [ + true, + false, + true, + false, + true, + false + ] + }, + { + "solution": "CLUMSY", + "scrambled": [ + { + "char": "M" + }, + { + "char": "L" + }, + { + "char": "U" + }, + { + "char": "S" + }, + { + "char": "C" + }, + { + "char": "Y" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true, + false + ] + }, + { + "solution": "PHENIX", + "scrambled": [ + { + "char": "I" + }, + { + "char": "P" + }, + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "X" + }, + { + "char": "H" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false, + false + ] + }, + { + "solution": "BOTTOM", + "scrambled": [ + { + "char": "M" + }, + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "B" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "M" + }, + { + "char": "E" + }, + { + "char": "C" + }, + { + "char": "M" + }, + { + "char": "T" + }, + { + "char": "M" + }, + { + "char": "S" + }, + { + "char": "T" + }, + { + "char": "O" + }, + { + "char": "I" + }, + { + "char": "N" + } + ], + "bonusAnswer": "COMMITMENTS" + }, + { + "level": 55, + "riddleId": "photographer-calm", + "riddle": "Q: Why did the photographer stay so calm?\nA: He kept it all in ___.", + "words": [ + { + "solution": "STRIVE", + "scrambled": [ + { + "char": "T" + }, + { + "char": "R" + }, + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "V" + }, + { + "char": "S" + } + ], + "circledSlots": [ + true, + false, + false, + true, + true, + false + ] + }, + { + "solution": "PACKET", + "scrambled": [ + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "K" + }, + { + "char": "C" + }, + { + "char": "P" + }, + { + "char": "T" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true, + true + ] + }, + { + "solution": "RECENT", + "scrambled": [ + { + "char": "C" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "T" + }, + { + "char": "R" + }, + { + "char": "N" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false, + false + ] + }, + { + "solution": "PACERS", + "scrambled": [ + { + "char": "S" + }, + { + "char": "C" + }, + { + "char": "P" + }, + { + "char": "A" + }, + { + "char": "R" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + false, + false, + true, + false + ] + } + ], + "bonusScrambled": [ + { + "char": "P" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "V" + }, + { + "char": "I" + }, + { + "char": "E" + }, + { + "char": "E" + }, + { + "char": "P" + }, + { + "char": "R" + }, + { + "char": "C" + }, + { + "char": "T" + } + ], + "bonusAnswer": "PERSPECTIVE" + }, + { + "level": 56, + "riddleId": "marathoner-map", + "riddle": "Q: Why did the marathoner carry a map?\nA: To avoid getting ___.", + "words": [ + { + "solution": "LIPASE", + "scrambled": [ + { + "char": "A" + }, + { + "char": "P" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + true, + false, + true, + false, + true + ] + }, + { + "solution": "HERALD", + "scrambled": [ + { + "char": "D" + }, + { + "char": "L" + }, + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "H" + }, + { + "char": "E" + } + ], + "circledSlots": [ + false, + true, + false, + false, + false, + true + ] + }, + { + "solution": "KISSED", + "scrambled": [ + { + "char": "S" + }, + { + "char": "E" + }, + { + "char": "S" + }, + { + "char": "I" + }, + { + "char": "D" + }, + { + "char": "K" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + true + ] + }, + { + "solution": "CENTER", + "scrambled": [ + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "T" + }, + { + "char": "N" + }, + { + "char": "C" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "C" + }, + { + "char": "I" + }, + { + "char": "R" + }, + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "D" + }, + { + "char": "T" + }, + { + "char": "K" + }, + { + "char": "A" + }, + { + "char": "E" + }, + { + "char": "S" + } + ], + "bonusAnswer": "SIDETRACKED" + }, + { + "level": 57, + "riddleId": "tailor-detective", + "riddle": "Q: Why did the tailor make a great detective?\nA: He noticed every ___.", + "words": [ + { + "solution": "HEADER", + "scrambled": [ + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "E" + }, + { + "char": "H" + }, + { + "char": "D" + }, + { + "char": "A" + } + ], + "circledSlots": [ + true, + false, + true, + false, + true, + false + ] + }, + { + "solution": "LENDER", + "scrambled": [ + { + "char": "N" + }, + { + "char": "E" + }, + { + "char": "L" + }, + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "R" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + true + ] + }, + { + "solution": "TUPELO", + "scrambled": [ + { + "char": "O" + }, + { + "char": "T" + }, + { + "char": "L" + }, + { + "char": "E" + }, + { + "char": "U" + }, + { + "char": "P" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + true + ] + }, + { + "solution": "DONNAS", + "scrambled": [ + { + "char": "N" + }, + { + "char": "A" + }, + { + "char": "O" + }, + { + "char": "N" + }, + { + "char": "S" + }, + { + "char": "D" + } + ], + "circledSlots": [ + false, + true, + false, + false, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "H" + }, + { + "char": "T" + }, + { + "char": "D" + }, + { + "char": "E" + }, + { + "char": "O" + }, + { + "char": "E" + }, + { + "char": "R" + }, + { + "char": "A" + }, + { + "char": "O" + }, + { + "char": "S" + }, + { + "char": "L" + } + ], + "bonusAnswer": "LOOSE THREAD" + }, + { + "level": 58, + "riddleId": "scarecrow-award", + "riddle": "Q: Why did the scarecrow win an award?\nA: He was outstanding in his ___.", + "words": [ + { + "solution": "PERFORM", + "scrambled": [ + { + "char": "O" + }, + { + "char": "M" + }, + { + "char": "R" + }, + { + "char": "F" + }, + { + "char": "R" + }, + { + "char": "P" + }, + { + "char": "E" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + false, + true + ] + }, + { + "solution": "CLINCH", + "scrambled": [ + { + "char": "L" + }, + { + "char": "N" + }, + { + "char": "I" + }, + { + "char": "C" + }, + { + "char": "C" + }, + { + "char": "H" + } + ], + "circledSlots": [ + false, + true, + true, + true, + false, + false + ] + }, + { + "solution": "PICKED", + "scrambled": [ + { + "char": "I" + }, + { + "char": "D" + }, + { + "char": "C" + }, + { + "char": "K" + }, + { + "char": "E" + }, + { + "char": "P" + } + ], + "circledSlots": [ + true, + false, + false, + true, + false, + true + ] + }, + { + "solution": "BRUISE", + "scrambled": [ + { + "char": "B" + }, + { + "char": "E" + }, + { + "char": "U" + }, + { + "char": "R" + }, + { + "char": "S" + }, + { + "char": "I" + } + ], + "circledSlots": [ + false, + false, + true, + true, + false, + true + ] + } + ], + "bonusScrambled": [ + { + "char": "E" + }, + { + "char": "I" + }, + { + "char": "N" + }, + { + "char": "D" + }, + { + "char": "P" + }, + { + "char": "K" + }, + { + "char": "I" + }, + { + "char": "P" + }, + { + "char": "M" + }, + { + "char": "U" + }, + { + "char": "F" + }, + { + "char": "L" + } + ], + "bonusAnswer": "PUMPKIN FIELD" + } + ] +} \ No newline at end of file diff --git a/data/music.json b/data/music.json index 6582b41..d38da32 100644 --- a/data/music.json +++ b/data/music.json @@ -89,6 +89,16 @@ "file": "track18.mp3", "artist": "How You Feel It", "title": "Mutemath" + }, + { + "file": "track19.mp3", + "artist": "Down the Line", + "title": "Journey" + }, + { + "file": "track20.mp3", + "artist": "Relaxin' Playing Fertig Games", + "title": "Sublime" } ] } \ No newline at end of file diff --git a/data/opponents.json b/data/opponents.json index 2d1379b..aa7c582 100644 --- a/data/opponents.json +++ b/data/opponents.json @@ -457,7 +457,8 @@ "bio": "Hey mon! Yous be knowin' I been wantin' ta play on da Fertig Games for a while now mon!", "voice": "Djed Spence", "convo": [ - "michael" + "terry", + "victor" ], "speech": { "intro": [ diff --git a/src/data/gamesRegistry.js b/src/data/gamesRegistry.js index 1fec779..568a186 100644 --- a/src/data/gamesRegistry.js +++ b/src/data/gamesRegistry.js @@ -99,6 +99,7 @@ registerGame({ slug: 'risk', name: 'Risk', category: 'tabletop', minPlayers: 2, registerGame({ slug: 'geniussquare', name: 'Genius Square', category: 'logic', minPlayers: 1, maxPlayers: 2, minOpponents: 1, maxOpponents: 1, iconFrame: 70 }); registerGame({ slug: 'katamino', name: 'Katamino', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 71 }); registerGame({ slug: 'bookwork', name: 'Bookwork', category: 'word', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 72 }); -registerGame({ slug: 'paigow', name: 'Pai Gow Poker', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 6, minOpponents: 0, maxOpponents: 5, defaultOpponents: 5, iconFrame: 73 }); +registerGame({ slug: 'paigow', name: 'Pai Gow Poker', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 5, minOpponents: 0, maxOpponents: 4, defaultOpponents: 4, iconFrame: 73 }); registerGame({ slug: 'spireclimb', name: 'Spire Climb', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 74 }); registerGame({ slug: 'azul', name: 'Azul', category: 'tabletop', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, defaultOpponents: 3, hasTutorial: true, iconFrame: 75 }); +registerGame({ slug: 'jumble', name: 'Jumble', category: 'word', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 76 }); diff --git a/src/games/jumble/JumbleGame.js b/src/games/jumble/JumbleGame.js new file mode 100644 index 0000000..1c30d41 --- /dev/null +++ b/src/games/jumble/JumbleGame.js @@ -0,0 +1,729 @@ +import * as Phaser from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js'; +import { api } from '../../services/api.js'; +import { Button } from '../../ui/Button.js'; +import { MusicPlayer } from '../../ui/MusicPlayer.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; +import { + checkWordGuess, checkBonusGuess, applyWordHint, starsForHints, + isPuzzleComplete, JUMBLE_HINT_WEIGHT, letterMultiset, +} from './JumbleLogic.js'; + +// ── Palette (notebook paper, matches Sudoku/WordSearch) ────────────────────── +const PAPER = 0xFFF8F0; +const PAPER_EDGE = 0xE8D8C0; +const INK = '#3a2a18'; +const TITLE_BROWN = '#5c3a1e'; +const FADED = '#b09a7a'; +const RED_CIRCLE = 0xc0392b; +const SPIRAL_CLR = 0x8a7060; +const TILE_BG = 0xfffdf5; +const TILE_BG_CORRECT = 0x9fe3ab; +const TILE_BG_WRONG = 0xf0a8a0; + +const D = { + bg: -2, paper: 0, lines: 1, slots: 2, tile: 10, ui: 20, overlay: 60, overlayUI: 62, +}; + +// ── Layout ──────────────────────────────────────────────────────────────────── +const LEGEND_W = 300; // left strip reserved for the how-to-play legend +const PX = LEGEND_W + 20, PY = 16, PW = GAME_WIDTH - PX - 40, PH = 985; +const HEADER_SHIFT = 75; // extra breathing room below the spiral binding, title/stars only +const TITLE_Y = PY + 56 + HEADER_SHIFT; +const RIDDLE_Y = PY + 140; +const WORD_ROWS_TOP = PY + 305; // fixed in place — not derived from RIDDLE_Y +const WORD_ROW_H = 142; +const BONUS_ROW_Y = WORD_ROWS_TOP + 4 * WORD_ROW_H + 34; + +const TILE_W = 58, TILE_H = 66, STEP = 68, SEGMENT_GAP = 30; +const SLOT_DY = -32, TRAY_DY = 40; + +const BTN_Y = GAME_HEIGHT - 46; + +export default class JumbleGame extends Phaser.Scene { + constructor() { super('JumbleGame'); } + + init(data) { + this.gameDef = data.game ?? { slug: 'jumble', name: 'Jumble' }; + this.bank = []; + this.levelsCompleted = 0; + this.canPersist = true; + this.view = 'select'; + + this.level = 0; + this.puzzle = null; + this.wordStates = []; + this.bonusState = null; + this.bonusSubsetsByWord = []; + this.hintsUsed = { word: 0, bonus: 0 }; + this.overlayUp = false; + this.starsText = null; + } + + async create() { + try { + const music = this.cache.json.get('music'); + if (music?.tracks) new MusicPlayer(this, music.tracks); + } catch (_) { /* optional */ } + + this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, COLORS.bg).setDepth(D.bg); + + const raw = this.cache.json.get('jumble'); + this.bank = (raw?.puzzles ?? []).slice().sort((a, b) => a.level - b.level); + + try { + const res = await api.get('/puzzles/jumble/progress'); + this.levelsCompleted = res?.levelsCompleted ?? 0; + } catch (_) { + this.canPersist = false; + this.levelsCompleted = 0; + } + + this.layer = this.add.container(0, 0); + this.showLevelSelect(); + } + + clearLayer() { + this.layer.removeAll(true); + this.starsText = null; + } + + bestStars(level) { + try { return Number(localStorage.getItem(`jumble-stars-${level}`)) || 0; } catch (_) { return 0; } + } + + saveStars(level, stars) { + try { + if (stars > this.bestStars(level)) localStorage.setItem(`jumble-stars-${level}`, String(stars)); + } catch (_) { /* ignore */ } + } + + // ── Level select ──────────────────────────────────────────────────────────── + + showLevelSelect() { + this.view = 'select'; + this.overlayUp = false; + this.clearLayer(); + const cx = GAME_WIDTH / 2; + + const title = this.add.text(cx, 84, 'JUMBLE', { + fontFamily: 'YummyCupcakes', fontSize: '84px', color: COLORS.goldHex, + }).setOrigin(0.5); + const sub = this.add.text(cx, 148, + 'Unscramble all 4 words, then unscramble the circled letters to solve the riddle.', { + fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex, + }).setOrigin(0.5); + this.layer.add([title, sub]); + + if (!this.bank.length) { + const msg = this.add.text(cx, 520, 'No puzzles found.\nRun: node tools/genJumble.js', { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center', + }).setOrigin(0.5); + this.layer.add(msg); + const back = new Button(this, cx, GAME_HEIGHT - 90, 'Back', () => this.scene.start('GameMenu'), { variant: 'ghost' }); + this.layer.add(back); + return; + } + + const nextLevel = Math.min(this.levelsCompleted + 1, this.bank.length); + const prog = this.add.text(cx, 190, `Completed ${this.levelsCompleted} / ${this.bank.length}`, { + fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex, + }).setOrigin(0.5); + this.layer.add(prog); + + const COLS = 12; + const SIZE = 104; + const GAP = 14; + const gridW = COLS * SIZE + (COLS - 1) * GAP; + const left = cx - gridW / 2 + SIZE / 2; + const top = 274; + + this.bank.forEach((p, i) => { + const col = i % COLS; + const row = Math.floor(i / COLS); + const x = left + col * (SIZE + GAP); + const y = top + row * (SIZE + GAP); + const level = p.level; + const cleared = level <= this.levelsCompleted; + const playable = level <= nextLevel; + + const fill = cleared ? 0x1f5c3a : playable ? 0x1e3a52 : 0x16202b; + const stroke = cleared ? 0x2ecc71 : playable ? COLORS.gold : 0x2a3744; + const tile = this.add.rectangle(x, y, SIZE, SIZE, fill).setStrokeStyle(playable || cleared ? 3 : 2, stroke, 1); + const num = this.add.text(x, y - 18, String(level), { + fontFamily: 'Righteous', fontSize: '34px', + color: playable || cleared ? COLORS.textHex : '#54606b', + }).setOrigin(0.5); + this.layer.add([tile, num]); + + if (playable || cleared) { + const earned = this.bestStars(level); + const stars = this.add.text(x, y + 26, '★★★'.slice(0, earned) + '☆☆☆'.slice(0, 3 - earned), { + fontFamily: 'serif', fontSize: '18px', color: earned ? '#ffd54a' : '#546080', + }).setOrigin(0.5); + this.layer.add(stars); + } else { + const lock = this.add.text(x, y + 26, 'locked', { + fontFamily: '"Julius Sans One"', fontSize: '13px', color: '#54606b', + }).setOrigin(0.5); + this.layer.add(lock); + } + + if (playable) { + tile.setInteractive({ useHandCursor: true }); + tile.on('pointerover', () => tile.setStrokeStyle(4, COLORS.gold, 1)); + tile.on('pointerout', () => tile.setStrokeStyle(3, stroke, 1)); + tile.on('pointerup', () => this.playLevel(level)); + } + }); + + const resume = new Button(this, cx - 150, GAME_HEIGHT - 78, `Play Level ${nextLevel}`, () => this.playLevel(nextLevel), + { width: 280, height: 58, fontSize: 24 }); + const back = new Button(this, cx + 170, GAME_HEIGHT - 78, 'Back', () => this.scene.start('GameMenu'), + { variant: 'ghost', width: 180, height: 58, fontSize: 24 }); + const reset = new Button(this, 210, GAME_HEIGHT - 78, 'Reset Progress', () => this.confirmResetProgress(), + { variant: 'ghost', width: 260, height: 58, fontSize: 22, textColor: COLORS.dangerHex }); + this.layer.add([resume, back, reset]); + + if (!this.canPersist) { + const note = this.add.text(cx, GAME_HEIGHT - 28, 'Sign in to save your progress across devices.', { + fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex, + }).setOrigin(0.5); + this.layer.add(note); + } + } + + confirmResetProgress() { + const cx = GAME_WIDTH / 2; + const cy = GAME_HEIGHT / 2; + const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setInteractive(); + const panel = this.add.graphics(); + panel.fillStyle(COLORS.panel, 0.98); + panel.fillRoundedRect(cx - 320, cy - 160, 640, 320, 20); + panel.lineStyle(3, COLORS.danger, 1); + panel.strokeRoundedRect(cx - 320, cy - 160, 640, 320, 20); + const title = this.add.text(cx, cy - 92, 'Reset Progress?', { + fontFamily: 'Righteous', fontSize: '52px', color: COLORS.dangerHex, + }).setOrigin(0.5); + const msg = this.add.text(cx, cy - 14, + 'This clears every level you have cleared and your star\nmedals, back to Level 1. This cannot be undone.', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, align: 'center', lineSpacing: 6, + }).setOrigin(0.5); + const yes = new Button(this, cx - 150, cy + 88, 'Reset', () => this.doResetProgress(), + { width: 250, height: 58, fontSize: 24, textColor: COLORS.dangerHex }); + const no = new Button(this, cx + 150, cy + 88, 'Cancel', () => this.showLevelSelect(), + { variant: 'ghost', width: 250, height: 58, fontSize: 24 }); + this.layer.add([dim, panel, title, msg, yes, no]); + } + + doResetProgress() { + api.post('/puzzles/jumble/reset').catch(() => { /* best effort */ }); + this.levelsCompleted = 0; + try { this.bank.forEach((p) => localStorage.removeItem(`jumble-stars-${p.level}`)); } catch (_) { /* ignore */ } + this.showLevelSelect(); + } + + // ── Play a level ──────────────────────────────────────────────────────────── + + partitionBonusByWord(puzzle) { + const claimed = new Array(puzzle.bonusScrambled.length).fill(false); + const subsets = []; + for (const w of puzzle.words) { + const circledChars = w.solution.split('').filter((_, i) => w.circledSlots[i]); + const remaining = new Map(letterMultiset(circledChars)); + const ids = []; + for (let i = 0; i < puzzle.bonusScrambled.length; i++) { + if (claimed[i]) continue; + const ch = puzzle.bonusScrambled[i].char; + const cnt = remaining.get(ch) ?? 0; + if (cnt > 0) { claimed[i] = true; remaining.set(ch, cnt - 1); ids.push(i); } + } + subsets.push(ids); + } + return subsets; + } + + playLevel(level) { + const puzzle = this.bank.find((p) => p.level === level); + if (!puzzle) return; + this.view = 'play'; + this.level = level; + this.puzzle = puzzle; + this.overlayUp = false; + this.hintsUsed = { word: 0, bonus: 0 }; + + this.wordStates = puzzle.words.map((w, i) => ({ + solution: w.solution, + tileDefs: w.scrambled, + circledSlots: w.circledSlots, + slotFill: Array(w.solution.length).fill(null), + trayOrder: w.scrambled.map((_, idx) => idx), + containers: new Map(), + solved: false, + _wordIdx: i, + })); + + this.bonusSubsetsByWord = this.partitionBonusByWord(puzzle); + const bonusLen = puzzle.bonusAnswer.replace(/[^A-Z]/g, '').length; + this.bonusState = { + answer: puzzle.bonusAnswer, + tileDefs: puzzle.bonusScrambled, + slotFill: Array(bonusLen).fill(null), + trayOrder: [], + containers: new Map(), + solved: false, + _wordIdx: null, + }; + + this.clearLayer(); + this.buildPaper(); + this.buildRuledLines(); + this.buildLegend(); + this.buildTitleAndRiddle(); + this.buildWordRows(); + this.buildBonusRow(); + this.buildHudButtons(); + this.updateStarText(); + } + + // ── Paper & notebook decoration ─────────────────────────────────────────────── + + buildPaper() { + const g = this.add.graphics().setDepth(D.paper); + g.postFX.addShadow(0, 8, 0.02, 1.2, 0x000000, 14, 0.55); + g.fillStyle(PAPER, 1); + g.fillRoundedRect(PX, PY, PW, PH, 16); + g.lineStyle(2, PAPER_EDGE, 1); + g.strokeRoundedRect(PX, PY, PW, PH, 16); + this.layer.add(g); + + const spiralG = this.add.graphics().setDepth(D.paper + 1); + const spiralCnt = 34; + const step = PW / (spiralCnt + 1); + for (let i = 1; i <= spiralCnt; i++) { + const sx = PX + step * i; + const sy = PY + 34; + spiralG.fillStyle(SPIRAL_CLR, 0.75); + spiralG.fillCircle(sx, sy, 14); + spiralG.fillStyle(0x000000, 1); + spiralG.fillCircle(sx, sy, 9); + } + this.layer.add(spiralG); + } + + buildRuledLines() { + const g = this.add.graphics().setDepth(D.lines); + g.lineStyle(1, 0xbcd0e0, 0.35); + for (let y = PY + 210; y < PY + PH - 20; y += 40) { + g.lineBetween(PX + 30, y, PX + PW - 30, y); + } + this.layer.add(g); + } + + // Left-strip legend: how to play, plus a color key for the word-shading + // feedback added once a word row is fully filled in. + buildLegend() { + const x = LEGEND_W / 2 + 10; + let y = PY + 30; + + const title = this.add.text(x, y, 'How to Play', { + fontFamily: 'Righteous', fontSize: '26px', color: COLORS.goldHex, + }).setOrigin(0.5, 0).setDepth(D.ui); + this.layer.add(title); + y += 44; + + const bodyStyle = { + fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.textHex, + align: 'left', wordWrap: { width: LEGEND_W - 20 }, lineSpacing: 5, + }; + const steps = [ + 'Tap a scrambled letter to drop it into the next blank.', + 'Tap a filled blank to send that letter back to the tray.', + 'Solve all 4 words, then unscramble the circled letters below to reveal the riddle’s answer.', + 'Hint fills in one correct letter, but costs you a star.', + ]; + for (const line of steps) { + const t = this.add.text(x - LEGEND_W / 2 + 10, y, line, bodyStyle).setOrigin(0, 0).setDepth(D.ui); + this.layer.add(t); + y += t.height + 14; + } + + y += 12; + const legendTitle = this.add.text(x, y, 'Color key', { + fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex, + }).setOrigin(0.5, 0).setDepth(D.ui); + this.layer.add(legendTitle); + y += 36; + + const swatchX = x - LEGEND_W / 2 + 22; + const rows = [ + { color: TILE_BG_CORRECT, label: 'Word is correct' }, + { color: TILE_BG_WRONG, label: 'Word is filled in wrong' }, + ]; + for (const row of rows) { + const sw = this.add.graphics().setDepth(D.ui); + sw.fillStyle(row.color, 1); + sw.fillRoundedRect(swatchX - 12, y, 24, 24, 5); + sw.lineStyle(2, PAPER_EDGE, 1); + sw.strokeRoundedRect(swatchX - 12, y, 24, 24, 5); + this.layer.add(sw); + const label = this.add.text(swatchX + 20, y + 12, row.label, { + ...bodyStyle, wordWrap: { width: LEGEND_W - 60 }, + }).setOrigin(0, 0.5).setDepth(D.ui); + this.layer.add(label); + y += 40; + } + } + + buildTitleAndRiddle() { + const cx = PX + PW / 2; + const title = this.add.text(PX + 60, TITLE_Y, `Jumble · Level ${this.level}`, { + fontFamily: 'YummyCupcakes', fontSize: '52px', color: TITLE_BROWN, + }).setOrigin(0, 0.5).setDepth(D.ui); + this.starsText = this.add.text(PX + PW - 60, TITLE_Y, '', { + fontFamily: 'serif', fontSize: '38px', color: '#c98a1f', + }).setOrigin(1, 0.5).setDepth(D.ui); + const riddle = this.add.text(cx, RIDDLE_Y, this.puzzle.riddle, { + fontFamily: 'YummyCupcakes', fontSize: '32px', color: INK, align: 'center', lineSpacing: 8, + wordWrap: { width: PW - 200 }, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add([title, this.starsText, riddle]); + } + + updateStarText() { + const weighted = this.hintsUsed.word * JUMBLE_HINT_WEIGHT.word + this.hintsUsed.bonus * JUMBLE_HINT_WEIGHT.bonus; + if (this.starsText) this.starsText.setText(`★ ${starsForHints(weighted)}/3`); + } + + // ── Tile rows ──────────────────────────────────────────────────────────────── + + centeredXs(n, cx, step) { + if (n === 0) return []; + const span = (n - 1) * step; + const offset = cx - span / 2; + return Array.from({ length: n }, (_, i) => offset + i * step); + } + + answerSegments(answer) { + return answer.trim().split(/\s+/).map((s) => s.replace(/[^A-Za-z]/g, '')); + } + + segmentedXs(segments, cx, step, gap) { + const xsRel = []; + let cursor = 0; + segments.forEach((seg, si) => { + for (let i = 0; i < seg.length; i++) { xsRel.push(cursor); cursor += step; } + if (si < segments.length - 1) cursor += gap; + }); + const span = xsRel[xsRel.length - 1] ?? 0; + const offset = cx - span / 2; + return xsRel.map((v) => v + offset); + } + + // Draws the empty answer-slot boxes at `xs`/`y`. Slots flagged in + // `circledSlots` (a boolean[] parallel to `xs`, word rows only — the + // bonus row never circles) get a permanent hand-drawn red ring around + // that fixed position, visible whether the slot is empty or a tile is + // sitting in it — the circle marks a spot in the answer, not a letter. + drawSlotOutlines(xs, y, circledSlots = null) { + const g = this.add.graphics().setDepth(D.slots); + g.lineStyle(2, PAPER_EDGE, 0.9); + for (const x of xs) { + g.strokeRoundedRect(x - TILE_W / 2, y - TILE_H / 2, TILE_W, TILE_H, 8); + } + this.layer.add(g); + + if (circledSlots) { + const ring = this.add.graphics().setDepth(D.slots + 1); + const ringW = TILE_W + 22, ringH = TILE_H + 22; + xs.forEach((x, i) => { + if (!circledSlots[i]) return; + ring.lineStyle(3, RED_CIRCLE, 0.85); + ring.strokeEllipse(x, y, ringW, ringH); + ring.lineStyle(2, RED_CIRCLE, 0.3); + ring.strokeEllipse(x + 2, y - 2, ringW + 6, ringH + 6); + }); + this.layer.add(ring); + } + } + + makeTileContainer(char) { + const c = this.add.container(0, 0).setDepth(D.tile); + const bg = this.add.graphics(); + c.bgGfx = bg; + const txt = this.add.text(0, 2, char, { + fontFamily: 'YummyCupcakes', fontSize: '40px', color: INK, + }).setOrigin(0.5); + c.add([bg, txt]); + c.setSize(TILE_W, TILE_H); + this.setTileFill(c, TILE_BG); + return c; + } + + // Recolors a tile's background — cream by default, green once its word is + // fully and correctly solved, red once all of a word's slots are filled + // but the arrangement is wrong (cleared back to cream as soon as the + // player pulls a tile back out to fix it). + setTileFill(c, color) { + if (!c?.bgGfx) return; + c.bgGfx.clear(); + c.bgGfx.fillStyle(color, 1); + c.bgGfx.fillRoundedRect(-TILE_W / 2, -TILE_H / 2, TILE_W, TILE_H, 8); + c.bgGfx.lineStyle(2, PAPER_EDGE, 1); + c.bgGfx.strokeRoundedRect(-TILE_W / 2, -TILE_H / 2, TILE_W, TILE_H, 8); + } + + updateWordShade(wordIdx) { + const st = this.wordStates[wordIdx]; + const allFilled = st.slotFill.every((id) => id != null); + const color = st.solved ? TILE_BG_CORRECT : allFilled ? TILE_BG_WRONG : TILE_BG; + st.slotFill.forEach((id) => { if (id != null) this.setTileFill(st.containers.get(id), color); }); + st.trayOrder.forEach((id) => this.setTileFill(st.containers.get(id), TILE_BG)); + } + + placeContainerAt(state, rowKind, id, x, y) { + let c = state.containers.get(id); + if (!c) { + const def = state.tileDefs[id]; + c = this.makeTileContainer(def.char); + c.setInteractive({ useHandCursor: true }); + c.on('pointerup', () => this.onTileTap(rowKind, state._wordIdx, id)); + c.setPosition(x, y); + this.layer.add(c); + state.containers.set(id, c); + return; + } + if (c.x === x && c.y === y) return; + this.tweens.add({ targets: c, x, y, duration: 150, ease: 'Quad.easeOut' }); + } + + buildWordRows() { + this.rowLayout = []; + const cx = PX + PW / 2; + for (let i = 0; i < 4; i++) { + const y = WORD_ROWS_TOP + i * WORD_ROW_H; + const st = this.wordStates[i]; + const slotXs = this.centeredXs(st.solution.length, cx, STEP); + this.rowLayout.push({ y, cx, slotXs }); + this.drawSlotOutlines(slotXs, y + SLOT_DY, st.circledSlots); + this.renderWordRow(i); + } + } + + renderWordRow(wordIdx) { + const st = this.wordStates[wordIdx]; + const { y, cx, slotXs } = this.rowLayout[wordIdx]; + const slotY = y + SLOT_DY, trayY = y + TRAY_DY; + st.slotFill.forEach((id, i) => { if (id != null) this.placeContainerAt(st, 'word', id, slotXs[i], slotY); }); + const trayXs = this.centeredXs(st.trayOrder.length, cx, STEP); + st.trayOrder.forEach((id, idx) => this.placeContainerAt(st, 'word', id, trayXs[idx], trayY)); + } + + buildBonusRow() { + const cx = PX + PW / 2; + const y = BONUS_ROW_Y; + const segments = this.answerSegments(this.puzzle.bonusAnswer); + const slotXs = this.segmentedXs(segments, cx, STEP, SEGMENT_GAP); + this.bonusRowLayout = { y, cx, slotXs }; + this.drawSlotOutlines(slotXs, y + SLOT_DY); + const label = this.add.text(cx, y + SLOT_DY - 54, 'Bonus answer', { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add(label); + this.renderBonusRow(); + } + + renderBonusRow() { + const st = this.bonusState; + const { y, cx, slotXs } = this.bonusRowLayout; + const slotY = y + SLOT_DY, trayY = y + TRAY_DY; + st.slotFill.forEach((id, i) => { if (id != null) this.placeContainerAt(st, 'bonus', id, slotXs[i], slotY); }); + const trayXs = this.centeredXs(st.trayOrder.length, cx, STEP); + st.trayOrder.forEach((id, idx) => this.placeContainerAt(st, 'bonus', id, trayXs[idx], trayY)); + } + + buildHudButtons() { + const cx = PX + PW / 2; + const hint = new Button(this, cx - 260, BTN_Y, 'Hint', () => this.useHint(), + { width: 220, height: 56, fontSize: 22, variant: 'ghost' }); + const levels = new Button(this, cx, BTN_Y, 'Levels', () => this.showLevelSelect(), + { width: 220, height: 56, fontSize: 22, variant: 'ghost' }); + const leave = new Button(this, cx + 260, BTN_Y, 'Leave', () => this.scene.start('GameMenu'), + { width: 220, height: 56, fontSize: 22, variant: 'ghost' }); + this.layer.add([hint, levels, leave]); + } + + // ── Interaction ────────────────────────────────────────────────────────────── + + onTileTap(rowKind, wordIdx, id) { + if (this.view !== 'play' || this.overlayUp) return; + const state = rowKind === 'word' ? this.wordStates[wordIdx] : this.bonusState; + if (state.solved) return; + if (state.slotFill.includes(id)) this.returnTile(rowKind, wordIdx, id); + else this.placeTile(rowKind, wordIdx, id); + } + + placeTile(rowKind, wordIdx, id) { + const state = rowKind === 'word' ? this.wordStates[wordIdx] : this.bonusState; + const slot = state.slotFill.indexOf(null); + if (slot === -1) return; + state.slotFill[slot] = id; + state.trayOrder = state.trayOrder.filter((x) => x !== id); + playSound(this, SFX.UI_PLACE); + if (rowKind === 'word') { this.renderWordRow(wordIdx); this.afterWordChange(wordIdx); } + else { this.renderBonusRow(); this.afterBonusChange(); } + } + + returnTile(rowKind, wordIdx, id) { + const state = rowKind === 'word' ? this.wordStates[wordIdx] : this.bonusState; + const slot = state.slotFill.indexOf(id); + if (slot === -1) return; + state.slotFill[slot] = null; + state.trayOrder.push(id); + playSound(this, SFX.UI_PICK); + if (rowKind === 'word') { this.renderWordRow(wordIdx); this.afterWordChange(wordIdx); } + else this.renderBonusRow(); + } + + afterWordChange(wordIdx) { + const st = this.wordStates[wordIdx]; + const arrangement = st.slotFill.map((id) => (id == null ? null : { char: st.tileDefs[id].char })); + if (!st.solved && checkWordGuess(arrangement, st.solution)) this.onWordSolved(wordIdx); + this.updateWordShade(wordIdx); + } + + afterBonusChange() { + const st = this.bonusState; + const arrangement = st.slotFill.map((id) => (id == null ? null : { char: st.tileDefs[id].char })); + if (!st.solved && checkBonusGuess(arrangement, st.answer)) this.onBonusSolved(); + } + + onWordSolved(wordIdx) { + const st = this.wordStates[wordIdx]; + st.solved = true; + playSound(this, SFX.UI_CHIME); + st.containers.forEach((c) => c.disableInteractive()); + const ids = this.bonusSubsetsByWord[wordIdx]; + this.bonusState.trayOrder.push(...ids); + this.renderBonusRow(); + this.checkPuzzleComplete(); + } + + onBonusSolved() { + this.bonusState.solved = true; + this.bonusState.containers.forEach((c) => c.disableInteractive()); + this.checkPuzzleComplete(); + } + + checkPuzzleComplete() { + if (isPuzzleComplete(this.wordStates.map((s) => s.solved), this.bonusState.solved)) this.onPuzzleComplete(); + } + + // ── Hint ────────────────────────────────────────────────────────────────────── + + useHint() { + if (this.view !== 'play' || this.overlayUp) return; + const idx = this.wordStates.findIndex((s) => !s.solved); + if (idx === -1) this.hintBonus(); + else this.hintWord(idx); + } + + hintWord(wordIdx) { + const st = this.wordStates[wordIdx]; + const arrangement = st.slotFill.map((id) => (id == null ? null : { char: st.tileDefs[id].char })); + const slotIdx = applyWordHint(arrangement, st.solution); + if (slotIdx === -1) return; + const existing = st.slotFill[slotIdx]; + if (existing != null) { st.slotFill[slotIdx] = null; st.trayOrder.push(existing); } + const ch = st.solution[slotIdx]; + const foundId = st.trayOrder.find((id) => st.tileDefs[id].char === ch); + if (foundId == null) return; + st.trayOrder = st.trayOrder.filter((x) => x !== foundId); + st.slotFill[slotIdx] = foundId; + this.hintsUsed.word++; + playSound(this, SFX.PENCIL_WRITE); + this.renderWordRow(wordIdx); + this.updateStarText(); + this.afterWordChange(wordIdx); + } + + hintBonus() { + const st = this.bonusState; + const target = st.answer.replace(/[^A-Z]/g, ''); + const arrangement = st.slotFill.map((id) => (id == null ? null : { char: st.tileDefs[id].char })); + const slotIdx = applyWordHint(arrangement, target); + if (slotIdx === -1) return; + const existing = st.slotFill[slotIdx]; + if (existing != null) { st.slotFill[slotIdx] = null; st.trayOrder.push(existing); } + const ch = target[slotIdx]; + const foundId = st.trayOrder.find((id) => st.tileDefs[id].char === ch); + if (foundId == null) return; + st.trayOrder = st.trayOrder.filter((x) => x !== foundId); + st.slotFill[slotIdx] = foundId; + this.hintsUsed.bonus++; + playSound(this, SFX.PENCIL_WRITE); + this.renderBonusRow(); + this.updateStarText(); + this.afterBonusChange(); + } + + // ── Complete flow ──────────────────────────────────────────────────────────── + + onPuzzleComplete() { + this.overlayUp = true; + const weighted = this.hintsUsed.word * JUMBLE_HINT_WEIGHT.word + this.hintsUsed.bonus * JUMBLE_HINT_WEIGHT.bonus; + const stars = starsForHints(weighted); + this.saveStars(this.level, stars); + if (this.level > this.levelsCompleted) this.levelsCompleted = this.level; + + playSound(this, SFX.VICTORY_SHORT); + api.post('/puzzles/jumble/complete', { level: this.level }) + .then((res) => { if (res?.levelsCompleted != null) this.levelsCompleted = Math.max(this.levelsCompleted, res.levelsCompleted); }) + .catch(() => { /* best effort */ }); + api.post('/history/single-player', { + slug: 'jumble', score: stars * 100, opponentScores: [], result: 'win', + }).catch(() => { /* best effort */ }); + + const cx = GAME_WIDTH / 2; + const cy = GAME_HEIGHT / 2; + const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setDepth(D.overlay).setInteractive(); + this.layer.add(dim); + + const panel = this.add.graphics().setDepth(D.overlay); + panel.fillStyle(COLORS.panel, 0.98); + panel.fillRoundedRect(cx - 440, cy - 270, 880, 540, 20); + panel.lineStyle(3, COLORS.accent, 1); + panel.strokeRoundedRect(cx - 440, cy - 270, 880, 540, 20); + this.layer.add(panel); + + const title = this.add.text(cx, cy - 200, 'Solved!', { + fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + const starRow = this.add.text(cx, cy - 126, '★★★'.slice(0, stars) + '☆☆☆'.slice(0, 3 - stars), { + fontFamily: 'serif', fontSize: '52px', color: '#ffd54a', + }).setOrigin(0.5).setDepth(D.overlayUI); + const riddleTxt = this.add.text(cx, cy - 44, this.puzzle.riddle, { + fontFamily: 'YummyCupcakes', fontSize: '30px', color: COLORS.textHex, align: 'center', + lineSpacing: 6, wordWrap: { width: 780 }, + }).setOrigin(0.5).setDepth(D.overlayUI); + const answerTxt = this.add.text(cx, cy + 60, this.puzzle.bonusAnswer, { + fontFamily: 'YummyCupcakes', fontSize: '48px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + this.layer.add([title, starRow, riddleTxt, answerTxt]); + + const hasNext = this.level < this.bank.length; + const btns = []; + if (hasNext) { + btns.push(new Button(this, cx, cy + 148, `Next Level (${this.level + 1})`, () => this.playLevel(this.level + 1), + { width: 360, height: 60, fontSize: 26 }).setDepth(D.overlayUI)); + } else { + btns.push(this.add.text(cx, cy + 138, 'You solved every puzzle. Bravo!', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI)); + } + const levels = new Button(this, cx, cy + 224, 'Levels', () => this.showLevelSelect(), + { width: 220, height: 54, fontSize: 22, variant: 'ghost' }).setDepth(D.overlayUI); + btns.push(levels); + this.layer.add(btns); + } +} diff --git a/src/games/jumble/JumbleLogic.js b/src/games/jumble/JumbleLogic.js new file mode 100644 index 0000000..cfa04a9 --- /dev/null +++ b/src/games/jumble/JumbleLogic.js @@ -0,0 +1,85 @@ +// Jumble — pure puzzle engine (no Phaser). Shared by the Phaser scene, the +// offline bank generator (tools/genJumble.js) and the verifier. +// +// A puzzle has 4 base words, each scrambled into tiles. Some tile letters are +// "circled" (per word). Solving all 4 words collects their circled letters +// (in solved word order) into a bonus pool; unscrambling that pool reveals +// the answer to a riddle shown on the puzzle. + +// ── Seeded RNG (mulberry32) ────────────────────────────────────────────────── +export function makeRng(seed) { + let a = seed >>> 0; + return () => { + a |= 0; a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// Fisher-Yates, in place. Returns the (mutated) array. +export function shuffle(arr, rng) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +// chars: string | string[] -> Map +export function letterMultiset(chars) { + const m = new Map(); + for (const ch of chars) m.set(ch, (m.get(ch) ?? 0) + 1); + return m; +} + +// True when every letter in `sub` appears in `sup` at least as many times. +export function isSupersetMultiset(sup, sub) { + for (const [ch, count] of sub) { + if ((sup.get(ch) ?? 0) < count) return false; + } + return true; +} + +// ── Gameplay (browser-side, but pure — no Phaser deps) ─────────────────────── + +// arrangement: array (one entry per slot) of tile-like {char}|null. +export function checkWordGuess(arrangement, solution) { + if (arrangement.length !== solution.length) return false; + if (arrangement.some((t) => t == null)) return false; + return arrangement.map((t) => t.char).join('') === solution; +} + +// `answer` may contain a space (multi-word answer) — arrangement only covers +// the letter slots (space slots are static separators, not interactive), so +// compare against the answer with spaces stripped. +export function checkBonusGuess(arrangement, answer) { + const target = answer.replace(/[^A-Z]/g, ''); + if (arrangement.length !== target.length) return false; + if (arrangement.some((t) => t == null)) return false; + return arrangement.map((t) => t.char).join('') === target; +} + +// Returns the index of the first slot that doesn't yet hold the correct +// letter, or -1 if `arrangement` already fully matches `solution`. +export function applyWordHint(arrangement, solution) { + for (let i = 0; i < solution.length; i++) { + if (arrangement[i]?.char !== solution[i]) return i; + } + return -1; +} + +// Bonus-round hints reveal more of the puzzle at once (a full riddle letter), +// so they cost double toward the star penalty. +export const JUMBLE_HINT_WEIGHT = { word: 1, bonus: 2 }; + +export function starsForHints(weightedHintsUsed) { + if (weightedHintsUsed === 0) return 3; + if (weightedHintsUsed <= 2) return 2; + return 1; +} + +// wordStates: bool[4] (one per base word), bonusSolved: bool. +export function isPuzzleComplete(wordStates, bonusSolved) { + return wordStates.every(Boolean) && bonusSolved; +} diff --git a/src/games/paigow/PaiGowPokerGame.js b/src/games/paigow/PaiGowPokerGame.js index cd364a7..6e35e37 100644 --- a/src/games/paigow/PaiGowPokerGame.js +++ b/src/games/paigow/PaiGowPokerGame.js @@ -142,6 +142,8 @@ export default class PaiGowPokerGame extends Phaser.Scene { const g = this.add.graphics().setDepth(D.table + 1); for (let seat = 0; seat < SEAT_POS.length; seat++) { + const active = seat === 0 ? true : !!(this.opponents[seat - 1] ?? null); + if (!active) continue; const pos = SEAT_POS[seat]; g.lineStyle(2, 0xffffff, 0.5); g.strokeRoundedRect(pos.x - CARD_W / 2 - 4, pos.y - CARD_H / 2 - 4, CARD_W + 8, CARD_H + 8, CARD_R + 2); diff --git a/src/main.js b/src/main.js index fab7894..f0967d0 100644 --- a/src/main.js +++ b/src/main.js @@ -83,6 +83,7 @@ import BookworkGame from './games/bookwork/BookworkGame.js'; import PaiGowPokerGame from './games/paigow/PaiGowPokerGame.js'; import SpireClimbGame from './games/spireclimb/SpireClimbGame.js'; import AzulGame from './games/azul/AzulGame.js'; +import JumbleGame from './games/jumble/JumbleGame.js'; const config = { type: Phaser.AUTO, @@ -179,6 +180,7 @@ const config = { PaiGowPokerGame, SpireClimbGame, AzulGame, + JumbleGame, ], }; diff --git a/src/scenes/GameRoomScene.js b/src/scenes/GameRoomScene.js index bb96153..8e439b5 100644 --- a/src/scenes/GameRoomScene.js +++ b/src/scenes/GameRoomScene.js @@ -22,7 +22,7 @@ export default class GameRoomScene extends Phaser.Scene { } create() { - const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame', jewelquest: 'JewelQuestGame', zuma: 'ZumaGame', bejeweled: 'BejeweledGame', minimotorways: 'MiniMotorwaysGame', slots: 'SlotsGame', cribbage: 'CribbageGame', canasta: 'CanastaGame', dotlink: 'DotLinkGame', '2048': '2048Game', rummikub: 'RummikubGame', ginrummy: 'GinRummyGame', risk: 'RiskGame', geniussquare: 'GeniusSquareGame', katamino: 'KataminoGame', bookwork: 'BookworkGame', paigow: 'PaiGowPokerGame', spireclimb: 'SpireClimbGame', azul: 'AzulGame' }; + const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame', jewelquest: 'JewelQuestGame', zuma: 'ZumaGame', bejeweled: 'BejeweledGame', minimotorways: 'MiniMotorwaysGame', slots: 'SlotsGame', cribbage: 'CribbageGame', canasta: 'CanastaGame', dotlink: 'DotLinkGame', '2048': '2048Game', rummikub: 'RummikubGame', ginrummy: 'GinRummyGame', risk: 'RiskGame', geniussquare: 'GeniusSquareGame', katamino: 'KataminoGame', bookwork: 'BookworkGame', paigow: 'PaiGowPokerGame', spireclimb: 'SpireClimbGame', azul: 'AzulGame', jumble: 'JumbleGame' }; if (slugDispatch[this.game.slug]) { this.scene.start(slugDispatch[this.game.slug], { game: this.game, diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index ba319db..45745fa 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -73,6 +73,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.json('katamino', 'data/katamino.json'); this.load.json('bookwork', 'data/bookwork.json'); this.load.json('spireclimb-artwork', 'data/spireclimb-artwork.json'); + this.load.json('jumble', 'data/jumble.json'); this.load.image('spireclimb-act1', 'assets/images/spireclimb-act1.png'); this.load.image('spireclimb-act2', 'assets/images/spireclimb-act2.png'); this.load.image('spireclimb-act3', 'assets/images/spireclimb-act3.png'); diff --git a/tools/genJumble.js b/tools/genJumble.js new file mode 100644 index 0000000..13acaae --- /dev/null +++ b/tools/genJumble.js @@ -0,0 +1,217 @@ +// Offline generator for Jumble puzzles. +// +// Takes curated riddle/answer pairs (data/jumble-riddles.json), partitions +// each answer's letters across 4 base words, finds a real common word to +// carry each partition (as a superset of its letters), marks which letter +// positions are "circled", and scrambles everything. Writes ordered levels +// to data/jumble.json. +// +// Usage: +// node tools/genJumble.js [seed] [outFile] +// +// Deterministic: same seed -> same bank (riddle bank + common word list held +// fixed). + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { makeRng, shuffle, letterMultiset, isSupersetMultiset } from '../src/games/jumble/JumbleLogic.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const RIDDLES_FILE = path.join(__dirname, '../data/jumble-riddles.json'); +const WORDS_FILE = path.join(__dirname, '../data/wordlists/common.txt'); +const OUT_FILE = process.argv[3] + ? path.resolve(process.argv[3]) + : path.join(__dirname, '../data/jumble.json'); +const SEED = process.argv[2] ? Number(process.argv[2]) >>> 0 : 0x4a4d424c; // 'JMBL' + +const MAX_ATTEMPTS = 60; +const MIN_ANSWER_LEN = 6; +const MAX_ANSWER_LEN = 14; +const WORD_LEN_MIN = 4; +const WORD_LEN_MAX = 8; + +// Difficulty curve: word length + bonus-answer length both grow with tier. +const TIERS = [ + { count: 12, wordLenRange: [4, 5], bonusLenRange: [6, 8] }, + { count: 12, wordLenRange: [4, 6], bonusLenRange: [7, 9] }, + { count: 12, wordLenRange: [5, 6], bonusLenRange: [8, 10] }, + { count: 12, wordLenRange: [5, 7], bonusLenRange: [9, 11] }, + { count: 10, wordLenRange: [6, 8], bonusLenRange: [10, 14] }, +]; + +function multisetsEqual(a, b) { + if (a.size !== b.size) return false; + for (const [ch, count] of a) if (b.get(ch) !== count) return false; + return true; +} + +function candidateLengths(groupSize, tier) { + const lo = Math.max(WORD_LEN_MIN, groupSize); + const hi = WORD_LEN_MAX; + const preferred = []; + for (let l = Math.max(lo, tier.wordLenRange[0]); l <= Math.min(hi, tier.wordLenRange[1]); l++) preferred.push(l); + const rest = []; + for (let l = lo; l <= hi; l++) if (!preferred.includes(l)) rest.push(l); + return [...preferred, ...rest]; +} + +function findWordForGroup(groupMultiset, groupSize, tier, wordsByLength, wordMultisets, usedWords, answerStripped, rng) { + for (const len of candidateLengths(groupSize, tier)) { + const bucket = wordsByLength.get(len); + if (!bucket || !bucket.length) continue; + const matches = []; + for (const w of bucket) { + if (usedWords.has(w) || w === answerStripped) continue; + if (isSupersetMultiset(wordMultisets.get(w), groupMultiset)) matches.push(w); + } + if (matches.length) return matches[Math.floor(rng() * matches.length)]; + } + return null; +} + +function pickCircledPositions(word, groupMultiset, rng) { + const indices = new Set(); + for (const [ch, count] of groupMultiset) { + const positions = []; + for (let i = 0; i < word.length; i++) if (word[i] === ch) positions.push(i); + shuffle(positions, rng); + for (let i = 0; i < count; i++) indices.add(positions[i]); + } + return indices; +} + +// Builds scrambled tile objects for `word`. Rerolls (up to 20 tries) if the +// shuffle lands on `identityTarget` (default: the word itself) so the display +// never shows an already-solved order. Tiles carry only their letter — which +// answer-slot positions are "circled" is tracked separately (see +// circledSlots below), since the circle marks a fixed spot in the SOLVED +// word, not a specific scrambled tile. +function buildScrambledTiles(word, rng, identityTarget = word) { + const tiles = word.split('').map((ch) => ({ char: ch })); + let tries = 0; + do { + shuffle(tiles, rng); + tries++; + } while (tiles.map((t) => t.char).join('') === identityTarget && tries < 20); + return tiles; +} + +function buildPuzzle(riddle, tier, wordsByLength, wordMultisets, rng) { + const answerStripped = riddle.answer.replace(/[^A-Za-z]/g, '').toUpperCase(); + const n = answerStripped.length; + + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + const shuffled = shuffle(answerStripped.split(''), rng); + const base = Math.floor(n / 4); + const rem = n % 4; + const sizes = [base, base, base, base]; + const bumpOrder = shuffle([0, 1, 2, 3], rng); + for (let i = 0; i < rem; i++) sizes[bumpOrder[i]]++; + + const groups = []; + let p = 0; + for (let i = 0; i < 4; i++) { groups.push(shuffled.slice(p, p + sizes[i])); p += sizes[i]; } + + const usedWords = new Set(); + const words = []; + let ok = true; + for (const group of groups) { + const groupMultiset = letterMultiset(group); + const word = findWordForGroup(groupMultiset, group.length, tier, wordsByLength, wordMultisets, usedWords, answerStripped, rng); + if (!word) { ok = false; break; } + usedWords.add(word); + const circledIndices = pickCircledPositions(word, groupMultiset, rng); + const tiles = buildScrambledTiles(word, rng); + const circledSlots = Array.from({ length: word.length }, (_, i) => circledIndices.has(i)); + words.push({ solution: word, scrambled: tiles, circledSlots }); + } + if (!ok) continue; + + // Bonus pool: each word's circled letters, in that word's solved + // (left-to-right) order, concatenated in word order. Read directly off + // the solution string at the circled slot positions — no dependency on + // how the tray tiles ended up scrambled. + const bonusLetters = []; + for (const w of words) { + for (let i = 0; i < w.solution.length; i++) { + if (w.circledSlots[i]) bonusLetters.push(w.solution[i]); + } + } + const bonusMultiset = letterMultiset(bonusLetters); + const answerMultiset = letterMultiset(answerStripped.split('')); + if (!multisetsEqual(bonusMultiset, answerMultiset)) continue; // defensive; should never trigger + + const bonusScrambled = buildScrambledTiles(bonusLetters.join(''), rng, answerStripped); + + return { + riddleId: riddle.id, + riddle: riddle.setup, + words: words.map((w) => ({ solution: w.solution, scrambled: w.scrambled, circledSlots: w.circledSlots })), + bonusScrambled, + bonusAnswer: riddle.answer.toUpperCase(), + }; + } + console.warn(`[jumble] could not build puzzle for riddle "${riddle.id}", skipping`); + return null; +} + +// ── Load inputs ────────────────────────────────────────────────────────────── +const riddlesRaw = JSON.parse(fs.readFileSync(RIDDLES_FILE, 'utf8')); +const allRiddles = riddlesRaw.riddles ?? []; + +const wordList = fs.readFileSync(WORDS_FILE, 'utf8').split('\n').map((w) => w.trim().toUpperCase()).filter(Boolean); +const wordsByLength = new Map(); +const wordMultisets = new Map(); +for (const w of wordList) { + if (w.length < WORD_LEN_MIN || w.length > WORD_LEN_MAX) continue; + if (!/^[A-Z]+$/.test(w)) continue; + if (!wordsByLength.has(w.length)) wordsByLength.set(w.length, []); + wordsByLength.get(w.length).push(w); + wordMultisets.set(w, letterMultiset(w.split(''))); +} + +// ── Assign riddles to tiers ────────────────────────────────────────────────── +// Sort valid-length riddles ascending by answer length, then slice +// consecutively into each tier's slot count — since tiers themselves are +// ordered by increasing difficulty/length, this naturally buckets shorter +// answers into earlier tiers without needing per-tier backfill logic. +const validRiddles = []; +for (const r of allRiddles) { + const len = r.answer.replace(/[^A-Za-z]/g, '').length; + if (len < MIN_ANSWER_LEN || len > MAX_ANSWER_LEN) { + console.warn(`[jumble] skip "${r.id}": answer length ${len} out of ${MIN_ANSWER_LEN}-${MAX_ANSWER_LEN} range`); + continue; + } + validRiddles.push(r); +} +validRiddles.sort((a, b) => a.answer.replace(/[^A-Za-z]/g, '').length - b.answer.replace(/[^A-Za-z]/g, '').length); + +const totalSlots = TIERS.reduce((s, t) => s + t.count, 0); +if (validRiddles.length < totalSlots) { + console.warn(`[jumble] only ${validRiddles.length} valid riddles for ${totalSlots} slots — bank will be shorter than planned`); +} + +const rng = makeRng(SEED); +console.log(`[jumble] generating with seed 0x${SEED.toString(16)}…`); + +const puzzles = []; +let riddleIdx = 0; +for (const tier of TIERS) { + for (let i = 0; i < tier.count && riddleIdx < validRiddles.length; i++, riddleIdx++) { + const riddle = validRiddles[riddleIdx]; + const puzzle = buildPuzzle(riddle, tier, wordsByLength, wordMultisets, rng); + if (!puzzle) continue; + puzzles.push({ level: puzzles.length + 1, ...puzzle }); + } +} + +const payload = { + generatedAt: new Date().toISOString(), + seed: SEED, + count: puzzles.length, + puzzles, +}; +fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true }); +fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2)); +console.log(`[jumble] wrote ${puzzles.length} levels -> ${OUT_FILE}`); diff --git a/tools/verifyJumble.js b/tools/verifyJumble.js new file mode 100644 index 0000000..d33917d --- /dev/null +++ b/tools/verifyJumble.js @@ -0,0 +1,125 @@ +// Verifier for Jumble (Node only — no browser). +// +// Asserts every level in data/jumble.json is internally consistent: base +// words are real dictionary words whose scrambles are true rearrangements, +// circled letters are genuine subsets of their word, and — the critical +// invariant — the circled letters collected across all 4 words exactly +// anagram to the bonus answer. +// +// Usage: node tools/verifyJumble.js + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const FILE = path.join(__dirname, '../data/jumble.json'); +const WORDS_FILE = path.join(__dirname, '../data/wordlists/common.txt'); + +let failures = 0; +const fail = (msg) => { console.error(` ✗ ${msg}`); failures++; }; + +const dict = new Set( + fs.readFileSync(WORDS_FILE, 'utf8').split('\n').map((w) => w.trim().toUpperCase()).filter(Boolean), +); + +function sortedChars(str) { + return str.split('').sort().join(''); +} + +// ── Bank checks ────────────────────────────────────────────────────────────── +const raw = JSON.parse(fs.readFileSync(FILE, 'utf8')); +const puzzles = raw.puzzles ?? []; +console.log(`[verify] ${FILE}`); +console.log(`[verify] ${puzzles.length} levels (seed 0x${(raw.seed >>> 0).toString(16)})`); + +if (puzzles.length < 40) fail(`expected at least 40 levels, found ${puzzles.length}`); + +const seenRiddleIds = new Set(); +const seenWordSets = new Set(); +let prevLevel = 0; + +for (const pz of puzzles) { + const tag = `L${pz.level}`; + + if (pz.level !== prevLevel + 1) fail(`${tag}: level numbering gap (expected ${prevLevel + 1})`); + prevLevel = pz.level; + + if (seenRiddleIds.has(pz.riddleId)) fail(`${tag}: duplicate riddleId "${pz.riddleId}"`); + seenRiddleIds.add(pz.riddleId); + + if (!pz.words || pz.words.length !== 4) { + fail(`${tag}: expected 4 base words, found ${pz.words?.length}`); + continue; + } + + const wordSetKey = pz.words.map((w) => w.solution).slice().sort().join('|'); + if (seenWordSets.has(wordSetKey)) fail(`${tag}: duplicate 4-word set across levels`); + seenWordSets.add(wordSetKey); + + const perWordCircledChars = []; + for (const w of pz.words) { + if (!dict.has(w.solution)) fail(`${tag}: "${w.solution}" is not a real dictionary word`); + + // Scramble must be a rearrangement of the solution (same multiset). + const scrambledStr = w.scrambled.map((t) => t.char).join(''); + if (sortedChars(scrambledStr) !== sortedChars(w.solution)) { + fail(`${tag}: word "${w.solution}" scramble letters don't match solution letters`); + } + if (scrambledStr === w.solution) { + fail(`${tag}: word "${w.solution}" scramble is identity order (not shuffled)`); + } + + // circledSlots marks fixed positions in the SOLVED word (not scrambled + // tiles) — read the circled letters directly off the solution string in + // left-to-right order, which is exactly the order the bonus round + // collects them in. + if (!w.circledSlots || w.circledSlots.length !== w.solution.length) { + fail(`${tag}: word "${w.solution}" circledSlots length doesn't match solution length`); + } + const circledChars = w.solution.split('').filter((_, i) => w.circledSlots?.[i]); + if (circledChars.length === 0) { + fail(`${tag}: word "${w.solution}" has no circled slots`); + } + perWordCircledChars.push(circledChars); + } + + // Critical invariant: the full circled-letter multiset across all 4 words + // (in word order) must exactly equal the bonus answer's letter multiset. + const collected = perWordCircledChars.flat(); + const bonusAnswerStripped = (pz.bonusAnswer ?? '').replace(/[^A-Z]/g, ''); + if (sortedChars(collected.join('')) !== sortedChars(bonusAnswerStripped)) { + fail(`${tag}: collected circled letters "${sortedChars(collected.join(''))}" != bonus answer letters "${sortedChars(bonusAnswerStripped)}"`); + } + + // Bonus scramble must be a rearrangement of the bonus answer, and shuffled. + if (!pz.bonusScrambled || !pz.bonusScrambled.length) { + fail(`${tag}: missing bonusScrambled`); + } else { + const bonusScrambledStr = pz.bonusScrambled.map((t) => t.char).join(''); + if (sortedChars(bonusScrambledStr) !== sortedChars(bonusAnswerStripped)) { + fail(`${tag}: bonusScrambled letters don't match bonusAnswer letters`); + } + if (bonusScrambledStr === bonusAnswerStripped) { + fail(`${tag}: bonusScrambled is identity order (not shuffled)`); + } + } +} + +// ── Per-tier stats ─────────────────────────────────────────────────────────── +const TIER_COUNTS = [12, 12, 12, 12, 10]; +let idx = 0; +TIER_COUNTS.forEach((count, tierIdx) => { + const slice = puzzles.slice(idx, idx + count); + idx += count; + if (!slice.length) return; + const avgWordLen = slice.flatMap((p) => p.words.map((w) => w.solution.length)).reduce((a, b) => a + b, 0) / (slice.length * 4); + const avgBonusLen = slice.reduce((a, p) => a + (p.bonusAnswer ?? '').replace(/[^A-Z]/g, '').length, 0) / slice.length; + console.log(`[verify] tier ${tierIdx + 1}: ${slice.length} levels, avg word len ${avgWordLen.toFixed(1)}, avg bonus len ${avgBonusLen.toFixed(1)}`); +}); + +if (failures > 0) { + console.error(`\n[verify] FAILED with ${failures} problem(s).`); + process.exit(1); +} +console.log('\n[verify] All Jumble checks passed.');