View Single Post
Old March 18, 2018, 04:08   #1
CrabMan
Rookie
 
Join Date: Mar 2018
Posts: 7
CrabMan is on a distinguished road
A newbie's way of choosing which upgrade to take

I am a new player of Sil 1.3, have not went deeper than 400' yet, I've only had 5 games so far. I wrote a small program which I use to choose what equipment to use, how to spend exp, etc.

The program simulates a character trying to hit another characted with a melee attack - it rolls attack roll, evasion roll, damage roll, defense roll, checks whethere there was a critical hit, calculates the result damage and repeats this for 40000 times to calculate average resulting value. Here it is:

Code:
# this is Python 3

from collections import Counter, namedtuple
from functools import partial
import re
import itertools
from random import randint

from typing import Callable, Any, Dict, List, TypeVar
T = TypeVar("T")

Character = namedtuple("Character", [
    "melee_score", "damage", "evasion_score", "defense", "critical_overflow"
])
# critical_overflow is how much melee_roll-evasion_roll must be to get
# a critical hit; by default this is 7 plus weapon weight

def d(dices: int, sides: int) -> int:
    """Roll `dices` d `sides` and return the result."""
    return sum(randint(1, sides) for i in range(dices))

def parse_dices(s: str) -> List[List[int]]:
    """Takes a string like "1d4 2d7 3d8" and returns
    a list of pairs like [[1, 4], [2, 7], [3, 8]]"""
    matches = re.findall(r"(\d+)d(\d+)", s)
    if not matches:
        raise ValueError()
    return [[int(dices), int(sides)] for (dices, sides) in matches]

def roll_dices(s: str) -> int:
    """Takes a string like "1d4 2d7 3d8 1d1" and returns
    sum of those rolls."""
    dices = parse_dices(s)
    return sum(d(*pair) for pair in dices)

