Heck yeah I'm back on my bullshit with another ZZT engine that plays a game ZZT really doesn't want to be able to play. Last time, it was working with dice. This time it's an attempted port of Wordle! With Zee!, my interest in creating it was based on ZZT's clunkiness with good random number generation. For Wordles of ZZT, my primary motivation was that it was a pun and April 1st was just a few short months away. I was unsure if general interest in Wordle would last that long, especially after it was purchased by the New York Times, but it's not often you get handed a freebie April Fool's Day gimmick, and surely no matter how much interest cooled, it would be better this year than next.
After some initial scrawlings, I was confident something approximating the game could be done, but it would take many afternoons of work, many unexpected insights, and many lengthy breaks to not be consumed by this engine's development in order to get it out the door in time.
As with any elaborate ZZT engine, the biggest issue is the 20,000 byte board size limitation. This will forever be not enough space. Modern enhanced source-ports like Classic Zoo are able to use XMS memory in DOS to raise that limit to 65,535 bytes before running into requiring adjustments to ZZT's file format, which provides significantly more breathing room. Alternatively, more powerful outright forks with new capabilities like WeaveZZT can provide a more accurate and more playable experience, as was inevitably seen in February when asie released Laworde.
ZZT has no support for strings. The puzzle solution would have to be encoded in some other form, whether that be flags or having tiles/colors represent letters and reading that information into code as necessary. ZZT has no keyboard input in standard gameplay. Input would have to be menu based, steering around an object to hunt-and-peck on a virtual keyboard, or use the cheat prompt to set flags. There was little thought as to how anything would actually work, just going and seeing where the roadblocks were, and either finding ways around them or trying new paths to getting a functional engine entirely.
I recently realized that much of my game development, ZZT or otherwise, is about surpassing expectations with harsh limitations. Zee! isn't a great way to play Yahtzee, and Wordles of ZZT is an even less playable version of Wordle. These are challenges for me, and ZZT as a medium provides me a small audience that can appreciate the efforts involved as they're familiar with what's being worked with here. This kind of ZZT coding where an approximation of an authentic source being scaled back but still treated the same isn't anything I'm pioneering here. Masamune's ZZTris from 1999 gets cited as being the first game to demonstrate that Tetris is possible in ZZT, even though it doesn't actually have tetrominoes. You can get into the same level of semantics with Wordles of ZZT and whether it's compromises make it Wordle or not.
The reality is that vanilla ZZT cannot run code that isn't on the board, and that board has 20,000 bytes of space to work with. Before even testing the waters in terms of writing any code for a Wordle engine it was obvious that the word list was going to be thrown out. Does it still count as Wordle or are we just playing Mastermind here with 26 possible colors for your password?
In addition to the lack of a word list, there's a lack of support for "hard" mode in which knowledge uncovered in prior guesses must be used in subsequent guesses. When I first started playing Wordle I was playing on easy mode. As a once a day challenge, I liked the extra decision of whether it would be worthwhile to guess a word that you'd know was wrong if it could potentially provide far more insight as to what letters were necessary. As time went on, I found myself playing Chordbug's Hello Wordl clone and being able to play an endless number of puzzles, with a soft goal of perhaps picking up on "good openers" in the original daily version of the game. During that brief window of people discussing Wordle's once and only once a day structure in contrast to games that demand constant attention, playing an endless version definitely gave credit to the idea that playing the game for longer periods of time would make interest wane faster. Playing these extra puzzles indeed burned me out a bit more, but it was entirely my own doing rather than the game itself encouraging it.
When you're able to select any word you like for any guess, you may find yourself starting to try to maximize the number of letters you use, without even considering the revealed colors. GIRLY BEAST FOUND CHAMP uses nineteen letters, and will frequently make the answer to any puzzle a simple anagram. That is what got me to ruin the game on easy mode, and you can do the same in this variation of the game with no word list. You can guess "AEIOU" if you want a quick jump-start as well. Of course, all this is based on caring more about having a streak than getting the word in the fewest number of guesses as possible, but that was the part that appealed to me. Without hard mode, it's up to you to opt not to cheat, so while you play Wordles of ZZT, I suggest you do opt to play as if hard mode was a thing when making your guesses.
That's the big compromise, but there's still a bit more that just couldn't fit. You'll notice this version only has five guesses per word rather than six. This was... somehow not a conscious choice. In the months spent working on this engine, I genuinely didn't realize that I had lopped off a guess until I was so done with development that I had made a template and was generating puzzles via script. It was only when testing against a game of Hello Wordl to make sure my game returned identical colors given identical inputs for the same word in both versions that I realized I somehow only had five!
By that point though I was too attached to all the flavoring to the game with the tiny room the player sits in while playing. It would require some sacrifices of a feature that I put a lot of love into with my remaining free memory, so consider it a balance tweak since you can guess non-words and aren't bound by hard mode rules.
The cut that hit harder was the lack of feedback via a displayed keyboard layout. When the core engine was complete, I really wanted to implement this, but just did not have the space to either code twenty-six objects for each letter to change color accordingly, or one very messy object to move around and plant some sort of marker. I don't 100% believe it to be impossible with the base engine, but it would definitely have meant removing the little room which got created entirely because of the extra memory I still had.
Also, it's slower than I would have liked. The amount of time to process an entire guess varies depending on what colors end up being potentially needed. When your guess is entirely right or entirely wrong the pace is reasonable enough. Get multiple yellows though, and you're looking at well over 10 seconds on ZZT's default speed. Like Zee! I wanted the engine to be playable at default speeds and not rely on adjusting the game speed (something almost no ZZT worlds request) to make things bearable. You still very much can, and get a zippier response time at higher speeds that make things more tolerable. Some attempts were made to hurry things along by using two parser objects and attempting to set the second one off before the first one finishes its own work, but no matter how much of a delay I added between the first parser parsing and the second parser finishing up they would sometimes end up colliding and breaking everything, resulting in the second object being pretty moot.
The input method is also very bad. Something like asie's in Laworde is still unsatisfying to me. That movement is still slow when working in an environment without full keyboard support. The giant list of letters is uglier, but honestly probably slightly faster if you're good with your page ups and downs. I had considered doing a nested set of menus with "A-E", "F-J", "K-O", "P-T", and "U-Z" then popping up smaller menus, but as the cursor always starts on top you're likely shuffling around all the same.
You may find it odd as well that when picking a letter, the letter is prefixed with an exclamation point. This is a quirk of a memory saving technique. Well-formatted hyperlinks in ZZT-OOP are written as
!label;Text to Select, but
!labelandtext will create a hyperlink whose label and text are identical, with the side effect that the text gets an exclamation as a prefix. I included a demonstration in my end of year experiment compilation 2021 dot ZZT where I unexpectedly predicted my use for it only really being beneficial if the player is inputting "a number or other very tiny string".
If you're reading this though, your interest in how this all works probably has little to do with the input method, and a lot more to do with working with strings in a medium that has no such concept.
A Rather SILLY Puzzle
The journey to getting a functional Wordle engine was a long one. The engine had basically three major versions of development with each one revealing a critical flaw that would lead to me basically starting from scratch each time. Knowing that if I could pull this off I'd definitely want to write about it, I actually did a very good job making regular backups of this, so if you want to know the whole story from beginning to end and not just about the final published product you've got more to look forward to up ahead!
Let's start with the script, "maker.py". This is a python3 script that uses the now quite old and janky Zookeeper library I started alongside the Museum to work with ZZT files. Most of what personal need I had for it was handled as the Museum's file viewer got more developed over time, but even in its messy state, Zookeeper can parse a board from a ZZT file, do some simple find/replace on relevant objects' ZZT-OOP, and then export a BRD file to be imported in the editor of your choice.
The maker script was designed for me and me alone to make puzzles, so it's very easy to break it with invalid input. To create a puzzle, a simple "make.py SILLY" will use a template board for the engine and set things up properly so that the solution is the word "SILLY". The actual work this script does is very little as the engine only needs to know the five letters, the number of occurrences of each letter (kind of), and then a set of labels for the parser to work with to determine green, potential yellow, and red letters.
For "SILLY", five flags are initially set for the solution "S1", "I2", "L3", "L4", and "Y5", this letter plus position format is used for all puzzle solutions. The occurrences are handled by flags named "MAX#", with # being the number of times the letter can occur. An object later on will set these flags in order (another object clearing them out before calling the next one) every time it is told to go to a ":get" label. Zaps and restores are used to make it a looped list.
The last patchwork is in a series of twenty-five objects running across the bottom of the screen. Before the engine was made presentable, these were just upward facing arrows so I've taken to calling them spikes. I'll get into the specifics when they're relevant, but for now, know that they work in groups of five, all have a check color label, and need the script to patch in if the combination of the current position in the puzzle and the current letter lead to a green, red, or potential yellow.
Upon entering the board, those solution flags are all set along with "POS1" to indicate that your letters being input for a guess belong in the first position of the five letters. The keyboard object has a list of all twenty-six possible letters and a backspace option. Selecting any of letter sets a guess flag for that letter "GA"-"GZ". The right side of the board has the main puzzle area which I'm calling the guess grid. There are twenty-five objects here used to display your letters for each guess, and that's where the first obstacle is reached. These letters have to be addressed individually, and after a guess has been parsed, the old guess needs to stop responding to input.
#bind comes to the rescue here. The first row of letter objects all immediately bind themselves to objects named "L1" through "L5" ("Letter 1" through "Letter 5"). The purple object indicating which row you're currently working with advances after parsing and then tells the next row of objects to jump to a label and bind themselves to the same letters. This allows the twenty-five objects to share five objects worth of code. These objects actually do very little. When a letter is selected, all objects are sent to a ":cc" label to check the character based on the current guessed letter. ...And here I am already noticing that I can shave nearly a kilobyte of space off these things.
These objects first check if they're actually the one being addressed. Objects L1 - L5 will halt execution if they don't see the matching POS1 - POS5 flag. The one letter that does get to proceed then runs down a list checking for GA - GZ and then jumping to a label and changing characters. If no flag is set it becomes blank, which is used for the backspace function.
(This can be optimized significantly by just have the object verify that it's the one to activate, executing
#char 32, and then just having
#if GA char 65 [...]
#if GZ char 90. These objects are some of the longest survivors of what code was written during development, and exist in basic form in even the earliest work in progress boards I have saved. I'm sure at one point they had more code in those labels to necessitate their existence, but in the final engine they don't have a need for a label here.)
You might wonder what keeps the original source objects from activating as well, as they should also be changing characters as input is produced. That's as simple as having the original objects be pre-
#locked. Binding an object only uses the actual code allowing the lock-state to differ between source and bound objects.
When input is generated, the spike objects mentioned before also have a ":cc" label to jump through. These objects are used to initially record the expected color. They are arranged in groups of five with each of those five objects representing the color of guessing the Xth letter in position Y.
|Spike 1||Spike 2|
The first spike will look to see if you've correctly guessed the solution's first letter of "S". If you have, it marks the tile above in green. If you have guessed any other letter in this position, it gets marked as red.
The second through fifth spikes will check positions two through five for the first letter of the solution. If you've guessed an "S" anywhere else, it will be marked as a potential yellow. The difference between a potential yellow and a confirmed yellow comes into play here. As the other three spikes in the group repeat spike 2's code only updating the POS flag. Right now a guess of "SSSSS" will result in one green invisible followed by four yellow invisibles. In reality, such a guess should have one green letter and four reds. Determining if a potential yellow should be yellow or red was by far the most significant challenge to conquer.
The next group of spikes works with the solution's second letter of "I". The code is basically identical with the "GS" flag changed to "GI", and the labels for match on the second letter going from "grn", "ylw", "ylw", "ylw", "ylw" to "ylw", "grn", "ylw", "ylw", "ylw". The same basic structure applies to the final set of spikes as well for the fifth letter "Y".
However, this puzzle solution also has a repeating letter! Two "L"s make up positions three and four. So how do those spikes look?
[...] #end :cc #if POS1 if GL ylw :red #if POS1 put n red invisible #end :grn #put n green invisible #end :ylw #put n yellow invisible
[...] #end :cc #if POS2 if GS ylw :red #if POS2 put n red invisible #end :grn #put n green invisible #end :ylw #put n yellow invisible
[...] #end :cc #if POS3 if GS grn :red #if POS3 put n red invisible #end :grn #put n green invisible #end :ylw #put n yellow invisible
[...] #end :cc #if POS4 if GS grn :red #if POS4 put n red invisible #end :grn #put n green invisible #end :ylw #put n yellow invisible
[...] #end :cc #if POS5 if GS ylw :red #if POS5 put n red invisible #end :grn #put n green invisible #end :ylw #put n yellow invisible
Here two spikes will return a green invisible for an "S" in position three or four. This is enough to handle the duplicate letter and normally would make the next set of spikes meant for the fourth letter ...pointless. This is completely true! On puzzles with repeating letters it could be a space saver, but in terms of a one-size-fits-all template board to make puzzles that are "SILLY" or "TEPID", an alternative had to be used. Plus I certainly didn't want to have to worry about the memory constraints on a board varying based on the solution.
For spikes sixteen through twenty, well the code is almost the same except now a match looks more like:
#if POS1 if GS red. This effectively bypasses the duplicate letter entirely as all conditions for all positions return red.
All of this information is encoded while the player is still entering their input, so it has to be done with invisible walls to not reveal information to the player before they confirm their guess. Once all five letters a picked, the "POS5" flag is cleared and replaced with "CD" for "Confirm/Deny". The input object checks for this when touched and returns an alternate menu with the only options being to confirm the entered word or to backspace and change letters. The same bock of code in the keyboard that handles cycling through "POS" flags to "CD" also clears out "GA" - "GZ". Upon actually confirming input, "CD" is cleared and promptly replaced by "PARSING" which prevents the keyboard from doing anything when the player touches it, keeping it locked and in a loop until "POS1" is set again for the next guess.
This is where the magic happens. First, hidden towards the lower left there's a basic message object that starts displaying the parsing animation message once it sees the "PARSING" flag has been set. Additionally, the parser object itself is also constantly polling in a loop for this flag (and as a safety for timing purposes, making sure that it's also blocked to the west by an object named "f" that does the final pass.
The parser object begins by moving east once to get into the proper position for parsing. On the first iteration through its loop it checks if it's blocked to the east which signifies that the parsing has finished as it's moved over every spike. It then requests the next "MAX" flag in the list, which for the first letter is "MAX1" as "S" only appears once.
Afterwards, for a total of five times the parser places a key and checks what color it is before hiding it again, zapping the loop label, moving east, and repeating. This moves the parser in groups of five to scan the colors in each position for each letter of the solution. Red keys require no work and are ignored. Green keys for correct letters in correct positions increase gems by one. Yellow keys for correct letters in incorrect positions increase ammo by one. In order to fully determine if a potential yellow should be confirmed or rejected, these counts of greens and potential yellows are essential.
Once the invisibles produced by all five spikes in a group have been scanned, the loop label is restored to be ready for the next set. This is where the code splits depending on how much work there is to be done. A single ammo is attempted to be taken. If it can't, that means there are no potential yellows, therefore everything is red or green, and those colors can be treated as correct.
You'll note that a group of spikes is only processing one letter of the solution. So a guess of "NILLY" will produce five reds despite having four correct letters. This is fine. The reds here don't mean the guessed letters in those positions are red, but rather that the guessed letters in those positions are not the first letter in the solution.
If you have no yellows, the code takes a huge leap forward to a label called "adv" for "advance" which does the final cleanup before returning to the earlier loop and beginning parsing of the next set of spikes. I'll return to that when we reach it linearly in the code.
If there are potential yellows, it's time to finally figure out if they should stay yellow or not. First one ammo is given back since the
#take was successful and we need our count of potential yellows to be accurate. The object then rewinds itself by moving counter-clockwise flow. Oh yeah, the parser object has a Y-step of -5 so it is perpetually trying to jump five tiles north of its current position. The board is arranged in such a way that it will never be able to do so. Movement relative to that direction allows the object to move five spaces west, winding up at the start of the spikes it couldn't fully parse in a single pass.
Now the "MAX#" flags finally come into play. For "S" we have "MAX1" set. Another missed optimization here where I was writing code expecting to do more work than I ended up actually needing to. Checks are made for each "MAX" flag and a jump is made to a matching label. These labels try to
#take a number of gems (that's green letters in the group) equal to the number in the "MAX" flag. This is another split in the code based on whether or not the gems can be taken. If they can be taken, the work is easy. Having that many gems means you got every occurrence of that letter in the word in the correct position. If you also guessed that letter elsewhere, that potential yellow needs to be made red. Another repeating loop once again places a key over the invisible below and checks its color. If its yellow, it turns it red. Otherwise, it leaves it alone. Once all five passes of the loop are complete, the label is restored for the next time it is needed and the code can jump to the "adv" label that would be jumped to if you didn't have any potential yellows in the first place.
This loop also takes one ammo during each iteration, regardless of whether it can or not. This was just a simple opportunity to zero out that value and be ready for the next group of spikes.
The alternative path is that trying to
#take gems equal the maximum number of occurrences of the letter failed, in which case the work again complicates itself. Your ammo is stripped away by taking powers of two (4, 2, and then 1). Then ammo is returned equal to the number in the "MAX" flag. This clamps your ammo to be no more than the total number of occurrences, so while a guess of "XSSSS" would have produced four yellow invisibles and originally given four ammo, that number is now reduced to just one.
But even then, that's not the true number of possible yellows to get out of this parsing. Another loop takes away a gem, and if it can it takes away an ammo as well. This reduces the maximum number of yellows permitted to the number of occurrences of the letter in the solution minus the number of times you guessed the letter in the correct position.
It's easier to follow when there are multiple occurrences. A guess of "LOWLY" would give one ammo for the first "L", and one gem for the second. Your ammo would then be zeroed out (it's not pointless to give that ammo in the first place as it's used to check if this extra parsing needs to be attempted at all), increased to two based on the "MAX2" flag that would be set, and then reduced by one due to the other "L" being in the right position. Only one ammo would remain so only one possible yellow can become a confirmed yellow.
Again the parser uses a repeating loop to make a second pass over the group of spikes. In the last instance it could just change all yellows to reds, but this time some of those yellows might stay based on your ammo count. Keys are placed one at a time, and this time if a yellow is spotted, one ammo is consumed to keep the tile yellow. Run out of ammo, and the tile is replaced with a red invisible instead. Once this loops completes, the code makes it to the ":adv" label and everything meets up with the object in place to begin parsing the next set of spikes.
The ":adv" label finishes preparations to repeat the initial pass on the next group of spikes. All possible "MAX#" flags are cleared, gems are zeroed out (this shouldn't need to happen, I was just paranoid by this point), and the "ylwmove" loop used to change some yellows to red is restored before jumping all the way back to the original loop used to count potential yellows and greens is repeated.
Do this enough times and eventually the parser hits a wall and runs out of spikes to process. At this point the invisible walls accurately reflect greens and yellows correctly. The parser object hauls ass with a few
/ccw flow directions to speed up getting back into position to parse the next guess.
The multiple paths used for parsing: "No yellows", "Yellows, but all greens", "Yellows, but not all greens" take a noticeable difference in time to parse. The first case doesn't require rewinding at all, while the others require an additional pass. I had hoped I could set off the final object somewhere in the middle of the main parser's duties without causing a collision, but that was unfortunately not the case. I suspect things could be made faster here, and the significant downtime between entering a guess with multiple potential yellows of different letters is a big sticking point of the engine for me.
Earlier iterations of the engine often had a dedicated parser object for each letter. The use of counters would prevent them from running simultaneously, but I think it should be possible to do twin parser objects like this with a pair for each spike group, though whether it would be possible to squeeze enough memory for such a thing without cutting other aspects of board seems unlikely.
The Final Pass
At this point, there's very little remaining. This extra object's code technically could have been rolled into the main parser and saved a little bit of memory for the non-OOP portion of memory used by a stat, but splitting it like this saves having to wait for a full rewind. Well, maybe I should have actually. Once the initial parser has done its job, the row of invisibles correctly holds all colors in the actual final state. The extra long rewind would have been required under the original system where the guess grid's colors would reveal themselves as soon as the parsing saw a yellow or green key. Going backwards across the line would lead to colors being revealed from right to left.
In the end, that system was tweaked so that the colors reveal themselves for all letters simultaneously in an explosion of colorful slime. Redundant or not, the final object gets a straight shot of putting keys over the invisibles. Due to the spikes being grouped by solution letter position and not guess letter position, the loop is actually a repeating pattern of five loops which are identical save for the letter that gets told about the color that was revealed.
This only applies to greens and yellows. Correctly guessing an "S" in the first position will lead to a green key from the first spike of the first group, but red for first spike in every other group which is only concerned with if the guess was a letter for each letter part of the solution. If a green key is detected, a single point is awarded as well which can be later used to determine if you're solved the puzzle by having a score of five. When non-red colors are found, the letter objects for the current guess are told which color to prepare to display. The colors aren't revealed until the entire line has been parsed.
Also, the line is really a line now. During testing I had two non-ZZTing friends try a few test puzzles with the mechanics still visible and using linewalls rather than invisibles. Both of them commented on enjoying watching the mechanical side of things. Too much information would be revealed if the line was visible before, but at this point all the information the line contains is going to be revealed to the player in a moment anyway so I kept this portion visible. ZZT's linewall drawing routine makes the appearance kind of strange, but I like the unusual appearance of it. I even hid some linewalls in dark blue tiles left of the line so that they all have an identical appearance as the line is drawn. (Except, when the line is erased so are those invisible ones. Oh well.)
Upon finally finishing parsing, a few sends are used to get everything ready for the next guess. Any object with a ":stop" label is sent to it. This includes the animated parsing message on the bottom of the screen to vanish, as well as makes the letters in the guess grid place a slime above them to generate an outline of the correct color. By default this is red, but the final parser object can send messages during its parsing to make this call lead to green or yellow slimes. The counts object that sets the "MAX#" flags is reset to ensure that it's ready to go for the next guess. Lastly, the guess count object left of the guess grid is told to advance which tells the next set of objects in the grid to bind themselves to the source letters so that they'll respond to the keyboard object's inputs. This object checks your score and handles winning or losing once there are no more guess objects to activate.
I really didn't do a lot of cycle counting for my timing so there's yet another safety mechanism here to be sure that things don't activate earlier than anticipated and potentially break. The guess count object will loop until there are no more slimes detected before clearing the PARSING flags, setting the cursor for guesses to POS1 (thus enabling the keyboard object), and telling the first parser object to start scanning for a switch to parsing mode.
That's all there is to it! The guess grid has now rendered its colors correctly, and wins/losses/guess advancement are handled resulting in the game state returning to how it began. This process repeats until you win or lose the puzzle.
Despite several attempts at recording guessed letters and their colors like any proper Wordle-like, I was unable to pull it off and interested in getting the engine into a state where I could set up a template and write the script for puzzle generation. I found myself with not enough space to do what I wanted, and decided to not let all that remaining space go to waste.
I decided to create a little room for the player to play the game from, turning the engine into an abstract of a game being played on a computer from the player element's perspective. I quickly filled it with all the usual home office furniture. A plant, a rug (with a sleeping cat), a book shelf, a calendar, and of course a desk with some papers, peripherals, and a steaming mug of coffee. After all this hard work, it was nice to design something that was less mechanical and more cozy.
The bookshelf of course, lent itself to having the book titles be readable, but with my lack of a proper word list, I got a very smart and clever idea because I am very smart and clever. ZZT will let you read external text files, and we now know scrolls to have a limit of 1024 lines. I found myself a list of words used by the official Wordle, and quickly sliced it up into groups of 1000, turning the books into a manual word list. Feel free to look up your guesses before confirming them! (In my searches I found something stating that the word list is actually filtered down even more, which I'd believe given the most outcry I've seen of "wait that's a word?" with regards to Wordle was for "pleat", and looking at that full list there's a lot of unusual words.) Including a thirteen volume dictionary was far funnier than any book titles I'd have been able to make up.
I opted for an animated coffee mug based on my own mornings these past months starting with coffee and once sufficiently awakening, playing the game. I realized I could do some tiny hooks into the engine's code and do some silly little effects. After three guesses, the coffee begins to cool and animates more slowly. After the fourth guess the coffee cools entirely and the steam goes away. I began trying to come up with some ideas for other things to happen based on the guesses in the game.
Solve the puzzle on the first or second guess, and the guess count object will be blocked by a black wall to the north, detect this, and wake up the cat that begins to purr when you touch them. When the game ends, win or lose, touching the calendar will add a mark whether or not you solved the puzzle or not. Lastly, and realistically not going to happen unless you go out of your way to make it happen, the plant will wilt and turn yellow if you lose the puzzle without getting a single green letter in your guess history. I considered doing more things along these lines, but now memory was getting tight and I did realize that I could in fact include one more feature of Wordle.
The Emoji Results
The final addition was a small area where boulders matching the colors of your guesses would be placed in order to create the shareable emoji. Combine this with the calendar above, (which originally was going to use white text to draw out large numbers for the current date) and you get the current puzzle, and a record of how many guesses and what those guesses looked like. The objects in here are named after the source letter objects, but only contain the color selection code and use boulders rather than slime, followed by moving down a row after placement. It's not much, but I'm glad it's in there!
The Timer a.k.a. 24 Hours of ZZZ
Wordle is intended to be a once-a-day game, and my calendar structure mirrored this. An early discussion with asie after the release of Laworde led to the suggestion of updating the zip file with a new puzzle every day for a little bit of time, but I hate effort and decided to cram as many puzzles into the world as I could. I had hoped for 30 to fill the month of April, but forgot that the world has an effective size limit as well, not just the boards. This has a few fun implications. Firstly, the ZZT file is more than
512 kilobytes. Well, it was in the twenty-five puzzle incarnation until I discovered that opening the dictionaries would cause a crash. Now it's a more slimmed down 472! Compare that with Adventures of Link 2 at 430 kilobytes and Wordles of ZZT claims the title of the single largest ZZT file designed for ZZT v3.2. Second, that file is so large, that you would not be able to run it on retro hardware running ZZT v3.2 under MS-DOS. With only 640 kilobytes of memory available to a program like ZZT and the memory footprint of ZZT itself along with the operating system, I don't think it's actually accurate to claim this game is playable on MS-DOS. Perhaps using Worlds of ZZT v3.56 with its lack of an editor and some stripped down version of the OS itself? Regardless, it is very much compatible with the amount of memory Zeta permits ZZT to have access to.
At the time of writing I'm in the process of playing a few puzzles at a time and saving, hoping that the saves can be restored as the file size inflates from the emoji results causing increases in board sizes. (Actually handful of objects die when the puzzle is solved so your saves actually get smaller as the game goes on.
Memory aside, after each puzzle you're returned to this unlock screen where a timer counts down a full twenty-four hours before the door opens and the passage to the next puzzle becomes accessible.
I wasn't that cruel of course. The timer is real though, and if you set ZZT's speed to the maximum and wait it out the door will indeed open. I used the old "ten cycles per second" estimate from this old clock by Chronos30 in the ZZT Encyclopedia. I seem to recall that this isn't actually all that accurate, with the actual count being closer to nine cycles, with that running fast. I did not actually see how long it takes in real time. That's stupid. With the speed maxed out, it took more than half an hour for the timer to expire on my desktop. I love it.
While I think people would appreciate the joke, there's enough interest out there in esoteric Wordle clones that I didn't want non-ZZTers unaware of how to just ?ZAP past the door to be forced to stop after a single puzzle. After ten seconds (of ZZT time) a wall becomes fake which allows the player to move around the door. Out of fear that this might be too subtle, after a minute, the fake becomes an empty to make it more obvious how to proceed. Hopefully that's enough for anybody to be able to actually play the entire set of puzzles. I'm already pressing my luck with the speed and input method here.
This board also is responsible for clearing flags. This requires clearing the flags used in every puzzle solution, which I ended up just coding by hand. My puzzle generation was a mix of words I came up with, words specifically relevant to ZZT, and then realizing I kept using similar letter patterns some words picked by playing Hello Wordle and immediately giving up to see the solution. Then I shuffled the boards around a little bit in KevEdit before deciding it didn't matter too much what the order was.
Writing out that list of flags to clear made me curious if I had actually used every letter at least once in a puzzle. As luck would have it, I wound up using all but one of them with the original puzzle set, and one of the puzzles was able to be hand edited to change a single letter to the unused one to ensure full coverage. There's still definite common threads with a few words, but I don't know what you would really want to consider coverage good for just twenty three words in contrast to the thousands normally available.
Mid development, it was announced that The New York Times had purchased Wordle from Mr. Wardle for a figure in the low seven figures. I decided this event would make for a fitting end to the game. Plenty of people have already said what needed to be said about this. Wardle is trying to survive like anybody else in this capitalist society, and I absolutely do not fault him for selling. (Me, the armchair businessman, would have wanted a slice of money earned from NYT Games subscriptions in some form in addition to a lump sum, but I'm also pretty sure they could've just made "WORD-O-RAMA" with identical mechanics and not given him a cent.) We all knew what the purchase would mean though. The game being bloated up with trackers and ads, selling player information to marketing firms. Wordle represented the Internet as we had hoped it would be. Something positive, that brought people together as we all shared our little morning triumphs, close calls, and disasters.
Instead it is now yet another cog in a machine we have no control over and are forced to participate in. On the day of the announcement, I took a screenshot of the page with network traffic displayed, just so I could compare numbers when the migration happened. Sure enough it went from 300 kilobytes to 1.8 megabytes when it was moved to the NYT domain. You'll find your browser requesting files like "show-ads.js" and "/track/pxl". Those who care about such things have either stopped playing entirely or moved on to alternatives.
Wordle in its original form was self contained, and could be easily saved and uploaded elsewhere, and of course there are plenty of clones like the repeatedly referenced Hello Wordl. It inspired a small shared cultural moment for a month or two, and for once gave people something pleasant to share with others. I'm sure that the purchase of the rights to the original does mean that a legal team can demand copies of the original be removed, funneling more players into having their data packaged and resold again and again.
...There's a short YouTube series known as Hard Drop which looks at various Tetris clones and what they do with the Tetris formula. The last episode introduced me to Lockjaw, a highly customizable game about dropping blocks in a well to make lines which eventually was taken down over fears of litigation. Its author, Damian Yerrick removed the game, and left some powerful words on the former web page. They are something I have thought about a lot ever since seeing it for many games, including Wordle.
“In fact, though Mr. Pajitnov and his partner Henk Rogers want Tetris to become an internationally competitive sport, as Mr. Pajitnov mentioned in earlier in the same interview, a policy against free software makes it that much harder. Imagine if there were a Basketball Company LLC that could sue a city or school district for copyright infringement for putting a basketball court with correct dimensions into a city park or school gymnasium. There are multiple competing suppliers of basketball and chess equipment, unlike software for playing Tetris. This is why Chess is a sport and Tetris is not: Chess has no owner.” — Damian Yerrick
By the standards of video games today, Wordle is immensely simple. There are already countless derivatives. You can play Wordle without the once-daily format, or with adjustable word lengths. You can play Wordle not guessing words, but geographic locations or Pokémon. You can play multiple games simultaneously.
Want to play Wordle on the Commodore 64? You can.
Want to play Wordle on a fantasy console like PICO-8? Here you go.
Bored in your 3rd period study hall? Hope you packed your graphing calculator.
Only have a 386DX? Are you running Windows 3.11? No? Just MS-DOS? I've got good news.
SSHed into your employer's web server? It's available in bash (, and like DOS there are countless more).
Wordle has done what Tetris has and in record time. A simple version of the game could be a freshman college student's Compsci 101 final project. It can be made in Weave ZZT. It can be (compromisingly) made in ZZT v3.2. Doubtless it can be made in MegaZeux. You can think of a five letter word and ask a friend to guess it. It's out there. It will never not be. So what did the New York Times buy? What do players of their version gain, and what do they lose?
Maybe in a year it will all be forgotten. Hell, maybe by the time I release this article to the public it will be forgotten, but at least to me, Wordle represents the type of game that is becoming increasingly hard to find these days. It doesn't ask you to spend money. It won't share your data. You won't be chastised for skipping a day, or playing an alternative version for hours on end. It is a game accessible to a tremendous number of people, and no no matter what a certain large corporation does with it, its same soul can be found in an endless number different versions of the game. Josh Wardle set something loose into our culture and that can never be taken away.