Wordle In A Day

This week the US news networks have been reporting on a free web game by Josh Wardle called Wordle, similar to Jotto, developed as a personal project and open for all.

His variation has:

  • one chance to play per day
  • every one shares the same daily word
  • words have exactly 5 letters
  • you get six guesses
  • You are told
    • which letters match in place (green),
    • which letters match out of place (yellow)
    • which letters don't match anywhere (dark grey).

I decided to try to replicate his app in AI2 under the constraint that I would only spend a day on it. This thread is an explanation of how I did it.

Screen shots of original Wordle:

I will be posting in short bursts in this thread, to avoid overwhelming the board and the reader.

11 Likes

The first step in AI2 always involves the Designer.

This project has display elements that benefit greatly from Designer Copy and Paste.

  • The board of guesses consists of Labels, a Vertical Arrangement (60% width) holding six Horizontal Arrangements of identical Labels.
  • The alphabetic keyboard has a Vertical Arrangement containing three Horizontal Arrangements, with diminishing button counts of 10, 9, 8 buttons
  • (The Enter and Delete keys will be added later, after I have collected the keyboard Button components in the Blocks Editor.)

Here a link to the 11 minute YouTube video I recorded of my Designer session. I apologize for the silence, and the inability to tell where I was doing Copy and Paste.

  • I called my new app wordull, to avoid name infringement and to avoid overselling how interesting my app would be.
  • I left the top Horizontal Arrangement empty for the app title, menu, stats button, and help menu, which I left out.
  • I set up everything centered, starting from Screen1 down, in the hope that the centering would be inherited.
  • I applied naming and sizing as much as possible to the first of everything, so that they would be inherited by clones.
  • I left a '1' at the end of the first of everything that I was going to Copy/Paste, to take advantage of AI2's autonumbering of the component clones.
  • I took an initial stab at font sizing and button text in the keyboard. I should have left them all blank to ease my later Designer step (unrecorded) of filling in the keyboard text.

After adding all my Guess Labels and Keyboard letter Buttons, it was time to

  • export my .aia file to save my work
  • collect their components in the Blocks Editor.
1 Like

Collecting my Guess Label components and Keyboard Buttons in lists and tables ...

This table of Labels holds my guesses. I set it up to look like the run time display of the Labels, for ease of debugging and understanding. To fill it easily in the Blocks Editor, I added all the empty sockets using its blue Mutator first. Adding new sockets is easiest when all the other sockets are empty, since it doesn't matter where you insert them. (No worry about having to scroll offscreen to get to that last socket.)

My first row was set to Inline, so it would look like a row.

After all 6 * 5 = 30 empty sockets are ready to accept Label components, it is time to take advantage of another secret AI2 facility, typeblocking ...

At this point all the empty sockets in my blocks workspace are in the make a list blocks I recently added.

At my keyboard, I typed
el1 and a Label1 component block appeared and popped into my first empty socket. I continued typing
el2
el3
...
el30
until I had filled in the entire table.

My list of keyboard buttons was built the same way, btn1 ... btn26 .
I did not name them by their letter associations, since I had them already filled in their .Text values in the Designer.

This was a good time to code some setup procedures for any associated data structures ...




I added a dictionary to map letter text back to the associated keyboard button to help later on in the game when I needed to color keyboard buttons according to their hits and misses.
global blankColor
global missColor
matchColor
misplacedColor
I have used global constants for my color options to centralize them. That helps keep them consistent across the guess Labels and keyboard, and makes it easier to implement different color combinations at some future date.

2 Likes

The Word List

Searching Google for 'list of five letter words', I came across
https://www-cs-faculty.stanford.edu/~knuth/sgb-words.txt
by lucky accident.

Judging by the URL, that site belongs to Donald Knuth at Stanford University.
(A hard cover copy of his The Art of Computer Programming Volume 1 has held a place of pride in my stacks since the 1970s. Some books you never sell.)

After downloading the word file and uploading it into the Media folder, it is time to deal with file loading and general initialization ...


FiveLetterWordsCapture
(A quick peek at the file in Notepad to check for surprises. No surprises.)

I admit a blunder here.

This would have been a good place to have upshifted all the text before splitting it at \n, so that the letters would match the .Text values in my keyboard. If you fail to plan, you plan to fail. I pay the price for my sloppiness later on, when I go to match letters between the word of the day and user guesses.

2 Likes

Did I already play Today?

global alreadyPlayed

I had a choice to make here, between making checkAlreadyPlayed a value procedure or having it set a global variable as its side effect, a poor programming practice. In my rush to test, I chose this option to let me disable the procedure call so I could test as I coded.

Also, in retrospect, I could have done this check in Screen1.Initialize, before deciding to load the words file. That could be tweaked later (which means never.)

3 Likes

Getting The Word Of The Day

global wordOfTheDay


tagOfTheDay

One of the game requirements was to use a common word of the day.
For lack of a central administrator, that leaves it up to the app itself to coordinate creation and dissemination of the daily word.

That is done by means of a CloudDB tag that contains the current date (yyyyMMdd) and a game-specific prefix (wordull/). The value is the word of the day, if any.

(I left off any cleanup of prior days' entries. What's a few bytes among friends?)

At this point, all the setup is complete except for pointing to the first row of the Guesses table ...
global currentGuessIndex

2 Likes

Typing in a Guess


This procedure returns the one dimensional list of Labels in the current guess.


This is a generic any Button Click event, intended to service my keyboard.
I have separate button Click Events for my Enter and Delete keys, so I added a test for
if notAlreadyHandled and a check that this button is in my keyboard buttons list.

When a letter button is pressed, its letter should appear in the first blank Label in the current Guess row. That requires a bit of searching and a local variable firstBlankLabel to keep track of our search result. We start at the end of the label row and work our way backwards, remembering each blank label we encounter. At the end of the loop, firstBlankLabel has our best guess where to drop the letter value, which we get from the Button.Text value.


The Delete button has similar logic, searching for the last nonblank Label in the current guess and clearing it. Here we scan from right to left. If they are all blank, we clear the first Label for simplicity.

3 Likes

Good job !!
I crested an app inspired to Wordle with Kodular:

The ENTER Button, finally


To help in the checking, we need a downcased version of the current guess to check against the word list. This game's rules require that each guess be a word, not just a cheat like aeiou. In this procedure, I pay the price for not having upshifted the word list when I loaded it. I downcase the guess before comparing it against the list of words.


(To read these blocks, right click on them and Open Image in New Tab. Then click on (+) to zoom in on them.)

If the guess is not in the word list, Notify them without penalty.

A local variable correctCount (initially 0) helps keep track of how many correct letters are in place, for deciding if the player made a winning guess.

It is time to loop through the letters of the guess, by index n. We need the index to help us match corresponding positions in the the word of the day and in the current guess.

We extract the current guess letter at index n.

We also keep a local color variable, to hold the current letter match result color, initially mismatch grey.

Again, I pay the price for not having made my word list the same case as my keyboard letters. The Labels are Upper Case and my word is lower case, so I downshift the current guess letter.

Time to compare the guess letter against the corresponding position in the word of the day, using the text segment block and the text compare block.

If we got a match, pick green for a good match and save it for later, and increment our match counter.
Otherwise, we have to check if the chosen letter appears elsewhere in the word of the day.
We use a text contains test, and if it is found, we set our color to yellow.

At this point, we have our color variable ready to drop into the Background Color of our Label and the keyboard Button for this letter. The Label is at index n, and we have to go through the dictionary we loaded at setup time to get to the keyboard Button for that letter.

At the end of the loop, we check our match count.
If all 5 match, we call procedure win to announce our win.
Otherwise, we check if have more guesses available, and increment the guess index.
If we reached our guess limit, call procedure lose to rub it in.


As a reward for following this far, here is the source code:
wordull.aia (30.6 KB)

Feel free to add whatever enhancements you like to it.

4 Likes

Your app has more features than mine, and is more polished.
Welcome to the Wordle clone club!

