Balatro
LocalThunk—the anonymous creator of Balatro, the hit poker-inspired roguelike videogame—revealed recently that 11,000 years of gameplay have been sunk into the game.
The average productivity of the universe has continued to drop now that the game is available on mobile. I am one of the suckers that made the purchase.
As a result, I wondered how viable a card game in R might be. Not just a simple blackjack simulator in the console, but a drag-and-droppable interface in the browser.
It must be possible, since R is a game engine (fight me).
Badlatro
And so I began experimenting.
I wanted to use {shiny} and existing R packages and to avoid writing much JavaScript and CSS. After all, it’s hard to fit in this frivolity after I’ve finished grinding all day at work and then grinding all night on Balatro.
Luckily, the {sortable} package does most of the hard work. The package from Andrie, Barrett and Kent wraps the SortableJS library and lets you drag list elements around, including into other lists.
Typically, you would use {sortable} to drag little boxes of text. The order can then be read to record preference or perhaps as part of a quiz. But now I’ve hijacked it to show little images of cards that you can drag between a ‘pool’ (randomly drawn cards) and a ‘hand’ (cards selected by the user).
App
You can check out the source on GitHub and find the app deployed online, thanks to {shinylive} and GitHub Pages. I’ve embedded it below as well (it’ll take a moment to load). I recommend using this on desktop for now, rather than mobile.
Features
So far it doesn’t do much, but it does enough to prove the concept. Here’s some notes on a few of the technicals.
Counting cards
I iterated over all suits and ranks with {magick} to apply text and symbols to a blank PNG image. I then read the 52 cards into a shiny::tagList()
and passed that to the label
argument of sortable::list()
. From there, the images could be matched to sampled card names and displayed in the app. This is slightly off-label (ha) compared to ‘normal’ use of the package, which generally involves providing text rather than images.
Drag ’til you drop
{sortable} has a nice feature where you can drag list elements between lists within ‘buckets’. I opted instead to use two rank_list()
s that shared a group
name in their options
argument. This meant I could restrict the list size (8 cards in the pool, 5 in hand), thanks to some JavaScript from Barrett in a response to a Posit community post.
A bit flushed
Detecting poker hands is tricky because you want to recognise that two jacks and three kings is a full house, not a pair of jacks or a three-of-a-kind, for example. I also made things harder by wanting to evaluate poker hands on the fly rather than when the user submits the hand.
I read about lots of very clever algorithms to do this. But I basically just brute-forced it, lol. It’s basically if
statements that evaluate and return strongest hands first. So the function will assess a royal flush (i.e. ace, king, queen, jack and 10 of the same suit) and confirm it before it checks for a more basic straight (consecutive ranks of any suit) or a flush (any suit of non-consecutive rank).
Hit the deck
The deck is stored in ‘dynamic memory’ as a reactieValues()
element. When cards are drawn, they’re removed from the deck and can’t be redrawn. This meant I could add a ‘draw’ button that adds previously unseen cards into the pool’s empty slots. Of course, we can take the length of the deck and present this back to the user as well.
Improvements
There’s many features that would improve the demo app. Below are some examples: one seems like it’s not possible, one won’t add that much to what I’ve learnt so far, one is obvious and one is just… bad programming.
Stop ‘n’ swop
It’s satisfying to pick up a card, drag it to a new position and drop it between the cards at that location. This action feels how it would when sorting real cards in your real hand. I’d like the option to be able to directly swap two cards between the pool and hand, though. This would be useful if you change your mind about the hand you’re building as new cards are drawn. As far as I can tell, SortableJS allows swap = TRUE
within each rank list and if they are part of the same group
then you can swap between them. I haven’t yet found a way to allow swapping to happen only when moving cards between lists, however.
Diss card
The awkward thing about having a ‘pool’ and a ‘hand’ is that there’s no natural way to discard. I haven’t properly explored solutions for earmarking cards to toss; as it stands, you have to drag the card somewhere for an action to be performed on it. The answer might be to drag these cards to a third area, where they’re binned. Compare this to a game like Balatro, where you first select cards in your hand and then click a button to either play or discard them.
U and I
Of course, as a proof of concept, I haven’t paid much attention to the user interface and experience. You can imagine making prettier cards, a nice green baise to mimic a poker table and even some animations to show cards being dealt. For now, I think the janky plainness is a good indicator that it’s just a demo.
Card trick
Oh yeah, haha, sometimes the cards disappear. It can happen when you drag from the pool to the hand, then back again and hit the ‘rank’ or ‘suit’ sorting buttons. We know that the card returns correctly to the pool by checking input$pool_list
in the app, but there’s some issue with displaying the image, maybe? This is the only true bug that needs fixing, but I’d rather just write this blog post and deal with it later.
A gamble
Obviously this isn’t yet a ‘game’. There’s a few ways this could go:
- remake Balatro in R (absolutely no chance)
- make a small simulator of something like video poker (too basic?)
- create a new, simple game to chase high scores with combos of poker hands, discards and chip ‘bets’, perhaps incorporating some Balatro-inspired joker-activated bonuses or wild cards like in Dungeons and Degenerate Gamblers (might actually be fun)
- do nothing (appealing option)
Feel free to chip (HA HA HA) in your ideas.
Environment
Session info
Last rendered: 2024-10-26 21:53:18 BST
R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Europe/London
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] htmlwidgets_1.6.4 compiler_4.4.0 fastmap_1.2.0 cli_3.6.3.9000
[5] tools_4.4.0 htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.10
[9] rmarkdown_2.28 knitr_1.48 jsonlite_1.8.9 xfun_0.48
[13] digest_0.6.37 rlang_1.1.4 evaluate_1.0.1