Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  

News:

Starsector 0.97a is out! (02/02/24); New blog post: Simulator Enhancements (03/13/24)

Pages: 1 ... 14 15 [16] 17 18 ... 32

Author Topic: Optimizing the Conquest: a Mathematical Model of Space Combat  (Read 24641 times)

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #225 on: December 08, 2022, 11:46:17 AM »

All slots were always filled. You can view the full table of weapon combinations and associated scores at https://pastebin.com/Yvrftsw1

A couple of things to note here.
1. While the Arbalest performed well on average, note that the strongest combos did not have Arbalests, and especially not double Arbalest. The numbers for single weapons are the average ttk for layouts that have this weapon. That is, "what is the average ttk for this weapon, when other equipment is random". It turns out Arbalest scores well with that metric, but must be aware of this nuance. As for why, my guess is because it is a kinetic weapon with better DPS than HVD and better damage/shot and accuracy than HAC.
2. However, note that further in the thread, Vanshilar simulated the combos and when paired with longer range weapons the AI simply did not fire the Arbalest most of the time, making it a pretty poor choice in reality unless you compensate for this somehow.
3. Not all medium ballistics were included, for example the HMG would likely have outperformed Arbalest had it been in, when  range is ignored in the model. Energy weapons were also absent. We are working on a more accurate model currently, having found some things to improve on, and hopefully a tool to provide custom and/or comprehensive tests.
« Last Edit: December 08, 2022, 11:50:45 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #226 on: December 08, 2022, 09:18:47 PM »

I want your feedback for the code I posted and, if it is good, to start writing unit and integration tests that would verify that it does.  The unit tests would cover functions with tricky math, while the integration tests would see how the whole arrangement works together.  I need your help to decide what numbers the test should start from and to determine what results it should yield.  I figure the integration test should cover high and low damage, all four damage types, and both individual and successive shots.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #227 on: December 08, 2022, 09:45:57 PM »

All right, let's see:


import copy

"""
Damage computing module.
"""

class ArmorGrid:
    """
    A Starsector ship armor grid.
    """
    _ARMOR_RATING_PER_CELL_FACTOR = 1 / 15
    _MINIMUM_ARMOR_FACTOR = 0.05
    _MINIMUM_DAMAGE_FACTOR = 0.15
    WEIGHTS = [[0.0, 0.5, 0.5, 0.5, 0.0],
               [0.5, 1.0, 1.0, 1.0, 0.5],
               [0.5, 1.0, 1.0, 1.0, 0.5],
               [0.5, 1.0, 1.0, 1.0, 0.5],
               [0.0, 0.5, 0.5, 0.5, 0.0]]
   
    def __init__(self, armor_rating: float, cell_size: float, width: int):
        self._minimum_armor = ArmorGrid._MINIMUM_ARMOR_FACTOR * armor_rating
        self._cells = [[armor_rating * ArmorGrid._ARMOR_RATING_PER_CELL_FACTOR
                       for _ in range(width + 4)] for _ in range(5)]
        self._bounds = [i * cell_size for i, _ in enumerate(self[2][2:-2])]
   
    def __getitem__(self, row) -> float:
        """
        Return the armor value of the jth cell of the ith row.
        """
        return self._cells[row]

    def _pool(self, index: int) -> float:
        """
        Return the pooled armor for the cell at this index of an armor
        row.
        """
        return sum([sum([self[j][i + index - 3] * ArmorGrid.WEIGHTS[j]
                         for i in range(0, 5)]) for j in range(0, 5)])
 
    def _effective_armor(self, index: int) -> float:
        """
        Return the effective armor for the cell at this index
        of an armor grid.
        """
        return max(self._minimum_armor, self._pool(index))
       
    def damage_factor(self, hit_strength: float, index: int) -> float:
        """
        Return the armor damage factor for a hit to the cell at
        this index of an armor grid.
        """
        return max(ArmorGrid._MINIMUM_DAMAGE_FACTOR,
                  1 / (1 + self._effective_armor(index) / hit_strength))
                  
Everything seems correct so far, if I understand the code right.

    @property
    def bounds(self):
        """
        The right bound of each cell in the middle row, except the two
        padding cells on both sides.
        """
        return self._bounds
       
       
class Target:
    """
    Holds an armor grid and potentially a shield and hull.
    """
    def __init__(self, armor_grid):
        self.armor_grid = armor_grid
If this is intended to be an object describing the ship, shouldn't it hold an armor grid, soft flux, hard flux, and hull?

class Shot:
    """
    A shot fired at a row of armor cells protected by a shield.

    base_shield_damage - starting amount of damage to be
                         inflicted on the target shield
    base_armor_damage - starting amount of damage to be
                        inflicted on the target armor
    strength - strength against armor for armor damage
               calculation
    """
    def __init__(
            self,
            base_shield_damage: float,
            base_armor_damage: float,
            strength: float):
        self.base_shield_damage = base_shield_damage
        self.base_armor_damage = base_armor_damage
        self.strength = strength

Also for the fully integrated code needs the type parameter (beam vs. other) to know whether we are dealing soft or hard flux and how to calculate
 damage (because for beams we should hand the function a real number describing beam ticks and beam intensity during that second, while for a gun
 type weapon we should hand the function an integer describing how many shots hit the target during that second)

Occurs to me: are there beam weapons in mods that deal hard flux? If so, then it actually needs two modifiers: one for flux type and one for weapon type

class DamageExpectation:
    """
    Expected damage to a target by a shot.

    Calculates the expectation value of the damage of a shot
    with a spread to a target with a shield, armor grid, and random
    positional deviation. 

    shot - what is being fired, whether a projectile, missile, or beam tick
    target - what is being hit
    distribution - spread of shots across a horizontal distance
    """
    def __init__(self, target: object, shot: object, distribution: object):
        self.shot = shot
        self.target = target
        self.probabilities = [distribution(bound) for bound in
                              target.armor_grid.bounds]
   
    def _expected_armor_damage_distribution(self) -> list:
        """
        Return the expected damage to each cell of the targeted armor
        row times the probability to hit it.
       
        damage - damage against armor after reduction by armor
        """
       
        return [self.shot.base_armor_damage
                * self.target.armor_grid.damage_factor(self.shot.strength, i+2)
                * self.probabilities
                for i, _ in enumerate(self.probabilities)]
       
    def _weighted_expected_armor_damage_distributions(self) -> list:
        """
        Return the weighted distribution across the surrounding armor grid
        of the expected damage to each cell of the targeted armor row.
       
        damage - damage against armor after reduction by armor
        """
        return [
            [[damage * weight for weight in row] for row in ArmorGrid.WEIGHTS]
             for damage in self._expected_armor_damage_distribution()
        ]

    def damage_armor_grid(self):
        """
        Reduce the values of the armor grid cells of the target
        by the expected value of the damage of the shot across them.
        """
        for i, distribution in enumerate(
            self._weighted_expected_armor_damage_distributions()):
            for j, row in enumerate(distribution):
                for k, damage in enumerate(row):
                    self.target.armor_grid[j][i+k] = (
                        max(0, self.target.armor_grid[j][i+k] - damage))


def hit_probability(bound: float): return 0.1 #dummy for test

Seems correct overall, but does not appear to implement error correction.
def main():
    armor_rating, cell_size, width = 100, 10, 10
    base_armor_damage, base_shield_damage, hit_strength = 10, 40, 10
   
    target = Target(ArmorGrid(armor_rating, cell_size, width))
    shot = Shot(base_armor_damage, base_shield_damage, hit_strength)
    expectation = DamageExpectation(target, shot, hit_probability)
   
    for row in expectation.target.armor_grid: print([round(x) for x in row])
   
    expectation.damage_armor_grid()
   
    print()
   
    for row in expectation.target.armor_grid: print([round(x) for x in row])
main()
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]

[7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7]
[6, 6, 5, 5, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6]
[6, 6, 5, 5, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6]
[6, 6, 5, 5, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6]
[7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7]


I have discovered that the code was not even calling the right methods and have refactored it, even removing the armor_damage_factors.


I would actually recommend first writing a test module to test the armor damage code versus a version of the code where you use the same functions to simulate dealing damage to Dominators 1 shot at a random cell at a time (using the same probability distribution as you do in the model), like I did with the "hull residual plots" above. 500 energy damage seems to be a good spot for detecting errors in the calculation and has the advantage that you have both intrinsic_parity's and my simulation data to test against to make sure the error is in the same range. Printing a "hull residual plot" (plot observed hull minus expected hull from model for a large number of sims) demonstrates the error in the model very cleanly. An alternative is an "armor residual plot" which should avoid any statistical weirdness leading to a bulge at the start, but also does not demonstrate that the hull damage computation is going right. Testing whether damage type modifiers are applied correctly should be almost trivial since they are just simple multipliers, but you can do that here by fiddling with the damage type and see that graphs change as expected.

Then should build the integrated code, with this specific weapons layout: Locust Locust Harpoon Harpoon Gauss HAG, and getting it to print graphs for flux, hull damage and armor damage. This is because we have actual real life sim data for what the numbers should be: https://fractalsoftworks.com/forum/index.php?topic=25459.msg379103#msg379103

