5 min read

Analysis of War - the card game

Introduciton

Have you ever played War? have you ever wondered When will this be over!?

Well I have, and after a weekend of being forced to play with my little cousin, I decided enough is enough. Lets do some simulation and some data analysis to answer some basic questions:

  • How long does the average game take
  • How does the initial hand affect your winning percentage?
    • How often will I win if I have all the aces?
    • How often will I win depending on the average hand strength?
  • How does the anti size affect game duration?

Rules

Simulation in python

Setup

Since I am not too used to dealing with numerical simulations of card games, I’m going to set up some classes in python. Once the simulations are done I’ll import the data into R to do some analysis.

Here are the two classes I set up:

from random import shuffle

class Player():
    """Player is the class that represents a player. Each player has a hand of cards. The class also includes getting cards from a players hand, and adding the winnings to the bottom of the players hand"""
    def __init__(self, hand, name):
        self.hand = hand[:]
        self.name = name
    # Once a player wins a battle, they need to add their winnings to the bottom of their hand. Since the rules don't seem to indicate their order, I explicitly shuffle the winnings before adding them.
    def add_winnings(self, cards):
        shuffle(cards)
        self.hand += cards
    # Method to get next card. If the player has run out of cards, this method returns None, which indicates to the Game class that the game is over and the player has lost. 
    def get_next_card(self):
        if len(self.hand)==0:
            return(None)
        else:
            next_card = self.hand.pop(0)
            return(next_card)

class Game():
    """docstring for Game."""
    def __init__(self, war_anti):
        self.war_anti = war_anti
        self.deck = [i for i in range(1,14) for j in range(0,4)]
        shuffle(self.deck)
        self.p0_start = self.deck[:26]
        self.p1_start = self.deck[26:]
        self.player_0 = Player(self.p0_start, "Player 0")
        self.player_1 = Player(self.p1_start, "Player 1")
        self.winner = None
        self.num_turns = 0
    def war(self, pot, extra_cards):
        pot += [self.player_0.get_next_card() for i in range(0,extra_cards)] + [self.player_1.get_next_card() for i in range(0,extra_cards)]
        card_0 = self.player_0.get_next_card()
        card_1 = self.player_1.get_next_card()
        if card_0 == None:
            self.winner = 1
            return()
        elif card_1 == None:
            self.winner = 0
            return()
        pot += [card_0, card_1]
        if card_0 > card_1:
            self.player_0.add_winnings(pot)
        elif card_0 < card_1:
            self.player_1.add_winnings(pot)
        elif card_0 == card_1:
            self.war(pot, self.war_anti)
    def play(self):
        while self.winner == None:
            self.num_turns += 1
            self.war([], 0)

Simulation

Now lets run the simulations:

import numpy as np
import pandas as pd
from game import *

for anti in range(0,11):
    games = [Game(anti) for i in range(0,10000)]
    [g.play() for g in games]
    all = [[g.war_anti, g.winner, g.num_turns, np.mean(g.p0_start)] + [g.p0_start.count(c) for c in range(1,14)] for g in games]
    all_df = pd.DataFrame.from_records(all)
    all_df.to_csv('../output/results_' + str(anti) + '.csv', index = False)

Running the analysis in R

Setup

First lets load up packages and set up the data from python

library(dplyr)
library(ggplot2)
results_paths = list.files('output', 'results_[0-9]+.csv', full.names = T)
results_all = do.call(rbind, lapply(results_paths, read.csv))

colnames(results_all) = c('war_anti', 'winner', 'num_turns','player0_mean', paste0("player0_", c(as.character(2:10), 'J', 'Q', 'K', 'A'),'s'))

Analysis

Lets take a look at the data:

head(results_all)
##   war_anti winner num_turns player0_mean player0_2s player0_3s player0_4s
## 1        0      1       180     6.576923          3          2          4
## 2        0      0       109     7.423077          2          3          2
## 3        0      0       538     7.692308          1          1          3
## 4        0      0       330     7.307692          1          2          1
## 5        0      1       865     7.807692          1          1          2
## 6        0      1       292     6.307692          1          3          3
##   player0_5s player0_6s player0_7s player0_8s player0_9s player0_10s
## 1          0          1          3          3          1           1
## 2          2          1          0          2          1           2
## 3          2          2          2          2          0           1
## 4          2          2          4          2          1           4
## 5          2          3          2          2          1           2
## 6          4          2          2          1          1           3
##   player0_Js player0_Qs player0_Ks player0_As
## 1          1          4          2          1
## 2          3          2          4          2
## 3          4          3          3          2
## 4          1          2          2          2
## 5          1          2          4          3
## 6          2          1          2          1

For normal games, the war anti is 2, so lets analyze that first:

results = results_all %>% filter(war_anti == 2)

Now lets get to the questions: ### How long does the average game take Lets use a histogram to look at the distribution of games

hist(results[,"num_turns"], 
     breaks = 100,
     xlab = 'Number of turns',
     main = 'Distribution of Game Lengths')

The number of turns seems to have a very large right sided tail, and is not normally distributed. Since I’m interested in knowing the number of turns in terms of order of magnitude, lets log the data. Logging the data will also make the distribution resemble the normal distrubution.

hist(log10(results[,"num_turns"]), 
     breaks = 100,
     xlab = 'log10(Number of turns)',
     main = 'Distribution of Game Lengths')

qqnorm(results[,'num_turns'], main = 'QQ plot for untransformed number of turns')
qqline(results[,'num_turns'])

qqnorm(log10(results[,'num_turns']), main = 'QQ plot for log10 transformed number of turns')
qqline(log10(results[,'num_turns']))

So it looks like 80% of regular games of war last between 85 and 657 turns, with a mean of ~320 turns.

How does the initial hand affect your winning percentage?

How often will I win if I have all the aces?

tab = results %>% select(winner, player0_As) %>% table
per = apply(tab,2,function(x) x / sum(x))

tab
##       player0_As
## winner    0    1    2    3    4
##      0   93  825 1924 1681  493
##      1  445 1649 1974  813  103
per
##       player0_As
## winner         0         1         2         3         4
##      0 0.1728625 0.3334681 0.4935865 0.6740176 0.8271812
##      1 0.8271375 0.6665319 0.5064135 0.3259824 0.1728188

It seems that with 4 aces, a player will win 82% of the time. with 3 aces tehy will win 67% of the time, and will have about even chances if each player has 2 aces.

How often will I win depending on the average hand strength?

results %>% ggplot(aes(player0_mean, fill=as.factor(winner))) + geom_density(alpha=.2)

model = glm(winner~player0_mean, family=binomial(link='logit'), data = results)

plot(results$player0_mean, 
     predict(model, type='response'), 
     xlim=c(5,9), ylim=c(0,1), 
     xlab = 'Player zero\'s starting strength', 
     ylab = 'Chance of player zero winning')

How does the anti size affect game duration?

results_all %>% ggplot(aes(x = factor(war_anti), y=log10(num_turns))) + geom_violin()

It seems that the larger the war_anti, the faster the game will be over! So if you’re playing your cousin and want the average game over in about 100 turns, changing the rules to have an anti of 6 cards during a “war” is the way to go!