def roll_combat_damage(attacker: Character, defender: Character) -> int:
    """Simulates `attacker` trying to hit `defender`: checks whether the
    attack connected, then calculates damage. Returns the resulting damage.
    If the attack didn't connect, returns 0."""
    hit = attacker.melee_score + roll_dices("1d20") \
        - (defender.evasion_score + roll_dices("1d20"))
    if hit <= 0:
        return 0
    [[attack_dices, attack_sides]] = parse_dices(attacker.damage)
    attack_dices += int(hit // (attacker.critical_overflow))
    damage_roll = d(attack_dices, attack_sides)
    defense_roll = roll_dices(defender.defense)
    return max(damage_roll - defense_roll, 0)

def calc_distribution(func: Callable[[], T]) -> Dict[T, float]:
    """Calls `func` many times, counts how many times each value was
    returned, returns probabilities (sum of them equals 1).
    Func must be a function that takes no arguments."""
    counter = Counter()
    iterations = 40000
    for i in range(iterations):
        counter[func()] += 1
    probabilities = {
        damage: count/iterations
        for (damage, count) in counter.items()
    }
    return probabilities

def calc_expected_damage(attacker: Character, defender: Character) -> float:
    """Calculates expected damage when `attacker` tries to hit `defender`."""
    distribution = calc_distribution(
        partial(roll_combat_damage, attacker, defender)
    )
    return sum(damage * prob for (damage, prob) in distribution.items())

def damage_per_damage(hero: Character, enemy: Character) -> None:
    our_dmg = calc_expected_damage(hero, enemy)
    their_dmg = calc_expected_damage(enemy, hero)

    print(
"""Expected damage vs enemy per attack: {0},
Expected damage from enemy per attack: {1},
If both attack with the same frequency, then
expected damage vs enemy per point of damage from them: {2}"""
        .format(our_dmg, their_dmg, our_dmg/their_dmg)
    )

# some monsters
easterling_archer = Character(9, "1d7", 9, "2d4", 7)
easterling_warrior = Character(7, "2d8", 5, "3d4", 7)
distended_spider = Character(7, "2d11", 7, "1d1", 12) # poisons
warg = Character(9, "2d7", 10, "1d4", 15)
mountain_troll = Character(6, "4d5", 3, "2d4", 9)
twisted_bat = Character(15, "2d5", 17, "1d4", 8)
grave_wight = Character(11, "2d9", 7, "3d4", 8) # is actually immune to crits

# behind the wight there was a small treasure-like thing - a fucking potion
# prolly not worth it
barrow_wight = Character(13, "2d9", 8, "3d4", 9)  # doesnt pursue, DRAINS STATS

# tries to disarm using his whip
orc_captain = Character(10, "2d8", 7, "3d4", 9)

# disarms too?
othrod = Character(15, "2d9", 9, "4d4", 7)
Let's say that my character has (+10, "2d9") attack with a weapon that weighs 3lb, [+12, 5-15] defense (actually 5-15 is 1d4 1d6 1d2 1d1 1d2), and I have neither power, nor finesse. My typical enemy is easterling warrior. I have some exp left and I am choosing between taking power, finesse or putting another point in evasion. I will do this in python REPL:

Code:
In [19]: me_power = Character(10, "2d10", 12, "1d4 1d6 1d2 1d1 1d2", 8+3)
In [20]: me_finesse = Character(10, "2d9", 12, "1d4 1d6 1d2 1d1 1d2", 6+3)
In [21]: me_evasion = Character(10, "2d9", 13, "1d4 1d6 1d2 1d1 1d2",7+3)
In [22]: damage_per_damage(me_power, easterling_warrior)
Expected damage vs enemy per attack: 4.274275,
Expected damage from enemy per attack: 0.5650249999999999,
If both attack with the same frequency, then
expected damage vs enemy per point of damage from them: 7.564753771956995
In [23]: damage_per_damage(me_finesse, easterling_warrior)
Expected damage vs enemy per attack: 4.080675000000002,
Expected damage from enemy per attack: 0.5656999999999999,
If both attack with the same frequency, then
expected damage vs enemy per point of damage from them: 7.213496552943261
In [24]: damage_per_damage(me_evasion, easterling_warrior)
Expected damage vs enemy per attack: 3.7949249999999997,
Expected damage from enemy per attack: 0.47392500000000004,
If both attack with the same frequency, then
expected damage vs enemy per point of damage from them: 8.007437885741414
So evasion seems the best. However to level evasion I need to spend 900 exp, while power or finesse costs only 500. So I will also calculate my current dmg per dmg:

Code:
In [25]: me_currently = Character(10, "2d9", 12, "1d4 1d6 1d2 1d1 1d2",7+3)
In [26]: damage_per_damage(me_currently, easterling_warrior)
Expected damage vs enemy per attack: 3.7500250000000004,
Expected damage from enemy per attack: 0.56525,
If both attack with the same frequency, then
expected damage vs enemy per point of damage from them: 6.6342768686421945
Let's see, I can spend 500 exp to make my character 7.56/6.63=1.14 times more powerful or I can spend 900 exp to make my character 8.00/6.63=1.2 times more powerful which is equivalent (like compound interest) to 1.2^(5/9)=1.11 times for 500 exp. By this method I decide that taking power is better.

If I have weapon of Gondolin, I would create a Character object, increase its number of sides by 1, and use it for estimates.

To calculate dmg per dmg for two weapon fighting I would calculate dmg per dmg for the main hand and for the 2nd hand separately and add them. For some other skills this algorithm is difficult or impossible to apply.

Now come some notes. People on this forum seem to think that finesse is good and power is bad. Every time I compared them that was not the case, in fact power seems very good every time. Also I learned that [0, 1d1] is usually better than [-1, 1d2]. But defense per evasion lost than this is usually good. Battle axes are good. Curved sword is almost always better than short sword. Hand-and-a-half weapon + shield is usually better than wielding it with 2 hands. Mountain trolls are unlike other creatures - their damage is so high that you are better off having high expected damage and evasion against them than defense.

Feel free to give feedback, to tell me why my algorithm is bad, etc.
CrabMan is offline   Reply With Quote