Then when we have that we can test that the overall code is working correctly (ignoring exact numbers, but seeing that the results are broadly similar with some fiddling of the SD factor).

Then after that, build the advanced error corrected armor damage module and test it versus the simulated Dominators from the first step to see that it improves the prediction. If your code runs very fast you could do what I dreamed of doing but never did, and compute the average hull residual at point of kill for each combination of weapon damage from 50 to 1000 and armor from 150 to 1750 for both the error corrected and the naive model, and make a nice plot to make sure it consistently improves predictions over the entire spectrum. That's optional though, we know it should be more correct mathematically so if it improves prediction for a few "hard" cases (ie. ones where damage is reasonably low but not so low that it is always reduced to minimum, which is where we expect non-linearity to be greatest) then that should be enough.

After that, it would probably be reasonable to test the beam weapon code vs. real life by running a similar simulation as Vanshilar did using, say, Tachyon Lances. Then if that's correct I'd say internal and external validity have been reasonably tested, enough to publish at least.
« Last Edit: December 08, 2022, 10:08:03 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

intrinsic_parity

  • Admiral
  • *****
  • Posts: 3071
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #228 on: December 08, 2022, 10:11:33 PM »

A couple thoughts on the structure of the code:

Instead of having target class, call it a 'ship' class, and potentially allow it to have info about weapons and flux as well as defenses. I think a very important aspect of a good simulation is that it should be symmetric (i.e. simulating both the friendly and enemy ships shooting and getting shot). The shield dynamics cannot be effectively simulated without considering both ships firing and getting hit.

Also, personally, I would not have a separate 'expected damage' class. I would wrap that all up as methods in the armor grid class.

Like hector said, the shot class also is not capable of handling beams. You also need a hull damage value for the shot, which is really a base damage value.

You also need to deal with hull damage somewhere. I might do it as methods in the ship class, but I think you will need to add some stuff to the armor class to deal with overkill damage to armor (when incoming damage exceeds remaining armor in a certain cell the overkill gets dealt to hull) and residual armor. Idk what the easiest way to handle it is.
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #229 on: December 08, 2022, 10:15:51 PM »

If you are aiming to build the error corrected model eventually then the place for hull damage would be that you compute overkill damage from the final (non-hypothetical) damage to armor and then deal that much damage to hull, adjusted for modifier. Of course there are surely many other ways to do it, but that seems natural.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #230 on: December 09, 2022, 05:54:09 PM »

Before implementing such fancier features as error correction or deeper abstraction, I want to ensure that the code is understandable and well-formed and therefore have addressed the concerns you both had about clarity and organization.  The code now has a ship class, which has a weapons attribute, and the shot is now in a weapon with a hit distribution, which I still must implement beyond a dummy flat value.  The testing ideas you suggested will be good later: for now I want to verify that each call of the armor-grid damaging function yields the expected result. 

I have noticed that my code does not determine whether the ship would have its shields up or down or a method to determine whether a ship would fire its high explosive weapons or not; e.g., a Hammerhead might fire Heavy Autocannons until the enemy drops its shield and then fire Harpoons to destroy its armor, but the enemy might leave its shield down to absorb the Heavy Autocannon shots with its armor instead.
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
[7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]

[7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7]
[6, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 5, 6, 6]
[6, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 5, 6, 6]
[6, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 5, 6, 6]
[7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7]
Code
Code
import copy
"""
Damage computing module.
"""
class ArmorGrid:
    """
    A Starsector ship armor grid.
    """
    _ARMOR_RATING_PER_CELL_FACTOR = 1 / 15
    _MINIMUM_ARMOR_FACTOR = 0.05
    _MINIMUM_DAMAGE_FACTOR = 0.15
    WEIGHTS = [[0.0, 0.5, 0.5, 0.5, 0.0],
               [0.5, 1.0, 1.0, 1.0, 0.5],
               [0.5, 1.0, 1.0, 1.0, 0.5],
               [0.5, 1.0, 1.0, 1.0, 0.5],
               [0.0, 0.5, 0.5, 0.5, 0.0]]
   
    def __init__(self, armor_rating: float, cell_size: float, width: int):
        self._minimum_armor = ArmorGrid._MINIMUM_ARMOR_FACTOR * armor_rating
        self._cells = [[armor_rating * ArmorGrid._ARMOR_RATING_PER_CELL_FACTOR
                       for _ in range(width + 4)] for _ in range(5)]
        self._bounds = [i * cell_size for i, _ in enumerate(self[2][2:-2])]
   
    def __getitem__(self, row) -> float:
        """
        Return the armor value of the jth cell of the ith row.
        """
        return self._cells[row]

    def _pool(self, index: int) -> float:
        """
        Return the pooled armor for the cell at this index of an armor
        row.
        """
        return sum([sum([self[i][j + index - 3] * ArmorGrid.WEIGHTS[i][j]
                         for j in range(0, 5)]) for i in range(0, 5)])
 
    def _effective_armor(self, index: int) -> float:
        """
        Return the effective armor for the cell at this index
        of an armor grid.
        """
        return max(self._minimum_armor, self._pool(index))
       
    def damage_factor(self, hit_strength: float, index: int) -> float:
        """
        Return the armor damage factor for a hit to the cell at
        this index of an armor grid.
        """
        return max(ArmorGrid._MINIMUM_DAMAGE_FACTOR,
                  1 / (1 + self._effective_armor(index) / hit_strength))

    @property
    def bounds(self):
        """
        The right bound of each cell in the middle row, except the two
        padding cells on both sides.
        """
        return self._bounds
       
       
class Ship:
    """
    Holds an armor grid and potentially a shield and hull.
   
    weapons - container of the weapons of the ship, with
              structure to be determined
    armor_grid - ArmorGrid of the ship
    hull - amount of hitpoints the ship has
    flux_capacity - amount of flux to overload the ship
    flux_dissipation - how much flux the ship can expel
                       without actively venting
    """
    def __init__(self,
            weapons: object,
            armor_grid: object,
            hull: float,
            flux_capacity: float,
            flux_dissipation: float):
        self.weapons = weapons
        self.armor_grid = armor_grid
        self.hull = hull
        self.flux_capacity = flux_capacity
        self.flux_dissipation = flux_dissipation
        self.hard_flux, self.soft_flux = 0, 0
   
    @property
    def will_overload(self):
        """
        Return whether the ship will now overload.
       
        A ship will overload if soft or hard flux exceeds
        the flux capacity of the ship.
        """
        return (self.hard_flux > self.flux_capacity
                or self.soft_flux > self.flux_capacity)
   
    @property
    def overloaded(self):
        """
        Return whether the ship is in the overloaded state.
       
        Ignores the duration of overloaded.
        -TODO: Implement overload duration
        """
        return self.will_overload
       
    @property
    def shield_up(self):
        """
        Return whether the shield is up or down.
       
        Presumes the shield to be up if the ship is not overloaded,
        ignoring smart tricks the AI can play to take kinetic damage
        on its armor and save its shield for incoming high explosive
        damage.
        """
        return not self.overloaded
       

class Shot:
    """
    A shot fired at a row of armor cells protected by a shield.

    Calculates the expectation value of the damage of a shot
    with a spread to a ship with a shield, armor grid, and random
    positional deviation. 

    shot - what is being fired: a projectile, missile, or beam tick
    ship - vessel being hit
    distribution - spread of shots across a horizontal distance

    base_damage - amount listed under damage in weapon_data.csv
    base_shield_damage - starting amount of damage to be inflicted
                         on the ship shield
    base_armor_damage - starting amount of damage to be inflicted
                        on the ship armor
    strength - strength against armor for armor damage calculation
    """
    def __init__(
            self,
            base_damage: float,
            base_shield_damage: float,
            base_armor_damage: float,
            strength: float,
            flux_hard: bool):
        self.base_shield_damage = base_shield_damage
        self.base_armor_damage = base_armor_damage
        self.strength = strength
        self.probabilities = None

    def distribute(self, ship: object, distribution: object):
        """
        Spread hit probability over each armor cell of a ship.
       
        Calculate the probability to hit each armor cell of a
        row and save the probabilities for later calculation.
        """
        self.probabilities = [distribution(bound) for bound in
                              ship.armor_grid.bounds]
   
    def _expected_armor_damage_distribution(self, ship: object) -> list:
        """
        Return the expected damage to each cell of the shiped armor
        row times the probability to hit it.
       
        damage - damage against armor after reduction by armor
        """
       
        return [self.base_armor_damage
                * ship.armor_grid.damage_factor(self.strength, i+2)
                * self.probabilities[i]
                for i, _ in enumerate(self.probabilities)]
       
    def _weighted_expected_armor_damage_distributions(
            self,
            ship: object) -> list:
        """
        Return the weighted distribution across the surrounding armor grid
        of the expected damage to each cell of the shiped armor row.
       
        damage - damage against armor after reduction by armor
        """
        return [
            [[damage * weight for weight in row] for row in ArmorGrid.WEIGHTS]
             for damage in self._expected_armor_damage_distribution(ship)
        ]

    def damage_armor_grid(self, ship):
        """
        Reduce the values of the armor grid cells of the ship
        by the expected value of the damage of the shot across them.
        """
        for i, distribution in enumerate(
            self._weighted_expected_armor_damage_distributions(ship)):
            for j, row in enumerate(distribution):
                for k, damage in enumerate(row):
                    ship.armor_grid[j][i+k] = (
                        max(0, ship.armor_grid[j][i+k] - damage))
                       
                       
