Skip to content

Instantly share code, notes, and snippets.

@matfournier
Last active July 12, 2020 22:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matfournier/389946fa542dcdb30e2b127403686bad to your computer and use it in GitHub Desktop.
Save matfournier/389946fa542dcdb30e2b127403686bad to your computer and use it in GitHub Desktop.
SW combat curves

INTRO

The goals of this quick analyses:

  • Create a conversion tool between OSR monsters and Suldokars wake
  • Create a few combat curves for Suldokar's wake to help with creature design
  • Explore player/boss survivability with endrolls

COMBAT SIMULATORS

Two combat simulators were written quick and dirty in R

  • assume 1d6 damage as a baseline for comparisons between DCC and Suldokar's Wake
  • roll attack and count number of attacks
  • if attack succeeds, roll damage (incorporating the special success die upgrade for SW)
  • count number of attacks until death
  • ignore crit, fumbles

This is a simplified analyses and really represents an upper bound on combat. You would expect actual combat to take less turns due to:

  • vulnerabilities
  • armour piercing bullets
  • burst / autofire and other weapon rules
  • enemies with multiple attacks
  • higher damage dies (d8, d10)
  • critical hit dice 1ed6 exploding damage
  • critical hit dice effects (table in issue 3)

The chart also presents the median. Remember, 50% of the time the combat will be faster. It's entirely possible to one-shot a boss. For example, a d8 die with a special success stepped up to 10 with a high roll against a bulk 6 enemy boss. The player then calls for an endroll (50% chance the boss goes to an injured state)

## common setup 
library(dplyr)
library(ggplot2)
n <- 15000 ## number of simulated combat scenarios per variable

The OSR one uses ascending AC from 10. It does a D20 roll against AC (must exceed to hit). No modifier.

## OSR simulator
dd_run <- function(ac, hp, weapon_damage) {
  df <-bind_rows(
    replicate(n, data.frame(case=c("dd"), hp=c(hp), ac=c(ac), weapon_damage=c(weapon_damage)), simplify = FALSE)
    )
  hit_until_dead <- function(ac, hp, weapon_damage) {
    cur_hp <- hp
    attacks <- 0 
    while(cur_hp > 0) {
      if (sample(1:20, 1, replace=T) >= ac) {
        attacks = attacks + 1
        cur_hp = cur_hp - sample(1:weapon_damage, 1, T)
      } else {
        #print("miss")
        attacks = attacks + 1
      }
    }
    return(attacks)
  }
  
  return(df %>% rowwise() %>% mutate(hits = hit_until_dead(ac, hp, weapon_damage)))
}

The Suldokar's Wake simulator is a little more complicated. There are more variables to track:

sw_run <- function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major) {
  df <-bind_rows(
    replicate(n, data.frame(case=c("sw"), bulk=c(bulk), combat_rating = c(combat_rating), armoured=c(armoured), weapon_damage=c(weapon_damage),
                            er_threshold = c(er_threshold), chance_endroll = c(chance_endroll), major=c(major)), simplify = FALSE)
  )
  
  hit_until_end <- function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major, harm_fn) {
    harm <- 0
    attacks <- 0
    alive <- TRUE
    while(alive) {
      hitroll <- sample(1:20, 1, replace=T)
      if(hitroll > 15 || hitroll <= combat_rating) {
        attacks = attacks + 1 
        if(armoured) {
          if(hitroll > 15) { ## step up damage die
            harm = harm + min(sample(1:weapon_damage+2, 2, T))
          } else {
            harm = harm + min(sample(1:weapon_damage, 2, T))
          }
        } else {
          if(hitroll > 15) { ## step up damage die
            harm = harm + sample(1:weapon_damage+2, 1, T)
          } else {
            harm = harm + sample(1:weapon_damage, 1, T)
          }
        }
        if(harm >= 20) {
          alive = FALSE
        } else {
          if(major) {
            if(harm > bulk && harm >= er_threshold) {
              shouldEndroll = sample(1:100, 1, T)
              if(shouldEndroll <= chance_endroll) {
                endroll <- sample(1:20, 1, T)
                if (endroll > harm) {
                  harm = 0
                } else {
                  alive = FALSE
                }
              }
            }
          } else {
            if (harm > bulk)
              alive = FALSE
          }
        }
      } else {
        attacks = attacks + 1
      }
    }
    return(attacks)
  }
  return(df %>% rowwise() %>% mutate(hits = hit_until_end(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major)))
}

