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 ... 25 26 [27] 28 29 ... 32

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

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #390 on: January 09, 2023, 08:17:39 PM »

Okay, sure you can build these first. However, the code here will likely contain a variety of fields (things like current weapon state, my current version also has a beam tick count to keep track of what tick we are on to compute intensity and permit flux calculations tick by tick rather than up front like the prototype, and of course the weapon cycle descriptions which are very convenient) that are not present in the object read from data, so these will likely be derived from the data containing object, unless all of these fields will be initialized when reading the data I guess.

It really does need the AI, because if it never attempts to save flux it will do what my graphed run above did, that is, maxes itself using tach lance, but then can not immediately fire it again (or even worse in the new version with tick by tick flux, stops firing it midway) because it is maxing itself on flux from Ion Pulsers since these have lower flux per shot and can be fired first. It only gets back to the tach lance when the Pulsers are out of ammo and it can get a more sustained dissipation going in the graph above.

Of course the logic could also be implemented using logical statements in the loop, but that gets really complicated (I was actually going that way first but it becomes like a maze) and I think an AI object will be simpler.

I'm not saying that we don't need an AI, or that the objects won't need improvements or modifications, but that because we need an AI and that it, alongside the objects, will need changes, we should write our future code on GitHub in Python rather than on the forum in R and then Python.  We could have open issues, tickets, automated tests, automated builds, an API, and more.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #391 on: January 09, 2023, 08:40:28 PM »

All right, let's wait until we're there then. Let me know when you think the time is ripe to proceed. Nice that the GitHub is going, thanks for making it; let's see if we can get other folks contributing.
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 #392 on: January 10, 2023, 12:30:21 PM »

All right, let's wait until we're there then. Let me know when you think the time is ripe to proceed. Nice that the GitHub is going, thanks for making it; let's see if we can get other folks contributing.

We're almost there.  I need to know how to distribute the hit probability of each weapon aboard the shooter ship across the target ship.  The GUI lets you create both and arm the former.   

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #393 on: January 10, 2023, 08:15:18 PM »

To find the probability of hitting each cell, use the code we built above (the optimum angle code) which outputs it, but instead of DPS use the following multipliers in the sum auc calculation: large weapons, 7, medium weapons, 3, small weapons, 1, point defense weapon, 0.5* score eg large PD, 3.5.
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 #394 on: January 11, 2023, 06:11:44 AM »

To find the probability of hitting each cell, use the code we built above (the optimum angle code) which outputs it, but instead of DPS use the following multipliers in the sum auc calculation: large weapons, 7, medium weapons, 3, small weapons, 1, point defense weapon, 0.5* score eg large PD, 3.5.

Can do!

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #395 on: January 12, 2023, 09:51:38 PM »

I have good news.  Ships now have weapon slots, to each of which a weapon can be assigned depending on the size and type of the slot and weapon, respectively, and the app lets you assign weapons manually, albeit only to the first slot of the same size.   Now, I can modify the main method of that analysis code you've mentioned to return rather than just print their hit distributions and then assign each one to the corresponding weapon.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #396 on: January 12, 2023, 09:59:46 PM »

Sounds like you have exactly what we want and this is taking shape rapidly! Unfortunately my personal potato died and is gone for the moment and I do not feel it would be appropriate to write Starsector stuff on my protected research laptop or in a university lab, so I will be out of the picture for some days at least. I guess I could get a third one, some used piece of junk that could run such as minimal Linux for fun computer hacking, but since there is in fact research work to do, maybe later in life.

Anyway keep us posted on progress! I think this will be publishable without a flux management AI, just note the infinite flux assumption and that we will fix it later.
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 #397 on: January 13, 2023, 11:21:47 AM »

Thanks!  Oh no, your potato!  If your potato was a desktop, then for under $100 you could replace it with a new micro-computer, and should you need more resources, many free cloud servers let you write-and-run code directly.

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #398 on: January 13, 2023, 04:25:07 PM »

More good news.  I have changed the analysis code to run on weapon, ship, and armor grid objects rather than rows of data.  Some bad news: I can't get it to work. 
Code
Code
"""
Calculate the optimum angle to place the enemy ship with regard to ours.

Assume
- possible angles are integers from -179 to 180
- angle exactly to the right is 0 degrees
- our ship is
  - heading towards +90 degrees
  - pointlike
- the enemy ship is
  - a single line of hittable armor cells
  - at constant range
  - oriented tangentially to the circle centered on our ship
- the secant from one armor cell on the enemy ship to another approximates
  the arc between them
"""
from statistics import NormalDist
from math import pi



WEAPON_SCORES = {
    ("LARGE", False) : 7.0,
    ("MEDIUM", False) : 3.0,
    ("SMALL", False) : 1.0,
    ("LARGE", True) : 3.5,
    ("MEDIUM", True) : 1.5,
    ("SMALL", True) : 0.5
}


def probability_hit_within(
        x: float,
        standard_deviation: float,
        uniform_distribution_width: float) -> float:
    """
    Return the probability to hit a coordinate less than x.
   
    x - real number
    standard_deviation - of the normal distribution N(0,a),
    uniform_distribution_width - of the symmetric uniform distribution (-b,b)
    """
    if standard_deviation == 0 and uniform_distribution_width == 0:
        return 0 if x < 0 else 1
    if standard_deviation == 0:
        return max(0, min(1, (1 + x / uniform_distribution_width) / 2))
    if uniform_distribution_width == 0:
        return NormalDist(0, standard_deviation).cdf(x)
    a = (x - uniform_distribution_width) / standard_deviation
    b = (x + uniform_distribution_width) / standard_deviation
    normal_distribution = NormalDist(0, 1)
    cdf, pdf = normal_distribution.cdf, normal_distribution.pdf
    return (standard_deviation / 2 / uniform_distribution_width
            * (b * cdf(b) + pdf(b) - (a * cdf(a) + pdf(a))))


def probability_hit_between(
        lower_bound: float,
        upper_bound: float,
        standard_deviation: float,
        uniform_distribution_width: float) -> float:
    """
    Return the probability to hit between two coordinates.
   
    x - real number
    standard_deviation - of the normal distribution N(0,a),
    uniform_distribution_width - of the symmetric uniform distribution (-b,b)
    """
    return (probability_hit_within(upper_bound, standard_deviation,
                                   uniform_distribution_width)
            - probability_hit_within(lower_bound, standard_deviation,
                                     uniform_distribution_width))
   
   
def deg_to_arc(degree: float, radius: float) -> float:
    """
    Return the arc corresponding to the central angle of a circle
    of this radius.
   
    degree - central angle of the arc in degrees
    radius - from the center of the circle to the edge
    """
    return degree * 2 * radius / 360 * pi


def arc_to_deg(arc: float, radius: float) -> float:
    """
    Return the central angle corresponding to an arc of the circle of
    this radius.
   
    arc - across the edge of a circle
    radius - from the center of the circle to the edge
    """
    return arc / 2 / radius * 360 / pi


def minimum_mean(spread: float, angle: float, arc: float) -> float:
    """
    Return the minimum mean hit probability of a weapon in a slot.

    spread - of the weapon
    angle - of the slot
    arc - of the weapon slot
    """
    return angle - (spread - arc) / 2


def maximum_mean(spread: float, angle: float, arc: float) -> float:
    """
    Return the maximum mean hit probability of a weapon in a slot.

    spread - of the weapon
    angle - of the slot
    arc - of the slot
    """
    return angle + (spread - arc) / 2


def transformed_angle(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle of the target relative to this weapon.
   
    weapon - a tuple ending with min_mean and max_mean
    angle - angle of the target relative to our ship
    """
    return angle - max(minimum_mean, min(maximum_mean, angle))


def upper_bounds(width: float, cells_across: int, distance: float) -> tuple:
    """
    Return the upper bounds of this ship at this distance.
   
    The bounds are a tuple with the lower edge of the ship in index 0
    and upper bounds of all its armor cells at successive indices.
   
    width - pixel width of the ship
    cells_across - how many cells across the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * pi * distance
    ship_arc = width / c
    cell_arc = ship_arc / cells_across
    angles = [-ship_arc / 2]
    for i in range(cells_across): angles.append(angles[-1] + cell_arc)
    return tuple(angle * c for angle in angles)


def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread_distance: float) -> tuple:
    """
    Return the hit distribution.
   
    The hit distribution is a tuple of probability masses wherein
    the first value is the chance to hit below lowest upper bound,
    the last value is chance to hit above highest upper bound, and the
    others are the probabilities for hits between upper bounds,
    adjusted for ship location.
   
    bounds - a tuple of upper bounds
    standard deviation - of a normal distribution N(0,a),
    spread_distance - a parameter of a symmetric uniform distribution
                      (-spread, spread)
    """
    if standard_deviation == 0 and spread_distance == 0:
        #all shots hit 1 cell even if the ship has evenly many to
        #prevent such ships from seeming tougher
        return 0, + tuple(1 if bounds[j] >= 0 and bounds[j-1] < 0 else 0
                          for j in range(len(bounds)))
    elif standard_deviation == 0: #return part of a box
        a = 2 * spread_distance
        return ((min(1, max(0, (bounds[0] + spread)) / a),)
                + tuple(
                    (min(1, max(0, (bounds[j] + spread_distance)) / a)
                    - min(1, max(0, (bounds[j-1] + spread_distance)) / a))
                  for j in range(1, len(bounds)))
                + ((1 - min(1, max(0, (bounds[-1] + spread_distance)) / a)),))
    elif spread_distance == 0: #normal distribution
        cdf = NormalDist(0, standard_deviation).cdf
        return ((cdf(bounds[0]),)
                + tuple(cdf(bounds[j]) - cdf(bounds[j-1]) for j in
                        range(1, len(bounds)))
                + ((1 - cdf(bounds[-1])),))
    return ((probability_hit_within(bounds[0], standard_deviation,
                                    spread_distance),)
            + tuple(probability_hit_between(bounds[j-1], bounds[j],
                                            standard_deviation,
                                            spread_distance)
                    for j in range(1, len(bounds)))
            + ((1 - probability_hit_within(bounds[-1], standard_deviation,
                                           spread_distance)),))


def middle_index_of_approximate_maxima(row: tuple) -> int:
    """
    Return the middle index of those indices where the row is nearly maximum.
   
    row - a row containing real numbers
    """
    rounded_row = tuple(round(element, 3) for element in row)
    indicies_of_approximate_maxima = tuple(i for i, x in enumerate(rounded_row)
                                           if x == max(rounded_row))
    middle = len(indicies_of_approximate_maxima) // 2
    return indicies_of_approximate_maxima[middle]


