2021-02-14 11:26:28 +01:00
# !/ usr / bin / swift
import Foundation
main ( )
/* * *
* Determine what AC to expect at the given CR / level
* - CR : Challenge Rating or Level of the opponent
* * */
func getACforCR ( CR : Int ) -> Int {
// A v e r a g e A C g e n e r a l l y f o l l o w s a t r e n d o f a d v a n c i n g w i t h C R , i n c r e a s i n g b y a n a d d i t i o n a l 1 e v e r y 4 l e v e l s ( h e n c e C R * 5 / 4 ) a n d a n o t h e r 1 o n l e v e l s 6 , 1 0 , 1 4 , 1 8 a n d 1 9 ( e x c e p t f o r C R - 1 ) a c c o r d i n g t o [ a s u r v e y o f B e s t i a r y 1 ] ( h t t p s : / / d o c s . g o o g l e . c o m / s p r e a d s h e e t s / d / 1 V Q d X I J M M e N l k L 1 t a _ b 9 q _ i I m A H o u j D C Y s 1 W a B J P - R j s / e d i t # g i d = 4 1 5 7 3 1 6 1 3 )
switch ( CR ) {
case - 1 : return 16
case 0. . . 5 : return ( CR * 5 / 4 ) + 16
case 6. . . 9 : return ( CR * 5 / 4 ) + 17
case 10. . . 13 : return ( CR * 5 / 4 ) + 18
case 14. . . 17 : return ( CR * 5 / 4 ) + 19
case 18 : return ( CR * 5 / 4 ) + 20
default : return ( CR * 5 / 4 ) + 21
}
}
/* * *
* Calculate the average result of a dice roll .
* - roll string should be provided like " 2d8+3 " ( meaning we roll 2 8 - sided dice , add their results and add another 3 ) or " 7d4-2 " ( roll 7 4 - sided dice , add their results and subtract 2 ) . The part behind the " + " ( or " - " ) sign may be expressed as an arithmatic formula ( like " 6-4 " ) for conveniance . Parsing errors will result in a return value of - 99.0 along with an error prompt . If the input is a fixed value that can be interpreted as a floating point number ( such as " 5 " ) , it will be returned .
* * */
2021-02-14 11:36:49 +01:00
func avgResultForDiceRoll ( rollArray : [ String ] ) -> Double {
2021-02-14 11:26:28 +01:00
var avgResult = 0.0
for roll in rollArray {
let rolls = roll . split ( separator : " d " )
let numberFormatter = NumberFormatter ( )
switch ( rolls . count ) {
case 0 :
print ( " Syntax error. Could not parse \( roll ) : Splitting the input resulted in an empty array. " )
return - 99.0
case 1 :
let floatVal = numberFormatter . number ( from : ( String ) ( rolls [ 0 ] ) )
if ( floatVal != nil ) {
return floatVal as ! Double
} else {
print ( " Syntax error. Could not parse \( roll ) : Unable to split the input and it doesn't look like a floating point number. " )
return - 99.0
}
case 2 :
let NSdiceCount = numberFormatter . number ( from : ( String ) ( rolls [ 0 ] ) )
if ( NSdiceCount = = nil ) {
print ( " Syntax error. Could not parse \( roll ) : Number of dice to roll doesn't look like a number. " )
return - 99.0
}
let diceCount = NSdiceCount as ! Double
let posModifierIndex = ( String ) ( rolls [ 1 ] ) . firstIndex ( of : " + " ) ? ? nil
let negModifierIndex = ( String ) ( rolls [ 1 ] ) . firstIndex ( of : " - " ) ? ? nil
var modifierIndex : String . Index
if ( posModifierIndex != nil ) {
if ( negModifierIndex != nil ) {
modifierIndex = min ( posModifierIndex ! , negModifierIndex ! )
} else {
modifierIndex = posModifierIndex !
}
} else if ( negModifierIndex != nil ) {
modifierIndex = negModifierIndex !
} else {
modifierIndex = ( String ) ( rolls [ 1 ] ) . endIndex
}
let dieSize = Int ( ( String ) ( rolls [ 1 ] ) [ . . < modifierIndex ] ) ? ? - 1
var expectedRoll = 0.5 + ( Double ( dieSize ) / 2.0 )
expectedRoll *= diceCount
if ( modifierIndex < ( String ) ( rolls [ 1 ] ) . endIndex ) {
let modifier = NSExpression ( format : " 0 " + ( String ) ( ( String ) ( rolls [ 1 ] ) [ modifierIndex . . < ( String ) ( rolls [ 1 ] ) . endIndex ] ) )
expectedRoll += modifier . expressionValue ( with : nil , context : nil ) as ! Double
}
avgResult += expectedRoll
default :
print ( " Syntax error. Could not parse \( roll ) : Split resulted in \( rolls . count ) fragments, but 2 were expected: (1) number of rolls and (2) die size + or - modifier. Specifically, the letter d should appear only once (between the two). " )
return - 99.0
}
}
return avgResult
}
/* * *
* A roll that represents a flat check , skill check , ability check , saving throw or attack roll .
* It contains a modifier and is tested against a DC . It will be treated as a success if it meets or exceeds the DC , as a critical success if
* it meets or exceeds 10 + DC and as a failure otherwise ( critical failures are not calculated at the moment ) .
* A natural 1 will be treated as 1 degree worse than it would numerically be ( if this makes a difference ) and conversely a natural 20 as 1 degree better .
* * */
struct checkRoll {
2021-02-14 11:36:49 +01:00
var modifier = 0
2021-02-14 11:26:28 +01:00
var DC = 10
func getProbToHit ( ) -> Double {
let requiredRoll = DC - modifier
switch ( requiredRoll ) {
case . min . . . - 9 : return 1.0 // A n a t u r a l 1 w o u l d n u m e r i c a l l y b e a c r i t i c a l s u c c e s s a n d t h u s s t i l l b e t r e a t e d a s a s u c c e s s .
case - 8 . . . 1 : return 0.95 // A n y t h i n g w o u l d n u m e r i c a l l y b e a s u c c e s s b u t s i n c e a n a t u r a l 1 w o u l d o n l y b e a n o r m a l s u c c e s s , i t i s t r e a t e d a s a f a i l u r e .
case 20 . . . 30 : return 0.05 // A n y t h i n g w o u l d n u m e r i c a l l y f a i l , b u t a n a t u r a l 2 0 w o u l d o n l y b e a n o r m a l f a i l u r e a n d i s t h u s t r e a t e d a s a s u c c e s s .
case 31 . . . . max : return 0.0 // E v e n a n a t u r a l 2 0 w o u l d n u m e r i c a l l y b e a c r i t i c a l f a i l u r e a n d t h u s s t i l l f a i l i f i t i s t r e a t e d o n e d e g r e e b e t t e r .
default : return ( 1.05 - ( ( Double ) ( requiredRoll ) / 20.0 ) )
}
}
func getProbToCrit ( ) -> Double {
let requiredRoll = 10 + DC - modifier
switch ( requiredRoll ) {
case . min . . . 1 : return 0.95 // A n y t h i n g w o u l d n u m e r i c a l l y b e a c r i t s u c c e s s , b u t a n a t u r a l 1 s t i l l g e t s d e m o t e d t o a n o r m a l s u c c e s s .
case 20 . . . 30 : return 0.05 // A n a t u r a l 2 0 w o u l d n u m e r i c a l l y b e a n o r m a l s u c c e s s a n d t h u s b e p r o m o t e d t o a c r i t .
case 31 . . . . max : return 0.0 // E v e n a n a t u r a l 2 0 w o u l d n ' t n u m e r i c a l l y b e a s u c c e s s .
default : return ( 1.05 - ( ( Double ) ( requiredRoll ) / 20.0 ) )
}
}
// C o n v e n i a n c e f u n c t i o n f o r w h e n w e w a n t t o t r e a t c r i t i c a l a n d n o n - c r i t i c a l h i t s s e p e r a t e l y
func getProbToNormalHit ( ) -> Double {
return getProbToHit ( ) - getProbToCrit ( )
}
}
/* * *
* Attack rolls contain
* - attackBonus : An Array of all applicable attack bonusses ( including MAP )
2021-02-14 11:36:49 +01:00
* - normalDmg : The average damage of a non - critical hit ( may be calculated via avgResultForDiceRoll ( ) )
* - critDmg : The average damage of a critical hit ( may be calculated via avgResultForDiceRoll ( ) )
2021-02-14 11:26:28 +01:00
* * */
struct attackRolls {
var attackBonus = [ 0 ]
var normalDmg = 0.0
var critDmg = 0.0
}
struct opponent {
var description : String
var CRAdjust : Int
}
func main ( ) {
/* d e f a u l t d a t a b l o c k i n c a s e w e f i n d n o v a l i d J S O N f i l e . */
var outputBeginning = " Average Damage: "
var level = 2
var attacks = [
attackRolls (
2021-02-14 11:36:49 +01:00
attackBonus : [ 10 , 5 , 0 ] ,
normalDmg : avgResultForDiceRoll ( rollArray : [ " 1d8-1 " ] ) ,
critDmg : avgResultForDiceRoll ( rollArray : [ " 2d8-2 " , " 1d10 " ] ) ) ,
2021-02-14 11:26:28 +01:00
attackRolls (
2021-02-14 11:36:49 +01:00
attackBonus : [ 7 , 2 ] ,
normalDmg : avgResultForDiceRoll ( rollArray : [ " 1d6+3 " ] ) ,
critDmg : avgResultForDiceRoll ( rollArray : [ " 2d6+6 " ] ) )
2021-02-14 11:26:28 +01:00
]
var jsonURLs : [ URL ] = [ ]
if ( CommandLine . arguments . count > 1 ) {
for i in 1. . < CommandLine . arguments . count {
jsonURLs . append ( NSURL ( fileURLWithPath : CommandLine . arguments [ i ] ) as URL )
}
} else {
print ( " Note: You can specify one or more JSON files as input as an argument to this script. Trying ./PC.json as a default.. " )
jsonURLs . append ( NSURL ( fileURLWithPath : " PC.json " ) as URL )
}
for jsonURL in jsonURLs {
do {
let data = try Data ( contentsOf : jsonURL , options : . mappedIfSafe )
let jsonResult = try JSONSerialization . jsonObject ( with : data , options : . mutableLeaves )
if let jsonResult = jsonResult as ? Dictionary < String , AnyObject > {
if let PCname = jsonResult [ " name " ] as ? String {
outputBeginning = " \( PCname ) does an average damage of "
}
if let PClevel = jsonResult [ " level " ] as ? Int {
level = PClevel
}
if let attackArr = jsonResult [ " attacks " ] as ? [ Any ] {
attacks = [ ]
for attack in attackArr {
if let thisAttack = attack as ? Dictionary < String , AnyObject > {
var thisAttackBonusses : [ Int ]
var thisNormalDmg : Double
var thisCritDmg : Double
if let normalDmgRolls = thisAttack [ " normalDmg " ] as ? [ String ] {
2021-02-14 11:36:49 +01:00
thisNormalDmg = avgResultForDiceRoll ( rollArray : normalDmgRolls )
2021-02-14 11:26:28 +01:00
if let thisAttackRolls = thisAttack [ " attackRolls " ] as ? [ Int ] {
thisAttackBonusses = thisAttackRolls
if let CritDmgRolls = thisAttack [ " critDmg " ] as ? [ String ] {
2021-02-14 11:36:49 +01:00
thisCritDmg = avgResultForDiceRoll ( rollArray : CritDmgRolls )
2021-02-14 11:26:28 +01:00
} else {
thisCritDmg = 2.0 * thisNormalDmg
}
attacks . append ( attackRolls (
attackBonus : thisAttackBonusses ,
normalDmg : thisNormalDmg ,
critDmg : thisCritDmg
) )
}
}
}
}
}
}
} catch let e {
print ( " Could not parse PC.json: \( e ) \n . Continuing with default data. " )
}
let opponents = [ opponent ( description : " Lackeys " , CRAdjust : - 2 ) , opponent ( description : " Normal Foes " , CRAdjust : 0 ) , opponent ( description : " Bosses " , CRAdjust : 2 ) ]
for foe in opponents {
var chk = checkRoll ( modifier : 0 , DC : getACforCR ( CR : level + foe . CRAdjust ) )
var avgDmg = 0.0
for attack in attacks {
for bonus in attack . attackBonus {
chk . modifier = bonus
avgDmg += chk . getProbToNormalHit ( ) * attack . normalDmg + chk . getProbToCrit ( ) * attack . critDmg
}
}
print ( " \( outputBeginning ) \( ( Double ) ( ( Int ) ( avgDmg * 1000 ) ) / 1000 ) against \( foe . description ) (AC \( chk . DC ) ) " )
}
}
}