9
$\begingroup$

Earlier today I saw that there was a deck of cards API. It's a relatively simple API, but it does require us to handle statefulness.

How can I link to this to, e.g., build out a Mathematica version of the card game Sheepshead?

$\endgroup$

1 Answer 1

13
$\begingroup$

This will be an answer in two parts. The first is how to build out a sophisticated, stateful ServiceConnection.

The second is how to use this to build out an interactive card game.

Just to whet your appetite, here's what the final output looks like:

game

Building a stateful ServiceConnection

Prerequisites

I'm going to use a toolchain I've developed, but which is totally open for anyone else to use.

I've got a package for implementing custom ServiceConnections. It lives in my larger BTools application.

You can download these like so:

PacletInstall["BTools",
 "Site"-> "https://www.wolframcloud.com/objects/b3m2a1.paclets/PacletServer"
 ]

A palette will be installed on your system under Palettes >> BTools >> Service Connections. Open this and just click New Service Connection.

This'll pop open the template for you:

temp

The template will walk you through all the steps you need. So I'll skip to the good stuff.

Raw Service Requests

The heart of any ServiceConnection is its collection of raw requests. For this service connection I've implemented everything listed on the main API doc page. In general I've found you'll want ~80% of the requests to make your connection sufficiently useful.

The way I implement these things is by setting a few global parameters in the template (don't worry, they're localized to the notebook). In this case we have:

$serviceBase = "https://deckofcardsapi.com/api/deck";
$serviceBaseParameters = {};

the $serviceBase is just the base URL requests will build off of. The serviceBaseParameters are a collection of parameters common to most requests. Generally I have at least a few of them.

Then we just set up a request like so:

$importFuncs["RawNew"] =
  With[{
    base = $serviceBase,
    stdParams = $serviceBaseParameters
    },
   {
    "URL" ->
     Function@
      URLBuild@{
        base, 
        "new", 
        If[TrueQ@#,
         "shuffle",
         Nothing
         ]
        },
    "Path" ->
     {
      "Shuffle"
      },
    "Parameters" -> {"deck_count", "cards"}
    }
   ];

The $importFuncs is an Association my template makes use of.

The "Path" is a collection of parameters that feed into the actual URL. The "Parameters" feed into the query.

There are a bunch of other possible parameters and values which I've used, for instance, in making a Google Drive connection. The most common is the "Required" parameter, e.g.:

$importFuncs["RawAddToPile"] =
  With[{
    base = $serviceBase,
    stdParams = $serviceBaseParameters
    },
   {
    "URL" ->
     Function@
      URLBuild@{base, 
        Replace[#, Except[_String] -> "`deck`"],
        "pile",
        #2,
        "add"
        },
    "Path" -> {"Deck", "Pile"},
    "Parameters" -> {"cards"},
    "Required" -> {"Pile", "cards"}
    }
   ];

Here's an example of what comes out of a raw request:

$docConnection["RawDraw"]

"{\"remaining\": 0, \"cards\": [], \"error\": \"Not enough cards \
remaining to draw 1 additional\", \"success\": false, \"deck_id\": \
\"6zcbug1l119n\"}"

(Those following closely will note that there should be a "deck_id" parameter in that request. I added statefulness on my side too to track all of the deck data imported and what the most recent "deck_id" was and supply that if none is passed.)

Cooked Requests

These raw requests get fed into a RawImport function I declare earlier in the template (and which is fully customizable). Now we move onto the requests that will actually make use of the raw request data. For instance, we'll turn that raw data into a collection of card Entities:

$cookedFuncs["Draw"] = {
   "Call" -> "RawDraw",
   "Post" ->
    With[{
      gce = ToExpression[$servicePrefix <> "GetCardEntities"],
      js = ToExpression[$servicePrefix <> "JSONImport"]
      },
     Function@
      Replace[
       Lookup[js@#, "cards"],
       crd_List :>
        gce@
         Lookup[crd, "code"]
       ]
     ]
   };

where that ToExpression[$servicePrefix <> "GetCardEntities"] references a custom function I defined in the template to load an EntityStore. Here's an example:

$docConnection["Draw", "Deck" -> "new"]

{Entity["DeckOfCardsPlayingCard", "5C"]}

EntityValue[Entity["DeckOfCardsPlayingCard", "5C"],
 {"Label", "ImageURL"}
 ]

{"5 of Clubs", "https://deckofcardsapi.com/static/img/5C.png"}

We'll make extensive use of these Entities when building out our game later.

Note that a cooked request doesn't even need to call the API:

$cookedFuncs["DeckSize"] = {
   "Import" ->
    With[{dv = ToExpression[$servicePrefix <> "GetDeckValues"]},
     Function[dv[#2, "remaining"]]
     ],
   "Parameters" -> {"Deck"}
   };

this works entirely with the cached data on the M- end for performance.

MetaInfo and Icons

The template will ask you to give your connection and icon and some meta info, in general the info will look like this:

$pacletInfo = {
   "Version" -> "1.0.2",
   "Description" -> "A stateful connection to the DeckOfCards API",
   "Creator" -> "[email protected]",
   "Tags" -> {"web", "serviceconnection"}
   };

Then just evaluate the entire first chunk of the template and it'll build you out a paclet you can use with ServiceConnect

Making an Interactive Card Game

If you want to follow along the package version of this lives on GitHub here

Making the backend

We'll use this system for building out the card game. First we'll just define something that'll load the connection:

$docConnection :=
  (
   If[Length@PacletFind["ServiceConnection_DeckOfCards"]==0,
    PacletInstall[
     "ServiceConnection_DeckOfCards",
     "Site" ->
      "http://www.wolframcloud.com/objects/b3m2a1.paclets/PacletServer"
     ]
    ];
   $docConnection = ServiceConnect["DeckOfCards"]
   );

This ensures the paclet has been installed before loading the connection. The loaded connection will look like this:

$docConnection

so

We'll then define a sheepsheadGame object which just relies on an object manager Association in the standard way:

If[! AssociationQ@$sheepsheadGames,
  $sheepsheadGames = <||>
  ];
sheepsheadGame["New"] :=
  With[{d = sheepsheadDeck[]},
   $sheepsheadGames[d["deck_id"]] =
    <|
     "ID" -> d["deck_id"],
     "Hand" -> 0,
     "Dealer" -> 1,
     "Chips" -> AssociationMap[10 &, Range[5]]
     |>;
   sheepsheadGame[d["deck_id"]]
   ];

And now this object will use the API when loading the deck:

sheepsheadDeck[] :=
  $docConnection["New",
   "cards" ->
    StringJoin /@
     Tuples[
      {
       {"7", "8", "9", "0", "K", "A", "J", "Q"},
       {"D", "H", "S", "C"}
       }
      ]
   ];
sheepsheadGame[id_]["Deal"] :=
  (
   $docConnection["Shuffle", "Deck" -> id];
   $sheepsheadGames[id, "Players"] =
    AssociationMap[
     <|
       "Hand" ->
        SortBy[
         SortBy[
          $docConnection[
           "PileDrawFromDeck",
           "Pile" -> "player_" <> ToString[#],
           "count" -> 6,
           "Deck" -> id
           ],
          #["Suit"] &
          ],
         sheepsheadCardPower[
           Entity["DeckOfCardsPlayingCard", "QC"], #] &
         ],
       "Tricks" -> {},
       "Picker" -> False,
       "Partner" -> False
       |> &,
     Range[5]
     ];
   $sheepsheadGames[id, "Blind"] =
    $docConnection["PileDrawFromDeck",
     "Pile" -> "blind",
     "count" -> 2,
     "Deck" -> id
     ];
   $sheepsheadGames[id, "CalledCard"] =
     None;
   $sheepsheadGames[id, "Leader"] =
    Set[
     $sheepsheadGames[id, "Dealer"],
     Mod[$sheepsheadGames[id, "Dealer"] + 1, 5, 1]
     ];
   $sheepsheadGames[id, "Current"] =
    $sheepsheadGames[id, "Dealer"];
   $sheepsheadGames[id, "Stack"] =
    <||>;
   );

Defining the rules of the game

We'll now just define a bunch of stuff based on this backend. For instance, in Sheepshead there is a "blind" that you "pick up" or "pass on". So we'd define that like so:

sheepsheadGame[id_]["PickOrPass", picking_] :=
  Module[
   {
    cur = $sheepsheadGames[id, "Current"],
    picker = TrueQ@picking,
    blind = $sheepsheadGames[id, "Blind"]
    },
   $sheepsheadGames[id, "Players", cur, "Picker"] = picker;
   If[picker,
    $sheepsheadGames[id, "Blind"] = {};
    $sheepsheadGames[id, "Players", cur, "Hand"] =
     Join[
      $sheepsheadGames[id, "Players", cur, "Hand"],
      blind
      ],
    $sheepsheadGames[id, "Current"] =
     Mod[$sheepsheadGames[id, "Current"] + 1, 5, 1]
    ]
   ];

The game tracks the current player ($sheepsheadGames[id, "Current"]) and if they "pass" that increments (mod 5 because there are 5 players).

If they choose to pick they become the "picker"( $sheepsheadGames[id, "Players", cur, "Picker"] = picker ) and need to do more things that turn so the "Current" does not increment.

Nothing mind blowing. Nothing all that tough. And we just load all the rules, making use of those card Entities we load through the ServiceConnection.

Making the Interface

This is where the majority of the work is. First we'll need a way to represent a hand of cards. Happily, the deck of cards API provides these images and the EntityStore implements a cached import for of them when requested. The first time a given card loads it'll have a slight hiccup, but beyond that it'll be fast.

Next, we'll need a back face, which I loaded from here, as I associate the classic red-and-blue Bicycle playing cards with Sheepshead.

Finally, we just build a static game board function using that sheepsheadGame API we designed earlier, to which we can bind a refresh function for when things change in it.

Then our Dynamic version of all these rules will set a listener variable via TrackedSymbols and pass a refresh function that updates this:

sheepsheadGameInterface[game_] :=
 DynamicModule[
   {refreshFlag},
   Dynamic[
    refreshFlag;
    sheepsheadGameBoard[
     game,
     Function[
      refreshFlag = RandomReal[]
      ]
     ],
    TrackedSymbols :> {refreshFlag}
    ]
   ] // Deploy

Let's Play!

Finally we expose a single top-level function that handles making and restarting a game, then passing this to sheepsheadGameInterface:

PlaySheepshead[redeal : True | False : False] :=

 Module[{redealFlag = redeal},
  If[! MatchQ[$currentSheepsheadGame, 
     sheepsheadGame[_?(KeyMemberQ[$sheepsheadGames, #] &)]],
   $currentSheepsheadGame = sheepsheadGame["New"];
   redealFlag = True
   ];
  If[redealFlag, $currentSheepsheadGame["Deal"]];
  sheepsheadGameInterface@$currentSheepsheadGame
  ]

And here's what that looks like:

Get["https://raw.githubusercontent.com/b3m2a1/mathematica-tools/master/Sheepshead.wl"]
PlaySheepshead[]

asdas

(shoddy quality a product of getting GIF size to the point where SE will take it)

For those in the know, I shouldn't have been able to play the Queen of Hearts (QH is its Entity identifier); it was a validation subcase that slipped through in the general validation function, but which should now be patched in the package.

This is obviously just a quick, semi-buggy version of the game, but it does give a sense for how interactive apps like this can be built.

Moving to Multiplayer

The place to go with this now would be a multi-player version of the game.

We could do this with the ChannelFramework and ChannelListen.

The basic idea would be that at the end of each player's turn they'd push the state on their computer into the channel and the other, listening to the channel, would intercept this state and load that into their interface

We won't worry too much about cheating, but if we wanted to confirm that people aren't messing with state before pushing, we could do this via a basic blockchain mechanism (by which I mean distributed hash-validation).

Past that it's not really worth bothering, I think.

Multiplayer will have to wait for another day, though.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.