Previously we looked at how we can quantify the likelihood of a certain outcome. This time I’m going to look at the effect of a certain action. Warjacks and warbeasts can normally romp around the battlefield, smashing things up. Warjacks can be allocated resources, called focus, by their warcaster. Each warjack can be given up to three focus. Focus can be spent to boost an attack or damage roll (roll an extra die), or buy an additional attack. Warbeasts can be forced by their warlocks, causing them to generate fury. Each time they are forced they may boost an attack or damage roll, or buy an additional attack. A warbeast has a fury stat that is the limit of the number of times they can be forced in a turn.

Some game effects can cause warjacks or warbeasts to be knocked down or frozen stationary. This makes them sad, as they have to forfeit their movement or action, so are less rompy and smashy. Luckily there’s a mechanic, shake, that allows them to spend a focus (or gain a fury) to leap up and continue their tasks.

But what is the cost of shaking these effects? Or put another way, how effective is it to make a certain warjack or warbeast stationary. We have already worked out how much damage can be applied by a Steelhead. But warjacks and warbeasts have weapons with special abilities. Each attack may have ramifications of how the subsequent attacks perform. So potentially there could be a lot of combinations. Rather than solving analytically, I’m going to simulate. This will provide an estimate of the analytical values, but will give us a good idea of the effects. In addition, we need to state our assumptions. The most obvious metric for warjack/warbeast effectiveness is damage output. I’m going to assume that an Ironclad and a Scythean are going to charge a target with the defensive stats of an Ironclad, and repeatedly hit it with their weapons. Ordering your warjack/warbeast to charge costs a focus but causes the damage roll to be boosted. I’m not going to take into account power attacks. Nor am I going to take other outcomes into account (is the target knocked down, are near-by targets hit with scything talons, etc.).

The Scythean, Broken Zealot Painting

I encoded the weapons of the Ironclad and Scythean as list objects. A list is a great data structure that allows mixtures of characters and numbers. The Ironclad is slightly more likely to hit and its hammer has a chance of knocking down the target, making the remaining attacks to automatically hit. Its fist is slightly lower power. The Scythean has two high power weapons and can make a free attack if it hits with both its intial attacks. In game terms, it costs more points to add to an army.

> ironclad <- list(CC = list(MAT = 7, weapons = list( + 'quake hammer' = list(PAS = 18, special = c("crit knockdown")), + 'open fist' = list(PAS = 14, special = 0))), RNG = NULL) > scythean <- list(CC = list(MAT = 6, weapons = list( + 'scythe' = list(PAS = 17, special = c("chain attack bloodbath")), + 'scythe' = list(PAS = 17, special = c("chain attack bloodbath")))),

+ RNG = NULL)

I created a pair of functions that will allow me to charge a single target. The function **attack** performs a sequence of dice rolls and spends focus as requested to boost where necessary. I don’t like calling functions to roll dice repeatedly within a function, so I pass in all the dice results I’ll need. This has the added benefit of making testing much easier as you can check what happens when you roll all sixes or all ones for example. It also has the advantage of allowing the code to work with true randomness (see www.random.org for truly random data generated from atmospheric noise) rather than computer generated quasi-random numbers. But true randomness is not strictly needed for this small study. The function takes a warjack object as shown above, and takes into account the effect of critical knockdown.