def distributions(
        weapons: tuple,
        target: tuple,
        distance: float,
        standard_deviation: float):
    """
    Return for each weapon the probability of to hit each of this
    target's cells, as well as of a miss due to hitting below ship's
    lowest bound in the first cell or above ship's highest bound in the
    last one.
   
    target - tuple of information about the ship being shot at
    distance - range to target
    standard deviation - of target position
    weapons - tuple of weapons
    """
    minimum_means, maximum_means = [], []
    for weapon in weapons:
        if weapon["spread"] < weapon.slot["arc"]:
            minimum_means.append(minimum_mean(weapon["spread"],
                                              weapon.slot["angle"],
                                              weapon.slot["arc"]))
            maximum_means.append(maximum_mean(weapon["spread"],
                                              weapon.slot["angle"],
                                              weapon.slot["arc"]))
        else:
            minimum_means.append(weapon.slot["angle"])
            maximum_means.append(weapon.slot["angle"])

    #now, for angles -359 to 360 (all possible signed angles, calculate dps)
    target_positional_angles = tuple(i for i in range(-359, 361))
 
    target_angular_size = arc_to_deg(target["width"], distance) / 4
    target_positional_angle_error = arc_to_deg(standard_deviation, distance)
    weapon_score_at_angles = []
    for target_positional_angle in target_positional_angles:
        weapon_score = 0
        for i, weapon in enumerate(weapons):
            angle = transformed_angle(target_positional_angle,
                                      minimum_means[i],
                                      maximum_means[i])
            probability = probability_hit_between(
                angle - target_angular_size,
                angle + target_angular_size,
                target_positional_angle_error,
                weapon["spread"])
            weapon_score += (WEAPON_SCORES[weapon["size"], weapon["pd"]]
                             * probability)
        weapon_score_at_angles.append(weapon_score)
 
    #now, note that angle -180 is just angle 180, angle -359 is angle
    #1, and so on, so
    #these must be summed with angles -179 to 180
    for i in range(180):
        weapon_score_at_angles[i+360] += weapon_score_at_angles[i]
    #likewise note that angle 360 is just angle 0, angle 359 is angle
    #-1, and so on
    for i in range(540, 720):
        weapon_score_at_angles[i-360] += weapon_score_at_angles[i]
 
    #having summed, select angles -179 to 180
    weapon_score_at_angles = weapon_score_at_angles[181:540]
   
    #we use a separate vector to keep track of angle, since vector
    #index 1 corresponds to angle -179 now
    x_axis = range(-179, 180)
   
    import matplotlib.pyplot as plt
    plt.scatter(x_axis, weapon_score_at_angles)
   
    optimum_angle_index = middle_index_of_approximate_maxima(
        weapon_score_at_angles)
    optimum_angle = x_axis[optimum_angle_index]
    print("Optimum Angle:", optimum_angle)
    print()
    bounds = upper_bounds(target["width"], len(target.armor_grid.cells[0]),
                          distance)
    print("Bounds")
    print(tuple(map(round, bounds)))
    print()
    distributions = []
    for i, weapon in enumerate(weapons):
        spread_distance = deg_to_arc(weapon["spread"], distance)
        print("means:", minimum_means[i], " | ", maximum_means[i])
        angle_difference = transformed_angle(optimum_angle,
                                             minimum_means[i],
                                             maximum_means[i])
        print("angle difference:", angle_difference)
        adjustment = deg_to_arc(angle_difference, distance)
        print("adjustment:", adjustment)
        adjusted_bounds = tuple(bound + adjustment for bound in bounds)
        print("AB", tuple(round(bound, 3) for bound in adjusted_bounds))
        distributions.append(hit_distribution(adjusted_bounds,
                                              standard_deviation,
                                              spread_distance))
        print()
    print("Distributions")
    for distribution in distributions:
        print(tuple(round(p, 3) for p in distribution))
    return distributions

if __name__ == "__main__":
    #testing section - not to be implemented in final code
    #print a graph of the distribution and our choice of angle
    #plot(dps_at_angles, x=x_axis)
    #abline(v=optimum_angle)

    #Test ship and weapons.

    #We will create a data frame containing 18 weapons for testing.
    #Weapons are formatted as "name", dps, facing, tracking arc, spread
    #first, 6 weapons with easy parameters to parse
    simple_weapons = (["right phaser", 100.0, -10.0, 20.0, 5.0],
                      ["left phaser", 100.0, 10.0, 20.0, 5.0],
                      ["pd gun 1", 30.0, -160.0, 20.0, 0.0],
                      ["pd gun 2", 30.0, 180.0, 20.0, 0.0],
                      ["pd gun 3", 30.0, 160.0, 20.0, 0.0],
                      ["photon torpedo", 120.0, 90.0, 0.0, 5.0])

    #then, 12 weapons with randomly generated data
    #the generation process was c(round(100*runif(1)),round(180*runif(1,-1,1)),round(180*(1-log10(runif(1,1,10)))),round(30*runif(1)))
    random_weapons = (["bb gun",5,-150,11,20],
                      ["space marine teleporter", 78,69,173,29],
                      ["turbolaser", 92,122,111,9],
                      ["hex bolter", 24,-136,38,20],
                      ["singularity projector", 95,28,122,25],
                      ["subspace resonance kazoo", 68,-139,12,2],
                      ["left nullspace projector", 10,28,54,0],
                      ["telepathic embarrassment generator", 30,-31,35,8],
                      ["perfectoid resonance torpedo", 34,72,10,17],
                      ["entropy inverter gun",78,-60,13,24],
                      ["mini-collapsar rifle", 27,28,16,13],
                      ["false vacuum tunneler", 32,78,157,20])

    #We will test against a ship formatted in the normal format
    target = (14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)

    class TestWeapon:
        def __init__(self, spread: float, pd: bool, size: str, slot: object):
            self._data = {"spread" : spread, "pd" : pd, "size" : size}
            self.slot = slot

        def __getitem__(self, name: str):
            return self._data[name]

    class TestSlot:
        def __init__(self, angle: float, arc: float):
            self._data = {"angle" : angle, "arc" : arc}

        def __getitem__(self, name: str):
            return self._data[name]

    class TestArmorGrid:
        def __init__(self, width: int):
            self.cells = [[i for i in range(width)]]
           
    class TestTarget:
        def __init__(self, width: float):
            self._data = {"width" : width}
            self.armor_grid = None

        def __getitem__(self, name: str):
            return self._data[name]
   
    test_weapons = (TestWeapon(5.0, False, "SMALL", TestSlot(-10.0, 20.0)),
                    TestWeapon(5.0, False, "SMALL", TestSlot(10.0, 20.0)),
                    TestWeapon(0.0, False, "SMALL", TestSlot(-160.0, 20.0)),
                    TestWeapon(0.0, False, "SMALL", TestSlot(180.0, 20.0)),
                    TestWeapon(0.0, False, "SMALL", TestSlot(160.0, 20.0)),
                    TestWeapon(5.0, False, "SMALL", TestSlot(120.0, 90.0)),)

    test_target = TestTarget(220)
    test_target.armor_grid = TestArmorGrid(12)
   
    distributions(test_weapons, test_target, 1000, 50)
[close]
Result
Optimum Angle: 168

Bounds
(-110, -92, -73, -55, -37, -18, 0, 18, 37, 55, 73, 92, 110)

means: -2.5  |  -17.5
angle difference: 170.5
adjustment: 2975.7863746503317
AB (2865.786, 2884.12, 2902.453, 2920.786, 2939.12, 2957.453, 2975.786, 2994.12, 3012.453, 3030.786, 3049.12, 3067.453, 3085.786)

means: 17.5  |  2.5
angle difference: 150.5
adjustment: 2626.720524251466
AB (2516.721, 2535.054, 2553.387, 2571.721, 2590.054, 2608.387, 2626.721, 2645.054, 2663.387, 2681.721, 2700.054, 2718.387, 2736.721)

means: -150.0  |  -170.0
angle difference: 318.0
adjustment: 5550.147021341968
AB (5440.147, 5458.48, 5476.814, 5495.147, 5513.48, 5531.814, 5550.147, 5568.48, 5586.814, 5605.147, 5623.48, 5641.814, 5660.147)

means: 190.0  |  170.0
angle difference: -22.0
adjustment: -383.9724354387525
AB (-493.972, -475.639, -457.306, -438.972, -420.639, -402.306, -383.972, -365.639, -347.306, -328.972, -310.639, -292.306, -273.972)

means: 170.0  |  150.0
angle difference: -2.0
adjustment: -34.906585039886586
AB (-144.907, -126.573, -108.24, -89.907, -71.573, -53.24, -34.907, -16.573, 1.76, 20.093, 38.427, 56.76, 75.093)

means: 162.5  |  77.5
angle difference: 5.5
adjustment: 95.99310885968812
AB (-14.007, 4.326, 22.66, 40.993, 59.326, 77.66, 95.993, 114.326, 132.66, 150.993, 169.326, 187.66, 205.993)

