it ain’t easy being a lefty

Image

The Ironclad, The PerezAres Studio

Sometimes a thing has to be seen to be believed. Warmachine warjacks are represented as a damage grid with six columns and up to six rows. The columns are labelled from 1 on the left to 6 on the right. A number of boxes are marked as L (for left) or R (for right). When all boxes of that type are marked as damaged, the corresponding arm is disabled. If an attack causes more damage to a column then there boxes in that column, the damage rolls over to the next column. If all boxes are marked on column 6, additional damage is marked on column 1.

1 to 6 selects column, L and R signify left and right arm systems

1 to 6 selects column, L and R signify left and right arm systems

It was proposed that the left arm is more likely to be damaged than the right arm. An Ironclad is armed with a big hammer in his left hand. Assuming random damage on the columns, this hypothesis suggests his left arm will be crippled first. This seems odd as damage is equally likely on any column. The reason for this difference is that it only takes 8 damage on column 1 to cripple the left arm, but 9 damage on column 5 to cripple the right arm by roll over.

Let’s see what really happens! The maths of working out the magnitude of the effect could be quite tricky. But we can simulate for a variety of behaviours quite quickly.

First of all we need an object to represent a warjack’s battle grid. I’d like this code to be reusable, so where possible I’ll create objects that can be easily changed.

> ironclad_definition <- list(boxes = c(4, 5, 6, 6, 5, 4),
+     left = c(0, 1, 6, 6, 5, 4),
+     right = c(4, 5, 6, 6, 1, 0))

The Ironclad has 30 boxes. Once 4 damage has hit column 1 and 2, the left arm is crippled. Once 4 damage has hit column 5 and 6, the right arm is crippled.

Next I need an object to represent an innocent Ironclad. This can be a simple vector of 6 numbers. However, I’d also like to keep track of the number of damage caused by a single attack, the column that’s been hit, and whether either arm has been crippled.

> object <- list(warjack = ironclad_definition$boxes, 
+     no_boxes = 12, column = 3, crippled = NA)

I need to be able to roll the damage over each column.

> # mark the boxes of a warjack object with a single attack roll
> #
> # the function subtracts the number of boxes from the selected 
> # column if the result is negative, it carries over and the  
> # column number is incremented.
> # this continues while all columns are greater than zero,
> # until all damage has been used.
> # object, list with elements warjack, no_boxes and column
>
> mark_boxes <- function(object) {
+
+     object$warjack[object$column] <- object$warjack[object$column] - 
+         object$no_boxes
+
+     if (object$warjack[object$column] < 0 & 
+         any(object$warjack > 0)) {
+
+         object$no_boxes <- -object$warjack[object$column]
+
+         object$warjack[object$column] <- 0
+
+         if (object$column == 6) { object$column <- 1
+
+         } else { object$column <- object$column + 1 }
+
+         object <- mark_boxes(object)
+
+     } else {
+
+         object$no_boxes <- 0
+
+         return(object) }
+ }

This is an example of functional programming. The function is recursive, and shows the same behaviour as a loop. Once all damage has been applied, the injured warjack object is returned. It’s generally worth creating a couple of tests when you create a function. This just comforts you that everything’s behaving as it should.

> # TEST 1 damage
> o1 <- list(warjack = ironclad_definition$boxes,
+         no_boxes = 12, column = 3, crippled = NA)
> test1 <- mark_boxes(o1)
> all(test1$warjack == c(4, 5, 0, 0, 5, 4))
[1] TRUE
>
> # TEST 2 roll over
> o2 <- list(warjack = ironclad_definition$boxes,
+         no_boxes = 16, column = 4, crippled = NA)
> test2 <- mark_boxes(o2)
> all(test2$warjack == c(3, 5, 6, 0, 0, 0))
[1] TRUE
>
> # TEST 3 scrappage
>
> o3 <- list(warjack = ironclad_definition$boxes,
+         no_boxes = 30, column = 2, crippled = NA)
> test3 <- mark_boxes(o3)
> all(test3$warjack == c(0, 0, 0, 0, 0, 0))
[1] TRUE
>    
> # TEST 4 annihilation
>
> o4 <- list(warjack = ironclad_definition$boxes,
+         no_boxes = 101, column = 5, crippled = NA)
> test4 <- mark_boxes(o4)
> all(test4$warjack == c(0, 0, 0, -71, 0, 0))
[1] TRUE

Okay, so we can now hurt a warjack. Now we need to hit a warjack a load of times and see whether its left or right arm breaks first. First of all some functions to roll dice.

> # single damage roll (greater than armour value)
>
> no_boxes <- function(max = 12) {
+    
+     if (max < 1) { stop("max must be at least one") }
+    
+     sample(seq_len(max), 1) }
>
> # roll for column
>
> colmn <- function() { sample(seq_len(6), size = 1) }