> attack <- function(warjack, which, DEF = 12, ARM = 18, charge = FALSE, + boost_hit = TRUE, boost_damage = TRUE, foc, kd = FALSE, dice, pos = 1) { + num_dice_hit <- 2 + num_dice_dam <- 2 + hit_roll <- 1 + damage <- 0 + hit <- FALSE + + # charge attack always spend focus first + if (charge) { + if (foc < 1) { stop("at least 1 focus required for charge attack") } + foc <- foc - 1 + num_dice_dam <- num_dice_dam + 1 + boost_damage <- FALSE + } + + # boost hit if able when not knocked down + if (!kd) { + if (boost_hit & foc > 0) { + num_dice_hit <- num_dice_hit + 1 + foc <- foc - 1 } + # hit + hit_roll <- dice[seq.int(from = pos, to = pos + num_dice_hit - 1)] + if (any(is.na(hit_roll))) { stop("insufficient dice for hit_roll") } + pos <- pos + num_dice_hit + } + + hit <- ((sum(hit_roll + warjack$CC$MAT) >= DEF & !all(hit_roll < 2)) | all(hit_roll > 5)) | kd + # cause damage when hit + if (hit) { + + # only boost damage roll when hit + if (boost_damage & foc > 0) { + num_dice_dam <- num_dice_dam + 1 + foc <- foc - 1 + } + + # check for critical effect + if ("crit knockdown" %in% warjack$CC$weapons[[which]]$special) { + if (sum(duplicated(hit_roll)) > 0) { kd <- TRUE } } + + damage_roll <- dice[seq.int(from = pos, to = pos + num_dice_dam - 1)] + if (any(is.na(damage_roll))) { stop("insufficient dice for damage_roll") } + pos <- pos + num_dice_dam + damage <- warjack$CC$weapons[[which]]$PAS + sum(damage_roll) - ARM + } + if (damage < 0) { damage <- 0 } + + return(c('damage' = damage, 'focus' = foc, 'knocked down' = kd, 'position' = pos, 'hit' = hit)) + } > > # TEST 1: no focus, all sixes > test1 <- attack(ironclad, which = 1, DEF = 0, ARM = 0, + boost_hit = FALSE, boost_damage = TRUE, foc = 0, kd = FALSE, dice = rep(6, 4)) > all(test1 == c(damage = 30, focus = 0, "knocked down" = 1, position = 5, hit = 1)) [1] TRUE > > # TEST 2: two focus, all ones > test2 <- attack(ironclad, which = 2, DEF = 0, ARM = 0, + boost_hit = TRUE, boost_damage = TRUE, foc = 2, kd = FALSE, dice = rep(1, 3)) > all(test2 == c(damage = 0, focus = 1, "knocked down" = 0, position = 4, hit = 0)) [1] TRUE > > # TEST 3: foc > 2, pos 4, kd > test3 <- attack(ironclad, which = 1, DEF = 0, ARM = 0, + boost_hit = TRUE, boost_damage = TRUE, foc = 4, kd = FALSE, + dice = c(1, 1, 1, 5, 5, 1, 1, 2, 3), pos = 4) > all(test3 == c(damage = 24, focus = 2, "knocked down" = 1, position = 10, hit = 1)) [1] TRUE > > # TEST 4: DEF 100 > test4 <- attack(ironclad, which = 1, DEF = 100, ARM = 100, + boost_hit = TRUE, boost_damage = TRUE, foc = 4, kd = TRUE, + dice = c(1, 1, 1, 5, 5, 1, 1, 2, 3), pos = 4) > all(test4 == c(damage = 0, focus = 3, "knocked down" = 1, position = 7, hit = 1)) [1] TRUE > > # TEST 5: DEF 100 > test5 <- attack(ironclad, which = 1, DEF = 100, ARM = 100, + boost_hit = TRUE, boost_damage = FALSE, foc = 1, kd = FALSE, + dice = c(5, 5, 3), pos = 1) > all(test5 == c(damage = 0, focus = 0, "knocked down" = 0, position = 4, hit = 0)) [1] TRUE > > # TEST 6: charge ironclad > test6 <- attack(ironclad, which = 1, DEF = 12, ARM = 18, charge = TRUE, + boost_hit = TRUE, boost_damage = TRUE, foc = 1, kd = FALSE, + dice = c(5, 4, 1, 1, 1), pos = 1) > all(test6 == c(damage = 3, focus = 0, "knocked down" = 0, position = 6, hit = 1)) [1] TRUE

The second function, **activation**, performs the entire activation of the warjack/warbeast. The warjack/warbeast first makes all its initial attacks, boosting as requested. Any remaining focus is then used to buy additional attacks. This function takes into account the ability Chain Attack: Bloodbath, which allows the Scythean to make an additional attack on each model in its melee range if it hits with both its initial attacks.

> activation <- function(warjack, DEF = 12, ARM = 18, + boost_hit = TRUE, boost_damage = TRUE, foc = 3, kd = FALSE, dice = sample(1:6, size = 30, replace = TRUE)) { + if (foc < 1) { + warning("warjack is unable to charge into combat") + return(as.numeric(0)) } + num_cc <- length(warjack$CC$weapons) + tot <- 0 + pos <- 1 + chain <- -1 + + for (i in seq_len(num_cc)) { + bd <- boost_damage + chg <- FALSE + if (i == 1) { chg <- TRUE } + out <- attack(warjack = warjack, which = i, DEF = DEF, ARM = ARM, charge = chg, + boost_hit = boost_hit, boost_damage = boost_damage, foc = foc, kd = kd, dice = dice, pos = pos) + tot <- tot + unname(out['damage']) + foc <- unname(out['focus']) + kd <- unname(out['knocked down']) + pos <- unname(out['position']) + if ("chain attack bloodbath" %in% warjack$CC$weapons[[i]]$special & out['hit']) { + chain <- chain + 1 } + } + if (chain != 1) { chain <- 0 } + for (i in seq_len(foc + chain)) { + if (foc > 0 | chain == 1) { + if (chain == 1) { chain <- 0 + } else {foc <- foc - 1 } + out <- attack(warjack = warjack, which = 1, DEF = DEF, ARM = ARM, + boost_hit = boost_hit, boost_damage = boost_damage, foc = foc, kd = kd, dice = dice, pos = pos) + tot <- tot + unname(out['damage']) + foc <- unname(out['focus']) + kd <- unname(out['knocked down']) + pos <- unname(out['position']) + } + } + return(tot) + } > > # TEST 1: all sixes, boost all > test1 <- activation(ironclad, DEF = 12, ARM = 18, boost_hit = TRUE, boost_damage = TRUE, + foc = 3, dice = rep(6, 9)) > test1 == 32 [1] TRUE > > # TEST 2: all sixes, no boost > test2 <- activation(ironclad, DEF = 12, ARM = 18, boost_hit = FALSE, boost_damage = FALSE, + foc = 3, dice = rep(6, 11)) > test2 == 50 [1] TRUE > > # TEST 3: all ones, no boost, knocked down > test3 <- activation(ironclad, DEF = 12, ARM = 18, boost_hit = FALSE, boost_damage = FALSE, + foc = 3, kd = TRUE, dice = rep(1, 9)) > test3 == 7 [1] TRUE > > # TEST 4: all ones, no boosted > test4 <- activation(ironclad, DEF = 12, ARM = 18, boost_hit = FALSE, boost_damage = FALSE, + foc = 3, dice = rep(1, 10)) > test4 == 0 [1] TRUE > > # TEST 5: max (17) dice, min damage > d5 <- c(1, 4, 1, 1, 1, 2, 3, 1, 1, 3, 2, 1, 1, 4, 2, 1, 1) > test5 <- activation(ironclad, DEF = 12, ARM = 18, + boost_hit = FALSE, boost_damage = FALSE, foc = 3, dice = d5) > test5 == 7 [1] TRUE > > # TEST 6: > d6 <- c(1, 1, 6, 2, 2, 1, 2, 6, 3, 1, 2, 1, 2, 2, 6, 2, 6) > test6 <- activation(ironclad, DEF = 12, ARM = 18, + boost_hit = FALSE, boost_damage = FALSE, foc = 3, dice = d6) > test6 == 8 [1] TRUE > > # TEST 7: > d7 <- c(6, 1, 3, 4, 2, 2, 5, 5, 5, 3, 1, 2, 2, 5, 4, 5, 2) > test7 <- activation(ironclad, DEF = 12, ARM = 18, + boost_hit = TRUE, boost_damage = FALSE, foc = 3, dice = d7) > test7 == 8 [1] TRUE > > # TEST 8: no charge > test8 <- activation(ironclad, DEF = 12, ARM = 18, + boost_hit = TRUE, boost_damage = FALSE, foc = 0, dice = d6) Warning message: In activation(ironclad, DEF = 12, ARM = 18, boost_hit = TRUE, boost_damage = FALSE, : warjack is unable to charge into combat > test8 == 0 [1] TRUE > > # TEST 9: scythean 6s > test9 <- activation(scythean, DEF = 12, ARM = 18, + boost_hit = FALSE, boost_damage = FALSE, foc = 3, dice = rep(6, 21)) > test9 == 6 * 11 - 5 [1] TRUE > > # TEST 10: scythean 1s > test10 <- activation(scythean, DEF = 12, ARM = 18, + boost_hit = FALSE, boost_damage = FALSE, foc = 3, dice = rep(1, 8)) > test10 == 0 [1] TRUE > > # TEST 11: scythean 1s, kd > test11 <- activation(scythean, DEF = 12, ARM = 18, kd = TRUE, + boost_hit = FALSE, boost_damage = FALSE, foc = 3, dice = rep(1, 11)) > test11 == 6 [1] TRUE

Note that this code can be used to represent both focus and fury. Spending a focus or gaining a fury amount to the same limitation on actions. I can now work out the damage output of the Ironclad and the Scythean with varying amounts of focus. The effect of causing either to be knocked down or stationary is equivalent to being allocated one less focus that activation.

> Nt <- 17 > Ni <- 10000 > > > combns <- expand.grid(FOC = 1:3, B_HIT = 0:1, B_DAM = 0:1) > combns <- combns[order(combns$FOC), ] > out <- vector(mode = "list", length = nrow(combns)) > names(out) <- apply(combns, 1, function(x) { + paste("FOC", x[1], "B_HIT", x[2], "B_DAM", x[3]) }) > for (i in seq_len(nrow(combns))) { + + dice <- matrix(sample(1:6, size = Nt * Ni, replace = TRUE), ncol = Ni) + + vals <- apply(dice, 2, function(x) { + activation(ironclad, DEF = 12, ARM = 18, + boost_hit = combns[i, "B_HIT"], boost_damage = combns[i, "B_DAM"], foc = combns[i, "FOC"], dice = x) }) + + out[[i]] <- list(quantiles = NA, x = NA, y = NA) + out[[i]]$quantiles <- quantile(vals, probs = c(0, 0.1, 0.25, 0.5, 0.75, 0.9, 1)) + + dens <- density(vals, bw = 1, from = min(vals), to = max(vals)) + + out[[i]]$x <- dens$x + out[[i]]$y <- dens$y + } > qntls <- sapply(out, function(x) { x$quantiles }) > t(qntls) 0% 10% 25% 50% 75% 90% 100% FOC 1 B_HIT 0 B_DAM 0 0 8 11 13 16 18 26 FOC 1 B_HIT 1 B_DAM 0 0 8 11 13 16 18 26 FOC 1 B_HIT 0 B_DAM 1 0 8 11 13 16 18 26 FOC 1 B_HIT 1 B_DAM 1 0 8 11 13 16 18 26 FOC 2 B_HIT 0 B_DAM 0 1 14 17 20 23 26 35 FOC 2 B_HIT 1 B_DAM 0 0 9 11 14 16 18 26 FOC 2 B_HIT 0 B_DAM 1 0 11 14 17 20 22 31 FOC 2 B_HIT 1 B_DAM 1 0 9 11 14 16 18 26 FOC 3 B_HIT 0 B_DAM 0 5 20 23 27 31 34 46 FOC 3 B_HIT 1 B_DAM 0 0 10 13 16 20 24 36 FOC 3 B_HIT 0 B_DAM 1 1 17 20 24 27 30 40 FOC 3 B_HIT 1 B_DAM 1 0 10 12 15 18 21 31

The quantiles show the value with each proportion of the data. Since the data are frequencies of events, these correspond to the probabilities. Here we have considered twelve scenarios: boosting to hit and/or damage or neither with one, two or three focus available. The expected damage output of the Ironclad with three focus (the 50-percentile) is 27 provided we buy attacks rather than boosting to hit or damage. Losing a focus to shake knock down or stationary causes this to drop to 20, so a 26% drop in expected effectiveness.

We can view these distributions more intuitively by looking at the probability density of the outcome of these activations.

> rnges <- lapply(out, function(x) { list(X = range(x$x), Y = range(x$y)) }) > rnges <- matrix(unlist(rnges), ncol = 4, byrow = TRUE) > cols <- rep(hsv(c(0.05, 0.35, 0.55), s = 0.8, v = 0.9, alpha = 0.7), each = 4) > ltys <- as.numeric(factor(paste("B_HIT", combns[, "B_HIT"], "B_DAM", combns[, "B_DAM"]))) > par(mar = c(5, 4, 1, 1), mfrow = c(2, 2)) > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 1:4) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 5:8) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 9:12) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > par(xpd = TRUE) > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "", ylab = "", type = "n", axes = FALSE) > legend("topright", legend = names(out), lwd = 2, lty = as.numeric(ltys), col = cols)

