A newbie's way of choosing which upgrade to take

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • CrabMan
    Rookie
    • Mar 2018
    • 7

    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.
  • Quirk
    Swordsman
    • Mar 2016
    • 462

    #2
    So firstly: it's great to have someone putting together some analysis code, I am having a wee play with it. As someone who cherishes game balance, I really like getting some data analysis done.

    However, there are some factors that aren't being tracked here which are relevant. The first is the cost of being surrounded - you get Evasion penalties based on being surrounded. Each enemy adjacent on the three sides opposite to your attacker brings your Evasion down by -2 and other adjacent enemies bring it down by -1.

    So from a corridor perspective, if you have three enemies approaching on the left and one on the right, you want to kill the one on the right first, and quickly. Doing more damage counts for more than efficiently outdamaging one foe. This makes all the non-evasion options slightly better along with two handed weapons. I'd need to extend your code and run some simulations to say exactly how this changes things, but the more turns you spend surrounded, the worse you will fare - two enemies are hitting you at effectively +2 instead of one at +0.

    The second issue I see is that mountain trolls are much more representative of the damage creatures deal at deeper depths; most enemies by 900 feet will outdamage them, often substantially. At the same time protection doesn't rise quite as fast: looking at the last three combat winners on the Sil ladder, their protection scores are:
    8-22, 8-29, 6-20.

    Typically hauberks are unloved as a result - that extra -1 to Melee and Evasion is too much to pay for 2 more protection.

    In general, protection is hugely important early on and steadily less relevant by the late game.

    Finesse works well with light weapons, high Melee and Momentum. If we have a 2lb 2d5 longsword, 4 strength and Momentum, Finesse outdamages Power against the Easterling warrior at the point your Melee outstrips their Evasion by +7 or better. The lower the weapon weight and the higher the dice sides, the more good Finesse does you (hence the combo with Momentum). With a 2d6 2lb longsword, a Ring of Damage and Momentum, Finesse beats Power for damage from a Melee/Evasion advantage of +3 onwards.

    This is very relevant in the throne room when Song of Slaying can reach accuracy bonuses of +30 or higher, and in combination with Subtlety. It's also worth noting that unwary enemies react at half Evasion, and sleeping monsters have Evasion set to -5; a stealthy character opens at a bigger advantage using Finesse.

    (Sil-Q has removed both momentum and Song of Slaying, so it's interesting for me to run the numbers and consider if Finesse maybe now needs a buff. Hauberks too perhaps could use some love.)

    Oh, and there is a minor code issue I see - I presume you didn't know how monster criticals were calculated? It's treated as if every damage die represented 2 lbs of weapon so e.g a 2d8 attack would be equivalent to a 4 lb weapon would be 7+4 = 11 per critical.
    Last edited by Quirk; March 18, 2018, 15:20.

    Comment

    • wobbly
      Prophet
      • May 2012
      • 2629

      #3
      Re hauberks: I use them. I mean a plain hauberk is just bad but fine hauberks or specials can be good. If you're comparing late game high protection characters spend a lot less on evasion which is more pts spent elsewhere.

      Comment

      • CrabMan
        Rookie
        • Mar 2018
        • 7

        #4
        Yes, I didn't know how monster criticals work, thank you.

        Comment

        • CrabMan
          Rookie
          • Mar 2018
          • 7

          #5
          Originally posted by Quirk
          However, there are some factors that aren't being tracked here which are relevant. The first is the cost of being surrounded - you get Evasion penalties based on being surrounded. Each enemy adjacent on the three sides opposite to your attacker brings your Evasion down by -2 and other adjacent enemies bring it down by -1.

          So from a corridor perspective, if you have three enemies approaching on the left and one on the right, you want to kill the one on the right first, and quickly. Doing more damage counts for more than efficiently outdamaging one foe. This makes all the non-evasion options slightly better along with two handed weapons. I'd need to extend your code and run some simulations to say exactly how this changes things, but the more turns you spend surrounded, the worse you will fare - two enemies are hitting you at effectively +2 instead of one at +0.
          Actually, when surrounded you are still equally happy if you increase your expected damage twofold or if you decrease the enemies' expected damage twofold. It's just now you need to calculate expected damage as if the enemies had more melee score (yes, the code can be improved to support that better). Consider the following:

          You are with two enemies of the same type in a corridor, one on each side. Let's say on average you deal d_h (imagine _ means subscript) to the monster with your attack (h stands for hero), a single monster on average deals d_m damage per attack, and when you are surrounded in a corridor, their melee score is increased by two, and then a monster deals d_{m2} damage on average. You will fight the first monster for t=h_m/d_h, where h_m? is the monster's hp. So in total the first monster will deal d_{m2} t damage to you, and the second monster will deal (d_{m2}+d_m)t damage - the sum of that is the total amount of health you will lose fighting these two monsters, and it equals D = (2d_{m2}+d_m?)t = (2d_{m2}+d_m) h_m? / d_h. Notice that if you increase d_h twofold, this value will decrease twofold, and the same will happen if you decrease some value between d_m and d_{m2} twofold. And if you increase d_m by some value and decrease d_m and d_{m2} in such way that hero's damage per point of monster's damage increases twofold, the same will happen. The conclusion is that you are equally happy. The same will hold for more enemies, e.g. for 3 enemies the formula will be D = (d_{m4}+2d_{m2}+2d_{m1}+d_m?) h_m? / d_h.

          This does not take into account that the longer you fight, the hungries you will get, the more light source turns you will lose and the more likely some more enemies will accidentally stumble upon you and make your life harder, so increasing damage output by x percent is slightly better than decreasing how much damage you take by the same percentage. Also note that if you are fighting multiple enemies, then you will suffer more hits with enemies having melee score bonus, so you should probably optimize damage per damage for the enemies having melee score +2, +3, or even +4 case.
          Last edited by CrabMan; March 19, 2018, 05:06.

          Comment

          • Quirk
            Swordsman
            • Mar 2016
            • 462

            #6
            You're still assuming you start surrounded until the end of the post; sorry if I didn't communicate clearly enough that this is the root of the issue.

            Assume we have just two enemies in a corridor. You have entered the corridor at full health so the second enemy is reluctant to pursue, until you meet another enemy and it decides it can surround you. It will take two turns to close the gap to do so.

            We will ignore the surround bonus.

            Case A: you damage one-third of an enemy's health each turn, and take one-tenth of your health in damage.
            Case B: you damage one-sixth of an enemy's health each turn, and take one-twentieth of your health in damage.

            Case A: The first enemy hits you twice, and you hit it twice, before the second enemy closes the gap. The first enemy is now at one-third health. Assuming the enemy is first to move, you are hit from both sides before killing the first enemy. You take two blows from the first enemy, one blow from each enemy while surrounded, and a parting blow before your next action. This reduces you to 50% health.

            Case B: The first enemy hits you twice, and you hit it twice, before the second enemy closes the gap. The first enemy is now at two-thirds health. Assuming the enemy is first to move, you are hit from both sides multiple times before killing the first enemy. You take two blows from the first enemy, eight blows from both sides while surrounded, and a parting blow before your next action. This reduces you to 45% health.

            However, in practice, the two blows while surrounded in case A are made at +2, and the eight blows while surrounded in case B are made at +2.

            Comment

            • Quirk
              Swordsman
              • Mar 2016
              • 462

              #7
              Originally posted by wobbly
              Re hauberks: I use them. I mean a plain hauberk is just bad but fine hauberks or specials can be good. If you're comparing late game high protection characters spend a lot less on evasion which is more pts spent elsewhere.
              Yeah - I spent some time playing with the numbers and while basic hauberks are just bad, there's no way to improve them that doesn't lead to fine hauberks being stupidly good.

              Comment

              • CrabMan
                Rookie
                • Mar 2018
                • 7

                #8
                Ah, I see what you are talking about. I guess doing (melee) damage is moderately better than evading damage.

                Comment

                Working...
                😀
                😂
                🥰
                😘
                🤢
                😎
                😞
                😡
                👍
                👎