Now apply the damage to the warjack object.

> # kill warjack object using repeated attacks
> #
> # function applies damage to a warjack object
> # until either/both arms are crippled
>
> kill_warjack <- function(object, max) {
+
+     object <- mark_boxes(object)
+
+     object$no_boxes <- no_boxes(max = max)
+
+     object$column <- colmn()
+
+     left_arm <- all(object$warjack <= ironclad_definition$left)
+
+     right_arm <- all(object$warjack <= ironclad_definition$right)
+
+     if (left_arm | right_arm) {
+
+         if (right_arm) { object$crippled <- "right" }
+
+         if (left_arm) { object$crippled <- "left" }
+
+         if (left_arm & right_arm) { object$crippled <- "both" }
+
+         return(object)
+
+     } else { object <- kill_warjack(object, max = max) }
+ }
>
> # TEST 1 max 1
> set.seed(11111)
> o1 <- list(warjack = ironclad_definition$boxes,
+         no_boxes = 1, column = 1, crippled = NA)
> test1 <- kill_warjack(o1, max = 1)
> all(test1$warjack == c(3, 5, 4, 4, 1, 0))
[1] TRUE
>
> # TEST 2 max -1
> o2 <- list(warjack = ironclad_definition$boxes,
+         no_boxes = 1, column = 1, crippled = NA)
> test2 <- try(kill_warjack(o1, max = -1), silent = TRUE)
> is(test2, "try-error")
[1] TRUE

Okay, so now we just need a huge pile of scrapped warjacks.

> N <- 100000
> results <- vector("character", length = N)
>
> system.time(for (i in seq_len(N)) {
+
+     object <- list(warjack = ironclad_definition$boxes,
+         no_boxes = no_boxes(), column = colmn(), 
+         crippled = NA)
+
+     out <- kill_warjack(object)
+
+     results[i] <- out$crippled
+ })
   user  system elapsed
  20.95    0.02   21.00
>
> table(results) / 1000
results
  both   left  right
19.265 42.745 37.990

So for uniform hits of between 1 and 12 damage, 43% of the time the left arm is crippled first, and 40% of the time the right arm is crippled first. Yikes! Let’s change up the range of damage values and see how that influences the stats. That’s not spoilers – we know that 1 damage hits can’t cripple both arms.

I’ll quickly use a loop to run through all of the options.

> N <- 100000
>
> results <- matrix(as.character(NA), nrow = N, ncol = 12)
>
> for (i in seq_len(12)) {
+     for (j in seq_len(N)) {
+
+         object <- list(warjack = ironclad_definition$boxes,
+             no_boxes = no_boxes(max = i), column = colmn(), 
+             crippled = NA)
+
+         out <- kill_warjack(object, max = i)
+
+         results[j, i] <- out$crippled
+     }
+ }
>
> tab_res <- apply(results, 2, function(x) { 
+     c(table(x) / 1000) })
> tab_res
[[1]]
  left  right
55.997 44.003
[[2]]
  both   left  right
 2.161 53.158 44.681
[[3]]
  both   left  right
 5.040 51.282 43.678
[[4]]
  both   left  right
 6.162 49.669 44.169
[[5]]
  both   left  right
 7.732 48.196 44.072
[[6]]
  both   left  right
 9.738 47.055 43.207
[[7]]
  both   left  right
12.502 45.560 41.938
[[8]]
  both   left  right
13.997 46.110 39.893
[[9]]
  both   left  right
14.918 45.458 39.624
[[10]]
  both   left  right
16.278 43.964 39.758
[[11]]
  both   left  right
17.840 43.096 39.064
[[12]]
  both   left  right
19.265 42.569 38.166

Hitting the Ironclad for 1 point of damage at a time corresponds to destroying the left arm before the right 56% of the time.

> tab_mat <- matrix(c(NA, unlist(tab_res)), ncol = 3, 
+     byrow = TRUE)
> plot(c(1, 12), c(0, 60), type = "n",
+     xlab = "Max Damage", ylab = "First System (%)")
> cols <- hsv(h = c(0, 0, 0.4), 
+     s = c(0.3, 0.6, 0.9), v = c(0.3, 0.9, 0.6))
> matlines(seq_len(12), tab_mat, lty = c(2, 1, 1), lwd = 2,
+     col = cols)
> legend("topright", legend = c("both", "left", "right"),
+     lty = c(2, 1, 1), lwd = 2,
+     col = cols)

Image

So as you can see, the more you chip away at an ironclad with small hits, the more likely you are to destroy the left arm first. I wondered if the waviness of the likes was due to randomness of the simulation and repeated again at 200000. The same features remained. These wobbles are due to idiosyncrasies of the spacings of the boxes on the damage grid.

It ain’t easy being a lefty!

Advertisements

One thought on “it ain’t easy being a lefty

  1. Pingback: keeping ahead | 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