Skip to content

Commit fb71cb4

Browse files
authored
Merge pull request #56 from healeycodes/queuedle
Add queuedle post
2 parents 484ccd0 + 2f9558b commit fb71cb4

File tree

5 files changed

+137
-0
lines changed

5 files changed

+137
-0
lines changed

data/projects.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export default [
2626
"A chess engine with alpha-beta pruning, piece-square tables, and move ordering.",
2727
to: "/building-my-own-chess-engine",
2828
},
29+
{
30+
name: "queuedle",
31+
link: "https://queuedle.com",
32+
desc: "A daily word sliding puzzle game inspired by Wordle and Scrabble.",
33+
to: "/how-i-made-queuedle",
34+
},
2935
{
3036
name: "jar",
3137
link: "https://github.com/healeycodes/jar",

posts/how-i-made-queuedle.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
title: "How I Made Queuedle"
3+
date: "2025-05-28"
4+
tags: ["javascript"]
5+
description: "A daily word sliding puzzle game inspired by Wordle and Scrabble."
6+
---
7+
8+
[Queuedle](https://queuedle.com) is a daily word sliding puzzle game inspired by Wordle and Scrabble. It combines the positional gameplay of Scrabble with the daily puzzle and discovery elements of Wordle. Everyone plays the same board and it can be played quickly or slowly.
9+
10+
Players pull from a letter queue and push onto the board, and words are automatically highlighted. Your score is the number of letters used in words. Letters can count twice so `MOON` is actually two words: `MOO` and `MOON`.
11+
12+
During playtesting, players who were into things like NYT puzzles tended to plan several moves ahead. Because part of the letter queue is hidden, it's not a game of perfect information, but you can still strategize many moves ahead. A big vocabulary helps, as does knowing the Scrabble dictionary (but two-letter words don’t count).
13+
14+
![A game of Queuedle being played.](overview.mp4)
15+
16+
## Daily Puzzles
17+
18+
Famously, Wordle had the next day's puzzle embedded in the source code. You could look it up if you wanted to spoil yourself. However, for Queuedle, there's a bit more setup required. I need to generate a board and a letter queue.
19+
20+
I didn't attempt to do this manually as it's far too much work. I don't think there's much of a payoff for the user if I design interesting starting states because there are too many starting _moves_ – and each move jumbles the board.
21+
22+
In order to generate a board and a letter queue with the same *vibe* as Scrabble, I use the Scrabble letter distribution when I randomly pick letters (Qs are rare, Es are everywhere).
23+
24+
```tsx
25+
// Scrabble tile frequencies
26+
const SCRABBLE_TILES = [
27+
{ letter: 'a', count: 9 },
28+
// ..
29+
{ letter: 'e', count: 12 },
30+
// ..
31+
{ letter: 'z', count: 1 }
32+
];
33+
```
34+
35+
To have everyone around the world play the same board at the same time, I use a seeded pseudorandom number generator. Given a starting state, the same endless list of numbers is generated.
36+
37+
Everyone who plays Queuedle does so with a shared seed: the current date.
38+
39+
```tsx
40+
// Get the current local day as a seed
41+
export const getCurrentDaySeed = (): number => {
42+
const now = new Date();
43+
const localDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
44+
return localDate.getTime();
45+
};
46+
```
47+
48+
Each date becomes a unique seed. The [Lehmer random number generator](https://en.wikipedia.org/wiki/Lehmer_random_number_generator) algorithm which consumes this is ~1 line of code. Giving us a new Queuedle game every single day.
49+
50+
```tsx
51+
// Generate a random number between 0 and 1
52+
next(): number {
53+
this.seed = (this.seed * 16807) % 2147483647;
54+
return (this.seed - 1) / 2147483646;
55+
}
56+
57+
// Generate a random integer between min (inclusive) and max (inclusive)
58+
nextInt(min: number, max: number): number {
59+
return Math.floor(this.next() * (max - min + 1)) + min;
60+
}
61+
```
62+
63+
The first time I set up the game generation logic, I ran into a problem. The starting board sometimes contained multiple outlined words! In retrospective, it's unsurprising that a random 5x5 board of letters would contain at least one three letter word from the 178691 words in the Scrabble tournament word list.
64+
65+
I decided to throw the user's CPU at this problem. I simply generate boards, deterministically, until the starting board contains zero words.
66+
67+
```tsx
68+
// Generate a valid game state with no words on the board
69+
export const generateValidGameState = (baseSeed: number) => {
70+
let attempt = 0;
71+
while (true) {
72+
73+
// Deterministically alter the seed
74+
const seed = baseSeed + attempt;
75+
const { grid, queue } = generateGameState(seed);
76+
const highlights = findWords(grid);
77+
if (highlights.length === 0) {
78+
return { grid, queue, attempt };
79+
}
80+
attempt++;
81+
}
82+
};
83+
```
84+
85+
This sometimes requires ~50 board generation cycles. With my unoptimised JavaScript code, this only takes a handle of milliseconds.
86+
87+
## Game UI
88+
89+
I like the understated design of Wordle and didn't spend too long on Queuedle's interface. The tiles and arrows have a slight shadow and respond to actions with brief animations.
90+
91+
![Zoomed in view of shadows and tile animations.](shadows.mp4)
92+
93+
I used [Motion](https://motion.dev) (my first time doing so) and my use case seems to live within the happy path of this library. I only needed to pass a few lines of configuration.
94+
95+
```tsx
96+
export default function Tile({ letter, isNew }: TileProps) {
97+
if (isNew) {
98+
return (
99+
<motion.div
100+
initial={{ scale: 0.7, opacity: 0 }}
101+
animate={{ scale: 1, opacity: 1 }}
102+
transition={{ type: 'spring', stiffness: 400, damping: 22 }}
103+
className={`
104+
m-1 p-1.5
105+
w-11 h-11 flex items-center justify-center
106+
rounded-lg
107+
text-2xl font-bold
108+
transition-colors duration-200
109+
bg-[#f5e6c5]
110+
shadow-sm
111+
`}
112+
>
113+
{letter.toUpperCase()}
114+
</motion.div>
115+
);
116+
}
117+
```
118+
119+
I added a fade-in delay to the outline to better communicate the effect of the user's move. I suppose this is subjective, but without the delay it felt like the outline was being drawn almost in advance of the move.
120+
121+
![Zoomed in view of the word list animation.](words.mp4)
122+
123+
The animation of the found words serves no function. I just like it.
124+
125+
One design problem I haven't solved yet is how to better communicate when a letter is used by multiple worlds. In the case of `MOO` and `MOON`, it's quite obvious that the `MOO` part is used twice as it's wrapped in two outlines. However, if words have multiple used-twice letters, it gets busy and confusing.
126+
127+
I tried to solve this problem by cycling through a small pallete of rainbow colors for the different word outlines to better distinguish one word outline from another. However, players kept asking me what the different colors mean, hah.
128+
129+
One idea I had, was to highlight the words a letter belongs to when it's tapped. But I don't want to give up the simple controls of: just tap the arrows. For example, on desktop, if I show that letters are clickable, people will try to move them, etc.
130+
131+
My goal was for [Queuedle](https://queuedle.com) to be roughly as hard to pick up for the first time as Wordle. I think I got there. I thought about adding a tutorial animation ... but I always skip them.
180 KB
Binary file not shown.
46.1 KB
Binary file not shown.
119 KB
Binary file not shown.

0 commit comments

Comments
 (0)