How I made 2048 for the Playdate


I started writing this post in late 2022 and then… I never posted it. I guess my imposter syndrome as a Playdate developer was kicking very hard. I stumbled upon it recently and think there might still be some value for some people. So almost two years later, I hope you'll enjoy this read.


Earlier in 2022, I got my PlaydateI fell in love with it the moment it was announced. I even made an interactive email based on it back in 2019. One of the biggest selling point for me is the fact that anyone can make and sideload games on the device. Thinking about it, this may be the first time I own a gaming console I can (legally and easily) make games for.

In 2022, I’ve been hard at work learning Lua and how to use the Playdate SDK. In august 2022, I released my first “app” for the system: Playorama, a video player (in which you can control video playback with the crank). After that project, I really wanted three things:

  1. To make a game.
  2. To learn more about using sprites.
  3. To make something fast, in a week or two, avoiding the three months of development it took me to release Playorama.

(Only two out of the three last statements did happen.)

This is how 2048 came to my mind. The web-based game was a huge hit almost a decade ago and spun a ton of clones (despite itself being a clone of Threes! on iOS). In october 2022, I released my adaptation of 2048 for Playdate. And although I’m still very new to this, coming from web development (and more specifically HTML emails), here are three things that I think are pretty interesting about this project.

Using the crank

I really wanted this game to support the crank. So I thought it could be fun to be able to use the crank instead of the d-Pad to move tiles around. Moving the crank up would move the tiles up. Moving the crank towards you would move the tiles left. And so on.

And to be able to visualize what the crank is doing, I wanted to have a little cursor moving around the grid. This is basically about moving a sprite around on specific coordinates. And in order to do this, I used… an Animator! According to the docs, animators are “lightweight objects that keep track of animation progress. They can animate between two numbers, two points, along a line segment, arc, or polygon”.

So first I created a polygon representing the outline of the grid. And then I created an animator using this polygon and set to a duration of 360.

self.animator = playdate.graphics.animator.new(360, {polygon}, playdate.easingFunctions.linear)

Then, all I have to do is get the crank current absolute position and get the animator value at that angle.

local angle = playdate.getCrankPosition()
self.animator:valueAtTime(angle + 45)

I used a + 45 here because I created my polygon from the top left. But the angle of the crank starts from the top center.

Here’s what it looks like in the finale version.

My first bug report

Although I did a beta version and asked for feedback on the Playdate Squad discord, the first feedback I got after release on Itch was… embarassing:

Hey, I got a bug. Somehow it combined a 1024 block with a 256 block and gave me a 512 block.

Oops. That’s pretty bad. One of these “You had one job” moment. The worst part is that, I’m fairly certain I had seen this bug early in development. But I didn’t manage to reproduce it consistently. And because I had so much going on at that time, and did so much refactoring since, I thought this bug was gone in the process.

Well… Nope.

After a lot of testing to reproduce this consistently, I figured it out. It all came back to how I managed collisions.

if other.mustBeRemoved or other.mustBeMerged or other:getTag() ~= self:getTag() then
    return playdate.graphics.sprite.kCollisionTypeFreeze
else
    return playdate.graphics.sprite.kCollisionTypeOverlap
end

See that getTag()? There was my problem. According to the SDK, a sprite’s tag is a “an integer value useful for identifying sprites later, particularly when working with collisions”. So for each tile, I assigned a tag equal to the tile’s value.

function Tile:init(value)
    -- …
    self:setTag(value)
    -- …
end

That seemed simple enough and straightforward at the time. A 256 tile would have a tag set to 256 as well. And a 1024 tile would have a tag set to 1024. So when each tile’s tag value would be compared, there’s no way they would be equal and thus would not merge. Right?

Little did I know that the integer used for the tile is an 8-bit integer. This was mentioned in the C SDK but completely absent from the Lua’s one at the time. An 8-bit integer means it can only hold values from 0 to 255. Any value superior to that would be the remainder of it divided by 256.

And now everything made sense. With my code, a 256 tile would have a tag set to 0. And so would a 1024 tile. And thus they would merge.

The fix was simple. All I needed was to set a value to tags between 0 and 255. Since the game is based on power of twos, I could use the binary logarithm of the tile’s value. So a 256 tile now has a tag set to 8. And a 1024 tile has a tag set to 10.

function Tile:init(value)
    -- …
    self:setTag(log2(value))
    -- …
end

According to a quick search, the largest theorical value of a tile is 131 072, making it a tag of 17. So there’s still enough room before hitting that 8-bit limit!


I hope you enjoyed reading this post. You can download 2048 for Playdate for free on Itch right now.

Files

2048.1.0.4.zip 51 kB
Jun 08, 2023

Get 2048 for Playdate

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.