Distributions
(1.0, -0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0)
(1.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0)
(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
(0.002, 0.004, 0.01, 0.021, 0.04, 0.067, 0.099, 0.128, 0.144, 0.142, 0.123, 0.093, 0.062, 0.067)
(0.427, 0.096, 0.095, 0.09, 0.081, 0.068, 0.053, 0.038, 0.025, 0.015, 0.008, 0.004, 0.002, 0.001)
[close]

Here, I have tried a simpler version of just replacing the original code's DPS numbers with weapon values and found that the graph becomes nice, so I suspect I've made a mistake.  Before I should try again, would you mind checking my work for mistakes?
Result of Just Changing DPS to Weapon Score of 7
Bounds: (-110, -92, -73, -55, -37, -18, 0, 18, 37, 55, 73, 92, 110)

['right phaser', 7, -10.0, 20.0, 5.0, -17.5, -2.5]
(1.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, -0.0, 0.0)

['left phaser', 7, 10.0, 20.0, 5.0, 2.5, 17.5]
(1.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0)

['pd gun 1', 7, -160.0, 20.0, 0.0, -170.0, -150.0]
(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)

['pd gun 2', 7, 180.0, 20.0, 0.0, 170.0, 190.0]
(0.005, 0.009, 0.02, 0.039, 0.066, 0.098, 0.126, 0.144, 0.143, 0.124, 0.094, 0.063, 0.037, 0.032)

['pd gun 3', 7, 160.0, 20.0, 0.0, 150.0, 170.0]
(0.014, 0.019, 0.038, 0.064, 0.096, 0.125, 0.143, 0.143, 0.125, 0.096, 0.064, 0.038, 0.019, 0.014)

['photon torpedo', 7, 90.0, 0.0, 5.0, 90.0, 90.0]
(1.0, 0.0, 0.0, -0.0, 0.0, -0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0)
[close]
« Last Edit: January 13, 2023, 04:30:06 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #399 on: January 13, 2023, 10:38:03 PM »

Well, copypasting my original code into rdrr.io (working on my phone) we get this
code
Code
#Objective: calculate the optimum angle to place the enemy ship, with regard to our ship
#Definition: angle exactly to the right is 0 degrees, so the ship is heading towards +90 degrees.
#Possible angles are integers, from -179 to 180.
#Assumptions: our ship is pointlike. The enemy ship is at a constant range. The enemy ship's orientation
#is tangential to the circle defined by our ship's position and range. The enemy ship is represented
#by a single line of hittable armor cells.
#Additionally, we assume sufficient range that we do not need to compensate for curvature (that range
#to the enemy ship's cells varies) In short: we assume that it is the case that the secant such that
#the average range is range is approximately the same as the arc and that inaccuracies that result
#from using the average instead of cell-specific range are insignificant.


#Section 0. Test ship and weapons. We will test against a ship formatted in the normal format
ship <- c(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)

#We will create a data frame containing 18 weapons for testing.
#Weapons are formatted as "name", dps, facing, tracking arc, spread
#first, 6 weapons with easy parameters to parse
weapon1 <- c("right phaser", 7, -10, 20, 5)
weapon2 <- c("left phaser", 7, 10, 20, 5)
weapon3 <- c("pd gun 1", 7, -160, 20, 0)
weapon4 <- c("pd gun 2",7, 180, 20, 0)
weapon5 <- c("pd gun 3",7, 160, 20, 0)
weapon6 <- c("photon torpedo", 7, 90, 0, 5)

#then, 12 weapons with randomly generated data
#the generation process was c(round(100*runif(1)),round(180*runif(1,-1,1)),round(180*(1-log10(runif(1,1,10)))),round(30*runif(1)))
weapon7 <- c("bb gun",5,-150,11,20)
weapon8 <- c("space marine teleporter", 78,69,173,29)
weapon9 <- c("turbolaser", 92,122,111,9)
weapon10 <- c("hex bolter", 24,-136,38,20)
weapon11 <- c("singularity projector", 95,28,122,25)
weapon12 <- c("subspace resonance kazoo", 68,-139,12,2)
weapon13 <- c("left nullspace projector", 10,28,54,0)
weapon14 <- c("telepathic embarrassment generator", 30,-31,35,8)
weapon15 <- c("perfectoid resonance torpedo", 34,72,10,17)
weapon16 <- c("entropy inverter gun",78,-60,13,24)
weapon17 <- c("mini-collapsar rifle", 27,28,16,13)
weapon18 <- c("false vacuum tunneler", 32,78,157,20)

#store all weapons in a data frame - this part should be replaced with however the final integrated program
#handles weapons
no_weapons <- 18
weapons <- data.frame()
for (i in 1:no_weapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
colnames(weapons) <- c("name","damage","facing","trackingrange","spread")

#Section 1. functions of variables

#In this and following sections, say
#1. name of function
#2. input of function
#3. mathematical statement
#4. output of function


#1. G
#2. a real number y
#3. y*Phi(y)+phi(y), where Phi is the cdf and phi is the PDF of a standard normal dist
#4. =#3.
G <- function(y) return(y*pnorm(y) + dnorm(y))

#1. Hit probability of coordinate less than x
#2. A real number z, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. if a > 0 and b > 0, a/2b * ( G(z/a + b/a) - G(z/a - b/a) )
#if a > 0 and b = 0, Phi(z)
#if b > 0 and a = 0, the integral of -infinity to x over a
#function corresponding to a rectangle with base from -b/2 to b/2 of area 1 ie. max(0, min(1, b/2 + z))
#if a = 0 and b = 0, 0 if z < 0 and 1 otherwise
#4. cumulative distribution function of the probability distribution of hits at z (CDF(z))

hit_probability_coord_lessthan_x <- function(z, a, b){
  if(a > 0 & b > 0) return(a/2/b*(G(z/a+b/a)-G(z/a-b/a)))
  if(a > 0 & b == 0) return(pnorm(z,0,a))
  if(a == 0 & b > 0) return(max(0,min(1,b/2+z)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

#1. Degrees to arc
#2. A radius and an angle
#3. 2*pi*r * degrees/360
#4. the arc corresponding to the central angle of the circle defined by radius
deg_to_arc <- function(deg, radius) return(deg*2*radius/360*pi)


#1. Arc to degrees
#2. An arc and a radius
#3. arc / (2*pi*r) * 360
#4. The central angle corresponding to an arc of the circle defined by radius

arc_to_deg <- function(arc, radius) return(arc/2/radius*360/pi)

#1. Min and max mean
#2. A weapon, containing the columns facing, spread and tracking_range at [3],[4],[5]
#3. If spread < tracking_range,
#a vector ( facing + tracking_range/2 - spread / 2, facing - tracking_range/2 + spread / 2 )
#Else the vector ( facing, facing )
#4. Given a weapon, the maximum and minimum means of the probability distribution that it can achieve
#by rotating

min_max_mean <- function(weapon){
  if(weapon[5] < weapon[4]){
    return(c(
      weapon[3]-weapon[4]/2+weapon[5]/2,
      weapon[3]+weapon[4]/2-weapon[5]/2
    ))
  } else {
    return(c(
      weapon[3], weapon[3]
    ))
  }
}


#1. Transform hit coordinate
#2. A weapon, containing the columns min_mean and max_mean, and an angle
#3. max( minmean, min( maxmean, angle ) )
#4. Given that angle is the angle of the target relative to our ship, output is the angle that the weapon
#will assume as it tries to target the target

transform_hit_coord <- function(angle, weapon) return(max(weapon$min_mean, min(weapon$max_mean,angle)))

#1. Generate ship upper bounds
#2. A vector representing a ship, with width stored at [5] and number of cells stored at [6], and a range
#3. algorithm: 1. calculate width of a ship cell, in angles
#              2. the lowest bound is -ship width
#              3. the next bound after any given bound is previous bound + increment
#              4. there are equally many such next bounds as the ship has cells
#                 calculate them successively by applying 3.
#              5. convert the vector to pixels multiplying it by 2*pi*range
#4. A vector with the lower edge of the ship in index 1 and upper bounds of all its armor cells at
#successive indices
generate_ship_upper_bounds <- function(ship, range){
  ship_angle <- ship[5]/(2* pi *range)
  cell_angle <- ship_angle/ship[6]
  angles <- vector(mode="double", length = ship[6]+1)
  angles[1] <- -ship_angle/2
  for (i in 1:(length(angles)-1)) angles[i+1] <- angles[i]+cell_angle
  return(angles * 2 * pi * range)
}

#Section 2. functions of functions and variables

#1. Transformed angle
#2. A weapon, containing the columns min_mean and max_mean, and an angle
#3. angle - transform_hit_coord(angle, weapon)
#4. Given that angle is the angle of the target relative to our ship (beta in illustration),
#output is the angle of the target relative to to the weapon (angle alpha in illustration)

transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))

#1. Weapon adjustment (pixels)
#2. A weapon and an angle (should be used for the optimum angle)
#3. deg_to_arc(transformed_angle)
#4. Returns the arc from weapon distribution mean to target

weaponadjustment_px <- function(weapon, optimum_angle,range){
  angle_difference <- transformed_angle(optimum_angle,weapon)
  arc <- deg_to_arc(angle_difference,range)
  return(arc)
}


#1. Hit distribution
#2. A vector of upper bounds, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. will not present the detailed calculations for this function, as this section is to be replaced
#by the hit distribution function of the integrated code and is not intended for translation
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds


hit_distribution <- function(upper_bounds, standard_deviation, spread){
  vector <- vector(mode="double", length = length(upper_bounds)+1)
  if (standard_deviation == 0){
    if (spread == 0){
      vector[1] <- 0
      for (j in 2:(length(upper_bounds))) {
        #if both spread and standard deviation are 0 then all shots hit 1 cell. this should be so even if
        #the ship has an even number of cells to prevent ships with even no. cells appearing tougher which is not
        #the case in the real game most likely
        if ((upper_bounds[j] >= 0) & (upper_bounds[j-1] < 0)) vector[j] <- 1
      }
      #return part of a box
    } else {
      vector[1] <- min(1,max(0,(upper_bounds[1]+spread))/(2*spread))
      for (j in 2:(length(upper_bounds))) vector[j] <- min(1,max(0,(upper_bounds[j]+spread))/(2*spread)) - min(1,max(0,(upper_bounds[j-1]+spread))/(2*spread))
      vector[length(upper_bounds)+1] <- 1-min(1,max(0,(upper_bounds[length(upper_bounds)]+spread))/(2*spread))
    }
  } else {
    if (spread != 0){
      vector[1] <- hit_probability_coord_lessthan_x(upper_bounds[1], standard_deviation, spread)
      for (j in 2:(length(upper_bounds))) vector[j] <- (hit_probability_coord_lessthan_x(upper_bounds[j], standard_deviation, spread)-hit_probability_coord_lessthan_x(upper_bounds[j-1], standard_deviation, spread))
      vector[length(upper_bounds)+1] <- (1-hit_probability_coord_lessthan_x(upper_bounds[length(upper_bounds)], standard_deviation, spread))
    } else {
      #if spread is 0 but standard deviation is not 0 we have a normal distribution
      vector[1] <- pnorm(upper_bounds[1],0,standard_deviation)
      for (j in 2:(length(upper_bounds))) vector[j] <- pnorm(upper_bounds[j],0,standard_deviation) - pnorm(upper_bounds[j-1], mean=0, sd=standard_deviation)
      vector[length(upper_bounds)+1] <- 1-pnorm(upper_bounds[length(upper_bounds)], mean=0, sd=standard_deviation)
    }
   
  }
  return(vector)
}

#1. Hit distribution at optimum angle
#2. The optimum angle, the standard error in pixels, a weapon with spread at index 5, a vector of upper bounds
#3. hit distribution for vector of upper bounds + arc from weapon to upper bound
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds, adjusted for ship location

hit_distribution_at_optimum_angle <- function(angle, sd, upper_bounds, weapon, range){
  #convert spread to pixels
  px_spread <- deg_to_arc(weapon[,5], range)
  #adjust upper bound vector
  adj_ubs <- upper_bounds + weaponadjustment_px(weapon, angle,range)
  return(hit_distribution(adj_ubs,sd,px_spread))
}

#1. Summed area under curve
#2. An angle, the standard deviation of the normal distribution in pixels, a set of weapons, and a ship
# s.t. ship width is stored at index 5, weapon damage at index 2 and weapon spread at index 5
#3. Algorithm: convert ship width to degrees and
#              convert standard error to degrees, for consistency in units
#              for each weapon, calculate the angles of the ship's lower bound and upper bound
#              relative to that gun
#              then calculate dps * (CDF of probability distribution of that weapon at upper bound
#              - CDF of probability distribution of that weapon at lower bound)
#4. Output: the sum of expected dps from this set of weapons to target ship at that angle relative to our ship

sum_auc <- function(angle, sd, weapons, ship, range) {
  summed_auc <- 0
  #convert the ship's width from segment to degrees
  shipwidth <- arc_to_deg(ship[5], range)/2
  d_error <- arc_to_deg(sd, range)
 
  for (i in 1:length(weapons[,1])){
    #angle of the ship's upper bound, in coordinates of the distribution mean
    #note that this is weapon specific
    #as a sanity check, furthermore, angle cannot be below -359 or above +360
    ship_upper_bound <- transformed_angle(angle,weapons[i,])+shipwidth/2
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth/2
   
    damage <- weapons[i,2]
    spread <- weapons[i,5]
    #now we calculate the sum
    summed_auc <- summed_auc + damage*(
      hit_probability_coord_lessthan_x(ship_upper_bound, d_error, spread) -
        hit_probability_coord_lessthan_x(ship_lower_bound, d_error, spread)
    )
  }
 
  return(summed_auc)
}

#Section 3. functions of functions of functions and variables

#1. main
#2. a ship, a range, a standard deviation, and a list of weapons
#3. described in comments in the function
#4. prints, for each weapon, the probability of a hit at each of the ship's cells, as well as
#of a miss due to hitting below ship's lowest bound in cell 1 and of a miss due to hitting above
#ship's highest bound in the last cell
main <- function(ship, range, sd, weapons){
  # 1. we were given a list of weapons with names etc. so formulate the list with proper types and
  # with room for min and max means
  weapons <- data.frame(name=weapons[,1], damage=as.double(weapons[ ,2]), facing=as.double(weapons[ ,3]),
                        tracking_arc=as.double(weapons[ ,4]),spread=as.double(weapons[ ,5]),min_mean=0,max_mean=0)
  # compute min and max means for weapons
 
  for (i in 1:length(weapons[,1])) {
    weapons[i, 6] <- min_max_mean(weapons[i, ])[1]
    weapons[i, 7] <- min_max_mean(weapons[i, ])[2]
  }

  #now, for angles -359 to 360 (all possible signed angles, calculate dps)
  angles <- seq(-359,360)
 
  dps_at_angles <- angles
  for (i in 1:720) {
    dps_at_angles[i] <- sum_auc(dps_at_angles[i], sd, weapons, ship, range)
  }
 
 
  #now, note that angle -180 is just angle 180, angle -359 is angle 1, and so on, so
  #these must be summed with angles -179 to 180
  for (i in 1:180) dps_at_angles[i+360] <- dps_at_angles[i+360]+dps_at_angles[i]
  #likewise note that angle 360 is just angle 0, angle 359 is angle -1, and so on
  for (i in 540:720) dps_at_angles[i-360] <- dps_at_angles[i-360]+dps_at_angles[i]
 
  #having summed, select angles -179 to 180
  dps_at_angles <- dps_at_angles[181:540]
 
  #we use a separate vector to keep track of angle, since vector index 1 corresponds to angle -179 now
  x_axis <- seq(-179,180)
 
  #find the optimum angle by selecting the midmost of those cells that have the highest dps,
  #and from the vector x_axis the angle corresponding to that cell
  #use rounding to avoid errors from numerical math
  optimum_angle <- x_axis[which(round(dps_at_angles,3) == round(max(dps_at_angles),3))
                        [ceiling(length(which(round(dps_at_angles,3) == round(max(dps_at_angles),3)))/2)]]
 
  #calculate ship upper bounds
  upper_bounds <- generate_ship_upper_bounds(ship, range)
 
  #calculate and report the distributions for weapons, round for human readability
  for (i in 1:length(weapons[,1])){
    print(paste0(weapons[i,1],":"))
    print(round(hit_distribution_at_optimum_angle(optimum_angle,sd,upper_bounds,weapons[i,],range),3))
  }
 
  #testing section - not to be implemented in final code
  #print a graph of the distribution and our choice of angle
  print(weapons)
  plot(dps_at_angles, x=x_axis)
  abline(v=optimum_angle)
 
}

#sample runs
main(ship, 1000, 50, weapons[1:6,])

[close]

Output

[1] "right phaser:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "left phaser:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "pd gun 1:"
 [1] 0.014 0.019 0.038 0.064 0.096 0.125 0.143 0.143 0.125 0.096 0.064 0.038
[13] 0.019 0.014
[1] "pd gun 2:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "photon torpedo:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
            name damage facing tracking_arc spread min_mean max_mean
1   right phaser      7    -10           20      5    -17.5     -2.5
2    left phaser      7     10           20      5      2.5     17.5
3       pd gun 1      7   -160           20      0   -170.0   -150.0
4       pd gun 2      7    180           20      0    170.0    190.0
5       pd gun 3      7    160           20      0    150.0    170.0
6 photon torpedo      7     90            0      5     90.0     90.0

Image output:


So we can see that there is an error in the original code because PD gun 2 should be able to hit here and in fact does according to the graph.

The image output tells us immediately what is wrong, that is why I am a fan of these: it is selecting the correct angle but giving the wrong distributions. I suspect this is because we did not wrap around with regard to the ship when computing the distributions even though we did in the angle selection function, so the former is still working with directional angles where 720!=0.

I do think your code is almost but not exactly correct, it just selects the other maximum. Editing my code so it selects the other maximum(graph:just imagine the vertical line at the other maximum) we get

[1] "right phaser:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "left phaser:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 1:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 2:"
 [1] 0.014 0.019 0.038 0.064 0.096 0.125 0.143 0.143 0.125 0.096 0.064 0.038
[13] 0.019 0.014
[1] "pd gun 3:"
 [1] 0.014 0.019 0.038 0.064 0.096 0.125 0.143 0.143 0.125 0.096 0.064 0.038
[13] 0.019 0.014
[1] "photon torpedo:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
            name damage facing tracking_arc spread min_mean max_mean
1   right phaser      7    -10           20      5    -17.5     -2.5
2    left phaser      7     10           20      5      2.5     17.5
3       pd gun 1      7   -160           20      0   -170.0   -150.0
4       pd gun 2      7    180           20      0    170.0    190.0
5       pd gun 3      7    160           20      0    150.0    170.0
6 photon torpedo      7     90            0      5     90.0     90.0


I highly recommend including graphs with the numbers as that makes this easier. I'll unfortunately not be immediately able to work on the wraparound problem.

Edit to add: just thinking through it, I think the fix is simply to use min((abs(optimum angle), abs(360-optimum angle)) for the enemy ship position in the distribution at optimum angle function. This is because we have two ways of describing a position in terms of directional (winding) angles. For example let's say we have the angle -90. We get there from 0 by either +270 or -90 (or of course same +-n*360, but we do not need to consider this as we know both weapons and the optimum angle are between -179 and +180). Now say that we want to express the angle from -90 to -85. Clearly it is either -355 or +5. For the purposes of the weapons which must use these signed angles for the distr function to be correct (the PDF and CDF are not periodic functions) we should use +5, so the one with minimum absolute value. Now let's say we have a fixed weapon at +175 and a target at -175. Compute the transformed angle to get weapon angle - target angle = 350, so clearly the weapon can't hit at this angle if it can only hit, say, between -15 and +15 (angle from weapon to target). However 360-350 = 10, which is the real angle we should use. This problem only becomes apparent in our current code when we are near the angle +180 and the enemy is near -179 so it was good to find it now

Note that no changes are needed to the sum auc and other optimum angle finding functions, because we handled it there with the sum operation at the end. The equivalent here would be summing the hit distributions at the two alternative descriptions of position, but this does not work because we are also calculating the chance to miss and not just the chance to hit here (so we would end up with the probability distribution over cells adding up to more than 100% - for example 100% chance to miss and a 100% chance to hit cell 7). It would work (approximately - because the hit probabilities are near 0 for the wrong description of position) if we didn't have the cells that are misses though. But if you want to you could switch those to using this and get rid of the sum operation I think.

It is actually quite interesting to think about whether we could define the PDF and CDF for angles in the first place rather than signed angles / windings / directed angles / rotations / real numbers or whatever they are most accurately called. The CDF would need to be periodical, so that means the probability density function must be negative or undefined at some point (since it is the derivative of the CDF). So it seems a little problematic. But that's a bit of a tangent. This discussion does demonstrate the assumption here: we are assuming that tracking angle and spread are both maximally 360 degrees and the SD is pretty small so the distribution at larger angles is approximately zero. If it were the case that we were permitted, say, a 1080 degree spread, or such a large SD that weapons will likely hit regardless of target position, then the math here would fall apart as the operation I described above would not be permitted (we would have to seriously consider shots mapping from say -1200 degrees to -120 degrees). If it seems a little abstract what -1200 degrees means then imagine that there is a string tied to the gun that wraps around it as it rotates, but not one tied to the target. Then if we permitted arbitrarily large parameters we would have to consider the probability the target gets hit by the gun with the string wrapped around it ...1 times clockwise, 0 times, 1 times counterclockwise, 2 times counterclockwise...

Edit 2: it gets a little more complex because we need to rewrite the transform angle function. And I actually wrote most of it on my phone on rdrr.io but then I accidentally refreshed the page... get back to you on this.


Edit 3: I got it fixed. It is a relatively small change because I decided to do it using logic rather than worry about geometry and such.

These functions must be changed:
Code
min_max_mean <- function(weapon){
  if(weapon[5] < weapon[4]){
    vec <- c(
      weapon[[3]]-weapon[[4]]/2+weapon[[5]]/2,
      weapon[[3]]+weapon[[4]]/2-weapon[[5]]/2
    )
    if(vec[1] > 180) vec[1] <- vec[1]-360
    if(vec[2] > 180) vec[2] <- vec[2]-360
    if(vec[1] < -179) vec[1] <- vec[1]+360
    if(vec[2] < -179) vec[2] <- vec[2]+360
    return(vec)
  } else {
    return(c(
      weapon[3], weapon[3]
    ))
  }
}

Code
transform_hit_coord <- function(angle, weapon){
  if(weapon$max_mean >= weapon$min_mean) return(max(weapon$min_mean, min(weapon$max_mean,angle)))
  if(angle < 0 & angle < weapon$max_mean) return(angle)
  if(angle >= 0 & angle > weapon$min_mean) return(angle)
  if(angle >= 0 & angle <= weapon$min_mean) return(weapon$min_mean)
  if(angle < 0 & angle >= weapon$max_mean) return(weapon$max_mean)
}

After doing this the sum operation at the end is no longer necessary and is in fact incorrect so remove it.

Code
  
  angles <- seq(-179,180)
 
  dps_at_angles <- angles
  for (i in 1:360) {
    dps_at_angles[i] <- sum_auc(dps_at_angles[i], sd, weapons, ship, range)
  }
 
 
  #we use a separate vector to keep track of angle, since vector index 1 corresponds to angle -179 now
  x_axis <- seq(-179,180)

So the entire code is
Spoiler
Code
#Objective: calculate the optimum angle to place the enemy ship, with regard to our ship
#Definition: angle exactly to the right is 0 degrees, so the ship is heading towards +90 degrees.
#Possible angles are integers, from -179 to 180.
#Assumptions: our ship is pointlike. The enemy ship is at a constant range. The enemy ship's orientation
#is tangential to the circle defined by our ship's position and range. The enemy ship is represented
#by a single line of hittable armor cells.
#Additionally, we assume sufficient range that we do not need to compensate for curvature (that range
#to the enemy ship's cells varies) In short: we assume that it is the case that the secant such that
#the average range is range is approximately the same as the arc and that inaccuracies that result
#from using the average instead of cell-specific range are insignificant.


#Section 0. Test ship and weapons. We will test against a ship formatted in the normal format
ship <- c(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)

#We will create a data frame containing 18 weapons for testing.
#Weapons are formatted as "name", dps, facing, tracking arc, spread
#first, 6 weapons with easy parameters to parse
weapon1 <- c("right phaser", 7, -10, 20, 5)
weapon2 <- c("left phaser", 7, 10, 20, 5)
weapon3 <- c("pd gun 1", 7, -160, 20, 0)
weapon4 <- c("pd gun 2",7, 180, 20, 0)
weapon5 <- c("pd gun 3",7, 160, 20, 0)
weapon6 <- c("photon torpedo", 7, 90, 0, 5)

#then, 12 weapons with randomly generated data
#the generation process was c(round(100*runif(1)),round(180*runif(1,-1,1)),round(180*(1-log10(runif(1,1,10)))),round(30*runif(1)))
weapon7 <- c("bb gun",5,-150,11,20)
weapon8 <- c("space marine teleporter", 78,69,173,29)
weapon9 <- c("turbolaser", 92,122,111,9)
weapon10 <- c("hex bolter", 24,-136,38,20)
weapon11 <- c("singularity projector", 95,28,122,25)
weapon12 <- c("subspace resonance kazoo", 68,-139,12,2)
weapon13 <- c("left nullspace projector", 10,28,54,0)
weapon14 <- c("telepathic embarrassment generator", 30,-31,35,8)
weapon15 <- c("perfectoid resonance torpedo", 34,72,10,17)
weapon16 <- c("entropy inverter gun",78,-60,13,24)
weapon17 <- c("mini-collapsar rifle", 27,28,16,13)
weapon18 <- c("false vacuum tunneler", 32,78,157,20)

#store all weapons in a data frame - this part should be replaced with however the final integrated program
#handles weapons
no_weapons <- 18
weapons <- data.frame()
for (i in 1:no_weapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
colnames(weapons) <- c("name","damage","facing","trackingrange","spread")

#Section 1. functions of variables

#In this and following sections, say
#1. name of function
#2. input of function
#3. mathematical statement
#4. output of function


#1. G
#2. a real number y
#3. y*Phi(y)+phi(y), where Phi is the cdf and phi is the PDF of a standard normal dist
#4. =#3.
G <- function(y) return(y*pnorm(y) + dnorm(y))

#1. Hit probability of coordinate less than x
#2. A real number z, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. if a > 0 and b > 0, a/2b * ( G(z/a + b/a) - G(z/a - b/a) )
#if a > 0 and b = 0, Phi(z)
#if b > 0 and a = 0, the integral of -infinity to x over a
#function corresponding to a rectangle with base from -b/2 to b/2 of area 1 ie. max(0, min(1, b/2 + z))
#if a = 0 and b = 0, 0 if z < 0 and 1 otherwise
#4. cumulative distribution function of the probability distribution of hits at z (CDF(z))

hit_probability_coord_lessthan_x <- function(z, a, b){
  if(a > 0 & b > 0) return(a/2/b*(G(z/a+b/a)-G(z/a-b/a)))
  if(a > 0 & b == 0) return(pnorm(z,0,a))
  if(a == 0 & b > 0) return(max(0,min(1,b/2+z)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

#1. Degrees to arc
#2. A radius and an angle
#3. 2*pi*r * degrees/360
#4. the arc corresponding to the central angle of the circle defined by radius
deg_to_arc <- function(deg, radius) return(deg*2*radius/360*pi)


#1. Arc to degrees
#2. An arc and a radius
#3. arc / (2*pi*r) * 360
#4. The central angle corresponding to an arc of the circle defined by radius

arc_to_deg <- function(arc, radius) return(arc/2/radius*360/pi)

#1. Min and max mean
#2. A weapon, containing the columns facing, spread and tracking_range at [3],[4],[5]
#3. If spread < tracking_range,
#a vector ( facing + tracking_range/2 - spread / 2, facing - tracking_range/2 + spread / 2 )
#Else the vector ( facing, facing )
#4. Given a weapon, the maximum and minimum means of the probability distribution that it can achieve
#by rotating

min_max_mean <- function(weapon){
  if(weapon[5] < weapon[4]){
    vec <- c(
      weapon[[3]]-weapon[[4]]/2+weapon[[5]]/2,
      weapon[[3]]+weapon[[4]]/2-weapon[[5]]/2
    )
    if(vec[1] > 180) vec[1] <- vec[1]-360
    if(vec[2] > 180) vec[2] <- vec[2]-360
    if(vec[1] < -179) vec[1] <- vec[1]+360
    if(vec[2] < -179) vec[2] <- vec[2]+360
    return(vec)
  } else {
    return(c(
      weapon[3], weapon[3]
    ))
  }
}

transform_hit_coord <- function(angle, weapon){
  if(weapon$max_mean >= weapon$min_mean) return(max(weapon$min_mean, min(weapon$max_mean,angle)))
  if(angle < 0 & angle < weapon$max_mean) return(angle)
  if(angle >= 0 & angle > weapon$min_mean) return(angle)
  if(angle >= 0 & angle <= weapon$min_mean) return(weapon$min_mean)
  if(angle < 0 & angle >= weapon$max_mean) return(weapon$max_mean)
}
#1. Generate ship upper bounds
#2. A vector representing a ship, with width stored at [5] and number of cells stored at [6], and a range
#3. algorithm: 1. calculate width of a ship cell, in angles
#              2. the lowest bound is -ship width
#              3. the next bound after any given bound is previous bound + increment
#              4. there are equally many such next bounds as the ship has cells
#                 calculate them successively by applying 3.
#              5. convert the vector to pixels multiplying it by 2*pi*range
#4. A vector with the lower edge of the ship in index 1 and upper bounds of all its armor cells at
#successive indices
generate_ship_upper_bounds <- function(ship, range){
  ship_angle <- ship[5]/(2* pi *range)
  cell_angle <- ship_angle/ship[6]
  angles <- vector(mode="double", length = ship[6]+1)
  angles[1] <- -ship_angle/2
  for (i in 1:(length(angles)-1)) angles[i+1] <- angles[i]+cell_angle
  return(angles * 2 * pi * range)
}

#Section 2. functions of functions and variables

#1. Transformed angle
#2. A weapon, containing the columns min_mean and max_mean, and an angle
#3. angle - transform_hit_coord(angle, weapon)
#4. Given that angle is the angle of the target relative to our ship (beta in illustration),
#output is the angle of the target relative to to the weapon (angle alpha in illustration)

transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))

#1. Weapon adjustment (pixels)
#2. A weapon and an angle (should be used for the optimum angle)
#3. deg_to_arc(transformed_angle)
#4. Returns the arc from weapon distribution mean to target

weaponadjustment_px <- function(weapon, optimum_angle,range){
  angle_difference <- transformed_angle(optimum_angle,weapon)
  arc <- deg_to_arc(angle_difference,range)
  return(arc)
}


#1. Hit distribution
#2. A vector of upper bounds, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. will not present the detailed calculations for this function, as this section is to be replaced
#by the hit distribution function of the integrated code and is not intended for translation
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds


hit_distribution <- function(upper_bounds, standard_deviation, spread){
  vector <- vector(mode="double", length = length(upper_bounds)+1)
  if (standard_deviation == 0){
    if (spread == 0){
      vector[1] <- 0
      for (j in 2:(length(upper_bounds))) {
        #if both spread and standard deviation are 0 then all shots hit 1 cell. this should be so even if
        #the ship has an even number of cells to prevent ships with even no. cells appearing tougher which is not
        #the case in the real game most likely
        if ((upper_bounds[j] >= 0) & (upper_bounds[j-1] < 0)) vector[j] <- 1
      }
      #return part of a box
    } else {
      vector[1] <- min(1,max(0,(upper_bounds[1]+spread))/(2*spread))
      for (j in 2:(length(upper_bounds))) vector[j] <- min(1,max(0,(upper_bounds[j]+spread))/(2*spread)) - min(1,max(0,(upper_bounds[j-1]+spread))/(2*spread))
      vector[length(upper_bounds)+1] <- 1-min(1,max(0,(upper_bounds[length(upper_bounds)]+spread))/(2*spread))
    }
  } else {
    if (spread != 0){
      vector[1] <- hit_probability_coord_lessthan_x(upper_bounds[1], standard_deviation, spread)
      for (j in 2:(length(upper_bounds))) vector[j] <- (hit_probability_coord_lessthan_x(upper_bounds[j], standard_deviation, spread)-hit_probability_coord_lessthan_x(upper_bounds[j-1], standard_deviation, spread))
      vector[length(upper_bounds)+1] <- (1-hit_probability_coord_lessthan_x(upper_bounds[length(upper_bounds)], standard_deviation, spread))
    } else {
      #if spread is 0 but standard deviation is not 0 we have a normal distribution
      vector[1] <- pnorm(upper_bounds[1],0,standard_deviation)
      for (j in 2:(length(upper_bounds))) vector[j] <- pnorm(upper_bounds[j],0,standard_deviation) - pnorm(upper_bounds[j-1], mean=0, sd=standard_deviation)
      vector[length(upper_bounds)+1] <- 1-pnorm(upper_bounds[length(upper_bounds)], mean=0, sd=standard_deviation)
    }
   
  }
  return(vector)
}

#1. Hit distribution at optimum angle
#2. The optimum angle, the standard error in pixels, a weapon with spread at index 5, a vector of upper bounds
#3. hit distribution for vector of upper bounds + arc from weapon to upper bound
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds, adjusted for ship location

hit_distribution_at_optimum_angle <- function(angle, sd, upper_bounds, weapon, range){
  #convert spread to pixels
  px_spread <- deg_to_arc(weapon[,5], range)
  #adjust upper bound vector
  adj_ubs <- upper_bounds + weaponadjustment_px(weapon, angle,range)
  return(hit_distribution(adj_ubs,sd,px_spread))
}

#1. Summed area under curve
#2. An angle, the standard deviation of the normal distribution in pixels, a set of weapons, and a ship
# s.t. ship width is stored at index 5, weapon damage at index 2 and weapon spread at index 5
#3. Algorithm: convert ship width to degrees and
#              convert standard error to degrees, for consistency in units
#              for each weapon, calculate the angles of the ship's lower bound and upper bound
#              relative to that gun
#              then calculate dps * (CDF of probability distribution of that weapon at upper bound
#              - CDF of probability distribution of that weapon at lower bound)
#4. Output: the sum of expected dps from this set of weapons to target ship at that angle relative to our ship

sum_auc <- function(angle, sd, weapons, ship, range) {
  summed_auc <- 0
  #convert the ship's width from segment to degrees
  shipwidth <- arc_to_deg(ship[5], range)/2
  d_error <- arc_to_deg(sd, range)
 
  for (i in 1:length(weapons[,1])){
    #angle of the ship's upper bound, in coordinates of the distribution mean
    #note that this is weapon specific
    #as a sanity check, furthermore, angle cannot be below -359 or above +360
    ship_upper_bound <- transformed_angle(angle,weapons[i,])+shipwidth/2
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth/2
   
    damage <- weapons[i,2]
    spread <- weapons[i,5]
    #now we calculate the sum
    summed_auc <- summed_auc + damage*(
      hit_probability_coord_lessthan_x(ship_upper_bound, d_error, spread) -
        hit_probability_coord_lessthan_x(ship_lower_bound, d_error, spread)
    )
  }
 
  return(summed_auc)
}

#Section 3. functions of functions of functions and variables

#1. main
#2. a ship, a range, a standard deviation, and a list of weapons
#3. described in comments in the function
#4. prints, for each weapon, the probability of a hit at each of the ship's cells, as well as
#of a miss due to hitting below ship's lowest bound in cell 1 and of a miss due to hitting above
#ship's highest bound in the last cell
main <- function(ship, range, sd, weapons){
  # 1. we were given a list of weapons with names etc. so formulate the list with proper types and
  # with room for min and max means
  weapons <- data.frame(name=weapons[,1], damage=as.double(weapons[ ,2]), facing=as.double(weapons[ ,3]),
                        tracking_arc=as.double(weapons[ ,4]),spread=as.double(weapons[ ,5]),min_mean=0,max_mean=0)
  # compute min and max means for weapons
 
  for (i in 1:length(weapons[,1])) {
    weapons[i, 6] <- min_max_mean(weapons[i, ])[1]
    weapons[i, 7] <- min_max_mean(weapons[i, ])[2]
  }
 
  angles <- seq(-179,180)
 
  dps_at_angles <- angles
  for (i in 1:360) {
    dps_at_angles[i] <- sum_auc(dps_at_angles[i], sd, weapons, ship, range)
  }
 
 
  #we use a separate vector to keep track of angle, since vector index 1 corresponds to angle -179 now
  x_axis <- seq(-179,180)
 
  #find the optimum angle by selecting the midmost of those cells that have the highest dps,
  #and from the vector x_axis the angle corresponding to that cell
  #use rounding to avoid errors from numerical math
  optimum_angle <- x_axis[which(round(dps_at_angles,3) == round(max(dps_at_angles),3))
                          [ceiling(length(which(round(dps_at_angles,3) == round(max(dps_at_angles),3)))/2)]]
 
  #calculate ship upper bounds
  upper_bounds <- generate_ship_upper_bounds(ship, range)
 
  #calculate and report the distributions for weapons, round for human readability
  for (i in 1:length(weapons[,1])){
    print(paste0(weapons[i,1],":"))
    print(round(hit_distribution_at_optimum_angle(optimum_angle,sd,upper_bounds,weapons[i,],range),3))
  }
 
  #testing section - not to be implemented in final code
  #print a graph of the distribution and our choice of angle
  print(weapons)
  plot(dps_at_angles, x=x_axis)
  abline(v=optimum_angle)
 
}

#sample runs
main(ship, 1000, 50, weapons[1:6,])
[close]

Then, this is the output and it is clearly correct despite no funky sum hijinks, PD gun #2 can spin itself from -170 to +170.

[1] "right phaser:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "left phaser:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "pd gun 1:"
 [1] 0.014 0.019 0.038 0.064 0.096 0.125 0.143 0.143 0.125 0.096 0.064 0.038
[13] 0.019 0.014
[1] "pd gun 2:"
 [1] 0.014 0.019 0.038 0.064 0.096 0.125 0.143 0.143 0.125 0.096 0.064 0.038
[13] 0.019 0.014
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "photon torpedo:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
            name damage facing tracking_arc spread min_mean max_mean
1   right phaser      7    -10           20      5    -17.5     -2.5
2    left phaser      7     10           20      5      2.5     17.5
3       pd gun 1      7   -160           20      0   -170.0   -150.0
4       pd gun 2      7    180           20      0    170.0   -170.0
5       pd gun 3      7    160           20      0    150.0    170.0
6 photon torpedo      7     90            0      5     90.0     90.0



You can tell that we have fixed a bug that we should have noticed in the first place by looking at PD gun #2: in the uncorrected code its min mean is 170 and max mean is 190 which is illegal. In the correct version the min mean is 170 and max mean is -170. Note however that just fixing the max mean is not sufficient but you must also fix the transform hit coord function so it understands what to do when the max mean is negative and min mean is positive.

(If it feels confusing why the maximum should be less than the minimum then note it is the maximum ie. largest rotation CCW from the gun's perspective, not the ship's. They disagree about which is larger only when the gun rotates from 180 to -179 from the ship's point of view which is where the special cases happen)
« Last Edit: January 14, 2023, 09:05:18 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 #400 on: January 14, 2023, 01:53:27 PM »

Glad to see you've found a way to code without your potato and seem to be enjoying it!   8)  Now that you've done some math and posted the code, I wonder if I should wait a little while because I have learned from experience that you might pass on some more edits...  :P

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #401 on: January 14, 2023, 08:16:33 PM »

Oh no worries, I edited that post several times already over the course of a day and by now it also starts with one theory then goes on an abstract tangent then does something entirely different from what was originally proposed. So the process is complete. Don't have plans to change it more. Unless you noticed some problems?

By the way, you can fairly easily run my code for any comparisons by going to rdrr.io and running it online there.

Edit: well, actually maybe you know me better than I do, it seems like I tend to approach correct code iteratively and have an easier time seeing what is going on after posting for some reason. There are two more special cases: cases where the gun can spin around nearly but not exactly 360 degrees so both min and max mean are ipsilateral but reversed in order. This is handled using logic like so:

Code
transform_hit_coord <- function(angle, weapon){
#weapon arc does not include 180 to -179
  if(weapon$max_mean >= weapon$min_mean) {
 if(angle >= weapon$min_mean & angle <= weapon$max_mean) return(angle)
 if(min(abs(angle-weapon$max_mean),360-abs(angle-weapon$max_mean)) > min(abs(angle-weapon$min_mean),360-abs(angle-weapon$min_mean))) return(weapon$min_mean)
 return(weapon$max_mean)
}
#weapon arc includes 180 to -179 but does not cross 0
if(weapon$max_mean <= 0 & weapon$min_mean >= 0){
  if(angle < 0 & angle < weapon$max_mean) return(angle)
  if(angle >= 0 & angle > weapon$min_mean) return(angle)
  if(angle >= 0 & angle <= weapon$min_mean) {
if(abs(angle-weapon$min_mean) >= angle-weapon$max_mean) return(weapon$max_mean)
return(weapon$min_mean)
}
  if(angle < 0 & angle >= weapon$max_mean) {
if(abs(angle-weapon$min_mean) >= abs(angle-weapon$max_mean)) return(weapon$max_mean)
return(weapon$min_mean)
}
}
#weapon arc includes 0 and 180 to -179
  if(angle < 0 & angle <= weapon$min_mean & angle >= weapon$max_mean){
 if(abs(angle-weapon$max_mean) > abs(angle-weapon$min_mean)) return(weapon$min_mean)
 return(weapon$max_mean)
}
  if(angle >= 0 & angle <= weapon$min_mean & angle >= weapon$max_mean){
 if(angle-weapon$max_mean > abs(angle-weapon$min_mean)) return(weapon$min_mean)
 return(weapon$max_mean)
}
  return(angle)
}


There might be a much smarter way of doing this using classical geometry but my discrete math is so much stronger than my classical geometry (which is probably my weakest subject) that this is easier for me to write.

Anyway let me explain real quick what is going on here. These are rules of deduction for whether the target is within the gun's tracking arc or not, when we look at the angles from the ship's perspective.
1) if the max mean is greater than the min mean then it is very simple, we are in the tracking arc if we are between the max mean and min mean and figuring out which of the two is closer s very simple too must be done as below due to an edge case of being very close to +180/-179.
2) if the min mean is greater than 0 and the max mean is less than 0, then we are in the tracking arc if we are between min mean and +180, or between max mean and -179. If we are not in the tracking arc, we should deduce which is closer of the min and max mean by looking at the difference in angle from each.
3) if both are greater than 0 or less than 0, but the min mean is greater than the max mean, then we are in the tracking arc in all cases except if we are below the min mean but above the max mean. In that case deduce which is closer as above.

This really should be all of it, since there are only two special points: +180/-179, which is where the wraparound happens, and 0, where "maximum" ceases to mean closer to 0 and "minimum" means that instead, but those are both considered now. In retrospect I kind of wish I had selected angles from 0 to 359 instead of -179 to +180 but what can you do, as it is it's easier to just roll with it.

Sorry about poor style btw I typed this on my phone. Tested it on rdrr.io and it works. Full code:

Code
#Objective: calculate the optimum angle to place the enemy ship, with regard to our ship
#Definition: angle exactly to the right is 0 degrees, so the ship is heading towards +90 degrees.
#Possible angles are integers, from -179 to 180.
#Assumptions: our ship is pointlike. The enemy ship is at a constant range. The enemy ship's orientation
#is tangential to the circle defined by our ship's position and range. The enemy ship is represented
#by a single line of hittable armor cells.
#Additionally, we assume sufficient range that we do not need to compensate for curvature (that range
#to the enemy ship's cells varies) In short: we assume that it is the case that the secant such that
#the average range is range is approximately the same as the arc and that inaccuracies that result
#from using the average instead of cell-specific range are insignificant.


#Section 0. Test ship and weapons. We will test against a ship formatted in the normal format
ship <- c(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)

#We will create a data frame containing 18 weapons for testing.
#Weapons are formatted as "name", dps, facing, tracking arc, spread
#first, 6 weapons with easy parameters to parse
weapon1 <- c("right phaser", 7, -10, 20, 5)
weapon2 <- c("left phaser", 7, 10, 20, 5)
weapon3 <- c("pd gun 1", 7, -160, 20, 0)
weapon4 <- c("pd gun 2",7, 180, 20, 0)
weapon5 <- c("pd gun 3",7, 160, 20, 0)
weapon6 <- c("photon torpedo", 7, 90, 0, 5)

#then, 12 weapons with randomly generated data
#the generation process was c(round(100*runif(1)),round(180*runif(1,-1,1)),round(180*(1-log10(runif(1,1,10)))),round(30*runif(1)))
weapon7 <- c("bb gun",5,-150,11,20)
weapon8 <- c("space marine teleporter", 78,69,173,29)
weapon9 <- c("turbolaser", 92,122,111,9)
weapon10 <- c("hex bolter", 24,-136,38,20)
weapon11 <- c("singularity projector", 95,28,122,25)
weapon12 <- c("subspace resonance kazoo", 68,-139,12,2)
weapon13 <- c("left nullspace projector", 10,28,54,0)
weapon14 <- c("telepathic embarrassment generator", 30,-31,35,8)
weapon15 <- c("perfectoid resonance torpedo", 34,72,10,17)
weapon16 <- c("entropy inverter gun",78,-60,13,24)
weapon17 <- c("mini-collapsar rifle", 27,28,16,13)
weapon18 <- c("false vacuum tunneler", 32,78,157,20)

#store all weapons in a data frame - this part should be replaced with however the final integrated program
#handles weapons
no_weapons <- 18
weapons <- data.frame()
for (i in 1:no_weapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
colnames(weapons) <- c("name","damage","facing","trackingrange","spread")

#Section 1. functions of variables

#In this and following sections, say
#1. name of function
#2. input of function
#3. mathematical statement
#4. output of function


#1. G
#2. a real number y
#3. y*Phi(y)+phi(y), where Phi is the cdf and phi is the PDF of a standard normal dist
#4. =#3.
G <- function(y) return(y*pnorm(y) + dnorm(y))

#1. Hit probability of coordinate less than x
#2. A real number z, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. if a > 0 and b > 0, a/2b * ( G(z/a + b/a) - G(z/a - b/a) )
#if a > 0 and b = 0, Phi(z)
#if b > 0 and a = 0, the integral of -infinity to x over a
#function corresponding to a rectangle with base from -b/2 to b/2 of area 1 ie. max(0, min(1, b/2 + z))
#if a = 0 and b = 0, 0 if z < 0 and 1 otherwise
#4. cumulative distribution function of the probability distribution of hits at z (CDF(z))

hit_probability_coord_lessthan_x <- function(z, a, b){
  if(a > 0 & b > 0) return(a/2/b*(G(z/a+b/a)-G(z/a-b/a)))
  if(a > 0 & b == 0) return(pnorm(z,0,a))
  if(a == 0 & b > 0) return(max(0,min(1,b/2+z)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

#1. Degrees to arc
#2. A radius and an angle
#3. 2*pi*r * degrees/360
#4. the arc corresponding to the central angle of the circle defined by radius
deg_to_arc <- function(deg, radius) return(deg*2*radius/360*pi)


#1. Arc to degrees
#2. An arc and a radius
#3. arc / (2*pi*r) * 360
#4. The central angle corresponding to an arc of the circle defined by radius

arc_to_deg <- function(arc, radius) return(arc/2/radius*360/pi)

#1. Min and max mean
#2. A weapon, containing the columns facing, spread and tracking_range at [3],[4],[5]
#3. If spread < tracking_range,
#a vector ( facing + tracking_range/2 - spread / 2, facing - tracking_range/2 + spread / 2 )
#Else the vector ( facing, facing )
#4. Given a weapon, the maximum and minimum means of the probability distribution that it can achieve
#by rotating

min_max_mean <- function(weapon){
  if(weapon[5] < weapon[4]){
    vec <- c(
      weapon[[3]]-weapon[[4]]/2+weapon[[5]]/2,
      weapon[[3]]+weapon[[4]]/2-weapon[[5]]/2
    )
    if(vec[1] > 180) vec[1] <- vec[1]-360
    if(vec[2] > 180) vec[2] <- vec[2]-360
    if(vec[1] < -179) vec[1] <- vec[1]+360
    if(vec[2] < -179) vec[2] <- vec[2]+360
    return(vec)
  } else {
    return(c(
      weapon[3], weapon[3]
    ))
  }
}

transform_hit_coord <- function(angle, weapon){
#weapon arc does not include 180 to -179
  if(weapon$max_mean >= weapon$min_mean) {
 if(angle >= weapon$min_mean & angle <= weapon$max_mean) return(angle)
 if(min(abs(angle-weapon$max_mean),360-abs(angle-weapon$max_mean)) > min(abs(angle-weapon$min_mean),360-abs(angle-weapon$min_mean))) return(weapon$min_mean)
 return(weapon$max_mean)
}
#weapon arc includes 180 to -179 but does not cross 0
if(weapon$max_mean <= 0 & weapon$min_mean >= 0){
  if(angle < 0 & angle < weapon$max_mean) return(angle)
  if(angle >= 0 & angle > weapon$min_mean) return(angle)
  if(angle >= 0 & angle <= weapon$min_mean) {
if(abs(angle-weapon$min_mean) >= angle-weapon$max_mean) return(weapon$max_mean)
return(weapon$min_mean)
}
  if(angle < 0 & angle >= weapon$max_mean) {
if(abs(angle-weapon$min_mean) >= abs(angle-weapon$max_mean)) return(weapon$max_mean)
return(weapon$min_mean)
}
}
#weapon arc includes 0 and 180 to -179
  if(angle < 0 & angle <= weapon$min_mean & angle >= weapon$max_mean){
 if(abs(angle-weapon$max_mean) > abs(angle-weapon$min_mean)) return(weapon$min_mean)
 return(weapon$max_mean)
}
  if(angle >= 0 & angle <= weapon$min_mean & angle >= weapon$max_mean){
 if(angle-weapon$max_mean > abs(angle-weapon$min_mean)) return(weapon$min_mean)
 return(weapon$max_mean)
}
  return(angle)
}
#1. Generate ship upper bounds
#2. A vector representing a ship, with width stored at [5] and number of cells stored at [6], and a range
#3. algorithm: 1. calculate width of a ship cell, in angles
#              2. the lowest bound is -ship width
#              3. the next bound after any given bound is previous bound + increment
#              4. there are equally many such next bounds as the ship has cells
#                 calculate them successively by applying 3.
#              5. convert the vector to pixels multiplying it by 2*pi*range
#4. A vector with the lower edge of the ship in index 1 and upper bounds of all its armor cells at
#successive indices
generate_ship_upper_bounds <- function(ship, range){
  ship_angle <- ship[5]/(2* pi *range)
  cell_angle <- ship_angle/ship[6]
  angles <- vector(mode="double", length = ship[6]+1)
  angles[1] <- -ship_angle/2
  for (i in 1:(length(angles)-1)) angles[i+1] <- angles[i]+cell_angle
  return(angles * 2 * pi * range)
}

#Section 2. functions of functions and variables

#1. Transformed angle
#2. A weapon, containing the columns min_mean and max_mean, and an angle
#3. angle - transform_hit_coord(angle, weapon)
#4. Given that angle is the angle of the target relative to our ship (beta in illustration),
#output is the angle of the target relative to to the weapon (angle alpha in illustration)

transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))

#1. Weapon adjustment (pixels)
#2. A weapon and an angle (should be used for the optimum angle)
#3. deg_to_arc(transformed_angle)
#4. Returns the arc from weapon distribution mean to target

weaponadjustment_px <- function(weapon, optimum_angle,range){
  angle_difference <- transformed_angle(optimum_angle,weapon)
  arc <- deg_to_arc(angle_difference,range)
  return(arc)
}


#1. Hit distribution
#2. A vector of upper bounds, a standard deviation a of a normal distribution N(0,a),
#a parameter b of a symmetric uniform distribution (-b/2,b/2)
#3. will not present the detailed calculations for this function, as this section is to be replaced
#by the hit distribution function of the integrated code and is not intended for translation
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds


hit_distribution <- function(upper_bounds, standard_deviation, spread){
  vector <- vector(mode="double", length = length(upper_bounds)+1)
  if (standard_deviation == 0){
    if (spread == 0){
      vector[1] <- 0
      for (j in 2:(length(upper_bounds))) {
        #if both spread and standard deviation are 0 then all shots hit 1 cell. this should be so even if
        #the ship has an even number of cells to prevent ships with even no. cells appearing tougher which is not
        #the case in the real game most likely
        if ((upper_bounds[j] >= 0) & (upper_bounds[j-1] < 0)) vector[j] <- 1
      }
      #return part of a box
    } else {
      vector[1] <- min(1,max(0,(upper_bounds[1]+spread))/(2*spread))
      for (j in 2:(length(upper_bounds))) vector[j] <- min(1,max(0,(upper_bounds[j]+spread))/(2*spread)) - min(1,max(0,(upper_bounds[j-1]+spread))/(2*spread))
      vector[length(upper_bounds)+1] <- 1-min(1,max(0,(upper_bounds[length(upper_bounds)]+spread))/(2*spread))
    }
  } else {
    if (spread != 0){
      vector[1] <- hit_probability_coord_lessthan_x(upper_bounds[1], standard_deviation, spread)
      for (j in 2:(length(upper_bounds))) vector[j] <- (hit_probability_coord_lessthan_x(upper_bounds[j], standard_deviation, spread)-hit_probability_coord_lessthan_x(upper_bounds[j-1], standard_deviation, spread))
      vector[length(upper_bounds)+1] <- (1-hit_probability_coord_lessthan_x(upper_bounds[length(upper_bounds)], standard_deviation, spread))
    } else {
      #if spread is 0 but standard deviation is not 0 we have a normal distribution
      vector[1] <- pnorm(upper_bounds[1],0,standard_deviation)
      for (j in 2:(length(upper_bounds))) vector[j] <- pnorm(upper_bounds[j],0,standard_deviation) - pnorm(upper_bounds[j-1], mean=0, sd=standard_deviation)
      vector[length(upper_bounds)+1] <- 1-pnorm(upper_bounds[length(upper_bounds)], mean=0, sd=standard_deviation)
    }
   
  }
  return(vector)
}

#1. Hit distribution at optimum angle
#2. The optimum angle, the standard error in pixels, a weapon with spread at index 5, a vector of upper bounds
#3. hit distribution for vector of upper bounds + arc from weapon to upper bound
#4. A vector of probability masses, such that cell 1 is chance to hit below lowest upper bound,
#the last cell is chance to hit above highest upper bound, and the others are the probabilities for
#hits between upper bounds, adjusted for ship location

hit_distribution_at_optimum_angle <- function(angle, sd, upper_bounds, weapon, range){
  #convert spread to pixels
  px_spread <- deg_to_arc(weapon[,5], range)
  #adjust upper bound vector
  adj_ubs <- upper_bounds + weaponadjustment_px(weapon, angle,range)
  return(hit_distribution(adj_ubs,sd,px_spread))
}

#1. Summed area under curve
#2. An angle, the standard deviation of the normal distribution in pixels, a set of weapons, and a ship
# s.t. ship width is stored at index 5, weapon damage at index 2 and weapon spread at index 5
#3. Algorithm: convert ship width to degrees and
#              convert standard error to degrees, for consistency in units
#              for each weapon, calculate the angles of the ship's lower bound and upper bound
#              relative to that gun
#              then calculate dps * (CDF of probability distribution of that weapon at upper bound
#              - CDF of probability distribution of that weapon at lower bound)
#4. Output: the sum of expected dps from this set of weapons to target ship at that angle relative to our ship

sum_auc <- function(angle, sd, weapons, ship, range) {
  summed_auc <- 0
  #convert the ship's width from segment to degrees
  shipwidth <- arc_to_deg(ship[5], range)/2
  d_error <- arc_to_deg(sd, range)
 
  for (i in 1:length(weapons[,1])){
    #angle of the ship's upper bound, in coordinates of the distribution mean
    #note that this is weapon specific
    #as a sanity check, furthermore, angle cannot be below -359 or above +360
    ship_upper_bound <- transformed_angle(angle,weapons[i,])+shipwidth/2
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth/2
   
    damage <- weapons[i,2]
    spread <- weapons[i,5]
    #now we calculate the sum
    summed_auc <- summed_auc + damage*(
      hit_probability_coord_lessthan_x(ship_upper_bound, d_error, spread) -
        hit_probability_coord_lessthan_x(ship_lower_bound, d_error, spread)
    )
  }
 
  return(summed_auc)
}

#Section 3. functions of functions of functions and variables

#1. main
#2. a ship, a range, a standard deviation, and a list of weapons
#3. described in comments in the function
#4. prints, for each weapon, the probability of a hit at each of the ship's cells, as well as
#of a miss due to hitting below ship's lowest bound in cell 1 and of a miss due to hitting above
#ship's highest bound in the last cell
main <- function(ship, range, sd, weapons){
  # 1. we were given a list of weapons with names etc. so formulate the list with proper types and
  # with room for min and max means
  weapons <- data.frame(name=weapons[,1], damage=as.double(weapons[ ,2]), facing=as.double(weapons[ ,3]),
                        tracking_arc=as.double(weapons[ ,4]),spread=as.double(weapons[ ,5]),min_mean=0,max_mean=0)
  # compute min and max means for weapons
 
  for (i in 1:length(weapons[,1])) {
    weapons[i, 6] <- min_max_mean(weapons[i, ])[1]
    weapons[i, 7] <- min_max_mean(weapons[i, ])[2]
  }
 
  angles <- seq(-179,180)
 
  dps_at_angles <- angles
  for (i in 1:360) {
    dps_at_angles[i] <- sum_auc(dps_at_angles[i], sd, weapons, ship, range)
  }
 
 
  #we use a separate vector to keep track of angle, since vector index 1 corresponds to angle -179 now
  x_axis <- seq(-179,180)
 
  #find the optimum angle by selecting the midmost of those cells that have the highest dps,
  #and from the vector x_axis the angle corresponding to that cell
  #use rounding to avoid errors from numerical math
  optimum_angle <- x_axis[which(round(dps_at_angles,3) == round(max(dps_at_angles),3))
                          [ceiling(length(which(round(dps_at_angles,3) == round(max(dps_at_angles),3)))/2)]]
 
  #calculate ship upper bounds
  upper_bounds <- generate_ship_upper_bounds(ship, range)
 
  #calculate and report the distributions for weapons, round for human readability
  for (i in 1:length(weapons[,1])){
    print(paste0(weapons[i,1],":"))
    print(round(hit_distribution_at_optimum_angle(optimum_angle,sd,upper_bounds,weapons[i,],range),3))
  }
 
  #testing section - not to be implemented in final code
  #print a graph of the distribution and our choice of angle
  print(weapons)
  plot(dps_at_angles, x=x_axis)
  abline(v=optimum_angle)
 
}

#sample runs
main(ship, 1000, 50, weapons[1:6,])

Edit: add a wraparound to the "which is closer" check because it is needed there too.
« Last Edit: January 15, 2023, 12:04:10 AM 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 #402 on: January 15, 2023, 12:18:01 AM »

Personally I would want to be using dot products of vectors to find angles between things. For instance, the dot product between the vector pointing from our ship to the target, and the vector pointing along the center of the weapon arc is equal to the cosine of the angle between those vectors, and if that angle is greater than 1/2 the weapon arc, then the target is not in the weapon arc. The definition of arccos where the domain is limited avoids all the 'wrap around' issues.
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #403 on: January 15, 2023, 12:47:27 AM »

That seems quite smart. Let's see how that would work. A weapon is pointing at 180 and the enemy ship is at -135 degrees. A vector pointing to the weapon is (-1,0) and one pointing to the enemy ship is (-1,-1). The dot product of these is 1. The length of vector 1 is 1 and of vector 2 is sqrt(2). The angle between vectors is arccos(1/sqrt(2)) = 45. Then if we assume that the weapon's tracking arc is 90 or more we are within it. If not then we calculate the angle between the ship and the upper and lower bounds of the tracking arc using the same logic and then return the one that is closer. So that is another way to do it. Can't be bothered to write it though because this already works but maybe that would have been cleaner since no special cases.

If Liral is feeling inspired to write this instead then here is how I would do the vector based algorithm in full noting that we use unit vectors:
1: find the unit vectors pointing to the enemy ship, to the gun's arc midpoint, max mean, and min mean (if the angle is alpha, they are (cos(alpha),sin(alpha))).
2: compute arccos of the dot product of arc midpoint (call it alpha) and target (call it theta), so arccos(sin(alpha)*sin(theta)+cos(alpha)*cos(theta)). If this is less than half of weapon's tracking arc, return theta
3: if that is not the case, repeat the process for the angles of the max mean and min mean, and return max mean if the angle to that is smaller, else min mean.

Well, actually having written it like that, it seemed to simplify things enough for Liral to be worth doing regardless, so here is the code and it produces the same output. Thanks i_p!
Code
transform_hit_coord <- function(angle, weapon){
  angle_rad <- angle * pi/180
  facing_rad <- weapon$facing * pi / 180
  angle_to_weapon <- acos(sin(angle_rad)*sin(facing_rad)+cos(angle_rad)*cos(facing_rad))*( 180 / pi )
  if(weapon$spread > weapon$tracking_arc) return(weapon$facing)
  if(angle_to_weapon <= (weapon$tracking_arc-weapon$spread)/2) return(angle)
  max_mean_rad <- weapon$max_mean * pi /180
  min_mean_rad <- weapon$min_mean * pi /180
  angle_to_min <- acos(sin(angle_rad)*sin(min_mean_rad)+cos(angle_rad)*cos(min_mean_rad))
  angle_to_max <- acos(sin(angle_rad)*sin(max_mean_rad)+cos(angle_rad)*cos(max_mean_rad))
  if(angle_to_max >= angle_to_min) return(weapon$min_mean)
  return(weapon$max_mean)
}

Note that the change to weapon min and max angles with the wraparound correction that I wrote above should still be made to the min_max_mean function. It is not necessary for this in fact, but it is still not good to have illegal values in a table in general.

Oh, almost forgot, documentation:

transform_hit_coord
input: an angle and a weapon, containing the cols facing, min mean and max mean
output: angle of the mean of the weapon's shot distribution as the weapon attempts to target the target
method: find the (unsigned) angle between angles (from the horizontal) alpha and beta by
arccos(sin(alpha)sin(beta)+cos(alpha)cos(beta))
find the angle from target to facing
If target is within tracking range/2 - spread/2 (since spread will kick the weapon away from the edge)
from facing weapon can track target and therefore returns target angle
Else returns whichever is closer to target, min mean or max mean
Special case: if spread>tracking range the weapon is poorly defined and can't track, because recoil can make the shot go anywhere despite tracking.
In this case return facing, the midpoint of the distribution
« Last Edit: January 15, 2023, 07:47:16 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 #404 on: January 15, 2023, 04:59:34 PM »

Also, the vector between ships is just the difference between the position vectors of the ships, and the magnitude of that difference vector is the range. So if you represent ship positions somewhere in the code (even if it is just static), that makes the calculations pretty trivial, and also sets up for considering movement in the future if we ever want to do that.

The vector pointing along the center of the weapon arc should also not be too complicated. I'm not sure how weapon arcs/mounts are defined in the simulation (or in the ship files), but if you know the angle to a vector from your coordinate system x axis, the corresponding unit vector is just [cos(angle) sin(angle)]. In general, you should be able to pre-define the weapon arc vector relative to a ship-fixed coordinate system, and then use the ship facing angle to rotate the vector into the global coordinate system (where the position vectors of the ships are defined) so that you can compute the dot product.
Logged
Pages: 1 ... 25 26 [27] 28 29 ... 32