Reassuringly the four curves overlap with one focus, since there is only one way of activating when we must spend one focus to charge. The curves with two and three focus show that for an Ironclad hitting an Ironclad, buying attacks is better than boosting the attack or damage rolls.

We can run the same analysis for the Scythean. The Scythean can be forced up to four times.

> combns <- expand.grid(FURY = 1:4, B_HIT = 0:1, B_DAM = 0:1) > combns <- combns[order(combns$FURY), ] > out <- vector(mode = "list", length = nrow(combns)) > names(out) <- apply(combns, 1, function(x) { + paste("FURY", x[1], "B_HIT", x[2], "B_DAM", x[3]) }) > > for (i in seq_len(nrow(combns))) { + + dice <- matrix(sample(1:6, size = Nt * Ni, replace = TRUE), ncol = Ni) + + vals <- apply(dice, 2, function(x) { + activation(scythean, DEF = 12, ARM = 18, + boost_hit = combns[i, "B_HIT"], boost_damage = combns[i, "B_DAM"], foc = combns[i, "FURY"], dice = x) }) + + out[[i]] <- list(quantiles = NA, x = NA, y = NA) + out[[i]]$quantiles <- quantile(vals, probs = c(0, 0.1, 0.25, 0.5, 0.75, 0.9, 1)) + + dens <- density(vals, bw = 1, from = min(vals), to = max(vals)) + + out[[i]]$x <- dens$x + out[[i]]$y <- dens$y + } > > qntls <- sapply(out, function(x) { x$quantiles }) > t(qntls) 0% 10% 25% 50% 75% 90% 100% FURY 1 B_HIT 0 B_DAM 0 0 14 18 21 24 27 37 FURY 1 B_HIT 1 B_DAM 0 0 14 18 21 24 27 36 FURY 1 B_HIT 0 B_DAM 1 0 14 18 21 24 27 37 FURY 1 B_HIT 1 B_DAM 1 0 14 18 21 24 27 38 FURY 2 B_HIT 0 B_DAM 0 1 19 23 27 31 34 45 FURY 2 B_HIT 1 B_DAM 0 0 15 18 21 24 27 36 FURY 2 B_HIT 0 B_DAM 1 2 17 21 24 28 31 43 FURY 2 B_HIT 1 B_DAM 1 0 15 18 21 24 27 36 FURY 3 B_HIT 0 B_DAM 0 6 24 28 32 37 40 53 FURY 3 B_HIT 1 B_DAM 0 2 15 18 21 25 27 38 FURY 3 B_HIT 0 B_DAM 1 6 20 24 28 32 35 48 FURY 3 B_HIT 1 B_DAM 1 1 15 18 21 24 27 37 FURY 4 B_HIT 0 B_DAM 0 8 29 34 38 43 47 62 FURY 4 B_HIT 1 B_DAM 0 2 15 18 21 25 27 36 FURY 4 B_HIT 0 B_DAM 1 6 25 30 34 38 42 53 FURY 4 B_HIT 1 B_DAM 1 3 18 21 25 28 31 41

