{sortable} card games in {shiny}

games
r
shiny
shinylive
sortable
Author
Published

October 25, 2024

Two rows of playing cards labelled 'pool' and 'hand'. Buttons are pressed to order the cards by suit and then by rank. Cards are dragged from the pool to the hand. An ace is dragged and the text updates from 'no hand' to 'high card'. Another ace and it changes to 'a pair'. A 'draw' button is pressed and two new cards are added to the pool. The pool is ordered again by rank. Then two queens are added to the hand and the text changes to 'two pair'.

You sunk my battleship!

tl;dr

Use {sortable} in a {shiny} app to build card games, maybe? I mocked up a demo and made it available on the web with {shinylive}.

Balatro

LocalThunk1—the anonymous creator of Balatro, the hit poker-inspired roguelike videogame2—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 quiz3. 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 well4 (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 later5.

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   

Footnotes

  1. Apparently LocalThunk’s name was inspired partly by R.↩︎

  2. If you’ve been here before, you know I’ve toyed around with roguelikes in R with the concept packages {r.oguelike} and {tilebased}.↩︎

  3. For example, Rasmus has done this recently in the climate impact sorting challenge. I scored 1 on my first go, ha.↩︎

  4. Psst, click the joker card at the bottom of the app to see ‘dev mode’ (you’ll have to scroll down in the embedded app to see it).↩︎

  5. This is basically the entire modus operandi of this blog, lol.↩︎

Reuse

CC BY-NC-SA 4.0