Plordle: yet another Wordle clone

I wanted to try pulp out and I didn’t have a good idea for a game so I thought I’d make a Wordle clone with a very limited Playdate-related vocabulary!

Plordle! Gameplay example

It was really hard and a lot of fun!

Files

The game (includes a very limited word list of 7 Playdate related words):
Plordle.pdx.zip (64.8 KB)

The Pulp json file:
Plordle.json.zip (30.3 KB)

A python script for generating a different word list:
word_gen.py.zip (848 Bytes)

See also

Ethan's take on Wordle!

Controls

  • Use the d-pad to move around the keyboard
  • Enter a letter by selecting it from the keyboard and hitting A
  • Submit a guess word by hitting A on the checkmark
  • The thing next to the checkmark is supposed to be backspace. Use it to delete the last letter you entered (you can also hit B)
  • Hint legend:
    • A letter with a white background = right letter in the right spot
    • A letter in a circle = right letter in the wrong spot
    • A letter in a square = wrong letter

Missing features

  • More words
  • Using the crank :slightly_frowning_face:
  • A dictionary to make sure you're inputting actual words (there are thousands of words and I don't know how I'd do it without using arrays)
  • A summary screen with your score for you to screenshot and share?
  • Persistence?
  • Sound effects

(Also something seems to be off with my flip animation but I can't put my finger on it)

Implementation details

Limiting player movement

The player can only move around the keyboard (and the submit/delete keys below it). This happens via calls to adjustHorizontalPosition or adjustVerticalPosition based on event.dx and event.dy within the player’s update event.

These adjustments use goto if the player tries to move outside of the keyboard area to move it back in.

The problem is that goto triggers an update call, so then I have an update call inside an update call and I need to ignore one of them to avoid running the same side effects twice (and avoid an infinite loop). This was the first bit of logic I wrote and I think I could have done it better.

Keyboard tiles

The keyboard tiles have two frames: one for their regular appearance and one for the selected appearance (which is pretty much the same but with inverted colors). They have fps = 0 so that they don’t flicker all the time.

An example of a letter tile with two frames

The keyboard tiles themselves don’t actually switch between the frames: the player swaps to them when it “visits” their tile:

tile = name event.x,event.y
swap tile
frame 1

(This is in the player’s update event)

Grid squares

Each grid square is made up of four tiles (top left, top right, bottom left, bottom right). Each square can have six possible states:

  1. No letter yet
  2. No letter, highlighted (2px border) (this is where the next letter you enter will go)
  3. With letter, highlighted (not submitted yet)
  4. With letter, after submission: hit (right letter in the right place in the goal word)
  5. With letter, after submission: present (right letter in the wrong place in the goal word)
  6. With letter, after submission: miss (letter not in the goal word)

The letter grid with indication of the six different tile types

The first two cases are handled with four tiles (top left, top right, bottom left, bottom right) that have two frames and fps = 0 (just like the keyboard keys):

An example of a grid tile with two frames

I manually switch between them using

tell x,y to
  frame 1
end

Then each of the last four cases is handled with a different tile. Cases 4-6 are where the flipping animation happens, which I trigger using

tell x,y to
  play "{tileName}"
end

This allows me to only play the flipping animation once.

Too many tiles!

Cases 3-6 above mean 4 different cases (not submitted yet, submitted and hit, submitted and present, submitted and miss) for 4 parts of the square (top left, top right, bottom left, bottom right) for each letter. That’s 4x4x26 = 416 tiles.

However, since each letter is split in four this allows for some optimization. Here’s what the four A tiles look like:

The top left tile for A, C, G, O, Q, and S is identical, and so is the top right tile for A, D, O, P, Q, and R. By finding these commonalities I narrowed the number of tiles down from 416 to “only” 200!
Here’s a handy tile diagram

In code it looks like this:

if letter=="A" then
  topLeftIndex = 1
  topRightIndex = 1
  bottomLeftIndex = 1
  bottomRightIndex = 1
elseif letter=="B" then
...
elseif letter=="T" then
  topLeftIndex = 6
  topRightIndex = 2
  bottomLeftIndex = 11
  bottomRightIndex = 0
elseif letter=="U" then
...

I named the tiles topLeft 0, topLeft 1, ..., topLeft 10, topRight 0, topRight 1, and so on, so my code looks like this:

call "calculateTileIndexes" // this is the massive if statement
	
tell x,y to
  swap "topLeft {topLeftIndex}"
end
x += 1
tell x,y to
  swap "topRight {topRightIndex}"
end
y += 1
tell x,y to
  swap "bottomRight {bottomRightIndex}"
end
x -= 1
tell x,y to
  swap "bottomLeft {bottomLeftIndex}"
end

and then similarly when it's time to flip the grid squares to show the hints (hint = hit/present/miss) I do a similar swap for tiles named topLeft 0 hit, topLeft 0 present, topLeft 0 miss, etc. and the code for that is

staticTile = name x,y
tell x,y to
  play "{staticTile} {hint}"
end

I made a separate test room to make sure I hooked everything up correctly. I gave the tiles a low fps initially to make them easier to debug, and then once I was happy with them I edited the json file to raise the fps on all the tiles at once. This made the test room unbearable because everything is moving around super fast.

Here's a frame from the test room close to the end of the flipping animation cycle:

You can see the miss animations on the top left, the hit animations on the top right, and the present animations at the bottom.

Array workaround

There are no arrays in pulpscript, which meant I had to write some ugly code. Here are the arrays I hoped to use in the game:

  • a list of goal words, from which to pick a word at random
  • the selected goal word (an array of length 5)
  • the current guess word (an array of length 5, to compare against the goal word to figure out the right hint for each letter)
  • the indices for the tiles that make up each letter

I came up with two possible solutions:

  1. using tiles!
  2. using huge if statements

By "using tiles" I mean storing data in tiles that are visually blank but whose names are valuable. Here's an example:

// write the current goal word into the first row of the screen:
tell 0,0 to
  swap "H"
end
tell 1,0 to
  swap "O"
end
tell 2,0 to
  swap "S"
end
tell 3,0 to
  swap "E"
end
tell 4,0 to
  swap "N"
end


// get the current letter of the goal word:

thirdLetter = name 2,0
// thirdLetter == "S"

You can do the same for arrays of numbers using frames:

// set
tell 4,0 to
  frame 82
end

// get
value = frame 4,0 // value == 82

I decided not go with this approach, but the Wordle clone that @ethan made uses tiles to store the word list and that seems really cool!

In the end I went with a bunch of long if statements, like those in the tile mapping from the "Too many tiles!" section above.

Secret tile 1: wordPicker for goal word selection

The way I pick a goal word is again using a massive if statement:

on load do
  wordIndex = random 17
    if wordIndex==0 then
      goal1 = "H"
      goal2 = "O"
      goal3 = "S"
      goal4 = "E"
      goal5 = "N"
    elseif wordIndex==1 then
      goal1 = "H"
      goal2 = "E"
      goal3 = "L"
      goal4 = "L"
      goal5 = "O"
    elseif wordIndex==2 then
      ...

where 17 is the number of possible goal words. This doesn't scale super well for an actual Wordle dictionary, which has thousands of words (the number of lines of code is the number of possible goal words times six, give or take) and it would require an automated script (see below).

I put the code for goal word selection in a separate sprite called wordPicker so that if someone wants to make a clone of this game and use a different set of words they'd only need to change this specific part of the game.

The wordPicker tile is blank and sits in the top right corner of the room.

Customizing the word list

If you want to use this game with a different set of goal words you can use the python script in the files section above to generate the script for wordPicker.

You'll need to edit the words array on line 3 and then run it in the terminal. It will print out the code you need to put in the wordPicker script.

If you're on a Mac you can put the output in your clipboard using:

python3 word_gen.py | pbcopy

and then paste it in the pulp editor.

Secret tile 2: columnBoy for grid management

This is a tile that deals with grid-square events. I called it columnBoy because I didn't want to call it columnManager and I was too tired to think of something that makes sense.

columnBoy is an item tile (so that it can have events). it's visually blank and there are five of them in the room: one for each column.

columnBoy has events like focus, unfocus, enterLetter and flip. These are all events that I could have put in the room script, but I didn't want to put all of the game logic in one massive file.

The interesting bit of columnBoy is the flip event: it calculates the right hint for the grid square, switches the tiles to the appropriate animation, then waits a bit and calls the flip event for the next columnBoy. It also checks if it's the last column, in which case it tells the room that all hints have been revealed so that the game can proceed.

Function argument naming

I used some custom events as functions that run complicated logic that's used in a bunch of events. I named these calculate[SOMETHING], for example calculateLetterCoordinates or calculateHint.
In order to avoid accidentally changing their inputs/outputs I used a little prefix for them: c_letter, g_guessLetterIndex, etc.
The first letter prefix specifies the entity that uses that variable (g = gameRoom, c = columnBoy).

9 Likes

Wow, love the polish of the animations and very cool to see the different approach used!

Awesome “dev diary” write up to boot!

The dictionary problem for identifying real words does seem tricky to do, I also punted on it.

Maybe it is possible to spatially encode a trie or generate a giant 5-deep nested if block for that :sweat_smile:.

Hey, this was a lot of fun! i was watching tv with my wife and started asking her "what's a five letter word, choose all different letters" and she totally got into it and took the laptop from me to play. Great and unique idea for the playdate. Good luck to you, excited to see more.

The lack of arrays was also something I struggled with. Ethan's Wordle and my NineFind both used the approach of creating Rooms with "variables" in the rooms as a way to get around that, but not sure that's scalable or the best approach

this was fun to play!!