Interestingly here our 100-percentile does not include our calculated maximum damage output of 61. This dream activation occurs when we roll three sixes on the boosted charge damage roll, then boxcars for the second initial, chain attack and two bought attacks. There is only one way of rolling 11 sixes out of 362797056 combinations, so with just 10000 simulations we’d be surprised if we caught that one. You’re actually 26 times more likely to win the National Lottery jackpot than roll that much fire in one activation. Actually, winning the Lottery is rather easy, as you also need to hit with each of those five attacks! Basically, putting out that much damage doesn’t happen in a lifetime, so these estimates are just fine.

The expected damage output of the Scythean when it was forced four times is 38 provided we buy attacks rather than boosting to hit or damage. Losing a focus to shake knock down or stationary causes this to drop to 32, so a 16% drop in expected effectiveness.

> rnges <- lapply(out, function(x) { list(X = range(x$x), Y = range(x$y)) }) > rnges <- matrix(unlist(rnges), ncol = 4, byrow = TRUE) > cols <- rep(hsv(c(0.05, 0.35, 0.55, 0.75), s = 0.8, v = 0.9, alpha = 0.7), each = 4) > ltys <- as.numeric(factor(paste("B_HIT", combns[, "B_HIT"], "B_DAM", combns[, "B_DAM"]))) > > par(mar = c(5, 4, 1, 1), mfrow = c(3, 2)) > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 1:4) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 5:8) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 9:12) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "Damage", ylab = "Density", type = "n") > for (i in 13:16) { + lines(out[[i]]$x, out[[i]]$y, lwd = 2, col = cols[i], lty = as.numeric(ltys[i])) + } > > z <- c(1:4, 9:12) > par(xpd = TRUE) > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "", ylab = "", type = "n", axes = FALSE) > legend("center", legend = names(out)[z], lwd = 2, lty = as.numeric(ltys)[z], col = cols[z]) > > z <- c(5:8, 13:16) > plot(0, 0, xlim = c(0, max(rnges[, 2])), ylim = c(0, max(rnges[, 4])), + xlab = "", ylab = "", type = "n", axes = FALSE) > legend("center", legend = names(out)[z], lwd = 2, lty = as.numeric(ltys)[z], col = cols[z])

The Scythean shows the same preference for buying attacks over boosting attacks as the Ironclad. Both pieces have relatively high melee attack and power and strength stats relative to the defence and armour stat used for this simulation. But even assuming the Scythean is only being forced three times, it still expects to cause 5 more damage than the Ironclad.

The main difference between these two pieces are the special abilities of the weapons. A critical effect is only going to come into effect in a small proportion of activations (a nineth when hitting on fives). Whereas two hits on sixes is quite likely (52%). Therefore, we are likely to get the extra attack whether or not our Scythean needs to shake stationary.

Knocking down the Ironclad is more punishing. Whatever we do to the Scythean, it keeps on getting up again.