In case you haven't heard of it, Wordle is a puzzle game that vent so vial during Covid the New York Times bought it from the developer (an independent who made it for his friends and family). The game is simply a word guessing game, with each letter being flagged as, right place, wrong place, or not in the word.
I had already made one Power Automate game, Canyon Escape, and wanted to come up with another, but there is one big problem, how do I create a UI for Power Automate. Luckily I had an idea while trying to come up with a unsubscribe flow, the http trigger can return not just JSON but html, and that was the beginning of the crazy idea π
My plan was to use the HTTP trigger and response to send a web page, in other words turning Power Automate into a mini server.
I would use 2 http trigger flows, one which returned the webpage, and a second that would communicate with the webpage.
I had a few challenges to overcome:
- Remembering Progress
- Updating Word Each Day
- Checking Word
- Time zones
If you read my Power Apps does Santa blog you will know I hate timezones
Setup
Before I start I want to give a quick overview, the html is 5 by 6 grid of boxes that will hold the previous guessed characters. There is also 5 input boxes that accept the guess, and then a button to submit the guess.
I'm not going to lie, I had to use some JavaScript to get the game to work, I'm not going to go into it much as that is not the point of the blog (and my JavaScript skills are not good), but the code is available to download and a quick explanation of what it does is:
- Get Current Status
- Update Grid HTML
- On Button Press Concat each character input & completed HTML Grid, and send to flow
- Uses returned HTML Grid to update Grid HTML
With that all said let's get into it.
1. Remembering Progress
A key requirement in the game is to remember the users current game, as the are only allowed to play once a day. Luckily there is a cool thing in JavaScript called LocalStorage, this remembers any string value you need. It is linked to the top level domain, which means it your page will be able to access it as long as you don't end up with a different Azure domain (which is quite possible), examples are:
- https://prod-21.westus.logic.azure.com/
- https://prod-101.westus.logic.azure.com/
- https://prod-10.eastus.logic.azure.com/
So my variables stored where:
- status - check if ran out of guesses or won
- date - date of game
- fullBoard - html of 5 by 6 board
- partBoard - html of 5 by i board, where i is rows with guesses
- i - number of guesses/rows completed
You can see the LocalStorage in the dev tools under the Application tab
2. Updating Word Each Day
I had a couple of options here, I could run a scheduled flow at 00:00 and update the word, or I could do the first run of the day and use that as the trigger.
I decided to go with the simplest approach (and least impact on game load). A third schedule flow would run every day, generating the word for that day. The next question was, where do I store the date and word. The obvious would be SharePoint or Dataverse, but that's complexity through 2 systems or spinning up custom table. But what if I could use an existing table, and the perfect one was the Environment variable table. I was going to use a flow to update its own environment variable, pretty cool π
The flow would randomly pick a new word and store it and the new date in an environment variable, using the Dataverse update item action.
We can get the value easily, as its in the dynamic value list, but to update it is a little more complex. We could do a filtered get items, but as that requires the Environment Variable definition list to link the variable name to the value tables value, I went with the easy way.
I simply stored the environment variables GUID in another environment variable and used that. To find the guid we just need to look at the Environment Variable Value table. There we want to sort by newest and show below columns.
The environment variable definition is the name (not display name) and the environment variable value is our GUID we need. Callout here, there won't be a row created in the value table until a value is added. Creating a variable only creates a row in the definition table (the default value goes there too), so if you want a value you need to hit that create value button and save it.
To select a random word we grab the text file with the words (as I wanted a Power Platform flavour our good friend Chat GPT provided the list). The below expression then splits it and picks one at random.
split(body('Get_file_content_words'),',')[rand(0,99)]
3. Checking Word
The HTTP trigger has 3 inputs, row so that it knows how many empty rows to add, guess, and board. The board is the rows of current guesses.
{
"row": 1,
"guess": "merge",
"board": "<div class=\"square location\" id=\"square-0-0\" >e</div><div class=\"square\" id=\"square-0-1\" >x</div><div class=\"square\" id=\"square-0-2\" >t</div><div class=\"square location\" id=\"square-0-3\" >r</div><div class=\"square\" id=\"square-0-4\" >a</div>"
}
The idea is to take the completed rows, add a new row with the guess, and then add on the remaining blank rows. As example the above has one guess already (extra), the guess is "merge", so it will add that row, then as it knows it has 1+1 row, it will add 4 blank rows.
So what we are doing is sending the board html back, there is no logic checking on the web page, it simply adds the html and the status text to the page.
I could have gone with a html table action, but that adds complexity in formating the characters (green for right, yellow for in word but wrong location), and it had a header row. So I used css Grid, which automatically wraps divs, I just send 30 div squares (5*6) and it automatically wraps into the right rows and columns. My clever idea was to use a DoUntil, with the escae being the HTML string having 30 'div's' in it (length(split(variables('sHTML'), '<div'))
). But I learned something hard, Power Automate is slow.
13 seconds for a 23 item loop is just terrible π (and sometimes it took even longer). So I had to try a less elegant approach. I made a simple array, the first item had rows 2-6 of html, the second had rows 3-6, then 4-6 and so on.
I then use the row integer to select the right item in the array, and concat it to the current rows + new row.
concat(variables('sHTML'),json(outputs('Empty_squares'))[body('Parse_JSON')?['row']])
Now for the fun bit, validations, and I have 4 of them:
- Is it a valid word
- Does the character match the correct character
- Does the character match another character in the word
- Do all characters match
The first one was more easier I then I thought, https://dictionaryapi.dev/ has a free api that returns info on any English word. The https://api.dictionaryapi.dev/api/v2/entries/en/<word>
api returns a 402 if it is not a valid word. So I check for this, if it is a 402 I use a escape condition and return "Not a valid word".
The second is the main bit. We split both words into an array of characters with the chunk expression. Then we use the array position to compare each (the iCounter is used to select each item).
toUpper(items('Apply_to_each'))= toUpper(chunk(body('Parse_JSON')?['guess'],1)[variables('iCounter')])
If it matches we add the div square to our html with the addition of a 'green' class. That way when rendered on the web page it will be green to show its correct. We also set bFound to true so that we skip the next 2 conditions (wrong location and no match), and increment iMatch (so we know if we match all and complete game).
The third is the hard one. I can't just use a condition to check if it contains that character (my original idea), as if the character was in the guess more then once it could be a false match.
If the correct word was 'sound', 'tools' would return the first 'o' as green and the second as yellow.
To fix this we have to do a crazy expression (I knew my Excel days would come in useful π). What we have to do is find the first instance of that letter in the word and then check to see if it has an exact match with the guess. If it does then we know not to turn it yellow.
The expression looks like this:
equals(
chunk(
toUpper(parameters('WordVar-PowerWordle (wd_WordVarPowerWordle)'))
,
1
)[indexOf(
toUpper(parameters('WordVar-PowerWordle (wd_WordVarPowerWordle)'))
,
chunk(
toUpper(body('Parse_JSON')?['guess'])
,
1
)[variables('iCounter')]
)]
,
chunk(
toUpper(body('Parse_JSON')?['guess'])
,
1
)[indexOf(
toUpper(parameters('WordVar-PowerWordle (wd_WordVarPowerWordle)'))
,
chunk(
toUpper(body('Parse_JSON')?['guess'])
,
1
)[variables('iCounter')]
)]
)
This covers if the character match is before, but what about after.
We just need to duplicate the monster expression and change out 'indexOf' for 'lastIndexOf'.
Finally the full validation is did you get the whole word right, and this one was easy. You can see above the Increment iMatch when its an exact match. So we just check at the end that iMatch = the word length (5 in this case).
if(equals(variables('iMatch'),5),
'<p>Congratulations you did it</p>'
,
if(equals(body('Parse_JSON')?['row'],5),
concat(
'<h1>'
,
body('Get_Word')[0]?['word']
,
'</h1><p>try again tomorrow</p>'
)
,
''
)
)
4. Time zones
Oh man how did I get another time zones leaning experience. Anyway the issue I had to figure out was that the game starts at midnight each day, and obviously different places hit that at different times. This means 2 people can be playing the game and getting 2 different answers. So my original way of holding the date and the word in 2 environment variables will not cut it now (this is why you should always plan your flows!).
The way I decided to fix this is to change the 2 environment variables to one json variable. This will become an array:
{
"value": [
{
"date": "08/03/2024",
"word": "hello"
},
{
"date": "09/03/2024",
"word": "world"
}
]
}
The idea is each day we are going to delete the first item and add a new one after.
First we do the same trick, get the words, split and select a random one. I then build an object to match the above.
Then we take the last item from the environment variable and create a new array with that and our new object.
{"value":[
@{parameters('Combined-PowerWordle (wd_CombinedPowerWordle)')?['value'][1]},
@{outputs('New_Word')}
]}
We also have to update the Main flow, replacing the word environment variable with a filter to find the right date.
Our monster expression then has to be updated with:
parameters('WordVar-PowerWordle (wd_WordVarPowerWordle)')
being replaced with:
body('Get_Word')[0]?['word']
And that's it, I decided to add a nice lick of paint on it by updating the CSS and added a button to another flow to show the list of 100 words.
You can play it here (for as long as my dev tenant lasts, also the link doesn't work in the forem app)
After all this what did I learn, well I still hate time zones for starters, but:
Power Automate is slow, 13 secs for a simple loop, and its inconsistent. These runs below were all the same values, but look at the different times.
So it's not the best for responsive things like games, but one off runs its fine. I'm already thinking of my own custom form (one run so doesn't matter), I have had issue where I wanted a public MS form but, with a unique reference to link back, and upload a file. MS forms wont allow this (no parameter and no file upload unless user is on your tenant).
Looking at my monster expression and the way I manipulated the array, I know definitively now that Power Automate Expressions are dam powerful.
As always the solution and all of the code is here to download and play with.
Top comments (1)
Thats bloody awesome !!!! @wyattdave ... Love the explanation behind complexity of logic to validate and clever use of grid data and managing using array !!!!