I run a small Canadian word game site, and a few months ago I started getting requests for a grid version. Not another single-word guesser, but the waffle format, where you swap tiles around a board until every row and column spells something.
It’s live now as Canuckle Waffle, and building it turned out to be a different problem from the original game. Here’s what I ran into.
The board is smaller than it looks
A waffle grid looks like 25 cells, but it’s really 21. Three words run across (rows 0, 2, 4) and three run down (columns 0, 2, 4), and the four cells in the middle gaps (1,1), (1,3), (3,1), (3,3) don’t exist.
Six words with five letters each would be 30 letters, but 9 of them are shared at the crossings, so you get 21 unique tiles. That sharing is what makes the puzzle work and also what makes generating it annoying.
const CELLS = [];
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 5; c++) {
const isGap = r % 2 === 1 && c % 2 === 1;
if (!isGap) CELLS.push([r, c]);
}
}
// CELLS.length === 21
Finding six words that actually fit
Picking six words isn’t hard. Picking six that agree on their crossing letters is. Each of the 9 intersections has to be a letter that works in both words, so you can’t just draw from a list at random.
With a general English dictionary there’s plenty of room. With a Canadian-themed list (provinces, slang, wildlife, food) it gets tight fast. There are only so many five-letter words that are both clearly Canadian and share the right letters at positions 1, 3 and 5 of each other.
My fix was to let the occasional plain English word in as a connector. A grid with five Canadian words and one filler is better than no grid at all. I mention it on the page too, since otherwise players wonder why one word looks out of place.
Scrambling without making it unsolvable
The easy way to scramble is to shuffle all 21 tiles randomly. The problem is that you then have no idea how many swaps the solution needs, and you want to tell the player a fair move limit.
So go the other way: start from the solved grid and apply a fixed number of random swaps.
function scramble(grid, swaps = 10) {
const g = grid.slice();
for (let i = 0; i < swaps; i++) {
const a = randIndex(g.length);
let b = randIndex(g.length);
while (b === a) b = randIndex(g.length);
[g[a], g[b]] = [g[b], g[a]];
}
return g;
}
Since you built the puzzle by working backwards, you know a solution exists within that many swaps. The player gets a few extra moves on top as a buffer, and unused moves turn into the score.
The catch is that random swaps can undo each other or move a tile that was already correct, so the real minimum can be lower than the number you applied. I also lock any tile that lands back in its right spot so the player starts with a few anchors.
Duplicate letters will bite you
Grids with repeated letters (two E’s, two O’s) break the naive version of the check. If you color tiles by comparing each one to the answer at the same index, a duplicate letter can show as wrong even though swapping it with its twin would make no difference.
You need to treat duplicates as interchangeable when deciding what’s solved, and compare words, not individual tiles, when checking for a win.
Input
Click-to-swap was the easy part. Drag and drop was worse, mostly because touch events on mobile fight with page scrolling. I ended up supporting both, since some people tap two tiles and some people drag, and neither group wanted to switch.
If you’ve built something like this and handled the duplicate-letter case more cleanly, I’d like to see how you did it.
Top comments (0)