clear and present danger

A feature of gaming is balancing risk. In a game of incomplete information, such as Poker, managing risk is the entirety of gameplay. In a game of complete information, such as chess, all possible game states are known, but even here risk can be part of the game. This interesting post describes a chess grand master’s experience of luck in chess. A player believes that their opponent will behave in a certain way (even more so at the higher echelons of the game). That player’s decisions are based on their predictions. But in fact, another human is a fundamentally inscrutable being, and we can never be certain of their behaviour. From the point of view of a player, their opponent’s moves are random.

ImageI do not mean by this that all outcomes are equally likely. For a given game state we might think that a certain move was quite likely, three or four other moves possible, several other moves rare and the dozens of other moves virtually impossible.

> con <- textConnection("
+ Move, Prob
+ Qa3, 0.7133513
+ Qc2, 0.1551001
+ Qa4, 0.0403191
+ Bc3, 0.03822303
+ Nd3, 0.02806436
+ Qd1, 0.01058643
+ O-O, 0.01018841
+ Qb4, 0.0009942973
+ Bxf6, 0.0004756444
+ Rb1, 0.0004467477
+ Rd1, 0.0002551076
+ Rc1, 0.0002442638
+ Ke2, 0.0002115225
+ Bh6, 0.0001140581
+ a4, 0.0001102081
+ Bf4, 0.0001093136
+ h4, 0.0001082806
+ Nh4, 0.0001079138
+ a3, 0.0001067275
+ Rg1, 0.0001006433
+ Bh4, 9.677298e-05
+ Be2, 9.046386e-05
+ Kd1, 8.637634e-05
+ Bd3, 8.498221e-05
+ Nb1, 8.203362e-05
+ Ra1, 8.095692e-05
+ g3, 7.710319e-05
+ g4, 5.188052e-05
+ Ne5, 4.85662e-05
+ Qxc4, 4.409369e-05
+ Ne4, 1.050668e-05
+ h3, 1.008284e-05
+ e4, 9.442239e-06
+ Nh4, 9.294394e-06
+ ")
> chess <- read.table(con, header = TRUE, sep = ",")
> col_pal <- diverge_hcl(34, c = 100,
+     l = (range(chess$Prob) + 1) * 45, power = 3)
> par(las = 2, mar = c(5, 4, 1, 1))
> prob_br <- cut(chess$Prob, breaks = 34)
> bp <- barplot(chess$Prob * 100, col = col_pal[prob_br], ylab = "%")
> axis(1, at = bp, labels = chess$Move, cex.axis = 0.6)
> # note that a percentage is 100 times a probability

Image

When we plan turns in Warmachine, we need to take into account this kind of positioning randomness. We have access to all information about our opponents’ army. Will an opponent send their best melee unit into combat with your tarpit unit? Or will they try to clear the tarpit with shooting? In which case, another target may survive to carry out its intended role.

But of course, Warmachine is a complex game. There may be interactions between pieces that are not obvious at first glance. There may be combinations of play, or sequences of activation that are hidden to the casual observer, unless you are well versed in the faction’s tricks. Similarly, our opponents might not know our tricks, and may not take these combinations into account during play.

In addition, we have dice rolls. This is an extra layer of randomness, that can turn the last chance of desperate survivors into glorious conquerors, or an unstoppable force into a fumbling mess of hopeless halfwits.

Luckily, dice are somewhat predictable. All outcomes are known. And by using our knowledge of the probabilities, we can mitigate risk accordingly.

> par(las = 2, mar = c(5, 4, 1, 1), mfrow = c(2, 2))
> barplot(diceDistr(n = 1, D = 6) * 100, col = col_pal[8],
+     names = seq_len(6), ylab = "%")
>
> brks <- strsplit(attr(prob_br, "levels"), "\\(|,|]")
> brk <- sapply(brks, function(x) { x[3] })
>
> twoD6 <- diceDistr(n = 2, D = 6)
> barplot(twoD6 * 100, col = col_pal[cut(twod6, breaks = c(0, brk))],
+     names = names(twod6), ylab = "%")
>
> threeD6 <- diceDistr(n = 3, D = 6)
> barplot(threeD6 * 100, col = col_pal[cut(threed6, breaks = c(0, brk))],
+     names = paste(names(threed6), rep(c("", "   "), 6)), ylab = "%")
>
> fourD6 <- diceDistr(n = 4, D = 6)
> barplot(fourD6 * 100, col = col_pal[cut(fourd6, breaks = c(0, brk))],
+     names = paste(names(fourd6), rep(c("", "   ", "      "), 6)), ylab = "%")

Image

We can allocate risk based on how certain we would like to be when we set out to achieve a task. But we should be aware of when we’re setting up conditional probabilities. Hitting and damaging on average dice is not that likely, since we only get to roll for damage on the condition that we first successfully hit.

> twoD6 <- diceDistr(n = 2, D = 6)
> sevens <- sum(twoD6[6:11])
> sevens
[1] 0.5833333
> sevens * sevens
[1] 0.3402778

We’ve only got a 34% chance of killing an enemy solo if we’re hitting and killing on sevens.

If we send one Steelhead to kill Ragman, needing nines to hit and sixes to kill on 2D6, then presumably we don’t really want to get the job done.

> nines <- sum(twoD6[8:11])
> sixes <- sum(twoD6[5:11])
> nines
[1] 0.2777778
> sixes
[1] 0.7222222
> nines * sixes
[1] 0.2006173

One Steelhead only has a 20% chance of killing Ragman when he’s undamaged. The non-fatal wounds will make him easier to kill at least. But he is likely to get his revenge before that happens.

Calculating the probability of two Steelheads killing Ragman is a little more involved, due to his boxes. The analytical solution is to add the probability of the first Steelhead killing him, to the probabilities of the second Steelhead killing him when he’s taken between zero and four damage. But it’s quicker for me to estimate that probability by rolling some virtual dice.

> kill_Ragman <- function() { ifelse(
+         sample(2:12, size = 1, prob = twoD6) >= 9,
+         sample(1:11, size = 1, prob = twoD6), 0) }
> N <- 100000
> x <- vector(length = N, mode = "logical")
> for (i in seq_len(N)) { x[i] <- kill_Ragman() + kill_Ragman() >= 5 }
> sum(x) / N
[1] 0.36461

Would they be more likely to kill Ragman if they perform a combined melee attack?

> fours <- sum(twoD6[3:11])
> sevens * fours
[1] 0.5347222

By combining they’re drawing their single roll into reasonable likelihood, so the cost of losing an attack is offset.

We can update our methods to reflect a given goal, rather than showing the expected value. For example, Steelheads attacking single wound warriors.

> # probability of causing at least x damage
> #
> # pas, power and strength (single numeric)
> # arm, armour value (single numeric)
> # distr, a named vector of dice probabilities
> # box, a minimum amount of damage to cause
>
> probX <- function(pas, arm, distr, box = 1) {
+
+     # the effect density
+     distrE <- pas + as.numeric(names(distr)) - arm
+    
+     # no negative effect
+     selectRolls <- distrE >= box
+
+     # probability of >= 1 damage per attack
+     prb <- sum(distr[selectRolls])
+
+     return(prb)
+ }
>
> # TEST 1 pas 6 vs arm 12 == 58%
> test1 <- probX(pas = 6, arm = 12, distr = twoD6, box = 1)
> test1 == sevens
[1] TRUE
>
> # TEST 2 pas 1 vs arm 3, 3D6 == 100%
> test2 <- probX(pas = 1, arm = 3, distr = threeD6, box = 1)
> test2 == 1
[1] TRUE
>
> # TEST 3 pas 1 vs arm 19, 3D6 == 0%
> test3 <- probX(pas = 1, arm = 19, distr = threeD6, box = 1)
> test3 == 0
[1] TRUE
>
>
> MAT <- seq_len(15)
>
> DEF <- seq_len(20)
>
> PAS <- seq_len(22)
>
> ARM <- seq_len(27)
>
> # pas by row
> pasMat <- matrix(rep(PAS, times = length(ARM)),
+     nrow = length(PAS))
>
> # arm by column
> armMat <- matrix(rep(ARM, each = length(PAS)),
+     nrow = length(PAS))
>
> # tabulation array
> dArray <- array(c(pasMat, armMat),
+     dim = c(length(PAS), length(ARM), 2))
>
> ###########################################################
>
> # calculate prob kill tables
> pkTable2 <- apply(dArray, 1:2, function(x) {
+         probX(pas = x[1], arm = x[2], distr = twoD6, box = 1) })
> dimnames(pkTable2) <- list(PAS, ARM)
> round(pkTable2[6:18, 10:25], 2)
     10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25
6  0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
7  0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00 0.00 0.00 0.00 0.00 0.00
8  0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00 0.00 0.00 0.00 0.00
9  1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00 0.00 0.00 0.00
10 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00 0.00 0.00
11 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00 0.00
12 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00 0.00
13 1.00 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03 0.00
14 1.00 1.00 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08 0.03
15 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17 0.08
16 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28 0.17
17 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42 0.28
18 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.97 0.92 0.83 0.72 0.58 0.42
> # for example a Steelhead has a 42% chance 
> # of killing an ARM 18 1 box warrior he has hit
> image(x = PAS, y = ARM, z = pkTable2,
+     col = diverge_hcl(60, c = 100,
+         l = (range(pkTable2) + 1) * 45, power = 1))
>
> text(seq(4, 19, by = 3),
+     seq(19, 4, by = -3),
+         labels = round(pkTable2[seq(4, 19, by = 3),
+             seq(19, 4, by = -3)][as.logical(diag(6))], 1))

Image

As expected we are more likely to harm a high ARM target with a high power attack.

To determine whether we should be attacking single wound warriors individually or with a combined melee attack we need to consider our goal. If we need to kill two warriors with two Steelheads then we must attack individually. If we are content to kill at least one, then let’s look at the numbers.

> mat <- 5
> pas <- 11
>
> p_damage <- array(data = 0, dim = c(length(DEF), length(ARM), 2),
+     dimnames = list(DEF, ARM, c("2ind", "cma2")))
>
> # 2 independent attacks
>
> ph <- matrix(probTable2[mat, , drop = TRUE],
+     nrow = length(ARM), ncol = length(DEF),
+     byrow = TRUE, dimnames = list(ARM, DEF))
>
> pk <- matrix(pkTable2[pas, ],
+     nrow = length(ARM), ncol = length(DEF),
+     byrow = FALSE, dimnames = list(ARM, DEF))
>
> comb <- ph * pk +  # first one hits AND kills
+     (1 - ph) * ph * pk + # first misses AND second kills
+     ph * (1 - pk) * ph * pk # first fails to kill AND second kills
> p_damage[, , 1] <- t(comb)
>
> # combined melee attack
>
> ph2 <- matrix(probTable2[mat + 2, , drop = TRUE],
+     nrow = length(ARM), ncol = length(DEF),
+     byrow = TRUE, dimnames = list(ARM, DEF))
>
> pk2 <- matrix(pkTable2[pas + 2, ],
+     nrow = length(ARM), ncol = length(DEF),
+     byrow = FALSE, dimnames = list(ARM, DEF))
> p_damage[, , 2] <- t(ph2 * pk2)
> pdam <- melt(p_damage, varnames = c("DEF", "ARM", "group"))
>
> cols <- rep(diverge_hcl(5, c = 100,
+     l = (range(pdam$value) + 1) * 45, power = 3), each = 20)
>
> require(lattice)
> i = 2
> trellis.par.set(regions = list(col = cols))
> wireframe(value ~ DEF * ARM, data = pdam, groups = group,
+     scales = list(arrows = FALSE),
+     drape = TRUE, colorkey = TRUE,
+     screen = list(z = i - 140, x = -50))

Image

Probabilities must exist between zero (impossible) and one (certain), so probability space is not linear. For example, increasing your likelihood of hitting from 10% to 20% means that you’re twice as likely to hit. But increasing your likelihood of hitting from 80% to 90% means you’re only 9/8th times more likely to hit. Rather than looking at the differences, we could look at the ratio of killing at least one warrior by CMA rather than two attacks.

> round(apply(p_damage, 1:2, function(x) { x[2] / x[1] })[7:18, 5:18], 2)
      5    6    7    8    9   10   11   12   13   14   15   16   17   18
7  0.97 0.97 0.97 0.97 0.97 0.97 0.97 0.97 0.98 0.98 0.98 0.98 1.00 1.09
8  0.97 0.97 0.97 0.97 0.97 0.97 0.97 0.97 0.98 0.98 0.98 0.98 1.00 1.09
9  0.98 0.98 0.98 0.98 0.98 0.98 0.98 0.98 0.98 1.00 1.00 1.01 1.03 1.14
10 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.01 1.03 1.04 1.06 1.10 1.22
11 0.99 0.99 0.99 0.99 0.99 0.99 0.99 0.99 1.01 1.03 1.06 1.09 1.15 1.29
12 1.01 1.01 1.01 1.01 1.01 1.01 1.01 1.01 1.03 1.06 1.10 1.15 1.23 1.41
13 1.09 1.09 1.09 1.09 1.09 1.09 1.09 1.09 1.12 1.17 1.22 1.29 1.41 1.65
14 1.22 1.22 1.22 1.22 1.22 1.22 1.22 1.22 1.25 1.31 1.39 1.48 1.63 1.93
15 1.36 1.36 1.36 1.36 1.36 1.36 1.36 1.36 1.40 1.48 1.57 1.69 1.88 2.24
16 1.74 1.74 1.74 1.74 1.74 1.74 1.74 1.74 1.79 1.89 2.01 2.18 2.44 2.94
17 3.04 3.04 3.04 3.04 3.04 3.04 3.04 3.04 3.13 3.31 3.54 3.85 4.32 5.23
18 1.52 1.52 1.52 1.52 1.52 1.52 1.52 1.52 1.56 1.66 1.77 1.92 2.16 2.62

For high DEF or high ARM (if either roll needs sevens or better for individual attacks), we are more likely to kill at least one warrior using a CMA. The distribution is curved , so if both rolls are highish, then CMA is also good. For weaker opponents, individual attacks are slightly better, plus there’s the opportunity of the second kill.

A better understanding of our risks can help us better manage our resources.

Advertisements

2 thoughts on “clear and present danger

  1. Pingback: i get knocked down | analytical gaming

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s