24 Days of R: Day 14

December 14, 2013
By

(This article was first published on PirateGrunt » R, and kindly contributed to R-bloggers)

I've had a statistical question that I've wanted to answer for more than 20 years. In college, I often played the strategy game “Axis & Allies” with friends. (Hi Brad, Rob, Brady and Marty!) The game is a crude simulation of the second world war, with various military units available to the player. The player chooses which units to utilize based on their available resources and the particular strengths and weaknesses of the units. A key feature is that units have different capabilities with respect to attack and defense. The most ubiquitous units are infantry and tank divisions. Infantry units attack with a 1/6 probability of success, but defend with a 1/3 probability. Tanks attack with a ½ probability of success, but defend with a 1/3. The tank is obviously a more powerful unit and so it costs more. Infantry units cost 3 resource points and tanks cost 5. So, for the same 30 resource points, which unit is more valuable? This obviously depends on other characteristics of the game such as the players' current positions and tactical objectives. I typically played Russia and the starting position of the game meant that I typically invested almost exclusively in infantry units. This tended to work as the German player would attack and I would defend. However, I never went about trying to demonstrate this mathematically.

It's possible- and quite likely- to have more than one type of unit available in a player's army. However, I'm going to ignore this for now and assume that a force composed of one type of unit is attacking a force of another type of unit. We'll code a simple function to resolve a battle and then show the results of one example. One of the really cool things about the list construct is that a function can return an arbitrarily complex set of information with no effort.

Battle = function(numAttack, numDefend, probAttack, probDefend) {

    while (numAttack > 0 & numDefend > 0) {
        casualtiesAttack = rbinom(1, numAttack, probDefend)
        casualtiesDefend = rbinom(1, numDefend, probAttack)
        numAttack = numAttack - casualtiesAttack
        numDefend = numDefend - casualtiesDefend
    }

    lstResult = list()
    lstResult$numAttack = max(numAttack, 0)
    lstResult$numDefend = max(numDefend, 0)
    lstResult$victor = ifelse(numAttack > numDefend, "Attacker", "Defender")
    lstResult$attackerWins = ifelse(numAttack > numDefend, 1, 0)
    lstResult
}

numAttack = 10
numDefend = 10
probAttack = 1/2
probDefend = 1/3
set.seed(1234)
lstResult = Battle(numAttack, numDefend, probAttack, probDefend)
print(paste("Victor is", lstResult$victor))
## [1] "Victor is Attacker"

Cool. We can resolve any arbitrary battle. Given equal sizes of an attacking force of tanks and a defending force of infantry, what's the probability of a defender victory?

simulations = 500

SimulateBattles = function(simulations, numAttack, numDefend, probAttack, probDefend) {
    lstResults = list()
    for (i in 1:simulations) {
        lstResults[[i]] = Battle(numAttack, numDefend, probAttack, probDefend)
    }

    lstResults
}

lstResults = SimulateBattles(simulations, numAttack, numDefend, probAttack, 
    probDefend)
attackerVictory = sapply(lstResults, "[[", "attackerWins")
probAttackWin = sum(attackerVictory)/simulations

Given equal sizes, we see (no surprise) that the attacker is more likely to win. But at what cost?

tankCost = 5
infantryCost = 3
survivingAttackers = sapply(lstResults, "[[", "numAttack")
survivingDefenders = sapply(lstResults, "[[", "numDefend")
totalAttackerCost = (tankCost * mean(survivingAttackers)) - tankCost * numAttack
totalDefenderCost = (infantryCost * mean(survivingDefenders)) - infantryCost * 
    numDefend

A tank attack is more likely to prevail, but the cost is higher. Again, no surprises. Finally, let's alter the premise and give each player equal resources. What kind of units should each player buy, presuming that one will attack and one defend? We'll consider four games:
Game 1 - Both players use infantry
Game 2 - Attacker uses infantry, defender uses tanks
Game 3 - Attacker uses tanks, defender uses infantry
Game 4 - Bot players use tanks

attackerResources = 30
defenderResources = 30

# Game 1 - All infantry
numAttack = attackerResources/infantryCost
numDefend = defenderResources/infantryCost
lstResults = SimulateBattles(simulations, numAttack, numDefend, 1/6, 1/3)

probAttackWin = sum(sapply(lstResults, "[[", "attackerWins"))/simulations

totalAttackerCost = (infantryCost * mean(sapply(lstResults, "[[", "numAttack"))) - 
    infantryCost * numAttack
totalDefenderCost = (infantryCost * mean(sapply(lstResults, "[[", "numDefend"))) - 
    infantryCost * numDefend

Game 1, with all infantry is a clear loser to the attacker, with a probability of victory only 0.066 . I'll simulate three other games, but not echo the code.

In Game 2, the attacker uses infantry and the defender uses tanks. This means that the attack and defense probabilities don't change, but the number of units involved- because of their cost- does. Little matter. The attacker has an increased likelihood of victory, 0.162, but it's still low.

In game 3, the attacker uses tanks and the defender uses infantry. In this case, the attacker has a greater than 50% chance of success. For this simulation, it's 0.636.

In game 4, each player uses tanks. This is not a great strategy for the defender as there is now just around a 1 in 4 chance of victory.

So, the strategy employed in game 3- attacker uses tanks and defender uses infantry- is the one which is optimal for each player. And that's typically how we played it. Of course, what makes the game fun is that there are those less likely situations when a defender would defeat the odds and prevail, or the attacker would win but at a ruinous cost. Over the course of many hours, there were plenty of new situations to consider and none of us relied on a computer to determine strategy. Good times.

Tomorrow: Very probably more monte carlo simulation.

sessionInfo()
## R version 3.0.2 (2013-09-25)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] knitr_1.4.1      RWordPress_0.2-3
## 
## loaded via a namespace (and not attached):
## [1] digest_0.6.3   evaluate_0.4.7 formatR_0.9    RCurl_1.95-4.1
## [5] stringr_0.6.2  tools_3.0.2    XML_3.98-1.1   XMLRPC_0.3-0

To leave a comment for the author, please follow the link and comment on his blog: PirateGrunt » R.

R-bloggers.com offers daily e-mail updates about R news and tutorials on topics such as: visualization (ggplot2, Boxplots, maps, animation), programming (RStudio, Sweave, LaTeX, SQL, Eclipse, git, hadoop, Web Scraping) statistics (regression, PCA, time series, trading) and more...



If you got this far, why not subscribe for updates from the site? Choose your flavor: e-mail, twitter, RSS, or facebook...

Comments are closed.