class Weapon:
    """
    A weapon for a Starsector ship in simulated battle.
   
    Is carried by a Ship instance, contains a shot with some
    distribution, and fires that shot at a target.
   
    shot - projectile, missile, or beam tick of the weapon
    distribution - function returning the probability that
                   of the shot to hit between two bounds
    """
    def __init__(self, shot: object, distribution: object):
        self.shot = shot
        self.distribution = distribution
       
    def fire(self, ship: object):
        """
        Fire the shot of this weapon at that ship.
       
        ship - a Ship instance
        """
        self.shot.distribute(ship, self.distribution)
        #TODO: implement general hit ship method on Shot
        self.shot.damage_armor_grid(ship)
        return #future code below
        if ship.shield_up:
            pass #TODO: implement shield damage, overloading, etc.
        else: self.shot.damage_armor_grid(ship)
       

def hit_probability(bound: float): return 0.1 #dummy for test


def main():
    ship_spec = (
        1_000,#hull
        1_000,#flux_capacity
        100#flux_dissipation
    )
    armor_grid_spec = (
        100,#armor_rating
        10,#cell_size
        10#width
    )
    weapons = []
    shot_spec = (
        20,#base_damage,
        10,#base_shield_damage
        40,#base_armor_damage
        20,#strength
        False#flux_hard
    )
   
    ship = Ship(weapons, ArmorGrid(*armor_grid_spec), *ship_spec)
    weapon = Weapon(Shot(*shot_spec), hit_probability)
   
    for row in ship.armor_grid: print([round(x) for x in row])
   
    weapon.fire(ship)
   
    print()
   
    for row in ship.armor_grid: print([round(x) for x in row])
main()
[close]
« Last Edit: December 09, 2022, 06:03:32 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #231 on: December 09, 2022, 06:57:51 PM »

If you possibly want to add support for realistic combat at some point then could also include a parameter for range. I am not sure if this is something we want to do, I think you had a good point earlier about preferring to just see how weapons work without a lot of ifs.

However it would be easy to implement a rudimentary "AI" such that
- both ships have a top speed and acceleration
- in combat, jink randomly (see stuff I posted earlier)
- when near max flux, increase range until beyond enemy max range, vent if you can actually get beyond max range
- when near 0 flux get back to range where all of your guns fire

Strictly rules based so should not actually burden calculations much, the big q is does it make the model more or less useful for modders and research. It is not necessarily good to have more assumptions. If adding features like AI simulation or enemy ship having weapons then at least there should definitely be a switch to turn it off for people who just want basic weapon damage data.

Does the new code use numpy? Performance should be essential here at least for the armor damage part.
« Last Edit: December 09, 2022, 07:05:56 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #232 on: December 09, 2022, 07:36:25 PM »

If you possibly want to add support for realistic combat at some point then could also include a parameter for range. I am not sure if this is something we want to do, I think you had a good point earlier about preferring to just see how weapons work without a lot of ifs.

However it would be easy to implement a rudimentary "AI" such that
- both ships have a top speed and acceleration
- in combat, jink randomly (see stuff I posted earlier)
- when near max flux, increase range until beyond enemy max range, vent if you can actually get beyond max range
- when near 0 flux get back to range where all of your guns fire

Strictly rules based so should not actually burden calculations much, the big q is does it make the model more or less useful for modders and research. It is not necessarily good to have more assumptions. If adding features like AI simulation or enemy ship having weapons then at least there should definitely be a switch to turn it off for people who just want basic weapon damage data.

Good point.  Maybe we could do all that later.

Quote
Does the new code use numpy? Performance should be essential here at least for the armor damage part.

The code I just posted does not yet use numpy because I want you to tell me if it yields the right numbers or not before I profile it and where needed add the numpy code, which can also be harder to read.

Edit: Here it is with numpy.  I ironically found it clearer to read.  This code is also vectorized for what it's worth.
[[7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.]
 [7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.]
 [7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.]
 [7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.]
 [7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.]]

[[7. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 7.]
 [6. 6. 5. 4. 4. 4. 4. 4. 4. 4. 4. 5. 6. 6.]
 [6. 6. 5. 4. 4. 4. 4. 4. 4. 4. 4. 5. 6. 6.]
 [6. 6. 5. 4. 4. 4. 4. 4. 4. 4. 4. 5. 6. 6.]
 [7. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 7.]]
Code
Code
import numpy as np
import copy
"""
Damage computing module.
"""
class ArmorGrid:
    """
    A Starsector ship armor grid.
   
    - Represents a horizontal frontal row of armor on a ship.
    - Includes 2 rows of vertical padding above; 2 below; 2 columns of
      left horizontal padding; 2 right.
   
    constants:
    _ARMOR_RATING_PER_CELL_FACTOR - multiply by armor rating to determine
                                    initial value of each cell of an
                                    ArmorGrid
    _MINIMUM_ARMOR_FACTOR - multiply by armor rating to determine
                            minimum effective value of armor for damage
                            calculations
    _MINIMUM_DAMAGE_FACTOR - least factor whereby this ArmorGrid can
                             cause incoming damage to be multiplied
    WEIGHTS - multiply by damage to the central cell to determine damage
              to surrounding ones
    """
    _ARMOR_RATING_PER_CELL_FACTOR = 1 / 15
    _MINIMUM_ARMOR_FACTOR = 0.05
    _MINIMUM_DAMAGE_FACTOR = 0.15
    WEIGHTS = np.array([[0.0, 0.5, 0.5, 0.5, 0.0],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.0, 0.5, 0.5, 0.5, 0.0]])
   
    def __init__(self, armor_rating: float, cell_size: float, width: int):
        """
        armor_rating - armor rating of the ship to which this ArmorGrid
                       belongs
        cell_size - size of each armor cell, which is a square, in pixels
        width - width of the armor grid in cells
        """
        self._minimum_armor = ArmorGrid._MINIMUM_ARMOR_FACTOR * armor_rating
        armor_per_cell = armor_rating * ArmorGrid._ARMOR_RATING_PER_CELL_FACTOR
        self.cells = np.full((5,width+4), armor_per_cell)
        self._bounds = np.arange(0, len(self.cells[2,2:-2])) * cell_size
                 
    def damage_factors(self, hit_strength: float) -> object:
        """
        Return the factor whereby to multiply the damage of each
        non-padding cell of the grid.
        """
        pooled_armor = np.array(
            [np.sum(ArmorGrid.WEIGHTS * self.cells[0:5,index:index+5]) for
             index, _ in enumerate(self.cells[2,2:-2])]
        )
       
        pooled_armor[pooled_armor<self._minimum_armor] = self._minimum_armor
        factors = 1 / (1 + pooled_armor / hit_strength)
        factors[factors<ArmorGrid._MINIMUM_DAMAGE_FACTOR] = (
            ArmorGrid._MINIMUM_DAMAGE_FACTOR)
        return factors

    @property
    def bounds(self):
        """
        The right bound of each cell in the middle row, except the two
        padding cells on both sides.
        """
        return self._bounds
       
       
class Ship:
    """
    Holds an armor grid and potentially a shield and hull.
   
    methods:
    - will_overload
    - overloaded
    - shield_up
    """
    def __init__(self,
            weapons: object,
            armor_grid: object,
            hull: float,
            flux_capacity: float,
            flux_dissipation: float):
        """
        weapons - container of the weapons of the ship, with
              structure to be determined
        armor_grid - ArmorGrid of the ship
        hull - amount of hitpoints the ship has
        flux_capacity - amount of flux to overload the ship
        flux_dissipation - how much flux the ship can expel
                           without actively venting
        """
        self.weapons = weapons
        self.armor_grid = armor_grid
        self.hull = hull
        self.flux_capacity = flux_capacity
        self.flux_dissipation = flux_dissipation
        self.hard_flux, self.soft_flux = 0, 0
   
    @property
    def will_overload(self):
        """
        Return whether the ship will now overload.
       
        A ship will overload if soft or hard flux exceeds
        the flux capacity of the ship.
        """
        return (self.hard_flux > self.flux_capacity
                or self.soft_flux > self.flux_capacity)
   
    @property
    def overloaded(self):
        """
        Return whether the ship is in the overloaded state.
       
        Ignores the duration of overloaded.
        -TODO: Implement overload duration
        """
        return self.will_overload
       
    @property
    def shield_up(self):
        """
        Return whether the shield is up or down.
       
        Presumes the shield to be up if the ship is not overloaded,
        ignoring smart tricks the AI can play to take kinetic damage
        on its armor and save its shield for incoming high explosive
        damage.
        """
        return not self.overloaded