2 Likes

Your guide is very simple to understand.
You are a great teacher :+1::+1:

Great stuff ABG.

To add to the mix herewith the game layout generated with a dynamic component (CompCreator)
No logic applied. I called this one "Wuddle".

SCREEN

BLOCKS

AIA

Wuddle.aia (16.7 KB)

3 Likes

The end result looks nice, but it feels more like code than visual design.
Do all those text boxes for attribute and component types need to be hand typed, or does the extension supply them?

I have updated this extension.
Use the CreateFrom with a template, no need to hand type the attribute and component type.

2 Likes

Yes, all hand typed. If you have an actual component you can use its blocks to create, and then you can use the anycomponent blocks for property setting.

I actually have an error (of omission) in the blocks, will update when the AI2 server comes online...

[edit - blocks and aia now updated]

With ref to my effort, now done the logic. I didn't look at @ABG 's blocks on purpose. Ended up with a couple of small procedures and one very big anyButton.Click event, which handled clicks for the letter buttons (Q,W,E etc.), the ENTER button, and the back button ( <x]). The tricky part for me was the colouring selection of the grid and the keypad, especially for duplicate letters (either in the word, or in the guessing word), but hopefully solved it. Think I have ironed out all the bugs. This is now installed on my wife and son's phones, and a quiet has descended on the house :slight_smile:

Player can start a new game at anytime, and the app will show the word if not guessed. If there is a double/duplicate letter in the word to be guessed, Wuddle will only show that one is present, you have to figure that part out :wink:

Thanks @ABG for the idea :slight_smile: - kept me quiet for a day as well ....

SCREEN

AIA

WuddleFinal.aia (65.1 KB)

APK

for those that won't need an aia or blocks to see the logic....
Wuddle

5 Likes

Brilliant and very educational. I am adapting Your app in French. Should someone be interested in a word file in french, along with notifications and keyboard in french, please ask me.

1 Like

@ABG try this

Octordle
:red_square::red_square: THING - DECAL
:clock12::red_square: SCALY - WHISK
:red_square::clock11: HENCE - SPILT
:red_square::three: SIXTY - PETAL

:white_large_square::white_large_square::green_square::white_large_square::white_large_square: :white_large_square::white_large_square::white_large_square::white_large_square::yellow_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::green_square::yellow_square::white_large_square::green_square:
:white_large_square::white_large_square::yellow_square::white_large_square::white_large_square: :white_large_square::green_square::white_large_square::green_square::green_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::green_square::white_large_square::green_square::green_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::yellow_square::yellow_square::yellow_square::white_large_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::yellow_square::yellow_square::yellow_square::yellow_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::yellow_square::yellow_square::yellow_square::white_large_square:
:white_large_square::white_large_square::white_large_square::yellow_square::white_large_square: :white_large_square::white_large_square::white_large_square::white_large_square::green_square:
:white_large_square::white_large_square::green_square::white_large_square::white_large_square: :white_large_square::white_large_square::white_large_square::white_large_square::green_square:
:white_large_square::white_large_square::green_square::white_large_square::white_large_square: :white_large_square::white_large_square::white_large_square::yellow_square::yellow_square:
:white_large_square::white_large_square::green_square::white_large_square::yellow_square: :white_large_square::white_large_square::white_large_square::yellow_square::white_large_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::yellow_square::yellow_square::yellow_square::white_large_square:
:white_large_square::white_large_square::yellow_square::white_large_square::white_large_square: :white_large_square::green_square::white_large_square::green_square::green_square:

:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :white_large_square::white_large_square::green_square::white_large_square::white_large_square:
:white_large_square::white_large_square::green_square::white_large_square::yellow_square: :white_large_square::white_large_square::white_large_square::white_large_square::white_large_square:
:white_large_square::white_large_square::white_large_square::yellow_square::yellow_square: :white_large_square::white_large_square::white_large_square::white_large_square::white_large_square:
:green_square::white_large_square::white_large_square::yellow_square::yellow_square: :yellow_square::white_large_square::white_large_square::white_large_square::white_large_square:
:green_square::yellow_square::yellow_square::white_large_square::white_large_square: :yellow_square::white_large_square::white_large_square::white_large_square::white_large_square:
:green_square::green_square::green_square::green_square::white_large_square: :yellow_square::white_large_square::white_large_square::white_large_square::white_large_square:
:green_square::green_square::green_square::green_square::white_large_square: :yellow_square::white_large_square::white_large_square::white_large_square::white_large_square:
:green_square::white_large_square::white_large_square::white_large_square::yellow_square: :yellow_square::white_large_square::white_large_square::yellow_square::white_large_square:
:green_square::white_large_square::white_large_square::green_square::white_large_square: :yellow_square::white_large_square::green_square::white_large_square::white_large_square:
:green_square::white_large_square::white_large_square::green_square::white_large_square: :yellow_square::white_large_square::green_square::white_large_square::white_large_square:
:green_square::white_large_square::white_large_square::green_square::white_large_square: :yellow_square::white_large_square::green_square::white_large_square::white_large_square:
:green_square::green_square::green_square::green_square::green_square: :yellow_square::white_large_square::white_large_square::white_large_square::white_large_square:
:black_large_square::black_large_square::black_large_square::black_large_square::black_large_square: :white_large_square::white_large_square::white_large_square::white_large_square::white_large_square:

:white_large_square::white_large_square::white_large_square::white_large_square::green_square: :yellow_square::white_large_square::green_square::white_large_square::white_large_square:
:white_large_square::green_square::white_large_square::white_large_square::white_large_square: :yellow_square::white_large_square::white_large_square::white_large_square::yellow_square:
:white_large_square::green_square::white_large_square::white_large_square::white_large_square: :yellow_square::white_large_square::yellow_square::white_large_square::yellow_square:
:white_large_square::green_square::white_large_square::white_large_square::white_large_square: :green_square::white_large_square::white_large_square::white_large_square::yellow_square:
:white_large_square::white_large_square::white_large_square::yellow_square::white_large_square: :green_square::white_large_square::yellow_square::white_large_square::white_large_square:
:white_large_square::yellow_square::white_large_square::white_large_square::green_square: :green_square::white_large_square::white_large_square::green_square::white_large_square:
:white_large_square::yellow_square::white_large_square::white_large_square::white_large_square: :green_square::white_large_square::white_large_square::green_square::yellow_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :green_square::green_square::white_large_square::yellow_square::yellow_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :green_square::green_square::green_square::green_square::white_large_square:
:white_large_square::white_large_square::white_large_square::white_large_square::green_square: :green_square::green_square::green_square::green_square::white_large_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :green_square::green_square::green_square::green_square::green_square:
:white_large_square::yellow_square::white_large_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:white_large_square::green_square::white_large_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:

:white_large_square::white_large_square::yellow_square::white_large_square::white_large_square: :green_square::white_large_square::white_large_square::white_large_square::yellow_square:
:white_large_square::white_large_square::white_large_square::white_large_square::white_large_square: :green_square::green_square::yellow_square::white_large_square::green_square:
:white_large_square::white_large_square::yellow_square::white_large_square::white_large_square: :green_square::green_square::green_square::green_square::green_square:
:green_square::white_large_square::white_large_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::white_large_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::white_large_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::white_large_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::white_large_square::yellow_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::yellow_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::yellow_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::yellow_square::white_large_square::yellow_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:green_square::white_large_square::white_large_square::white_large_square::green_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:
:white_large_square::white_large_square::yellow_square::white_large_square::white_large_square: :black_large_square::black_large_square::black_large_square::black_large_square::black_large_square:

2 Likes

@ABG Hello,

I haven't used App Inventor in a while. I'm following your tutorial here and I'm loving it. My problem (sorry I'm not very smart) is I can't find the when do control button in my controls. Is there anyway you can direct me in where to find it. also when initializing the screen I can't find the filWords to call. What am I missing? Sorry I'm not very smart. Rick

Actually it is File component renamed to fillWords