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!
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
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
- 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.
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:
- No letter yet
- No letter, highlighted (2px border) (this is where the next letter you enter will go)
- With letter, highlighted (not submitted yet)
- With letter, after submission: hit (right letter in the right place in the goal word)
- With letter, after submission: present (right letter in the wrong place in the goal word)
- With letter, after submission: miss (letter not in the goal word)
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):
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:
- using tiles!
- 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).