class Shot:
    """
    A shot fired at a row of armor cells protected by a shield.

    Calculates the expectation value of the damage of a shot
    with a spread to a ship with a shield, armor grid, and random
    positional deviation.
   
    methods:
    - distribute
    - _damage_distribution
    - damage_armor_grid
    """
    def __init__(
            self,
            base_damage: float,
            base_shield_damage: float,
            base_armor_damage: float,
            strength: float,
            flux_hard: bool):
        """
        base_damage - amount listed under damage in weapon_data.csv
        base_shield_damage - starting amount of damage to be inflicted
                             on the ship shield
        base_armor_damage - starting amount of damage to be inflicted
                            on the ship armor
        strength - strength against armor for armor damage calculation
        flux_hard: whether the flux damage against shields is hard or not
        """
        self.base_shield_damage = base_shield_damage
        self.base_armor_damage = base_armor_damage
        self.strength = strength
        self.probabilities = None

    def distribute(self, ship: object, distribution: object):
        """
        Spread hit probability over each armor cell of a ship.
       
        Calculate the probability to hit each armor cell of a
        row and save the probabilities for later calculation.
        """
        self.probabilities = np.vectorize(distribution)(ship.armor_grid.bounds)
   
    def _damage_distribution(self, ship: object) -> list:
        """
        Return the expected damage to each cell of the shiped armor
        row times the probability to hit it.
       
        damage - damage against armor after reduction by armor
        """
        return (self.base_armor_damage
                * self.probabilities
                * ship.armor_grid.damage_factors(self.strength))

    def damage_armor_grid(self, ship):
        """
        Reduce the values of the armor grid cells of the ship
        by the expected value of the damage of the shot across them.
        """
        for i, damage in enumerate(self._damage_distribution(ship)):
            ship.armor_grid.cells[0:5,i:i+5] -= damage * ArmorGrid.WEIGHTS
        ship.armor_grid.cells[ship.armor_grid.cells < 0.0] = 0.0
                       
                       
class Weapon:
    """
    A weapon for a Starsector ship in simulated battle.
   
    Is carried by a Ship instance, contains a shot with some
    distribution, and fires that shot at a target.
    """
    def __init__(self, shot: object, distribution: object):
        """
        shot - projectile, missile, or beam tick of the weapon
        distribution - function returning the probability that
                       of the shot to hit between two bounds
        """
        self.shot = shot
        self.distribution = distribution
       
    def fire(self, ship: object):
        """
        Fire the shot of this weapon at that ship.
       
        ship - a Ship instance
        """
        self.shot.distribute(ship, self.distribution)
        #TODO: implement general hit ship method on Shot
        self.shot.damage_armor_grid(ship)
        return #future code below
        if ship.shield_up:
            pass #TODO: implement shield damage, overloading, etc.
        else: self.shot.damage_armor_grid(ship)
       

def hit_probability(bound: float): return 0.1 #dummy for test


def main():
    ship_spec = (
        1_000,#hull
        1_000,#flux_capacity
        100#flux_dissipation
    )
    armor_grid_spec = (
        100,#armor_rating
        10,#cell_size
        10#width
    )
    weapons = []
    shot_spec = (
        20,#base_damage,
        10,#base_shield_damage
        40,#base_armor_damage
        20,#strength
        False#flux_hard
    )
   
    ship = Ship(weapons, ArmorGrid(*armor_grid_spec), *ship_spec)
    weapon = Weapon(Shot(*shot_spec), hit_probability)
   
    print(np.round(ship.armor_grid.cells))
    weapon.fire(ship)
    print()
    print(np.round(ship.armor_grid.cells))
main()
[close]
« Last Edit: December 09, 2022, 09:16:29 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #233 on: December 09, 2022, 09:26:17 PM »

Deleted - error in calculations
« Last Edit: December 10, 2022, 04:01:48 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #234 on: December 09, 2022, 11:06:24 PM »

Oh ok. I didn't realize you wanted feedback on the print too.

This should be easy to math out without reference to any implementation.

Then would you please work an example, from starting numbers to resulting grid, for me to test against?

Quote
You have 7 armor per cell, so the ship's starting armor value was 105. (I think these should be of type double, since it looks like you specified 100 armor? They must be real numbers anyway because the incoming damage is. Even if the game were to store them as integers in reality, they must be reals for dealing with probability in our model.)

I think you didn't read the main function, which spells out all the data.  The ship's starting armor was 100.  I rounded the numbers when printing them.

Quote
The pooled armor value was 105 at each of the middle cells. Hit strength was 10, so for a shot of 10 damage we expect minimum damage to kick in and do 1.5 damage spread according to the probability distribution of 10% chance to hit each cell. So that doesn't seem correct? Were those the values used?

Presumably they were used, yes.  Are you worried that they weren't?

Quote
This will be easier if you post the shot strength, damage type and ship parameters and probability distribution alongside the print as I may not understand the code correctly.

See my earlier point about the main function.

Quote
The print you posted does seem like an appropriate damage distribution and we can guess the parameters as follows: one cell in the middle takes 3 damage. Presumably probability was uniform so 1/14 chance to hit each cell. Given shot hit strength and damage x, the middle cell should have taken x^2/(x+105)*(1/15*1/14*3+1/30*1/14*2) damage. Solving for x we get a shot damage and hit strength of approximately 230. The edge cell of armor should then take 230/(335)*230*(1/15*1/14*2+1/30*1/14)=1.85 damage so that is correct. The edge cells at top and bottom should take 230/(335)*230*(3*1/30*1/14)=1.12 damage so that is correct. And the padding cells at the very edge should take 230/(335)*230*(1/30*1/14), so 0.38 damage. Which may be correct if it's rounding up.

Probability was uniform, but only the inner cells of the middle row (NumPy array slice of [2,2:-2]) 'received' a probability to hit.

Quote
A simple way to test whether this is going correctly is, for 1 shot vs undamaged armor, pool the armor back after damaging it and see that the total armor damage dealt to pooled armor is equal to hit strength/(hit strength + starting armor)*damage * hit prob. This literally must be so for the first shot when armor is uniform, though not later. Generally that relationship should hold while we are above minimum armor rule and no cells are hitting 0.

That test seems good, though I still want a worked example because I could mess that code up, too.

Quote
This would be a good point to add a "total armor damage dealt" function, as that is needed to test the model later anyway.  The way it works out is that should just be a mean of all armor cells (including padding, but not including corners!) * 15.

I'll add it after we do this.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #235 on: December 10, 2022, 01:21:29 AM »

Deleted - error in calculations
« Last Edit: December 10, 2022, 04:01:33 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #236 on: December 10, 2022, 03:59:59 AM »

All right so it turns out I was wrong, you cannot expect the damage dealt to armor to be equal to decrease in armor HP even for the first shot (in the sense of calculating a total armor HP as if we were to distribute it equally to get the same average armor cell value). It's just not the way it works. I'm going to delete the posts above so as not to confuse people.

Instead, the test calculation that you can make is that, for the first shot, it should be the case that the expected damage you are going to distribute over the armor should be the same as you would get from calculating the whole thing in bulk, because armor is uniform and probs sum to 1. Ie. sum of damage distributed over armor = damage * hit strength/(hit strength + armor). But this is accurate for the first shot only, since after that considering the armor in bulk is no longer accurate and there is no simple way to restore a full armor hp as a description of the armor!

An armor HP gauge should still exist for comparisons with real life data, it just must be understood that it does not accurately reflect the state of the armor but rather what the enemy ship captain would see (armor can be at half and ship still be taking hull damage as we all know).

Anyway I created this "dumb" and very verbose version of the basic naive armor damage code for a "reference". All the operations are implemented using loops and elementary operations, so it should be quite easy to see that they are correct.

It seems like damage is indeed much higher in your code than it should be. Question: when you write
Code
            ship.armor_grid.cells[0:5,i:i+5] -= damage * ArmorGrid.WEIGHTS

