Andy Presents: Cribbage is a cribbage game for Playdate. You can purchase it now at https://play.date/games/andy-presents-cribbage/.
This is the first game I've ever released. I'm so impressed by people who make games, especially the members of the incredible Playdate community. You can view their games at Playdate Catalog.
I wanted to share some of what I learned making this game, since there's a few novel parts of it. I've jammed it all into one post.
Making a Playdate Game in TypeScript
I've never really written much Lua. I'm bad at it and I write bad code. On the other hand, writing a game in TypeScript has been great; I'm much faster in TypeScript and I almost never have unexpected behavior at runtime. I believe this is the only game on Catalog written in TypeScript!
I've written a bit about how to write Playdate games in TypeScript in this thread here. There's a project called TypeScriptToLua that compiles TypeScript to Lua, and can produce Lua that will call into an API you define with a .d.ts
file. I have a ~3,000 line playdate.d.ts
that describes the types of everything that the API can do.
The only really custom pieces of Playdate's Lua runtime are using import
to load code at runtime and the custom OOP stuff. Orta Therox made a plugin for TypeScriptToLua for handling import
statements, and I created one for handling Playdate's OOP. With that, I've been able to use TypeScript's classes and compile them into Lua that uses Playdate's CoreLibs/object
code.
The two shortcomings I haven't addressed are that class definitions can only be exported from modules as the default export (class Foo{} ... export default Foo
) and subclasses must accept the same arguments as their superclasses. The latter is easy to fix, but not fun to fix, so I haven't done it
Making a Cribbage Computer Player
This part requires some knowledge of cribbage. If you know how the game works, you can skip the next paragraph.
In cribbage, you earn points for specific combinations of cards in your hand: cards that sum to 15, cards that share a rank, runs (consecutive cards), nobs (not important to understand) and flushes. At the beginning of each hand, you're dealt 6 cards, and must choose and discard 2 of your 6 cards, with the goal of ending up with 4 remaining cards that maximize the number of points you earn. A hand of 4, 5, 6, 7, for example, would give you 4 points for a run and 2 points for having 4, 5, and 6, which sum to 15.
Which 2 cards you discard from your 6 card hand is probably the single most important decision you make in a game of cribbage. A computer player needs to make good discard decisions in order to be competitive.
There are 15 possible discards to consider for a 6 card hand. As humans, we generally have a heuristic in mind for evaluating our discards: keep a hand with double runs, don't discard a 5 to the opponent's crib. But I found that coding up a good, well-rounded heuristic was difficult, and it was additionally challenging to create several difficulty levels when using a heuristic like that. Instead, I'm brute forcing the entire possibility space to figure out which decision the computer should make. This was probably a bad idea!
For all 15 discard options, the computer considers all possible outcomes and the score they would produce (it never knows what's in your hand, which was important to me). The score of a hand depends on the top card of the deck, which is included in scoring, and the 2 possible cards the opponent could discard to the crib (essentially a third hand, if you're not familiar with the rules of cribbage). For each 2 card discard, there are something like 45,500 possibilities to consider. Each of those possibilities needs to be scored to understand the minimum, maximum, and average/median score the choice would make. Scoring the possibility requires counting the runs, doubles, fifteens, flushes, and nobs. All of this is to say: a ton of math. I had this written in TypeScript -> Lua, optimistically ran it, and it crashed my device. It wasn't the 10 second timeout, so I think it must have been an OOM issue. It took around 7 seconds in simulator.
I ported all of this code to C, which was fast enough to run on device, but still took something like 6 seconds, which wasn't fast enough. The computer has to do all of this evaluation in order to make the very first decision of the game and it's very unpleasant to immediately have to wait for the computer.
I found a shortcut here, which was the saving grace. For most scoring in cribbage, the suit of the card doesn't matter. Suit is only relevant for flushes and nobs, but not for runs, fifteens, or duplicates. Surprisingly (to me), there are only 6,175 suit-independent combinations of cards. You can precompute each of these 6,175 suit-independent scores, store it somewhere, then evaluate a hand by summing the precomputed, suit-independent score and the suit-dependent score. (Cool: Someone named William H. Green did all of that math, presumably by hand? and published an 82 page book with the results in 1882.)
I calculated the 6,175 suit-independent scores, dumped it to a file, then put it directly into a static array in my C code. The keys for the score lookup are prime numbers, generated by assigning prime numbers to each card rank (Ace through King) and taking their product. I wrote some binary search code to look up these scores, and now I'm able to evaluate all possible discards in less than 2 seconds, including yields to the Playdate. When the computer needs to decide which cards to discard, it evaluates all 683,000-whatever possible scenarios, sorts each discard by the average point value it would produce, and chooses one based on difficulty. The highest-difficulty computer player will almost always choose the statistically optimal choice, and lower difficulty computer players make choices elsewhere on the point distribution.
The analysis tool, with its incredible border
This same code is used for the optional analysis tool, which ranks the choice you made among the 14 other choices you could have made. It's great for learning how to make better discard decisions.
Organizing a Game with Coroutines
Most games designed for in-person play have a very predictable back-and-forth structure. Cribbage has distinct stages where you and your opponent take turns. No two things really need to happen simultaneously.
Inside Playdate links to this document, which is about how to use coroutines to organize cutscenes. A non-real-time game like cribbage is a lot like a cutscene, and coroutines are a great way to organize it.
As the document shows, typical game code often looks like:
if (gameState == DEALING_CARDS) {
numberOfCardsAlreadyDealt = ...
cardToDeal = ...
startCardDealAnimation()
} elif (gameState == CARD_DEAL_ANIMATION) {
...
} elif (gameState == CHOOSING_DISCARD ) {
...
}
It really stresses me out thinking about maintaining this.
With coroutines, you're able to write code that describes what should happen in a step-by-step way. When your code automatically yields to the Playdate, you don't need to store state in some global variables. Your state becomes the closure your code is executing in, and the code's program counter becomes the state managing where you are in execution.
When using coroutines, the code for a game of cribbage reads exactly like you would describe a game of cribbage:
deal cards
choose player discard
choose computer discard
until the hand is over {
if its the players turn
play the players card
}
...
}
Without coroutines, these functions would either all execute immediately in sequence without blocking, or they'd block infinitely and the Playdate would crash.
To handle all of my concurrency, I'm using code Dustin Mierau posted to this forum post. The basic idea is to create abstractions around the built-in coroutine
functions in Lua. Dustin includes a together
function, which is an abstraction around parallel execution of coroutines, and I added a bunch more as I worked on it.
Here's the basic building block of my coroutine code:
export const Action = (
initialize: () => void,
update: (dt: number, t: number, finish: () => void) => void
) => {
let finished = false;
let lastTime = playdate.getCurrentTimeMilliseconds();
const finish = () => {
finished = true;
};
initialize();
while (!finished) {
coroutine.yield();
let now = playdate.getCurrentTimeMilliseconds();
const dt = now - lastTime;
lastTime = now;
update(dt, now, finish);
}
};
And here's an example of a common function, loop
. loop
will call a function every update cycle, until the done()
function is called.
export const loop = (fn: (done: () => void) => any) => {
const update = (dt: number, t: number, finish: () => void) => {
fn(finish);
};
Action(() => {}, update);
};
In action, the main game loop:
loop((done) => {
const [
playerCards,
playerDiscards,
computerCards,
computerDiscards,
cribCards,
cutCard,
] = playHand();
cribCards.forEach((card) => card.flip(false));
const scoringOrder = GlobalState.playerDealer
? [1, 0, 2]
: [0, 1, 2];
scoreHands(
playerCards,
playerDiscards,
computerCards,
computerDiscards,
cribCards,
cutCard,
GlobalState.playerDealer,
scoringOrder
);
if (
GlobalState.playerPoints >= 121 ||
GlobalState.computerPoints >= 121
) {
done();
}
resetHand();
GlobalState.playerDealer = !GlobalState.playerDealer;
GlobalState.playerTurn = !GlobalState.playerDealer;
});
Here's another function which uses together
to simultaneously animate several cards at once:
const highlightInvolvedCards = (cards: Card[]) => {
together(
cards.map((card, i) => {
return () => {
waitFrames(i * 2);
sendSprite(card, point(card.x, card.y - 5), 100);
waitFrames(2);
sendSprite(card, point(card.x, card.y + 5), 100);
};
})
);
};
With old art, before Neven saved me
I could write about this for ages. It's made this game so much fun to develop. Adding animations, sounds, or new logic is so simple and joyful.
How Do You Save the Game When You're Writing a Game That's All Coroutines
After feeling like such a clever little boy writing all my coroutines, I ran into a great big problem of saving and resuming games.
The Playdate itself can be slept and woken up without interrupting the game, but I wanted the game to be able to preserve its state between opens and closes.
Because of the way I structured the game code, the state of the game at any moment is entirely dependent on what happened before it. I had a coroutine that would deal cards, and anywhere after that I could assume that each player's hand had those cards in it. Those cards weren't being stored in some global data structure, they were just reliably where they were supposed to be. The same was true for the positions of sprites which may have moved around during animations. But because I wasn't storing anything in a global data structure, I couldn't restore the game from file if it was closed then reopened.
In order to solve this, a "save file" is a log of every action the user took during the game. To restore the saved game, the game is played through using these recorded inputs at something like 50x speed. The game uses seeded RNG, so everything is deterministic.
To record inputs, I added an abstraction layer around the Playdate SDK's input methods (getButtonState()
, buttonJustPressed()
, etc.) that records the button state into a queue if the game is being played, or pop button state out of the queue if the game is being replayed from a save file. If the button state is empty (no buttons pressed and no crank change) I don't add anything to the queue, just to prevent the file from getting too large.
There was one really sticky issues I ran into with this. Here's my untilButtonPressed
coroutine, which runs an update function until the specified button(s) are pressed:
export const untilButtonsPressed = (
buttons: playdate.Buttons[],
tick: (
dt: number,
t: number,
) => any
): playdate.ButtonState => {
let exitCondition = 0;
for (const i of $range(0, buttons.length - 1)) {
exitCondition += buttons[i];
}
let buttonState: playdate.ButtonState = 0;
const update = (dt: number, t: number, done: () => void) => {
let [current, pressed, released] = Input.getButtonState();
tick(dt, t, done);
if ((pressed & exitCondition) > 0) {
buttonState = pressed;
done();
}
};
Action(() => {}, update);
return buttonState;
};
Every time the untilButtonsPressed
function runs, it checks the current button state (Input.getButtonState()
) to determine if it should exit. When replaying from a saved game, this will pop an input off the queue every update.
The problem arises when checking input within untilButtonsPressed
, like this:
showMenu();
untilButtonsPressed([playdate.kButtonA], () => {
if (Input.buttonJustPressed(playdate.kButtonUp)) {
// do something
}
});
closeMenu();
When this code runs during gameplay, the player might hit kButtonUp
once, then hit kButtonA
to exit the loop and proceed to closeMenu()
. The inputs might be something like this: [ ..., kButtonUp, ..., kButtonA]
where ...
is any number of empty frames, which aren't included in the input queue to save space. So the saved input is just [kButtonUp, kButtonA]
.
Now, when this code is run during a replay, this function behaves differently: untilButtonsPressed
pops off first kButtonUp
, then Input.buttonJustPressed
pops off kButtonA
, then the next time untilButtonsPressed
checks for input, the queue is empty, and the replay ends. During playthrough, the player pressed kButtonUp
which would result in running the code indicated with "do something," but during replay it's never run.
The root of the issue is that Playdate's input methods return the inputs for the entire frame, so during live playthrough the initial call to Input.getButtonState()
within untilButtonsPressed
and the Input.buttonJustPressed()
would both return the same value. During replay, though, two inputs are consumed.
One way to solve this is to make the abstraction layer around inputs pop only a single input every frame, which would be returned by every call to input methods. Something like this:
// Input module
let buttonState = [0, 0, 0];
const tick = () => {
buttonState = InputQueue.shift();
};
const getButtonState = () => {
return buttonState;
};
const buttonJustPressed = (button: playdate.Button) => {
return (buttonState[1] & button) > 0;
};
tick
would be called during every frame of replay.
This solution makes more sense to me, and is probably what I would do if I were to reimplement it now.
But the solution I came up with first (and won't redo) was to make sure that the only ways the game can access input are through untilButtonsPressed
and loopWithInput
, coroutine functions that provide input as an argument to their callback:
untilButtonsPressed(
[playdate.kButtonA],
(dt, t, done, [current, pressed, released], crankPosition) => {
// do something
}
)
The inputs consumed to check whether to exit within untilButtonPressed
are then re-used within the update function, which prevents an extra input from being popped off the queue:
export const untilButtonsPressed = (
buttons: playdate.Buttons[],
tick: (
dt: number,
t: number,
done: () => void,
[current, pressed, released]: [
playdate.ButtonState,
playdate.ButtonState,
playdate.ButtonState
],
crankPosition: number
) => any
): playdate.ButtonState => {
let exitCondition = 0;
for (const i of $range(0, buttons.length - 1)) {
exitCondition += buttons[i];
}
let buttonState: playdate.ButtonState = 0;
const update = (dt: number, t: number, done: () => void) => {
let [[current, pressed, released], crankPosition] = getInputs();
tick(dt, t, done, [current, pressed, released], crankPosition);
if ((pressed & exitCondition) > 0) {
buttonState = pressed;
done();
}
};
Action(() => {}, update);
return buttonState;
};
The save files are keyed based on build number, so if I publish a new build that consumes a different number of inputs the game won't get confused. This means that updating the game will abandon any in-progress game, but that's fine.
Learning 3D
I commissioned an artist to create the web assets for the game, but unfortunately things fell through without enough time left to find someone else. I've always wanted to learn 3D rendering and this was a sufficient kick in the butt, so this winter holiday I spent a lot of time learning Blender.
I watched this video from Louie Zong, then tried a few test projects (one of which is still a secret), then got started.
I wanted to evoke the feeling of comedic gravity, like someone who's taking cribbage much too seriously. In the game I had already started to use a king (from a playing card) as an avatar, and that felt like a fun thing to continue with. I sketched a few ideas out then started playing in Blender.
AHHHHH!!!!My first attempts were not very good. This king's face is genuinely horrifying to me, and there's something to be said about making something that's frightening to yourself. It feels like it shouldn't be possible, like how you can't tickle yourself
I wanted the king to be silly, not frightening, so I drew reference images to import into Blender and sculpt on. This also did not work out how I wanted it to.
For the final model I sculpted directly over a reference of the King of Clubs, which is the silliest looking king in my opinion. I wanted to replicate the impossible geometry of his head, so I just combined meshes however I needed to then remeshed it all to make something reasonable.
The mesh is a nightmare; in the final render his eyeballs are barely staying in his head. But what I most appreciated from Louie Zong's video I linked above is his approach that focuses on producing the final illustrative effect at the cost of a well-formed mesh.
I put a robe over the skeleton, then put a rig in it to pose it. I don't think I really needed to do that, but I learned something, which is what I wanted.
Flexing so hard your armpits tear open
The one interesting part of the render, which I think might be interesting even to people who know Blender, is the king's beard. I love the collage effect of the kings silly curly white and blue beard, and tried a few different ways to capture that in the final render. The technique I settled on is a negative thickness Solidify modifier with flipped normals. The lines around his nose and edge of his face are a grease pencil, and his facial features are planes stuck on his head, which saved me from needing to make a good mesh.
Overall, I'm so thrilled by how much working Blender feels like play. I get to try things, putting different materials in different places and moving lights around.
Here's the scene in Blender, which I think is cool.
The title image is also made in Blender, because I do not have any other illustration software.
1-bit Assets
I also needed 1-bit assets for the on-device store and game card, which was another challenge for me. A main inspiration for the game title and marketing was Ken Griffey Jr. Presents Major League Baseball, which is such a funny title to me.
I love Ken Griffey Jr. is coming through the box art, I love his signature taking up so much room.I tried to evoke this in my original draft for game art, but I don't have the chops to create such a chunky, graphical illustration.
I didn't touch the 1-bit illustration again until I had finished my Blender render. My plan was to dump line art from Blender then fill in dither patterns and detail manually until I got the effect I wanted. Lucas Pope has a dev log for Mars After Midnight that includes some screenshots of a Blender material shader and compositor and an output that's pretty much exactly what I wanted. I got to work duplicating it from those screenshots and ended up with something I was roughly satisfied with.
I played color-by-numbers with these lines, but couldn't capture the moody lighting from the render.
So I ran the full render through a few ditherers I found on the web and extracted portions of the image using the exported lines as my selection boundaries, then pasted them back in.
I think it looks nice!