Some games, like Poker, Mah Jong and Yahtzee have no spatial dimensions. These games have an abstract scoring dimension and a simple score tally dimension. But many games assign meaning to one or more spatial dimensions. Spatial thinking is so engrained in our world view that it makes a lot of sense to include relative position to game pieces. Snakes and Ladders, Monopoly and Dominoes make use of one spatial dimension. Chess, Go and Warmachine make use of two.

In a game where spatial positioning is important, the consequences of having pieces in one position compared to another can be critical. Selecting the correct distance and angle between pieces during early turns can have a significant impact on the outcome of the game.

To investigate the effect of spatial positioning on our pieces, we need to be able to represent the effect of each game component. For example, in Warmachine, firing a weapon that creates an explosion has a circular area of effect. This ordnance may not directly hit the intended target, yet still cause damage to the model in the resulting conflagration as flame and shrapnel blasts across the battlefield. Similarly, if the weapon is out of range, the area of effect may scatter onto the target by chance.

In Warmachine, an area of effect template travels D6 inches in one of six directions; straight forward, 60° right, 120° right, straight back, 120° left, or 60° left. To calculate whether a shot that misses still randomly scatters onto the target we roll two dice and measure whether there is any overlap between the area of effect template and the model’s base.

The area of effect will scatter in one of six directions. The system is symmetric along the line of attack, so we actually only need to check distances in four directions. Straight back and straight forward are simple; we can add up the distances between the template and the base. We also have to work out what happens when we roll a 2 or 3. This calls for a little trigonometry.

In the diagram below, after taking a shot with an area of effect weapon, at maximum range there is still 2.5″ separation between the centres of a 3″ area of effect (orange circle) and a large (50 mm) based model (blue circle). Since the radius of a 3″ template is 1.5″ and the model is 0.98″, this is a miss. We roll and score 1 for distance and 2 for direction (orange line).

If the centres of the template and model (blue line) are closer together than the sum of the template and model radii, we have hit.

> # horizontal and vertical components > x <- cos(pi/6) > y <- sin(pi/6) > > # lines to add > x3 <- 4 * c(-x, x, 0, 0, x, -x) > y3 <- 4 * c(y, -y, 1, -1, y, -y) > > # target base radius > trad <- 0.5 * 50 / 25.4 > > sep <- 2.5 > # blank plot > plot(c(-5, 5), c(-5, 5), type = "n", axes = FALSE,

+ xlab = "", ylab = "") > > # add lines > matlines(matrix(x3, ncol = 3), + matrix(y3, ncol = 3), + lty = 2, col = 1) > text(x3 * 1.1, y3 * 1.1,

+ labels = c(6, 3, 1, 4, 2, 5), col = "grey") > > # add circles > symbols(c(0, 0, x), c(0, sep, y), + circles = c(1.5, trad, 1.5), add = TRUE, inches = FALSE, + bg = c("#EC772044", "#2A63E244", "#EC772088")) > > # construction > matlines(matrix(c(0, x, 0, x, 0, x), ncol = 3), + matrix(c(0, y, sep, y, y, y), ncol = 3), + lty = c(1, 1, 3), col = c("#EC7720", "#2A63E2", 1),

+ lwd = c(3, 3, 1))

We can calculate the component of the scatter perpendicular to the attack as scatter * cos(60°) and parallel to the attack as scatter * sin(60°). We can then calculate the separation between the centres (blue line) using Pythagoras’ Theorem (C = sqrt(A^2 + B^2)).

#' @title is target hit by scatter? #' @param weapon length 2 list with elements stats and special. #' stats is a length three vector with named elements RNG, POW and AOE #' special is a character vector #' @param dist single numeric with value 0 if the target was in range #' or a positive number indicating inches out of range (default 0) #' @param base single numeric indicating size of target base in mm (default 30) #' @param max single numeric indicating the maximum scatter

#' of the shot (default 6) #' @param dice length 2 numeric vector specifing (distance, direction) #' @return single logical #' @examples is.scatter.hit(weapon = list(stats = c(RNG = 14, POW = 14, AOE = 3), #' special = c("arcing"))) is.scatter.hit <- function(weapon, dist = 0, base = 30, max = 6,

dice = sample(1:6, size = 2, replace = TRUE)) { rad <- 0.5 * weapon$stats["AOE"] radBase <- 0.5 * base / 25.4 inRange <- FALSE if (!is.na(rad)) { moves <- dice[1] if (moves > max) { moves <- max } x <- cos(pi/6) y <- sin(pi/6) # system is symmetrical, so only solve for one half of x dir <- switch(dice[2], c(0, 1), # straight forward c(x, y), # forward c(x, -y), # backward c(0, -1), # straight backward c(x, -y), # backward c(x, y)) # forward sep <- 0 if (dist != 0) { sep <- dist + radBase } dmove <- sqrt((sep - moves * dir[2])^2 + (moves * dir[1])^2) inRange <- unname(abs(dmove) < radBase + rad) } return(inRange) }

As always, we create tests to make sure the code is behaving as expected.

tests <- vector(mode = "logical", length = 12) # TEST 1: 1" scatter following miss is hit tests[1] <- is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dice = c(1, 1)) # TEST 2: 1" scatter following 1" short is hit tests[2] <- is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dist = 1 + 0.5 * 30 / 25.4, dice = c(1, 1)) # TEST 3: 1" scatter following 2.5" short is miss tests[3] <- !is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dist = 2.5 + 0.5 * 30 / 25.4, dice = c(1, 1)) # TEST 4: 6" scatter following 2.5" short is miss tests[4] <- !is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dist = 2.5 + 0.5 * 30 / 25.4, dice = c(6, 1)) # TEST 5: 5" scatter following 2.5" short is hit tests[5] <- is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dist = 2.5 + 0.5 * 30 / 25.4, dice = c(5, 1)) # TEST 6: 6" scatter following 2.5" short to 4 is hit tests[6] <- !is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dist = 2.5 + 0.5 * 30 / 25.4, dice = c(6, 4)) # TEST 7: 1" scatter following miss to 2 is hit tests[7] <- is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dice = c(1, 2)) # TEST 8: 2" scatter following miss to 3 is hit tests[8] <- is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dice = c(2, 3)) # TEST 9: 3" scatter following miss to 5 is miss tests[9] <- !is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dice = c(3, 5)) # TEST 10: 4" scatter following miss to 6 is miss tests[10] <- !is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), dice = c(4, 6)) # TEST 11: 4" scatter following miss to 6 from 2" range is hit tests[11] <- is.scatter.hit(list(stats = c(RNG = 14, POW = 14, AOE = 3), special = c("arcing")), max = 1, dice = c(4, 6)) # TEST 12: 3" scatter following AOE 5 miss to 6 is hit tests[12] <- is.scatter.hit(list(stats = c(AOE = 5)), dice = c(3, 6)) if (all(tests)) { cat("isscatterhit is okay\n") } else { cat(sum(!tests), "problem(s)\n") }

This system is quite nice and simple. There are only 36 combinations of scatter positions for a template and model a certain distance apart.

> combs <- expand.grid(dist = 1:6, dir = 1:6) > out <- vector(mode = "logical", length = nrow(combs)) > for (i in seq_along(out)) {

+ out[i] <- is.scatter.hit(list(stats = c(AOE = 3)), + dist = 2.5, dice = unlist(combs[i, ])) } > sum(out) / length(out) [1] 0.1111111

For example, in the diagram above, 4/36 (or 11%) of the time, we’ll get a hit on the intended target when a 3″ area of effect is 2.5″ away from the centre target (or 0.02″ from the rim of the target).