A Cribbage Simulator
Cribbage is the most popular card game among my family members- it’s pretty much the only game we play with any regularity these days, and we’ve spent countless hours facing off on planes, trains, automobiles and the kitchen table. “We” refers primarily to my parents and I; my sister plays a handful of times a year and thus needs to be refreshed of the rules nearly every time- she’s a quick learner, so after 2 or 3 hands she picks it back up again, and after a game or two vows to play more frequently in the future so as to etch the rules permanently in her brain… alas, like Drew Barrymore in 50 First Dates, the cycle repeats itself each time. I can see it in my mind: “50 First Games of Cribbage: The Madeline Overby Chronicles”. 10ish down, 40-some to go…
Once again, I’ve decided to take a cherished game and break it down into programming terms. I’ve wanted to build a cribbage expected points calculator for some time, but I never sat down to actually do it until earlier this week. So I’ll briefly apprise my dear readers of the rules of cribbage and then walk through my functions which, based on a user’s hand, will calculate the “expected value” of each possible subset.
Brief Cribbage Overview (obviously brief… otherwise it would just be a view)
Cribbage works like this:
- 2–4 players- two is most common, by far, and my descriptions will refer to the two-person variant henceforth
- Each player gets dealt 6 cards; at the start of the game, one person is randomly designated as dealer, and the two players alternate dealing throughout the game.
- Both players choose 4 of their 6 cards to serve as their main hand; the remaining two are discarded face-down into a pile, called the “crib”, which later on will act as a second “hand” for the dealer. Players typically keep their four best cards and dump their worst two, so the “crib” yields far fewer points on average than the players’ hands.
- The non-dealer cuts the deck (the remaining 52–12 = 40 cards), and the dealer uncovers the card, which serves as a communal fifth card (a.k.a. the “cut card”) for both players’ hands and the crib.
- Starting with the non-dealer, players lay down their cards face-up, one by one and try to score points against each other in a variety of ways; this stage of the game is known as “pegging” and will not be covered in this article.
- After pegging, players announce the score of their hands. The non-dealer starts, then the dealer scores their hand, and then they score the crib. This order becomes quite important at the end of the game…
- …because the first player to reach a total of 121 or more points is the winner!
In this article, I’ll only be handling the hand-evaluation phase of the game. My goal is to come up with a function that calculates the expected value of each subset of 4 cards from a player’s 6-card hand, which they could use to determine the optimal 4-card hand, either during the game or afterwards, to analyze their play postgame. Given a 6-card hand, there are 46 unseen cards from a player’s perspective. My function would calculate the average hand score, based on the 46 possible cut cards. Students of probability would note that there are 15 4-cards subsets in each 6-card hand, as “six choose four” = 15.
Basic Hand Counting Rules
A player’s 5-card hand (4 personal cards + 1 communal card) gets points in the following ways:
- Each subset of cards that adds up to 15 yields two points; e.g. 4–5–6-J-K has three 15 subsets: 4+5+6 and two 5+10s (all face cards are worth 10, as is the 10, of course), so this hand would get 6 points via 15 sums.
- Pairs are worth 2, 3-of-a-kind is worth 6, and the rare-but-not-unheard-of 4-of-a-kind gives a whopping 12!
- Any run of three/four/five consecutive cards gives 3/4/5 points. e.g. A-2–3–4–5, since 1–2–3–4–5 come in a row, would give 5 points. Only the maximum run length is used; you wouldn’t get 3 points for A-2–3, or 4 points for 2–3–4–5. Just five!
- A “flush”, i.e. all player cards having the same suit, is worth 4 points. If the communal card is also a member of this suit, the player gets a fifth point. Flushes are not common, but they do happen! Bizarrely enough, a 4-card crib flush is worth 0 points, but if the cut card is of the same suit, the dealer gets 5! So the crib can only produce a 5-point flush, *not* a 4-point flush. Don’t ask me why!
- Nobs: almost as bizarre as the crib flush rule. If a player holds a Jack in either their hand or crib, and the cut card matches the suit of said Jack… that’s worth one point! I can only conclude that the inventor of cribbage was some English bloke named Jack. As a fellow Jack, I sympathize, I guess?
Here’s my game plan:
- Create Card, Deck and Hand classes
- Write a function for each one of the 5 ways of scoring
- Make a function that invokes each of the above 5 scoring functions and adds them together
- Whip up a function that determines the unseen 46 potential cut cards, calculates the hand score for each one and spits out the average
- Finally, compose a function that gives the average (i.e. expected) hand score for each of the 15 4-card subsets of a player’s initial hand. No problemo, easy peasy, andiamo!
Classes
class Card {
constructor(suit='S',rank='A') {
this.suit = suit;
this.rank = rank;
}score() {
const rank = this.rank;
if (rank==='A') return 1;
else if (['T','J','Q','K'].find(c=>c===rank)) return 10;
else return parseInt(rank);
}}export default class Deck {
constructor() {
this.cards = [];
this.fillDeck();
this.shuffle();
}fillDeck() {
['A','2','3','4','5','6','7','8','9','T','J','Q','K'].forEach(rank=>['C','D','H','S'].forEach(suit=>this.cards.push(new Card(suit,rank))));
}shuffle() {
for (let i=51;i>=0;i--) {
this.cards.push(this.cards.splice(Math.floor(Math.random()*i),1)[0]);
}
}}import totalHandScore from '../totalHandScore';
import Deck from './Deck';class Hand {
constructor() {
this.hand = [];
}score(cards=this.hand) {
return totalHandScore(cards);
}addCard(card) {
this.hand.push(card);
}simulateSub(indices) {
const handCards = indices.map(ix=>this.hand[ix]);
const sampCards = (new Deck()).cards;
const others = sampCards.filter(card=>!handCards.find(handCard=>card.rank===handCard.rank && card.suit===handCard.suit));
const scores = others.map(otherCard=>this.score(handCards.concat(otherCard)));
return scores.reduce((card,sum)=>card+sum, 0) / scores.length;
}
}import Deck from './Deck';
import Hand from './Hand';class Cribbage {
constructor() {
this.deck = new Deck();
this.hands = {
1: new Hand(),
2: new Hand()
}
}dealCards() {
for (let i=0;i<6;i++) {
for (let j=1;j<=2;j++) {
this.hands[j].addCard(this.deck.cards.shift());
}
}
}
}
Hand Score Functions
Here is a helper function that takes in an array and returns a dictionary with keys being unique array members and values being the ‘count’ of each key in the array:
function counter(hand) {
return hand.reduce((dict,ch)=>(dict[ch]=dict[ch]+1||1,dict),{});
}import counter from './counter';export default function dupes(hand) {
hand = hand.map(card=>card.score());
let points = 0;
const counts = counter(hand);
Object.values(counts).forEach(count=>{
if (count===2) points += 2;
else if (count===3) points += 6;
else if (count===4) points += 12;
})
return points;
}export default function fifteens(hand) {
let sums=0;
hand = hand.map(card=>card.score()).sort((a,b)=>a>b?1:-1);
for (let i=0;i<hand.length;i++) {
for (let j=i+1;j<hand.length;j++) {
if (hand[i]+hand[j]===15) sums+=1;
for (let k=j+1;k<hand.length;k++) {
if (hand[i]+hand[j]+hand[k]===15) sums+=1;
for (let m=k+1;m<hand.length;m++) {
if (hand[i]+hand[j]+hand[k]+hand[m]===15) sums+=1;
for (let n=m+1;n<hand.length;n++) {
if (hand[i]+hand[j]+hand[k]+hand[m]+hand[n]===15) sums+=1;
}
}
}
}
}
return sums * 2;
}import counter from './counter';export default function dupes(hand) {
hand = hand.map(card=>card.suit);
hand = counter(hand);
const max = Math.max(...hand);
return max >= 4 ? max : 0;
}export default function streak(hand) {
let len = 1;
let streaks=[];
for (let i=1;i<hand.length;i++) {
if (hand[i]===hand[i-1]+1) {
len += 1;
if (i===hand.length-1) streaks.push(len);
}
else {
streaks.push(len);
len = 1;
}
}
const max = Math.max(...streaks);
return max >= 3 ? max : 0;
}import fifteens from './fifteens';
import dupes from './dupes';
import flush from './flush';
import streak from './streak';export default function totalHandScore(hand) {
return fifteens(hand)+dupes(hand)+flush(hand)+streak(hand);
}