Waffle puzzles are basically 2D Wordle puzzles for which you must swap letters. I find optimal (fewest-swap) solutions to waffles of various rectangular sizes! I can also generate waffles!
All the rectangular ones that I can find online are actually square. The puzzles...
https://wafflegame.net/daily is 5×5
https://wafflegame.net/deluxe is 7×7
https://wordwaffle.org/unlimited has 3×3 Waffle puzzles
I have since found a similar type of puzzle called a hashtag...
https://everydaypuzzlesgame.com/g/hashtag/index.html
My code could be easily modified to handle these easier puzzles if that is something you would wish to do, but I intentionally designed my code to solve the more difficult puzzles that have letters completely filling all outer edges.
The 3 waffle files (not solidWaffle files) currently have you download words_alpha.txt (code explains how) as the word list. However, this list does not have data about the frequencies of each word. I did find a file called freq_map.json (instructions on how to download it are in the code) that has this data, and the waffle files use it, but it only has 5-letter words.
I want word lists with word-frequency data for words of all lengths. Mathematica's (or WolframAlpha's??) WordFrequencyData[] could add frequencies to words_alpha.txt, but I don't have access to Mathematica. I then was talking with https://github.com/CodingKraken, and he gave me the idea to use the Python wordfreq module and use its word list file found at https://github.com/rspeer/wordfreq/tree/master/wordfreq/data. Here is my Python 3 code to make frequency lists for the desired word lengths...
from msgpack import unpackb
from wordfreq import word_frequency
import json
# first, download file from https://github.com/rspeer/wordfreq/tree/master/wordfreq/data
with open("large_en.msgpack", 'rb') as f:
dataLong = unpackb(f.read())
lengths = [3, 5, 7, 9] # choose word lengths
freqCutoffs = [1E-5, 1E-5, 1E-6, 1E-6] # set frequency cutoffs
data = [{} for i in range(len(lengths))]
for wordGroup in dataLong[1:]:
for word in wordGroup:
freq = word_frequency(word, 'en')
if word.islower() and word.isalpha() and word.isascii():
for j in range(len(lengths)):
if len(word)==lengths[j] and freq > freqCutoffs[j]:
data[j][word] = freq
for j in range(len(lengths)):
print(lengths[j], len(data[j]))
with open("words" + str(lengths[j]) + ".json", "w") as f:
json.dump(data[j], f)
I believe you need at least Python 3.7 to run the above code since isascii() was introduced in 3.7. I was using 3.11. I haven't changed my 3 waffle files to use these word lists yet, but feel free to use the resulting .json files even for 5-letter words! Note that I exclusively use the above word lists in my 3 solidWaffle files, so you can reference these files to see how to load in word-list files.
However, I am now convinced that word frequency is a poor metric for whether a word should be included in the word list. For puzzle generation, the best lists are probably hand-curated by several people who throw out words that are generally unknown. Ideally, waffleGen.py should use smaller word lists than the solvers used in waffleGen2.py and waffle.py. I have never attempted to hand-curate a list.
To run the code, enter the initial puzzle into the top section of the code in the format provided within the code. That is, you need to set two and only two variables. You will also need to download the word-list file(s) in the links specified at the top of the code. The solution and step-by-step optimal swaps will be output to a terminal (or, in Windows, PowerShell or whatever).
The word lengths must be an odd number larger than 1. Note that 5-letter words use a better word list, but the list only has 5-letter words. The word list used for other sizes has lowercase English words of all lengths.
Waffle puzzles are not super difficult by hand. Though, writing the code was a bit tricky (that is to say, fun!).
Trying to then minimize the number of swaps was most interesting. This is trivial if there are no duplicates of initially-non-green letters (just put letters where they belong), but duplicates often occur. If duplicates occur in a puzzle, swapping the letters that do not have duplicates is also trivial (safely put them where they go at any time). For remaining duplicates, you can brute force all permutations of where duplicates should go, and I do try this as one of my approaches (see comments in code for more details). With duplicates, the hard part is choosing which copy of each repeated letter should be assigned to which target location. Equivalently, this asks for a cycle decomposition of the resulting directed multigraph with as many cycles as possible. This is closely related to known "minimum swaps with duplicate elements" problems, which are much harder than the distinct-letter case, so my old code brute-forced the duplicate assignments.
Instead of trying all permutations, waffle.py and waffleGen2.py now have a faster optimal solver for very hard puzzles. This new solver has better printing of results due to a more natural way of thinking about things. The old permutation code is still there, but it is commented out. An import, the permuteToGetMinSwaps() function, and the call to the function are commented out and replaced by findCyclesToGetMinSwaps(). I have not updated solidWaffle.py and solidWaffleGen2.py to have this new solver yet (because solving solid waffles is easier because the puzzles can never get very large). Here, a cycle means a set of misplaced letters that can be resolved by rotating their contents into their target locations; a cycle of length k costs k−1 swaps. Cycles are defined here, though this link does not consider duplicates. The new function, findCyclesToGetMinSwaps(), first searches the waffle for all possible unique cycles. Then, it finds all possible combinations of these cycles that could complete the puzzle. If a group of cycles fixes every remaining letter exactly once, then the group with the most cycles gives the fewest swaps! I am not convinced that this new algorithm would be faster for hypothetical extremely large puzzles with 1000s of letters, in which case it would also use lots of RAM.
My code has a call to swapToTwoGreens() that can greatly speed up the permutations. The idea is to swap two letters if they both become green after. Starting from the upper left then going right, it swaps the first pairs that work, though the order of scanning does not affect how useful this algorithm will be. I was not sure if swapToTwoGreens() would affect my code's ability to find optimal swaps, so I wrote some code that did extensive experimental testing, and swapToTwoGreens() seemed to be safe, but I wanted a proof. I especially wanted a proof because the claim "a swap that creates no greens is always bad" is not true because, taking abcd as the correct order, dcab optimally has 3 swaps, and cdab, which is obtained from dcab after a single no-green-producing swap, optimally takes 2 swaps. However, "a swap that creates no greens can be required for the optimal score" is not true because a no-green-producing swap can reduce the remaining optimal distance, but there is always an optimal solution that avoids needing such a swap first.
Let's prove that it is always safe to swap to two new greens. With duplicates, there are many ways of drawing cycles that include all numbers. Let us consider only those groups of cycles that have the most cycles (that is, give optimal swaps). There are three cases. (1) For these groups of cycles that contain the swap-to-two-new-greens as a 2-cycle, the swap is obviously safe. (2) For the groups of cycles that have the two identical letters in different cycles, the swap will merge two cycles into one but also will remove two letters (instead of just one letter) from that cycle, so the effects cancel. It is impossible to stay as two cycles (they must merge) because we are only considering groups of cycles with the most cycles. (3) There are no other groups of cycles because you cannot have both of the two letters be both part of a longer-than-two cycle because such a thing could be split into more cycles, and we are only considering groups of cycles that already have the most cycles.
I was wondering if there are more tricks like the swap-to-two-new-greens trick. Note that a swap to two new greens is a cycle of length 2. I wonder if, after checking for any 2-cycles, you can safely do any 3-cycles? The answer is no. For the puzzle DBDFAFECBCAE with solution AABBCCDDEEFF, there are 5 unique 3-cycles and no immediate 2-cycles. Doing the cycle FEB results in a puzzle that has 7 optimal swaps. Doing any other cycle (such as DCA) results in a puzzle that has 6 optimal swaps.
In 2026, I had ChatGPT look at my code. It found this paper: https://arxiv.org/abs/1803.06816, which includes my swap-counting problem as a special case! ChatGPT informally confirmed my proofs above and explained them to me in fancy terms of multisets and Eulerian directed multigraphs. It then helped me implement some of my ideas: it coded swapForcedSameSource() for me and edited findCyclesToGetMinSwaps() to make cycles_seen a set. I implemented these changes in waffle.py and waffleGen2.py. The paper also describes an integer linear programming (ILP) method, but I am not motivated to use it here. My goal is not to solve the largest possible abstract instance of colored token swapping; my goal is to solve normal Waffle puzzles while keeping the code understandable, self-contained, and easy to print as human-readable swaps.
Then, ChatGPT gave me a way to speed up findCyclesToGetMinSwaps(): abandon a group of cycles as soon as it cannot possibly beat the best solution already found. After all 2-cycles have been removed, every remaining cycle has length at least 3. So, if there are 12 letters still uncovered, at most 4 more cycles could be added. If even that would not beat the current best solution, there is no reason to keep exploring that branch. I implemented this, then ChatGPT helped me validate all of its changes including this one!
Next steps...
- Other shapes? I believe the whole idea of a waffle is to have maximal shared letters given a word size without having parallel words "touch". A 3-letter word square waffle could be made with two words (it would be a plus sign), but two words do not have maximal shared letters so would be very boring (I suppose a yellow in the center spot would be a curiosity). I suppose that 4-letter words could make 4-word square waffles in various ways, and it would not be hard to modify my code to handle this, but I have never seen these. If I were to do another shape, it might be this, though I would think that a 5-letter-word by 7-letter-word rectangle, which my code can already solve, would be more interesting!
- In 2026, ChatGPT suggested another possible speedup: split the remaining letter problem into disconnected groups. For example, if the remaining misplaced letters involving A, B, and C never interact with the remaining misplaced letters involving D, E, and F, then those two groups could be solved separately and the swap counts could be added. This could greatly reduce runtime for some large puzzles because two small searches are much easier than one large search. I have not coded this because normal waffles are small, my earlier safe-swap tricks already shrink the problem a lot, and my current code structure mutates global lists while printing swaps, which makes splitting and recombining components more annoying than it is probably worth.
I made waffleGen.py to generate all possible waffle solution grids for any odd-by-odd rectangular waffle shape.
Reducing the size of a word list greatly helps runtime and is always crucial. Without reducing the word list, nearly all printed puzzles are garbage because they have at least one ridiculously uncommon word. I suppose the user could always hand select the desired sublist then run the generator code!
If the length of a word list is somehow not changed, the length of the word does not greatly affect runtime. This is because few scenarios make it past 3 or 4 words (which prevents later letters from mattering). Regardless of word size, word lists should be less than 1000 if you want to finish them in a reasonable amount of time. If you want to use multiple CPU cores, quickly modify my code so that each process could be assigned different starting words, keeping in mind that starting words with a starting letter that is a common starting letter in the word list will likely take longer to run.
If n1 and n2 are the number of letters per word, the number of words of length n1 in the waffle is (n2 + 1)/2, and the number of shared letters is (n2 + 1)(n1 + 1)/4. If num1 is the number of words in the n1 word list, and num2 is the number of words in the n2 word list, then, assuming that letters appear in a word independent of nearby letters, the number of puzzles found should be roughly...
Note that, for square waffles, the numerator is an approximation instead of using the permutation formula. The denominator arises because the probability that a valid waffle can be made from a permutation is 0.06124^(number of shared letters), and (1/0.06124)^(1/4) equals 2.01. In the case of square waffles, you should get less than this because then symmetric and repeated-word solutions are prevented.
Where did the 0.06124 come from? I first got letter frequencies in the dictionary. I will assume that all word lists have the same frequencies and that the location of the letters in the word (odd vs. even letter location) makes no difference. By summing the squares of the frequencies, I get 0.06124.
My current interesting results for square waffles are...
- Because I have frequency data for 5-letter words, I can easily reduce the word list, which is a very successful strategy! With a list of 156 words (from a frequency cutoff of 0.0001), all 31 waffle solutions were printed within seconds! The prediction for the 31 seems to be 174, which is (156/66)^6, where the 66 is 2.01^6. If using the actual permutation formula instead of 156^6, the prediction becomes 158. Dividing by 2 to remove symmetric solutions gives 79, which is between 31 and 92, where 92 is the number of solutions that my code prints if words are allowed to be repeated within a puzzle. I would expect the prediction to be between 31 and 92 because the 31 does not include repeats, but, when including repeats, you can get more waffles than you should (as can be seen from the ratio of 92 to 31) because repeated words can fit together in a waffle shape with high probability.
- For 9-letter words, I found a list of 215 common words. After making all of the words lowercase, in about half a minute, the search was completed and no puzzle solutions were found. I then found a sublist of 1180 words, however the words are not exactly common. It finished running after over a day, but no puzzles were found.
- For 7-letter words, I found a list of 1371 common words. After making all of the words lowercase, many solutions were found after several minutes, though, with such a long list, running on a single core would take at least a week.
- Here is a good list of 404 common 3-letter words. Though, I had to remove a couple 4-letter words and remove capitalization on the words with q, and I noticed that que is not an English word.
- Using the full 7-letter-word list, after many minutes, solutions do eventually start to print. After roughly an hour, solutions started to print for 9-letter words when using the full 9-letter-word list! Though some of the words were stupidly uncommon. The first to print was aardvarks aaronical rabatting rabbanist nearabout vitiation chibinite rhinolite latinless sightless followed shortly by aardvarks aaronical rabatting rabbanist nearabout vitiation chilicote rhizopods lutanists sightsees. Of course, neither search was any real progress through the entire search space.
- With the word lists I tested, I found no 21-letter-or-larger waffles. After roughly an hour when I stopped the code, no puzzle solutions were found for 19-letter words or 17-letter words. The 19-letter words at least made significant progress through the search space, and, if I cared, I could have finished it.
In 2026, ChatGPT made waffleGen.py (and solidWaffleGen.py) about 60 times faster in my tests by precomputing dictionaries of words indexed by their required shared letters!
Next steps...
- Maybe I could make the code faster by placing words in the hardest locations first. For example, if it is time to place a horizontal word, and the leftmost vertical word has the letter z in it, place the horizontal word where the z is (if possible). I am not convinced that this would even be faster.
- Requiring a certain word to appear could be fun, and it probably wouldn't take much coding, though, for speed, you would want to place it first, which would make the coding more tricky (unless it is placed as the 0th word).
I wrote waffleGen2.py to take a solution and make a puzzle by swapping the letters. It uses the same solver and swap counting algorithm as waffle.py (basically copied and pasted). Currently, it is a toolbox for you to edit the final "main code" section. You can select between strategies...
- completely shuffling all the letters, which, for large puzzles, can produce puzzles that take forever for the code to solve due to not having many greens (especially greens in shared locations)
- force certain locations to be green then shuffle all the letters
- keep doing random swaps until you get a certain number of optimal swaps or more (or until multiple solutions occur)
- write your own strategies!
The final puzzle should ideally not have trivial moves where a yellow letter has only one letter that it could swap with by only thinking about colors of letters (without even taking into account what the actual letters are). My code currently makes sure that there is one solution and that there are no immediate trivial swaps. The significant new code in waffleGen2.py is the colorPuzzle() function (besides what I copied from waffle.py), which colors the yellow letters after coloring all green letters.
There are several possible coloring goals: maximize shared yellows, minimize non-shared yellows, minimize total yellows, or mimic an existing website. I currently prefer minimizing unnecessary yellows because extra yellows can make the puzzle feel misleading, though things are complicated.
A way to color yellow letters is to always count a yellow on a shared location towards both words if possible. Because the alternative would require an arbitrary choice, this is what I will do.
A way to color yellows is to look at all non-green shared locations to try to color in every shared location we can. Once all shared locations are handled, fill in yellows in non-shared locations. This could be considered "hard mode", when the idea is to use the fewest possible non-shared yellows. "Easy mode" would be doing the non-shared locations first.
When coloring the shared locations, the order that the locations are considered can affect the number of solutions and the number of yellows. Non-shared locations are simpler because they only remove a letter from one word. Shared locations are trickier because a yellow shared location can remove a letter from one word, from the other word, or from both crossing words. Only the number of shared yellows can be affected by the coloring order of the shared yellows.
To think about such things, we only need to think about a single letter—let's say, n—at a time because other letters being yellow cannot affect any n being yellow. The number of solutions could be affected because, for a horizontal word with a single n in its solution (and no green ns yet), N-n-- and n-N--, where capital N means yellow, allow a different number of ns in the vertical words. The number of yellows can also be affected. In the following 2 puzzles, a • means the location of the n in the solution...
N•N-- n•N--
| | • or | | •
N-•-- N-•--
N•N n•N
• | or • |
N-• N-•
In either puzzle above, depending on which shared locations are chosen to count as yellow, coloring all 3 ns can occur, but you could also only color the two ns not on the upper left.
My current coloring code uses a simple greedy strategy. It first colors shared locations that can remove the letter from both crossing words. Then it colors non-shared locations. Then it goes back to the remaining shared locations. I like this because it tends to avoid extra yellows that feel misleading or gross. Changing this order in the code would not be difficult.
However, I have shown that this greedy strategy does not always give the fewest yellows. If I cared enough to make the coloring optimal, the right exact approach would probably be to handle each letter separately and check combinations of shared yellow choices for that letter.
I was curious how https://wafflegame.net/daily and https://wordwaffle.org/unlimited handle certain situations, so I did some limited testing.
- If the solution for a word has a single instance of a letter, but two of the squares—one shared and one not—have that letter (and the other word's solution does not contain that letter), the game did not prefer to make a shared letter yellow, and it also did not prefer to make a non-shared letter yellow. The game should either consistently choose to make the shared letter yellow or the non-shared letter yellow.
- If a shared letter is yellow and appears once in both words' solution, a duplicate of that letter wasn't yellow if in one of the words, but was yellow if in the other word. This might be considered a bug because the letter should count toward both words.
- Both online games seem to just color in the first yellow that can be made yellow and associating it with the first word it can while using the normal left-to-right top-to-bottom order. For simplicity and runtime, I recommend such a strategy eventually (it certainly will eventually be necessary to decide between equivalent choices for non-shared locations), but I also recommend starting with either shared or non-shared locations.
Next steps...
- To optimize the coloring of shared locations, check combinations of shared yellow choices for each letter.
- Currently, if the words THESE and THEME were in the same puzzle, with the M and S unsolved for and the M or S start not in one of these words as a yellow letter, my code would say that there are at least 2 solutions, and it would reject the puzzle. However, it could still be possible for the user to do the optimal swaps in a way that allows them to figure out which word is which! The following puzzle (green are capitalized) can be solved in 4 or 5 swaps depending on the solution. First, swap the V to the correct location, then swap the D to the correct location. You have now done completely safe swaps and know which word is THEME and which is THESE. Alternatively, you could have swapped the E with the L that would make a swap to two greens, then swap the remaining L. My code checks uniqueness from the initial color information, not uniqueness after the player performs logically forced swaps.
AdlRT AVERT
L s H L L H
eOUSE -> LOUSE
O m l O D *
THEvE THE*E
This is a JavaScript player to play the waffle puzzles you just made! See the comments at the top of the file.
I define "solid" waffle as those with no "holes". See the 3 files whose names start with solid. These files are nearly the same as the non-solid files. They use the words#.json file lists described above, though now word lengths can be even numbers.
There are no longer non-shared locations in puzzles.
For solidWaffleGen.py, the approximate number-of-puzzles formula now becomes...
Square waffles should have a bit less (due to things like removing half of the symmetric solutions). In general, the above prediction is over 10 times larger than what is observed! Perhaps this is due to the most common words favoring common letters less than general words. I wrote the following Python 3 code to find out (only valid for square waffles)...
import json
file = "words5.json"
with open(file, 'rb') as f:
data = json.load(f)
# get counts
count = 0
counts = [0 for i in range(26)]
for word in data:
for letter in word:
count += 1
counts[ord(letter) - ord('a')] += 1
# print the sum of the squares of frequencies
sum = 0
for c in counts:
sum += c**2
print(sum / count**2) # chance that a random letter matches another random letter
I learn that, given my frequency cutoffs, 3-letter and 4-letter words have a more uniform distribution of letter frequencies (the chance of a random letter matching another random letter is less than 0.06), but 5-letter, 6-letter, and 7-letter words have certain letters be quite common.
In 2026, ChatGPT says: "The prediction is often much larger than what is observed, probably because it uses one global letter-frequency match probability. In reality, letter frequencies depend strongly on position within a word, and solid waffles require position-specific crossings. For example, first letters, middle letters, and final letters have very different distributions, so the actual crossing probabilities can be much smaller than the global estimate." ChatGPT then wondered if my non-solid waffle code had this issue, and I don't think it does, which ChatGPT was not super worried about.
Using my frequency cutoffs, for 4×5, the following are most of the few good puzzles...
draw
rare
idea
liar
loss
ages
draw
mate
idea
tent
cats
hurt
idea
liar
lots
odds
wrap
note
even
rest
caps
hurt
idea
list
dose
caps
hurt
idea
list
lose
caps
hurt
idea
list
loss
For 3×6, the following is most of the few good puzzles...
dog
era
far
end
age
ten
Here is a good 3×7...
its
net
see
one
far
age
red
Ignoring a word length of 2, this leaves 3×3, 3×4, 3×5, and 4×4.
Solid waffles are essentially small crossword grids where every row and every column must be a common and familiar word. I came across a great 5×5 crossword-puzzle solution with generally-known 5-letter words that would not be generated using my frequency cutoffs. When I tried to get my code to replicate it, I had to reduce the frequency cutoff to very low to include the least frequent word: fazes. With this cutoff, nearly all returned solutions had garbage words, and the progress through the entire search space was essentially a rate of 0. A hand-curated list of words would be necessary to generate all interesting puzzles of a certain size.
