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?
Mathematica Stack Exchange is a question and answer site for users of Wolfram Mathematica. It only takes a minute to sign up.
Sign up to join this communityEarlier 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?
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:
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:
The template will walk you through all the steps you need. So I'll skip to the good stuff.
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.)
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.
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
If you want to follow along the package version of this lives on GitHub here
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
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"] =
<||>;
);
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
.
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
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[]
(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.
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.