are you accounting for the fact that when distributing damage, the weights should be 1/15th of those used to pool armor? So that it sums up to 1 and not 15. (because for armor we have already applied the 1/15th when generating the armor matrix, so if we sum it to a total of 15 we get back the starting armor, but we haven't done that for damage so if we use the 1/2 and 1 weights we end up with 15 x too high damage)

Code
damage <- 20
hitstrength <- 40
startingarmor <- 100
hitstrengthcalc <- function(hitstrength, armor) return(max(0.15,hitstrength/(hitstrength+max(armor,0.05*startingarmor))))
shipcells <- 10
probabilities <- c(0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1)
norounds <- 3


weights <- matrix(c(0,0.5,0.5,0.5,0,0.5,1,1,1,0.5,0.5,1,1,1,0.5,0.5,1,1,1,0.5,0,0.5,0.5,0.5,0),5,5)
weights

poolarmor <- function(armormatrix, index) {
  sum <- 0
  for(i in 1:5)for(j in 1:5) sum <- sum + weights[i,j]*armormatrix[i,index-3+j]
  return(sum)
}
print("Starting armor matrix")
print(armormatrix <- matrix(startingarmor / 15, 5, shipcells+4))

print("Equivalent armor hp:")
fullarmorhp <- (sum(armormatrix[2:4,])+sum(armormatrix[1,2:13])+sum(armormatrix[5,2:13]))/(5*14-4)*15
print(fullarmorhp)

for(r in 1:norounds){

armordamagereductions <- vector(shipcells, mode="double")
for (x in 1:length(armordamagereductions)) {
  armordamagereductions[x] <- hitstrengthcalc(hitstrength,poolarmor(armormatrix,x+2))
}
print("Armor damage expected based on full armor hp:")
print(damage*hitstrength/(hitstrength+fullarmorhp))

armordamagesatmiddlecells <- armordamagereductions*damage
print("Armor damage at middle cells given full shot:")
print(armordamagesatmiddlecells)
print("Probability adjusted armor damage at middle cells:")
for(x in 1:length(armordamagesatmiddlecells)) {
  armordamagesatmiddlecells[x] <- armordamagesatmiddlecells[x]*probabilities[x]
}
print(armordamagesatmiddlecells)
print("Total armor damage incoming at middle cells:")
print(sum(armordamagesatmiddlecells))

for (i in 1:length(armordamagesatmiddlecells)){
  for (j in 1:5){
    for (k in 1:5){
      armormatrix[j,i+k-1] <- armormatrix[j,i+k-1] - armordamagesatmiddlecells[i]*weights[j,k]/15
    }
  }
}
print("New armor matrix:")
print(round(armormatrix,2))

print("Equivalent armor hp:")
fullarmorhp <- (sum(armormatrix[2:4,])+sum(armormatrix[1,2:13])+sum(armormatrix[5,2:13]))/(5*14-4)*15
print(fullarmorhp)
}

Output: using

damage <- 20
hitstrength <- 40
startingarmor <- 100
shipcells <- 10
probabilities <- c(0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1)
norounds <- 3



[1] "Starting armor matrix"
> print(armormatrix <- matrix(startingarmor / 15, 5, shipcells+4))
         [,1]     [,2]     [,3]     [,4]     [,5]     [,6]     [,7]     [,8]     [,9]    [,10]    [,11]
[1,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[2,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[3,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[4,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[5,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
        [,12]    [,13]    [,14]
[1,] 6.666667 6.666667 6.666667
[2,] 6.666667 6.666667 6.666667
[3,] 6.666667 6.666667 6.666667
[4,] 6.666667 6.666667 6.666667
[5,] 6.666667 6.666667 6.666667
>
> print("Equivalent armor hp:")
[1] "Equivalent armor hp:"
> fullarmorhp <- (sum(armormatrix[2:4,])+sum(armormatrix[1,2:13])+sum(armormatrix[5,2:13]))/(5*14-4)*15
> print(fullarmorhp)
[1] 100


[1] "Armor damage expected based on full armor hp:"
[1] 5.714286
[1] "Armor damage at middle cells given full shot:"
 [1] 5.714286 5.714286 5.714286 5.714286 5.714286 5.714286 5.714286 5.714286 5.714286 5.714286
[1] "Probability adjusted armor damage at middle cells:"
 [1] 0.5714286 0.5714286 0.5714286 0.5714286 0.5714286 0.5714286 0.5714286 0.5714286 0.5714286 0.5714286
[1] "Total armor damage incoming at middle cells:"
[1] 5.714286
[1] "New armor matrix:"
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 6.67 6.65 6.63 6.61 6.61 6.61 6.61 6.61 6.61  6.61  6.61  6.63  6.65  6.67
[2,] 6.65 6.61 6.57 6.53 6.51 6.51 6.51 6.51 6.51  6.51  6.53  6.57  6.61  6.65
[3,] 6.65 6.61 6.57 6.53 6.51 6.51 6.51 6.51 6.51  6.51  6.53  6.57  6.61  6.65
[4,] 6.65 6.61 6.57 6.53 6.51 6.51 6.51 6.51 6.51  6.51  6.53  6.57  6.61  6.65
[5,] 6.67 6.65 6.63 6.61 6.61 6.61 6.61 6.61 6.61  6.61  6.61  6.63  6.65  6.67
[1] "Equivalent armor hp:"
[1] 98.7013
[1] "Armor damage expected based on full armor hp:"
[1] 5.76779
[1] "Armor damage at middle cells given full shot:"
 [1] 5.764875 5.780745 5.791107 5.795901 5.797101 5.797101 5.795901 5.791107 5.780745 5.764875
[1] "Probability adjusted armor damage at middle cells:"
 [1] 0.5764875 0.5780745 0.5791107 0.5795901 0.5797101 0.5797101 0.5795901 0.5791107 0.5780745 0.5764875
[1] "Total armor damage incoming at middle cells:"
[1] 5.785946
[1] "New armor matrix:"
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 6.67 6.63 6.59 6.55 6.55 6.55 6.55 6.55 6.55  6.55  6.55  6.59  6.63  6.67
[2,] 6.63 6.55 6.48 6.40 6.36 6.36 6.36 6.36 6.36  6.36  6.40  6.48  6.55  6.63
[3,] 6.63 6.55 6.48 6.40 6.36 6.36 6.36 6.36 6.36  6.36  6.40  6.48  6.55  6.63
[4,] 6.63 6.55 6.48 6.40 6.36 6.36 6.36 6.36 6.36  6.36  6.40  6.48  6.55  6.63
[5,] 6.67 6.63 6.59 6.55 6.55 6.55 6.55 6.55 6.55  6.55  6.55  6.59  6.63  6.67
[1] "Equivalent armor hp:"
[1] 97.38631
[1] "Armor damage expected based on full armor hp:"
[1] 5.822996
[1] "Armor damage at middle cells given full shot:"
 [1] 5.816955 5.849598 5.871049 5.881035 5.883560 5.883560 5.881035 5.871049 5.849598 5.816955
[1] "Probability adjusted armor damage at middle cells:"
 [1] 0.5816955 0.5849598 0.5871049 0.5881035 0.5883560 0.5883560 0.5881035 0.5871049 0.5849598 0.5816955
[1] "Total armor damage incoming at middle cells:"
[1] 5.860439
[1] "New armor matrix:"
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 6.67 6.61 6.55 6.49 6.49 6.49 6.49 6.49 6.49  6.49  6.49  6.55  6.61  6.67
[2,] 6.61 6.49 6.38 6.26 6.20 6.20 6.20 6.20 6.20  6.20  6.26  6.38  6.49  6.61
[3,] 6.61 6.49 6.38 6.26 6.20 6.20 6.20 6.20 6.20  6.20  6.26  6.38  6.49  6.61
[4,] 6.61 6.49 6.38 6.26 6.20 6.20 6.20 6.20 6.20  6.20  6.26  6.38  6.49  6.61
[5,] 6.67 6.61 6.55 6.49 6.49 6.49 6.49 6.49 6.49  6.49  6.49  6.55  6.61  6.67
[1] "Equivalent armor hp:"
[1] 96.05439



Using

damage <- 100
hitstrength <- 100
startingarmor <- 100
shipcells <- 10
probabilities <- c(0.00,0.05,0.10,0.15,0.20,0.20,0.15,0.10,0.05,0.00)
norounds <- 3



Output

[1] "Starting armor matrix"
> print(armormatrix <- matrix(startingarmor / 15, 5, shipcells+4))
         [,1]     [,2]     [,3]     [,4]     [,5]     [,6]     [,7]     [,8]     [,9]    [,10]    [,11]
[1,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[2,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[3,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[4,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
[5,] 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667 6.666667
        [,12]    [,13]    [,14]
[1,] 6.666667 6.666667 6.666667
[2,] 6.666667 6.666667 6.666667
[3,] 6.666667 6.666667 6.666667
[4,] 6.666667 6.666667 6.666667
[5,] 6.666667 6.666667 6.666667
>
> print("Equivalent armor hp:")
[1] "Equivalent armor hp:"
> fullarmorhp <- (sum(armormatrix[2:4,])+sum(armormatrix[1,2:13])+sum(armormatrix[5,2:13]))/(5*14-4)*15
> print(fullarmorhp)
[1] 100

[1] "Armor damage expected based on full armor hp:"
[1] 50
[1] "Armor damage at middle cells given full shot:"
 [1] 50 50 50 50 50 50 50 50 50 50
[1] "Probability adjusted armor damage at middle cells:"
 [1]  0.0  2.5  5.0  7.5 10.0 10.0  7.5  5.0  2.5  0.0
[1] "Total armor damage incoming at middle cells:"
[1] 50
[1] "New armor matrix:"
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 6.67 6.67 6.58 6.42 6.17 5.92 5.75 5.75 5.92  6.17  6.42  6.58  6.67  6.67
[2,] 6.67 6.58 6.33 5.92 5.33 4.75 4.42 4.42 4.75  5.33  5.92  6.33  6.58  6.67
[3,] 6.67 6.58 6.33 5.92 5.33 4.75 4.42 4.42 4.75  5.33  5.92  6.33  6.58  6.67
[4,] 6.67 6.58 6.33 5.92 5.33 4.75 4.42 4.42 4.75  5.33  5.92  6.33  6.58  6.67
[5,] 6.67 6.67 6.58 6.42 6.17 5.92 5.75 5.75 5.92  6.17  6.42  6.58  6.67  6.67
[1] "Equivalent armor hp:"
[1] 88.63636
[1] "Armor damage expected based on full armor hp:"
[1] 53.01205
[1] "Armor damage at middle cells given full shot:"
 [1] 51.50215 52.93339 54.75702 56.55042 57.70618 57.70618 56.55042 54.75702 52.93339 51.50215
[1] "Probability adjusted armor damage at middle cells:"
 [1]  0.000000  2.646670  5.475702  8.482564 11.541236 11.541236  8.482564  5.475702  2.646670  0.000000
[1] "Total armor damage incoming at middle cells:"
[1] 56.29234
[1] "New armor matrix:"
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 6.67 6.67 6.50 6.15 5.61 5.07 4.70 4.70 5.07  5.61  6.15  6.50  6.67  6.67
[2,] 6.67 6.50 5.97 5.09 3.84 2.58 1.85 1.85 2.58  3.84  5.09  5.97  6.50  6.67
[3,] 6.67 6.50 5.97 5.09 3.84 2.58 1.85 1.85 2.58  3.84  5.09  5.97  6.50  6.67
[4,] 6.67 6.50 5.97 5.09 3.84 2.58 1.85 1.85 2.58  3.84  5.09  5.97  6.50  6.67
[5,] 6.67 6.67 6.50 6.15 5.61 5.07 4.70 4.70 5.07  5.61  6.15  6.50  6.67  6.67
[1] "Equivalent armor hp:"
[1] 75.84265
[1] "Armor damage expected based on full armor hp:"
[1] 56.86902
[1] "Armor damage at middle cells given full shot:"
 [1] 53.26066 56.62906 61.31532 66.40800 69.97700 69.97700 66.40800 61.31532 56.62906 53.26066
[1] "Probability adjusted armor damage at middle cells:"
 [1]  0.000000  2.831453  6.131532  9.961200 13.995401 13.995401  9.961200  6.131532  2.831453  0.000000
[1] "Total armor damage incoming at middle cells:"
[1] 65.83917
[1] "New armor matrix:"
     [,1] [,2] [,3] [,4] [,5] [,6]  [,7]  [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 6.67 6.67 6.40 5.85 4.98 4.06  3.43  3.43 4.06  4.98  5.85  6.40  6.67  6.67
[2,] 6.67 6.40 5.58 4.16 2.11 0.01 -1.22 -1.22 0.01  2.11  4.16  5.58  6.40  6.67
[3,] 6.67 6.40 5.58 4.16 2.11 0.01 -1.22 -1.22 0.01  2.11  4.16  5.58  6.40  6.67
[4,] 6.67 6.40 5.58 4.16 2.11 0.01 -1.22 -1.22 0.01  2.11  4.16  5.58  6.40  6.67
[5,] 6.67 6.67 6.40 5.85 4.98 4.06  3.43  3.43 4.06  4.98  5.85  6.40  6.67  6.67
[1] "Equivalent armor hp:"
[1] 60.8792
« Last Edit: December 10, 2022, 04:38:01 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #237 on: December 10, 2022, 09:29:25 AM »

It seems like damage is indeed much higher in your code than it should be. Question: when you write
Code

            ship.armor_grid.cells[0:5,i:i+5] -= damage * ArmorGrid.WEIGHTS


are you accounting for the fact that when distributing damage, the weights should be 1/15th of those used to pool armor? So that it sums up to 1 and not 15. (because for armor we have already applied the 1/15th when generating the armor matrix, so if we sum it to a total of 15 we get back the starting armor, but we haven't done that for damage so if we use the 1/2 and 1 weights we end up with 15 x too high damage)

I was wondering about that.  I've now fixed that bug and obtained different results.  The numbers I have input before differed from yours only in hit_strength, which I had incorrectly set to 20 rather than your 40.  Inputting these numbers yields a result almost like yours for the first shot, differing by 0.02 for the middle cells.  Correcting the hit strength yields the result that you obtained for the second shot.

I've noticed that your code uses both 14 and 15, but mine uses only 15, and I don't know which is right.  Maybe you could enlighten me.

Code
import numpy as np
import copy
"""
Damage computing module.

methods:
- hit_probability
- main

classes:
- ArmorGrid
- Ship
- Shot
- Weapon
"""
class ArmorGrid:
    """
    A Starsector ship armor grid.
   
    - Represents a horizontal frontal row of armor on a ship.
    - Includes 2 rows of vertical padding above; 2 below; 2 columns of
      left horizontal padding; 2 right.
   
    constants:
    _MINIMUM_ARMOR_FACTOR - multiply by armor rating to determine
                            minimum effective value of pooled armor for
                            damage calculations
    _MINIMUM_DAMAGE_FACTOR - least factor whereby this ArmorGrid can
                             cause incoming damage to be multiplied
    ARMOR_RATING_PER_CELL_FACTOR - multiply by armor rating to determine
                                   initial value of each cell of an
                                   ArmorGrid
    WEIGHTS - multiply by damage to the central cell to determine damage
              to surrounding ones
             
    variables:
    _minimum_armor - minimum effective value of armor for damage
                     calculations
    bounds - right bound of each cell in the middle row, except the two
             padding cells on both sides
    cells - 2d array of armor grid cell values
   
    methods:
    - _pooled_values
    - damage_factors
    """
    _MINIMUM_ARMOR_FACTOR = 0.05
    _MINIMUM_DAMAGE_FACTOR = 0.15
    ARMOR_RATING_PER_CELL_FACTOR = 1 / 15
    WEIGHTS = np.array([[0.0, 0.5, 0.5, 0.5, 0.0],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.0, 0.5, 0.5, 0.5, 0.0]])
   
    def __init__(self, armor_rating: float, cell_size: float, width: int):
        """
        armor_rating - armor rating of the ship to which this ArmorGrid
                       belongs
        cell_size - size of each armor cell, which is a square, in
                    pixels
        width - width of the armor grid in cells
        """
        self._minimum_armor = ArmorGrid._MINIMUM_ARMOR_FACTOR * armor_rating
        armor_per_cell = armor_rating * ArmorGrid.ARMOR_RATING_PER_CELL_FACTOR
        self.cells = np.full((5,width+4), armor_per_cell)
        self.bounds = np.arange(0, len(self.cells[2,2:-2])) * cell_size
       
    def _pooled_values(self) -> object:
        """
        Return the armor value pooled around each cell of the middle
        row, from third through third-last.
        """
        return np.maximum(self._minimum_armor, np.array(
            [np.sum(ArmorGrid.WEIGHTS * self.cells[0:5,i:i+5]) for i, _ in
            enumerate(self.cells[2,2:-2])]))
                 
    def damage_factors(self, hit_strength: float) -> object:
        """
        Return the factor whereby to multiply the damage of each
        non-padding cell of the grid.
        """
        return np.maximum(ArmorGrid._MINIMUM_DAMAGE_FACTOR,
                          1 / (1 + self._pooled_values() / hit_strength))
       
       
class Ship:
    """
    A Starsector ship.
   
    methods:
    - will_overload
    - overloaded
    - shield_up
   
    variables:
    weapons - container of the weapons of the ship, with structure
              to be determined
    armor_grid - ArmorGrid of the ship
    hull - amount of hitpoints the ship has
    flux_capacity - amount of flux to overload the ship
    flux_dissipation - how much flux the ship can expel without
                       actively venting
    hard_flux - hard flux amount
    soft_flux - soft flux amount
    """
    def __init__(self,
            weapons: object,
            armor_grid: object,
            hull: float,
            flux_capacity: float,
            flux_dissipation: float):
        """
        weapons - container of the weapons of the ship, with
              structure to be determined
        armor_grid - ArmorGrid of the ship
        hull - amount of hitpoints the ship has
        flux_capacity - amount of flux to overload the ship
        flux_dissipation - how much flux the ship can expel
                           without actively venting
        """
        self.weapons = weapons
        self.armor_grid = armor_grid
        self.hull = hull
        self.flux_capacity = flux_capacity
        self.flux_dissipation = flux_dissipation
        self.hard_flux, self.soft_flux = 0, 0
   
    @property
    def will_overload(self):
        """
        Return whether the ship will now overload.
       
        A ship will overload if soft or hard flux exceeds
        the flux capacity of the ship.
        """
        return (self.hard_flux > self.flux_capacity
                or self.soft_flux > self.flux_capacity)
   
    @property
    def overloaded(self):
        """
        Return whether the ship is in the overloaded state.
       
        Ignores the duration of overloaded.
        -TODO: Implement overload duration
        """
        return self.will_overload
       
    @property
    def shield_up(self):
        """
        Return whether the shield is up or down.
       
        Presumes the shield to be up if the ship is not overloaded,
        ignoring smart tricks the AI can play to take kinetic damage
        on its armor and save its shield for incoming high explosive
        damage.
        """
        return not self.overloaded


class Shot:
    """
    A shot fired at a row of armor cells protected by a shield.

    Calculates the expectation value of the damage of a shot
    with a spread to a ship with a shield, armor grid, and random
    positional deviation.
   
    variables:
    base_damage - amount listed under damage in weapon_data.csv
    base_shield_damage - starting amount of damage to be inflicted
                         on the ship shield
    base_armor_damage - starting amount of damage to be inflicted
                        on the ship armor
    strength - strength against armor for armor damage calculation
    flux_hard: whether the flux damage against shields is hard or not
   
    methods:
    - distribute
    - _damage_distribution
    - damage_armor_grid
    """
    def __init__(
            self,
            base_damage: float,
            base_shield_damage: float,
            base_armor_damage: float,
            strength: float,
            flux_hard: bool):
        """
        base_damage - amount listed under damage in weapon_data.csv
        base_shield_damage - starting amount of damage to be inflicted
                             on the ship shield
        base_armor_damage - starting amount of damage to be inflicted
                            on the ship armor
        strength - strength against armor for armor damage calculation
        flux_hard: whether the flux damage against shields is hard or not
        """
        self.base_damage = base_damage
        self.base_shield_damage = base_shield_damage
        self.base_armor_damage = base_armor_damage
        self.strength = strength
        self.probabilities = None

    def distribute(self, ship: object, distribution: object):
        """
        Spread hit probability over each armor cell of a ship.
       
        Calculate the probability to hit each armor cell of a
        row and save the probabilities for later calculation.
        """
        self.probabilities = np.vectorize(distribution)(ship.armor_grid.bounds)
   
    def _damage_distribution(self, ship: object) -> list:
        """
        Return the expected damage to each cell of the shiped armor
        row times the probability to hit it.
       
        damage - damage against armor after reduction by armor
        """
        return (self.base_armor_damage
                * self.probabilities
                * ship.armor_grid.damage_factors(self.strength))

    def damage_armor_grid(self, ship):
        """
        Reduce the values of the armor grid cells of the ship
        by the expected value of the damage of the shot across them.
        """
        for i, damage in enumerate(self._damage_distribution(ship)):
            ship.armor_grid.cells[0:5,i:i+5] -= (
                damage
                * ArmorGrid.WEIGHTS
                * ArmorGrid.ARMOR_RATING_PER_CELL_FACTOR
            )
        ship.armor_grid.cells[ship.armor_grid.cells < 0.0] = 0.0
                       
                       
class Weapon:
    """
    A weapon for a Starsector ship in simulated battle.
   
    Is carried by a Ship instance, contains a shot with some
    distribution, and fires that shot at a target.
    """
    def __init__(self, shot: object, distribution: object):
        """
        shot - projectile, missile, or beam tick of the weapon
        distribution - function returning the probability that
                       of the shot to hit between two bounds
        """
        self.shot = shot
        self.distribution = distribution
       
    def fire(self, ship: object):
        """
        Fire the shot of this weapon at that ship.
       
        ship - a Ship instance
        """
        self.shot.distribute(ship, self.distribution)
        #TODO: implement general hit ship method on Shot
        self.shot.damage_armor_grid(ship)
        return #future code below
        if ship.shield_up:
            pass #TODO: implement shield damage, overloading, etc.
        else: self.shot.damage_armor_grid(ship)
       

def hit_probability(bound: float): return 0.1 #dummy for test


def main():
    ship_spec = (
        1_000,#hull
        1_000,#flux_capacity
        100#flux_dissipation
    )
    armor_grid_spec = (
        100,#armor_rating
        10,#cell_size
        10#width
    )
    weapons = []
    shot_spec = (
        20,#base_damage,
        10,#base_shield_damage
        40,#base_armor_damage
        20,#strength
        False#flux_hard
    )
   
    ship = Ship(weapons, ArmorGrid(*armor_grid_spec), *ship_spec)
    weapon = Weapon(Shot(*shot_spec), hit_probability)
   
    print(np.round(ship.armor_grid.cells, 2))
    weapon.fire(ship)
    print()
    print(np.round(ship.armor_grid.cells, 2))
main()
[close]
starting armor
[[6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]]
20 strength
[[6.67 6.64 6.62 6.6  6.6  6.6  6.6  6.6  6.6  6.6  6.6  6.62 6.64 6.67]
 [6.64 6.6  6.56 6.51 6.49 6.49 6.49 6.49 6.49 6.49 6.51 6.56 6.6  6.64]
 [6.64 6.6  6.56 6.51 6.49 6.49 6.49 6.49 6.49 6.49 6.51 6.56 6.6  6.64]
 [6.64 6.6  6.56 6.51 6.49 6.49 6.49 6.49 6.49 6.49 6.51 6.56 6.6  6.64]
 [6.67 6.64 6.62 6.6  6.6  6.6  6.6  6.6  6.6  6.6  6.6  6.62 6.64 6.67]]
40 strength
[[6.67 6.63 6.59 6.55 6.55 6.55 6.55 6.55 6.55 6.55 6.55 6.59 6.63 6.67]
 [6.63 6.55 6.48 6.4  6.36 6.36 6.36 6.36 6.36 6.36 6.4  6.48 6.55 6.63]
 [6.63 6.55 6.48 6.4  6.36 6.36 6.36 6.36 6.36 6.36 6.4  6.48 6.55 6.63]
 [6.63 6.55 6.48 6.4  6.36 6.36 6.36 6.36 6.36 6.36 6.4  6.48 6.55 6.63]
 [6.67 6.63 6.59 6.55 6.55 6.55 6.55 6.55 6.55 6.55 6.55 6.59 6.63 6.67]]
« Last Edit: December 10, 2022, 09:40:31 AM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #238 on: December 10, 2022, 10:00:32 AM »

Well, the 14 is only used in the equivalent sum armor HP calculation and that is because the armor matrix is 14 cells wide, so a total of 5*14-4 cells (-4 from excluding corners). Nothing to do with this, 1/15 for central (1/30 for edge) is the correct damage distribution factor.

However, did you notice that in my code I actually had 20 damage and 40 hit strength for the first run, not 40 damage and 20 hit strength? What kind of results do you get with that? What about the other test?
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #239 on: December 10, 2022, 10:06:25 AM »

Well, the 14 is only used in the equivalent sum armor HP calculation and that is because the armor matrix is 14 cells wide, so a total of 5*14-4 cells (-4 from excluding corners). Nothing to do with this, 1/15 for central (1/30 for edge) is the correct damage distribution factor.

However, did you notice that in my code I actually had 20 damage and 40 hit strength for the first run, not 40 damage and 20 hit strength? What kind of results do you get with that? What about the other test?

Oh, I see now.  Changing my numbers to match yours yields your results!  We've done it!  The expected value calculation now works in Python!

Code
Code
import numpy as np
import copy
"""
Damage computing module.

methods:
- hit_probability
- main

classes:
- ArmorGrid
- Ship
- Shot
- Weapon
"""
class ArmorGrid:
    """
    A Starsector ship armor grid.
   
    - Represents a horizontal frontal row of armor on a ship.
    - Includes 2 rows of vertical padding above; 2 below; 2 columns of
      left horizontal padding; 2 right.
   
    constants:
    _MINIMUM_ARMOR_FACTOR - multiply by armor rating to determine
                            minimum effective value of pooled armor for
                            damage calculations
    _MINIMUM_DAMAGE_FACTOR - least factor whereby this ArmorGrid can
                             cause incoming damage to be multiplied
    ARMOR_RATING_PER_CELL_FACTOR - multiply by armor rating to determine
                                   initial value of each cell of an
                                   ArmorGrid
    WEIGHTS - multiply by damage to the central cell to determine damage
              to surrounding ones
             
    variables:
    _minimum_armor - minimum effective value of armor for damage
                     calculations
    bounds - right bound of each cell in the middle row, except the two
             padding cells on both sides
    cells - 2d array of armor grid cell values
   
    methods:
    - _pooled_values
    - damage_factors
    """
    _MINIMUM_ARMOR_FACTOR = 0.05
    _MINIMUM_DAMAGE_FACTOR = 0.15
    ARMOR_RATING_PER_CELL_FACTOR = 1 / 15
    WEIGHTS = np.array([[0.0, 0.5, 0.5, 0.5, 0.0],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.5, 1.0, 1.0, 1.0, 0.5],
                        [0.0, 0.5, 0.5, 0.5, 0.0]])
   
    def __init__(self, armor_rating: float, cell_size: float, width: int):
        """
        armor_rating - armor rating of the ship to which this ArmorGrid
                       belongs
        cell_size - size of each armor cell, which is a square, in
                    pixels
        width - width of the armor grid in cells
        """
        self._minimum_armor = ArmorGrid._MINIMUM_ARMOR_FACTOR * armor_rating
        armor_per_cell = armor_rating * ArmorGrid.ARMOR_RATING_PER_CELL_FACTOR
        self.cells = np.full((5,width+4), armor_per_cell)
        self.bounds = np.arange(0, len(self.cells[2,2:-2])) * cell_size
       
    def _pooled_values(self) -> object:
        """
        Return the armor value pooled around each cell of the middle
        row, from third through third-last.
        """
        return np.maximum(self._minimum_armor, np.array(
            [np.sum(ArmorGrid.WEIGHTS * self.cells[0:5,i:i+5]) for i, _ in
            enumerate(self.cells[2,2:-2])]))
                 
    def damage_factors(self, hit_strength: float) -> object:
        """
        Return the factor whereby to multiply the damage of each
        non-padding cell of the grid.
        """
        return np.maximum(ArmorGrid._MINIMUM_DAMAGE_FACTOR,
                          1 / (1 + self._pooled_values() / hit_strength))
       
       
class Ship:
    """
    A Starsector ship.
   
    methods:
    - will_overload
    - overloaded
    - shield_up
   
    variables:
    weapons - container of the weapons of the ship, with structure
              to be determined
    armor_grid - ArmorGrid of the ship
    hull - amount of hitpoints the ship has
    flux_capacity - amount of flux to overload the ship
    flux_dissipation - how much flux the ship can expel without
                       actively venting
    hard_flux - hard flux amount
    soft_flux - soft flux amount
    """
    def __init__(self,
            weapons: object,
            armor_grid: object,
            hull: float,
            flux_capacity: float,
            flux_dissipation: float):
        """
        weapons - container of the weapons of the ship, with
              structure to be determined
        armor_grid - ArmorGrid of the ship
        hull - amount of hitpoints the ship has
        flux_capacity - amount of flux to overload the ship
        flux_dissipation - how much flux the ship can expel
                           without actively venting
        """
        self.weapons = weapons
        self.armor_grid = armor_grid
        self.hull = hull
        self.flux_capacity = flux_capacity
        self.flux_dissipation = flux_dissipation
        self.hard_flux, self.soft_flux = 0, 0
   
    @property
    def will_overload(self):
        """
        Return whether the ship will now overload.
       
        A ship will overload if soft or hard flux exceeds
        the flux capacity of the ship.
        """
        return (self.hard_flux > self.flux_capacity
                or self.soft_flux > self.flux_capacity)
   
    @property
    def overloaded(self):
        """
        Return whether the ship is in the overloaded state.
       
        Ignores the duration of overloaded.
        -TODO: Implement overload duration
        """
        return self.will_overload
       
    @property
    def shield_up(self):
        """
        Return whether the shield is up or down.
       
        Presumes the shield to be up if the ship is not overloaded,
        ignoring smart tricks the AI can play to take kinetic damage
        on its armor and save its shield for incoming high explosive
        damage.
        """
        return not self.overloaded


class Shot:
    """
    A shot fired at a row of armor cells protected by a shield.

    Calculates the expectation value of the damage of a shot
    with a spread to a ship with a shield, armor grid, and random
    positional deviation.
   
    variables:
    base_damage - amount listed under damage in weapon_data.csv
    base_shield_damage - starting amount of damage to be inflicted
                         on the ship shield
    base_armor_damage - starting amount of damage to be inflicted
                        on the ship armor
    strength - strength against armor for armor damage calculation
    flux_hard: whether the flux damage against shields is hard or not
   
    methods:
    - distribute
    - _damage_distribution
    - damage_armor_grid
    """
    def __init__(
            self,
            base_damage: float,
            base_shield_damage: float,
            base_armor_damage: float,
            strength: float,
            flux_hard: bool):
        """
        base_damage - amount listed under damage in weapon_data.csv
        base_shield_damage - starting amount of damage to be inflicted
                             on the ship shield
        base_armor_damage - starting amount of damage to be inflicted
                            on the ship armor
        strength - strength against armor for armor damage calculation
        flux_hard: whether the flux damage against shields is hard or not
        """
        self.base_damage = base_damage
        self.base_shield_damage = base_shield_damage
        self.base_armor_damage = base_armor_damage
        self.strength = strength
        self.probabilities = None

    def distribute(self, ship: object, distribution: object):
        """
        Spread hit probability over each armor cell of a ship.
       
        Calculate the probability to hit each armor cell of a
        row and save the probabilities for later calculation.
        """
        self.probabilities = np.vectorize(distribution)(ship.armor_grid.bounds)
   
    def _damage_distribution(self, ship: object) -> list:
        """
        Return the expected damage to each cell of the shiped armor
        row times the probability to hit it.
       
        damage - damage against armor after reduction by armor
        """
        return (self.base_armor_damage
                * self.probabilities
                * ship.armor_grid.damage_factors(self.strength))

    def damage_armor_grid(self, ship):
        """
        Reduce the values of the armor grid cells of the ship
        by the expected value of the damage of the shot across them.
        """
        for i, damage in enumerate(self._damage_distribution(ship)):
            ship.armor_grid.cells[0:5,i:i+5] = np.maximum(0,
                ship.armor_grid.cells[0:5,i:i+5]
                - damage
                * ArmorGrid.WEIGHTS
                * ArmorGrid.ARMOR_RATING_PER_CELL_FACTOR)
                       
                       
class Weapon:
    """
    A weapon for a Starsector ship in simulated battle.
   
    Is carried by a Ship instance, contains a shot with some
    distribution, and fires that shot at a target.
    """
    def __init__(self, shot: object, distribution: object):
        """
        shot - projectile, missile, or beam tick of the weapon
        distribution - function returning the probability that
                       of the shot to hit between two bounds
        """
        self.shot = shot
        self.distribution = distribution
       
    def fire(self, ship: object):
        """
        Fire the shot of this weapon at that ship.
       
        ship - a Ship instance
        """
        self.shot.distribute(ship, self.distribution)
        #TODO: implement general hit ship method on Shot
        self.shot.damage_armor_grid(ship)
        return #future code below
        if ship.shield_up:
            pass #TODO: implement shield damage, overloading, etc.
        else: self.shot.damage_armor_grid(ship)
       

def hit_probability(bound: float): return 0.1 #dummy for test


def main():
    ship_spec = (
        1_000,#hull
        1_000,#flux_capacity
        100#flux_dissipation
    )
    armor_grid_spec = (
        100,#armor_rating
        10,#cell_size
        10#width
    )
    weapons = []
    shot_spec = (
        20,#base_damage,
        10,#base_shield_damage
        20,#base_armor_damage
        40,#strength
        False#flux_hard
    )
   
    ship = Ship(weapons, ArmorGrid(*armor_grid_spec), *ship_spec)
    weapon = Weapon(Shot(*shot_spec), hit_probability)
   
    print(np.round(ship.armor_grid.cells, 2))
    weapon.fire(ship)
    print()
    print(np.round(ship.armor_grid.cells, 2))
main()
[close]
[[6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]
 [6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67 6.67]]

[[6.67 6.65 6.63 6.61 6.61 6.61 6.61 6.61 6.61 6.61 6.61 6.63 6.65 6.67]
 [6.65 6.61 6.57 6.53 6.51 6.51 6.51 6.51 6.51 6.51 6.53 6.57 6.61 6.65]
 [6.65 6.61 6.57 6.53 6.51 6.51 6.51 6.51 6.51 6.51 6.53 6.57 6.61 6.65]
 [6.65 6.61 6.57 6.53 6.51 6.51 6.51 6.51 6.51 6.51 6.53 6.57 6.61 6.65]
 [6.67 6.65 6.63 6.61 6.61 6.61 6.61 6.61 6.61 6.61 6.61 6.63 6.65 6.67]]
Now I need your results without the pretty rounding so we can compare them to my un-rounded results below and then agree on an accepted result to save in a test to verify that this function works as intended should it later be changed. 
[[6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667]
 [6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667]
 [6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667]
 [6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667]
 [6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667 6.66666667]]

[[6.66666667 6.64761905 6.62857143 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.62857143 6.64761905 6.66666667]
 [6.64761905 6.60952381 6.57142857 6.53333333 6.51428571 6.51428571 6.51428571 6.51428571 6.51428571 6.51428571 6.53333333 6.57142857 6.60952381 6.64761905]
 [6.64761905 6.60952381 6.57142857 6.53333333 6.51428571 6.51428571 6.51428571 6.51428571 6.51428571 6.51428571 6.53333333 6.57142857 6.60952381 6.64761905]
 [6.64761905 6.60952381 6.57142857 6.53333333 6.51428571 6.51428571 6.51428571 6.51428571 6.51428571 6.51428571 6.53333333 6.57142857 6.60952381 6.64761905]
 [6.66666667 6.64761905 6.62857143 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.60952381 6.62857143 6.64761905 6.66666667]]
When comparing our results, we should note and consider any slight numerical differences and also agree to some number of decimals of precision and amount of accuracy if necessary.
« Last Edit: December 10, 2022, 10:13:36 AM by Liral »
Logged
Pages: 1 ... 14 15 [16] 17 18 ... 32