When fighting non-boss, non-players it's a simple check if the hitroll > 15 or <= combat_rating, do damage, and if the cummulative damage exceeds bulk then it's dead.

Someone with a Combat Rating 2, attacking a 2 bulk creature, with 1d6 damage die, that is unarmoured looks like the following:

sw_sum(2, 2, 6, FALSE, 20, 100, FALSE)

Only the first four inputs matter.

Against player / boss

Two new variables come into play:

  • er_threshold: the threshold at which to start calling for an end_roll *chance_endroll: for a given opportunity where harm > er_threshold, probability that the end_roll is called for. If the endroll succeeds, harm goes back to 0

OSR CONVERSION CURVES

Method based on survivability in combat:

  • lookup OSR monster HP (convert from HitDice as you need to) and lookup AC
  • find number of hits until it dies
  • lookup the number of hits on the suldokar's wake chart on the y-axis
    • you may need to map to armoured
    • hit a curve (pick an appropriate combat rating, or an average value)
    • drop down to the x-axis to figure out bulk

Because PH harm is fixed, we don't have the same range as a buff, high level D&D style creature. Things you may want to consider for even tankier enemies:

  • resistences
  • giving a modifier on harm rolls (maybe this armoured tank like construct gets +5 or +10 on it's end_rolls)
  • stepping up the harm pool to 24, 30, higher and using higher dice.

LOOKING AT PLAYER/BOSS COMBAT

Here we generate some tables:

  • waiting until player/boss hits 20 harm, no end roll called for
  • various scenarios for end roll via er_threshold and chance_endroll

For example, if er_threshold is at 10 and chance_endroll is at 50% it means the following:

  • every time they take damage and their harm is above 10, there is a 50% chance the simulation will call for an endroll
    • if the endroll succeeds, arm is reduced back to 0
    • if the endroll fails, put in injured/end state

Remember: this is a simplified analyses representing an upperbound that is looking at mediansi. Combat can be quite lethal! Look at the example in the appendix of Issue 3 where an enemy with two attacks gets a lucky crit with an exploding die on one of the PCs.

These are medians, so 50% of the time. Unlucky dice by the players on their end_roll can put them in an injured state early. Mind you, they are still tough as they can use Gunta to turn the clean roll into a normal roll; as well as various

Below is the median with some analyses on the effect of calling for an endroll. In general, calling for an endroll starting around harm 10 will lower the number of attacks by 2-3 on average.

It can be a little swingy when we look at quantiles:

case 0.10 percentile attacks 0.25 percentile_attacks
d6 armoured 5 7
d6 unarmoued 4 6
d8 armoured 4 6
d8 unarmoued 3 5
d10 armoued 4 5
d10 unarmoued 3 4

Again: this is an upper bound, if you factor in crits, armour piercing, burst/auto, you would expect those number of attacks to go down.

Player/Boss D6 Armoured and Unarmoured

Armoured:

er_threshold chance_endroll n median_attacks mean_attacks
10 0 20000 12 12.37185
10 100 20000 9 11.32275
10 25 20000 11 11.83950
10 50 20000 10 11.59295
10 75 20000 9 11.36855
13 100 20000 9 11.37700
13 25 20000 11 11.96395
13 50 20000 10 11.64085
13 75 20000 10 11.44420
15 100 20000 10 11.29380
15 25 20000 11 11.94015
15 50 20000 11 11.67740
15 75 20000 10 11.51155

unarmoured:

er_threshold chance_endroll n median_attacks mean_attacks
10 0 20000 9 9.84820
10 25 20000 9 9.44010
10 50 20000 8 9.12245
10 75 20000 7 9.02790
10 100 20000 7 8.91115
13 25 20000 9 9.47790
13 50 20000 8 9.26965
13 75 20000 8 9.05585
13 100 20000 7 8.86650
15 25 20000 9 9.59115
15 50 20000 8 9.32790
15 75 20000 8 9.16945
15 100 20000 8 8.94960

Player/Boss D8 Armoured and Unarmoured

Armoured:

er_threshold chance_endroll n median_attacks mean_attacks
10 0 20000 10 10.54795
10 25 20000 9 10.07630
10 50 20000 8 9.77245
10 75 20000 8 9.58775
10 100 20000 8 9.51535
13 25 20000 9 10.22020
13 50 20000 9 9.93965
13 75 20000 8 9.66580
13 100 20000 8 9.60910
15 25 20000 10 10.33490
15 50 20000 9 9.98940
15 75 20000 9 9.74660
15 100 20000 8 9.58485

Unarmoured:

er_threshold chance_endroll n median_attacks mean_attacks
10 0 20000 8 8.29980
10 25 20000 7 7.91085
10 50 20000 7 7.60175
10 75 20000 6 7.42600
10 100 20000 6 7.23565
13 25 20000 7 7.99820
13 50 20000 7 7.71005
13 75 20000 6 7.50150
13 100 20000 6 7.29695
15 25 20000 7 8.02650
15 50 20000 7 7.80835
15 75 20000 7 7.60130
15 100 20000 6 7.35970

Player/Boss D8 Armoured and Unarmoured

Armoured:

er_threshold chance_endroll n median_hits mean_hits
10 0 20000 9 9.35685
10 25 20000 8 8.85140
10 50 20000 7 8.59005
10 75 20000 7 8.34590
10 100 20000 7 8.30160
13 25 20000 8 8.97210
13 50 20000 8 8.72400
13 75 20000 7 8.42790
13 100 20000 7 8.26890
15 25 20000 8 9.02640
15 50 20000 8 8.79330
15 75 20000 8 8.57940
15 100 20000 7 8.35305

Unarmoured:

er_threshold chance_endroll n median_hits mean_hits
10 0 20000 7 7.20475
10 25 20000 6 6.90545
10 50 20000 6 6.54085
10 75 20000 5 6.36240
10 100 20000 5 6.14810
13 25 20000 6 6.92405
13 50 20000 6 6.66190
13 75 20000 6 6.50830
13 100 20000 5 6.25840
15 25 20000 6 6.97195
15 50 20000 6 6.77085
15 75 20000 6 6.56035
15 100 20000 6 6.32255
library(dplyr)
library(ggplot2)
library(knitr)
n = 20000
sw_run <- function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major) {
df <-bind_rows(
replicate(n, data.frame(case=c("sw"), bulk=c(bulk), combat_rating = c(combat_rating), armoured=c(armoured), weapon_damage=c(weapon_damage),
er_threshold = c(er_threshold), chance_endroll = c(chance_endroll), major=c(major)), simplify = FALSE)
)
hit_until_end <- function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major, harm_fn) {
harm <- 0
attacks <- 0
alive <- TRUE
while(alive) {
hitroll <- sample(1:20, 1, replace=T)
if(hitroll > 15 || hitroll <= combat_rating) {
attacks = attacks + 1
if(armoured) {
if(hitroll > 15) { ## step up damage die
harm = harm + min(sample(1:weapon_damage+2, 2, T))
} else {
harm = harm + min(sample(1:weapon_damage, 2, T))
}
} else {
if(hitroll > 15) { ## step up damage die
harm = harm + sample(1:weapon_damage+2, 1, T)
} else {
harm = harm + sample(1:weapon_damage, 1, T)
}
}
if(harm >= 20) {
alive = FALSE
} else {
if(major) {
if(harm > bulk && harm >= er_threshold) {
shouldEndroll = sample(1:100, 1, T)
if(shouldEndroll <= chance_endroll) {
endroll <- sample(1:20, 1, T)
if (endroll > harm) {
harm = 0
} else {
alive = FALSE
}
}
}
} else {
if (harm > bulk)
alive = FALSE
}
}
} else {
attacks = attacks + 1
}
}
return(attacks)
}
return(df %>% rowwise() %>% mutate(hits = hit_until_end(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major)))
}
sw_sum <-function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major) {
df <- sw_run(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major)
return(
df %>% group_by(bulk) %>% summarize(
n = n(),
combat_rating = max(combat_rating),
weapon_damage = max(weapon_damage),
armoured=max(armoured),
er_threshold=max(er_threshold),
chance_endroll=max(chance_endroll),
major=max(major),
med_hit = median(hits))
)
}
## non-major analyses
non_major_2_unarm <- rbind(
sw_sum(2, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(4, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(6, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(8, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(10, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(12, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(14, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(16, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(18, 2, 6, FALSE, 20, 100, FALSE),
sw_sum(20, 2, 6, FALSE, 20, 100, FALSE)
)
non_major_4_unarm <- rbind(
sw_sum(2, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(4, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(6, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(8, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(10, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(12, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(14, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(16, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(18, 4, 6, FALSE, 20, 100, FALSE),
sw_sum(20, 4, 6, FALSE, 20, 100, FALSE)
)
non_major_6_unarm <- rbind(
sw_sum(2, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(4, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(6, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(8, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(10, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(12, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(14, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(16, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(18, 6, 6, FALSE, 20, 100, FALSE),
sw_sum(20, 6, 6, FALSE, 20, 100, FALSE)
)
non_major_8_unarm <- rbind(
sw_sum(2, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(4, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(6, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(8, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(10, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(12, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(14, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(16, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(18, 8, 6, FALSE, 20, 100, FALSE),
sw_sum(20, 8, 6, FALSE, 20, 100, FALSE)
)
non_major_2_arm <- rbind(
sw_sum(2, 2, 6, T, 20, 100, FALSE),
sw_sum(4, 2, 6, T, 20, 100, FALSE),
sw_sum(6, 2, 6, T, 20, 100, FALSE),
sw_sum(8, 2, 6, T, 20, 100, FALSE),
sw_sum(10, 2, 6, T, 20, 100, FALSE),
sw_sum(12, 2, 6, T, 20, 100, FALSE),
sw_sum(14, 2, 6, T, 20, 100, FALSE),
sw_sum(16, 2, 6, T, 20, 100, FALSE),
sw_sum(18, 2, 6, T, 20, 100, FALSE),
sw_sum(20, 2, 6, T, 20, 100, FALSE)
)
non_major_4_arm <- rbind(
sw_sum(2, 4, 6, T, 20, 100, FALSE),
sw_sum(4, 4, 6, T, 20, 100, FALSE),
sw_sum(6, 4, 6, T, 20, 100, FALSE),
sw_sum(8, 4, 6, T, 20, 100, FALSE),
sw_sum(10, 4, 6, T, 20, 100, FALSE),
sw_sum(12, 4, 6, T, 20, 100, FALSE),
sw_sum(14, 4, 6, T, 20, 100, FALSE),
sw_sum(16, 4, 6, T, 20, 100, FALSE),
sw_sum(18, 4, 6, T, 20, 100, FALSE),
sw_sum(20, 4, 6, T, 20, 100, FALSE)
)
non_major_6_arm <- rbind(
sw_sum(2, 6, 6, T, 20, 100, FALSE),
sw_sum(4, 6, 6, T, 20, 100, FALSE),
sw_sum(6, 6, 6, T, 20, 100, FALSE),
sw_sum(8, 6, 6, T, 20, 100, FALSE),
sw_sum(10, 6, 6, T, 20, 100, FALSE),
sw_sum(12, 6, 6, T, 20, 100, FALSE),
sw_sum(14, 6, 6, T, 20, 100, FALSE),
sw_sum(16, 6, 6, T, 20, 100, FALSE),
sw_sum(18, 6, 6, T, 20, 100, FALSE),
sw_sum(20, 6, 6, T, 20, 100, FALSE)
)
non_major_8_arm <- rbind(
sw_sum(2, 8, 6, T, 20, 100, FALSE),
sw_sum(4, 8, 6, T, 20, 100, FALSE),
sw_sum(6, 8, 6, T, 20, 100, FALSE),
sw_sum(8, 8, 6, T, 20, 100, FALSE),
sw_sum(10, 8, 6, T, 20, 100, FALSE),
sw_sum(12, 8, 6, T, 20, 100, FALSE),
sw_sum(14, 8, 6, T, 20, 100, FALSE),
sw_sum(16, 8, 6, T, 20, 100, FALSE),
sw_sum(18, 8, 6, T, 20, 100, FALSE),
sw_sum(20, 8, 6, T, 20, 100, FALSE)
)
sw_non_major_all = rbind(
non_major_2_unarm,
non_major_4_unarm,
non_major_6_unarm,
non_major_8_unarm,
non_major_2_arm,
non_major_4_arm,
non_major_6_arm,
non_major_8_arm
) %>% mutate(combat_rating = as.factor(combat_rating)) %>%
mutate(armoured = case_when(armoured == 0 ~ "unarmoured", armoured == 1 ~ "armoured"))
ggplot(sw_non_major_all, aes(x=bulk, y=med_hit, color=combat_rating)) + geom_line() + facet_grid(~armoured) +
ggtitle("suldokar's wake non-major characters d6 damage die, 20k simulations per point") + ylab("median number of attacks until death") +
xlab("enemy bulk")
######################
# SULDOKAR'S WAKE
# END_ROLL PLAYER SURVIVABILITY
major <- rbind(
sw_run(5, 5, 6, T, 10, 0, TRUE),
sw_run(5, 5, 6, T, 10, 25, TRUE),
sw_run(5, 5, 6, T, 13, 25, TRUE),
sw_run(5, 5, 6, T, 15, 25, TRUE),
sw_run(5, 5, 6, T, 10, 50, TRUE),
sw_run(5, 5, 6, T, 13, 50, TRUE),
sw_run(5, 5, 6, T, 15, 50, TRUE),
sw_run(5, 5, 6, T, 10, 75, TRUE),
sw_run(5, 5, 6, T, 13, 75, TRUE),
sw_run(5, 5, 6, T, 15, 75, TRUE),
sw_run(5, 5, 6, T, 10, 100, TRUE),
sw_run(5, 5, 6, T, 13, 100, TRUE),
sw_run(5, 5, 6, T, 15, 100, TRUE)
)
major <- major %>% mutate(er_threshold = as.factor(er_threshold), chance_endroll = as.factor(chance_endroll))
ggplot(major, aes(x=hits, fill=chance_endroll)) + geom_density(alpha=0.25) + xlim(0, 50) + facet_grid(. ~ er_threshold) +
ggtitle("Rolling for endRoll above threshold at given chance per opportunity")
major_table <- major %>%
group_by(er_threshold, chance_endroll) %>%
summarize(
n = n(),
median_hits = median(hits),
mean_hits = mean(hits)
)
major_noarm <- rbind(
sw_run(5, 5, 6, F, 10, 0, TRUE),
sw_run(5, 5, 6, F, 10, 25, TRUE),
sw_run(5, 5, 6, F, 13, 25, TRUE),
sw_run(5, 5, 6, F, 15, 25, TRUE),
sw_run(5, 5, 6, F, 10, 50, TRUE),
sw_run(5, 5, 6, F, 13, 50, TRUE),
sw_run(5, 5, 6, F, 15, 50, TRUE),
sw_run(5, 5, 6, F, 10, 75, TRUE),
sw_run(5, 5, 6, F, 13, 75, TRUE),
sw_run(5, 5, 6, F, 15, 75, TRUE),
sw_run(5, 5, 6, F, 10, 100, TRUE),
sw_run(5, 5, 6, F, 13, 100, TRUE),
sw_run(5, 5, 6, F, 15, 100, TRUE)
)
major_table_noarm <- major_noarm %>%
group_by(er_threshold, chance_endroll) %>%
summarize(
n = n(),
median_hits = median(hits),
mean_hits = mean(hits)
)
major_d8 <- rbind(
sw_run(5, 5, 8, T, 10, 0, TRUE),
sw_run(5, 5, 8, T, 10, 25, TRUE),
sw_run(5, 5, 8, T, 13, 25, TRUE),
sw_run(5, 5, 8, T, 15, 25, TRUE),
sw_run(5, 5, 8, T, 10, 50, TRUE),
sw_run(5, 5, 8, T, 13, 50, TRUE),
sw_run(5, 5, 8, T, 15, 50, TRUE),
sw_run(5, 5, 8, T, 10, 75, TRUE),
sw_run(5, 5, 8, T, 13, 75, TRUE),
sw_run(5, 5, 8, T, 15, 75, TRUE),
sw_run(5, 5, 8, T, 10, 100, TRUE),
sw_run(5, 5, 8, T, 13, 100, TRUE),
sw_run(5, 5, 8, T, 15, 100, TRUE)
)
major_noarm_d8 <- rbind(
sw_run(5, 5, 8, F, 10, 0, TRUE),
sw_run(5, 5, 8, F, 10, 25, TRUE),
sw_run(5, 5, 8, F, 13, 25, TRUE),
sw_run(5, 5, 8, F, 15, 25, TRUE),
sw_run(5, 5, 8, F, 10, 50, TRUE),
sw_run(5, 5, 8, F, 13, 50, TRUE),
sw_run(5, 5, 8, F, 15, 50, TRUE),
sw_run(5, 5, 8, F, 10, 75, TRUE),
sw_run(5, 5, 8, F, 13, 75, TRUE),
sw_run(5, 5, 8, F, 15, 75, TRUE),
sw_run(5, 5, 8, F, 10, 100, TRUE),
sw_run(5, 5, 8, F, 13, 100, TRUE),
sw_run(5, 5, 8, F, 15, 100, TRUE)
)
major_d10 <- rbind(
sw_run(5, 5, 10, T, 10, 0, TRUE),
sw_run(5, 5, 10, T, 10, 25, TRUE),
sw_run(5, 5, 10, T, 13, 25, TRUE),
sw_run(5, 5, 10, T, 15, 25, TRUE),
sw_run(5, 5, 10, T, 10, 50, TRUE),
sw_run(5, 5, 10, T, 13, 50, TRUE),
sw_run(5, 5, 10, T, 15, 50, TRUE),
sw_run(5, 5, 10, T, 10, 75, TRUE),
sw_run(5, 5, 10, T, 13, 75, TRUE),
sw_run(5, 5, 10, T, 15, 75, TRUE),
sw_run(5, 5, 10, T, 10, 100, TRUE),
sw_run(5, 5, 10, T, 13, 100, TRUE),
sw_run(5, 5, 10, T, 15, 100, TRUE)
)
major_noarm_d10 <- rbind(
sw_run(5, 5, 10, F, 10, 0, TRUE),
sw_run(5, 5, 10, F, 10, 25, TRUE),
sw_run(5, 5, 10, F, 13, 25, TRUE),
sw_run(5, 5, 10, F, 15, 25, TRUE),
sw_run(5, 5, 10, F, 10, 50, TRUE),
sw_run(5, 5, 10, F, 13, 50, TRUE),
sw_run(5, 5, 10, F, 15, 50, TRUE),
sw_run(5, 5, 10, F, 10, 75, TRUE),
sw_run(5, 5, 10, F, 13, 75, TRUE),
sw_run(5, 5, 10, F, 15, 75, TRUE),
sw_run(5, 5, 10, F, 10, 100, TRUE),
sw_run(5, 5, 10, F, 13, 100, TRUE),
sw_run(5, 5, 10, F, 15, 100, TRUE)
)
major_table_arm_d8 <- major_d8 %>%
group_by(er_threshold, chance_endroll) %>%
summarize(
n = n(),
median_hits = median(hits),
mean_hits = mean(hits)
)
major_table_arm_d10 <- major_d10 %>%
group_by(er_threshold, chance_endroll) %>%
summarize(
n = n(),
median_hits = median(hits),
mean_hits = mean(hits)
)
major_table_noarm_d8 <- major_noarm_d8 %>%
group_by(er_threshold, chance_endroll) %>%
summarize(
n = n(),
median_hits = median(hits),
mean_hits = mean(hits)
)
major_table_noarm_d10 <- major_noarm_d10 %>%
group_by(er_threshold, chance_endroll) %>%
summarize(
n = n(),
median_hits = median(hits),
mean_hits = mean(hits)
)
## OSR COMPARISON
#######################################
## using DCC attacks, AC starts at 10.
dd_run <- function(ac, hp, weapon_damage) {
df <-bind_rows(
replicate(n, data.frame(case=c("dd"), hp=c(hp), ac=c(ac), weapon_damage=c(weapon_damage)), simplify = FALSE)
)
hit_until_dead <- function(ac, hp, weapon_damage) {
cur_hp <- hp
attacks <- 0
while(cur_hp > 0) {
if (sample(1:20, 1, replace=T) >= ac) {
attacks = attacks + 1
cur_hp = cur_hp - sample(1:weapon_damage, 1, T)
} else {
#print("miss")
attacks = attacks + 1
}
}
return(attacks)
}
return(df %>% rowwise() %>% mutate(hits = hit_until_dead(ac, hp, weapon_damage)))
}
dd_sum <-function(ac, hp, weapon_damage) {
df <- dd_run(ac, hp, weapon_damage)
return(
df %>% group_by(ac) %>% summarize(
n = n(),
hp = max(hp),
med_hit = median(hits))
)
}
dd_sum_for_ac <- function(ac, weapon_damage) {
df <- lapply(list(5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100), function(hp) {
dd_sum(ac, hp, weapon_damage)
})
all <- do.call("rbind", df)
return(all %>% mutate(ac = as.factor(ac)))
}
dd_all <- rbind(
dd_sum_for_ac(10, 6),
dd_sum_for_ac(11, 6),
dd_sum_for_ac(12, 6),
dd_sum_for_ac(13, 6),
dd_sum_for_ac(14, 6),
dd_sum_for_ac(15, 6),
dd_sum_for_ac(16, 6)
)
dd_all <- dd_all %>% mutate(ac = as.factor(ac))
ggplot(dd_all, aes(x=hp, y=med_hit, colour=as.factor(ac))) + geom_line() +
ggtitle("DCC for D&D, no str/dex bonus, scaled to match") +
ylab("median number of attacks until death") +
ylim(0, 35)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment