Skip to content

Instantly share code, notes, and snippets.

@mobeets
Created June 17, 2018 03:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mobeets/780b34a518113ec08e4fe3c1b11baf63 to your computer and use it in GitHub Desktop.
Save mobeets/780b34a518113ec08e4fe3c1b11baf63 to your computer and use it in GitHub Desktop.
estimating what percentage of cribbage games are winnable by random play
import itertools
import numpy as np
# https://github.com/relsqui/pydeck
from pydeck import standard, cribbage
CARDS_PER_DEAL = 6
CARDS_PER_HAND = 4
POINTS_TO_WIN = 121
VERBOSE = False
def score_hand(hand):
return sum(cribbage.score_hand(hand).values())
def deal_hands(deck):
return deck.deal(CARDS_PER_DEAL), deck.deal(CARDS_PER_DEAL)
def random_player(hand, gets_crib):
"""
discard without even looking
"""
crib = hand.deal(CARDS_PER_DEAL-CARDS_PER_HAND)
return hand, crib
hand_inds = range(CARDS_PER_DEAL)
hand_ind_subsets = list(itertools.combinations(hand_inds, CARDS_PER_HAND))
crib_ind_subsets = []
for i in range(len(hand_ind_subsets)):
cinds = hand_ind_subsets[i]
ccribinds = [i for i in range(CARDS_PER_DEAL) if i not in cinds]
crib_ind_subsets.append(ccribinds)
def get_hand_from_inds(hand, inds):
return standard.StandardHand([hand[i] for i in inds])
def smart_player(hand, gets_crib):
"""
smart player evaluates all possible hands/discards,
and chooses the one that gives them the most points
"""
best_ind = 0
best_score = 0
for j in range(len(hand_ind_subsets)):
cinds = hand_ind_subsets[j]
ccribinds = crib_ind_subsets[j]
chand = get_hand_from_inds(hand, cinds)
ccrib = get_hand_from_inds(hand, ccribinds)
chand_score = score_hand(chand)
ccrib_score = score_hand(ccrib)
cscore = chand_score - ccrib_score
if cscore > best_score:
best_ind = j
best_score = cscore
cinds = hand_ind_subsets[best_ind]
ccribinds = crib_ind_subsets[best_ind]
chand = get_hand_from_inds(hand, cinds)
ccrib = get_hand_from_inds(hand, ccribinds)
return chand, ccrib
def card_value(card, faces_are_ten=True):
short = card.rank.short
if short in ['T', 'J', 'Q', 'K']:
if faces_are_ten or short == 'T':
return 10
elif short == 'J':
return 11
elif short == 'Q':
return 12
else:
return 13
elif short == 'A':
return 1
else:
return int(short)
def random_player_round1(card_opts, csum):
return card_opts[0]
def score_stack(stack, csum):
score = 0
# sums to 15 or 31
if csum == 15:
score += 1
if csum == 31:
score += 2
# pairs, doubles, triples
if len(stack) > 1 and stack[-2].rank == stack[-1].rank:
score += 2
if len(stack) > 2 and stack[-3].rank == stack[-1].rank:
# triples
score += 4
if len(stack) > 3 and stack[-4].rank == stack[-1].rank:
# quadruples
score += 6
# runs
vals = [card_value(c, faces_are_ten=False) for c in stack]
is_run = lambda x: len(np.unique(np.diff(sorted(x)))) == 1
for rng in np.arange(-len(vals), -2):
if is_run(vals[rng:]):
score += len(vals[rng:])
break
return score
def choose_card_and_score(hand, popts, player, stack, csum):
# choose card
c = player(popts, csum)
# update hand, cumulative sum, and stack of cards played
hand = [h for h in hand if h != c]
csum += card_value(c)
stack.append(c)
# get score of card
score = score_stack(stack, csum)
return c, hand, score, stack, csum
def score_early_play(hand1, hand2, player1, player2, verbose=VERBOSE):
GOAL_SUM = 31
h1 = [h for h in hand1]
h2 = [h for h in hand2]
p1 = 0
p2 = 0
p2_went_last = True
if verbose:
print "NEW first round."
while len(h1) > 0 or len(h2) > 0:
csum = 0
stack = []
n_gos = 0
ended_round = False
if verbose:
print "Start round."
while csum < GOAL_SUM and (len(h1) > 0 or len(h2) > 0):
if p2_went_last:
# p1's turn
p1opts = [x for x in h1 if card_value(x)+csum <= GOAL_SUM]
if len(p1opts) > 0:
c, h1, score, stack, csum = choose_card_and_score(h1, p1opts, player1, stack, csum)
p1 += score
if verbose:
print "P1 plays {}, scores {}; sum is {}.".format(c, score, csum)
else:
# "Go"
if n_gos == 0:
n_gos += 1
if verbose:
print "P1: Go."
else:
# round over
n_gos = 0
csum = 0
stack = []
if verbose:
print "P1 ends round."
ended_round = True
else:
# p2's turn
p2opts = [x for x in h2 if card_value(x)+csum <= GOAL_SUM]
if len(p2opts) > 0:
c, h2, score, stack, csum = choose_card_and_score(h2, p2opts, player2, stack, csum)
p2 += score
if verbose:
print "P2 plays {}, scores {}; sum is {}.".format(c, score, csum)
else:
# "Go"
if n_gos == 0:
n_gos += 1
if verbose:
print "P2: Go."
else:
# round over
n_gos = 0
csum = 0
stack = []
if verbose:
print "P2 ends round."
ended_round = True
if (len(h1) == 0 and len(h2) == 0) or ended_round:
if p2_went_last:
if verbose:
print "P1 scores 1 for going last"
p1 += 1
else:
if verbose:
print "P2 scores 1 for going last"
p2 += 1
ended_round = False
p2_went_last = not p2_went_last
return p1, p2
def main(ngames=100):
"""
estimate what percentage of cribbage is chance vs. skill
estimates the lower bound of the percent chance,
since player 2 is not optimal
round 1 is played randomly by both players
this means the "percent chance" reported is a lower-bound,
since if player 2 played round 1 skillfully, he could only do better
round 2 is played randomly by player 1, skillfully by player 2
again, player 2 could be even more skillful,
but this would only lower our estimate of how much chance is involved
note:
- if game is 100% chance, you expect to win 50% of games
- if game is 0% chance, you expect to win 100% of games
- if game is 50% chance, you expect to win 75% of games
"""
# player 1 chooses his crib randomly
player1 = random_player
# player 2 chooses the hand that maximizes points,
# where the points in the 2 cards he discards are subtracted
player2 = smart_player
# round 1 is played randomly by both players,
# where players know the rules but not the scoring procedure
player1_round1 = random_player_round1
player2_round1 = random_player_round1
scores = np.zeros((ngames, 2))
for i in range(ngames):
# game passes crib back and forth until a player has enough points
p1score = 0
p2score = 0
player_1_gets_crib = True
while max(p1score, p2score) < POINTS_TO_WIN:
# deal hands
deck = standard.make_deck(shuffle=True)
hand1, hand2 = deal_hands(deck)
# players choose which cards to discard to crib
hand1, crib1 = player1(hand1, gets_crib=player_1_gets_crib)
hand2, crib2 = player2(hand2, gets_crib=not player_1_gets_crib)
crib = crib1 + crib2
assert(len(hand1) == CARDS_PER_HAND)
assert(len(hand2) == CARDS_PER_HAND)
assert(len(crib) == CARDS_PER_HAND)
# score second round of play
flip = deck.deal(1)
score_1 = score_hand(hand1 + flip)
score_2 = score_hand(hand2 + flip)
p1score += score_1
p2score += score_2
score_crib = score_hand(crib + flip)
if player_1_gets_crib:
p1score += score_crib
else:
p2score += score_crib
# score first round of play
if player_1_gets_crib:
# p1's crib, so p2 goes first
score_2, score_1 = score_early_play(hand2, hand1, player2_round1, player1_round1)
else:
# p2's crib, so p1 goes first
score_1, score_2 = score_early_play(hand1, hand2, player1_round1, player2_round1)
p1score += score_1
p2score += score_2
player_1_gets_crib = not player_1_gets_crib
scores[i,0] = p1score
scores[i,1] = p2score
mu = scores.mean(axis=0)
se = scores.std(axis=0)/np.sqrt(ngames)
print "Random player's average score: {:0.2f} +/- {:0.2f}".format(mu[0], se[0])
print "Skilled player's average score: {:0.2f} +/- {:0.2f}".format(mu[1], se[1])
games_skunked = (scores[:,0] < 91).sum()
pct_skunked = 100*(1.0*games_skunked)/ngames
games_won = (scores[:,1] > scores[:,0]).sum()
pct_winnings = 100*(1.0*games_won)/ngames
pct_chance = 100 - 100*(pct_winnings - 50.)/50.
print "Skilled player skunked {}% of the time ({} of {} games).".format(pct_skunked, games_skunked, ngames)
print "Skilled player won {}% of the time ({} of {} games).".format(pct_winnings, games_won, ngames)
print "The game is {}% chance.".format(pct_chance)
if __name__ == '__main__':
import sys
ngames = int(sys.argv[1]) if len(sys.argv) > 1 else 100
main(ngames)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment