Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Pages: 1 ... 28 29 [30] 31 32

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

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #435 on: January 19, 2023, 12:45:08 PM »

Oh, there we go!  Sorry I missed it.  Here's the relevant Python code before adding your change:

Code
def transformed_hit_coordinate(
        target_positional_angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the mean of the distribution of the probability of a weapon
    to hit a target positioned at an angle.
   
    target_positional_angle - angle from the direction the shooter faces
                              to the direction from the shooter to the
                              target
    minimum_mean - minimum mean hit probability of this weapon
    maximum_mean - maximum mean hit probability of this weapon
    """
    #weapon arc does not include 180 to  - 179
    if maximum_mean >= minimum_mean:
        if minimum_mean <= angle <= maximum_mean:
            return target_positional_angle
        a = abs(target_positional_angle - maximum_mean)
        b = abs(target_positional_angle - minimum_mean)
        return (minimum_mean if min(a, 360 - a) > min(b, 360 - b) else
                maximum_mean)
    #weapon arc includes 180 to  - 179 but does not cross 0
    if maximum_mean <= 0 <= minimum_mean:
        if angle < 0:
            if angle < maximum_mean: return angle
            if (abs(target_positional_angle - minimum_mean)
                >= abs(target_positional_angle - maximum_mean)):
                return maximum_mean
            return minimum_mean
        if target_positional_angle > minimum_mean:
            return target_positional_angle
        if (abs(target_positional_angle - minimum_mean)
            >= target_positional_angle - maximum_mean):
            return maximum_mean
        return minimum_mean
       
    #weapon arc includes 0 and 180 to  - 179
    if maximum_mean <= angle <= minimum_mean:
        if target_positional_angle < 0:
            if (abs(target_positional_angle - maximum_mean)
                > abs(target_positional_angle - minimum_mean)):
                return minimum_mean
            return maximum_mean
        return (minimum_mean if target_positional_angle - maximum_mean
                                > abs(target_positional_angle - minimum_mean)
                else maximum_mean)
    return target_positional_angle


def transformed_angle(
        target_positional_angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle, relative to the direction the shooter faces, pointing to
    the mean of the distribution of the probability of a weapon to hit a target
    positioned at an angle relative to the direction the shooter faces.
   
    target_positional_angle - angle from the direction the shooter faces to
                              to the direction from the shooter target to the
                              target
    minimum_mean - minimum mean hit probability of this weapon
    maximum_mean - maximum mean hit probability of this weapon
    """
    return target_positional_angle - transformed_hit_coordinate(
        target_positional_angle, minimum_mean, maximum_mean)

Your change is that, if the weapon has a 360 degree tracking arc, then transformed_hit_coordinate should return the target_positional_angle.  If so, then transformed_angle should return target_positional_angle - target_positional_angle = 0, so we could instead type

Code
angle_difference = (0 if weapon.slot["arc"] == 360 else transformed_angle(
    target_positional_angle, minimum_mean, maximum_mean)

Because transforming the angle would be unnecessary for a weapon of 360 degree tracking, right?  Also, I wish we could remember the purpose of each of these two functions should we ever review or change them.  I suspect it has something to do with tracking arcs, though not strongly enough to have included it in the documentation, so would you please remind me?

Glad the test results match, too!

Thanks for patiently cooperating with my quest for edge cases because every one we catch now is one fewer to hear about, and hope to fix, from a user's bug report later.  8)

Ok, just to be sure, here's my code and the result:

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
import math



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 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 * math.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 / math.pi
   
   
def minimum_mean(spread: float, angle: float, arc: float):
    """
    Return the minimum mean hit probability of a weapon in a slot.

    spread - of the weapon
    facing - of the slot
    tracking_range - of the slot
    """
    minimum_mean = angle - (arc - spread) / 2
    if minimum_mean > 180: minimum_mean -= 360
    elif minimum_mean < -179: minimum_mean += 360
    return minimum_mean
   
   
def maximum_mean(spread: float, angle: float, arc: float):
    """
    Return the maximum mean hit probability of a weapon in a slot.

    spread - of the weapon
    facing - of the slot
    tracking_range - of the slot
    """
    maximum_mean = angle + (arc - spread) / 2
    if maximum_mean > 180: maximum_mean -= 360
    elif maximum_mean < -179: maximum_mean += 360
    return maximum_mean
   
   
def minimum_and_maximum_means(weapons: tuple) -> tuple:
    """
    Reurn the minimum and maximum means of a 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"])
    return minimum_means, maximum_means
   

def transformed_hit_coordinate(
        target_positional_angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the mean of the distribution of the probability of a weapon
    to hit a target positioned at an angle.
   
    target_positional_angle - angle from the direction the shooter faces
                              to the direction from the shooter to the
                              target
    minimum_mean - minimum mean hit probability of this weapon
    maximum_mean - maximum mean hit probability of this weapon
    """
    #weapon arc does not include 180 to  - 179
    if maximum_mean >= minimum_mean:
        if minimum_mean <= target_positional_angle <= maximum_mean:
            return target_positional_angle
        a = abs(target_positional_angle - maximum_mean)
        b = abs(target_positional_angle - minimum_mean)
        return (minimum_mean if min(a, 360 - a) > min(b, 360 - b) else
                maximum_mean)
    #weapon arc includes 180 to  - 179 but does not cross 0
    if maximum_mean <= 0 <= minimum_mean:
        if target_positional_angle < 0:
            if target_positional_angle < maximum_mean:
                return target_positional_angle
            if (abs(target_positional_angle - minimum_mean)
                >= abs(target_positional_angle - maximum_mean)):
                return maximum_mean
            return minimum_mean
        if target_positional_angle > minimum_mean:
            return target_positional_angle
        if (abs(target_positional_angle - minimum_mean)
            >= target_positional_angle - maximum_mean):
            return maximum_mean
        return minimum_mean
       
    #weapon arc includes 0 and 180 to  - 179
    if maximum_mean <= target_positional_angle <= minimum_mean:
        if target_positional_angle < 0:
            if (abs(target_positional_angle - maximum_mean)
                > abs(target_positional_angle - minimum_mean)):
                return minimum_mean
            return maximum_mean
        return (minimum_mean if target_positional_angle - maximum_mean
                                > abs(target_positional_angle - minimum_mean)
                else maximum_mean)
    return target_positional_angle


def transformed_angle(
        target_positional_angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle of the target relative to this weapon.
   
    target_positional_angle - angle from the direction the shooter faces to
                              to the direction from the shooter target to the
                              target
    minimum_mean - minimum mean hit probability of this weapon
    maximum_mean - maximum mean hit probability of this weapon
    """
    return target_positional_angle - transformed_hit_coordinate(
        target_positional_angle, minimum_mean, maximum_mean)


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_distance)) / 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_within(bounds[j],
                                           standard_deviation,
                                           spread_distance)
                    - probability_hit_within(bounds[j-1],
                                             standard_deviation,
                                             spread_distance)
                    for j in range(1, len(bounds)))
            + ((1 - probability_hit_within(bounds[-1], standard_deviation,
                                           spread_distance)),))


def probability_hit_at_angle(
        weapon_angle: float,
        spread_angle: float,
        target_angular_size: float,
        target_positional_angle: float,
        target_positional_angle_error: float) -> float:
    """
    Return the probability for a weapon of this spread and facing this
    direction to hit a target of this angular size positioned at this angle
    with this standard deviation thereof.
   
    weapon_angle - direction the weapon is pointing (-179, 180)
    spread_angle - uniform angular dispersion of weapon shot facing
    target_angular_size - size of the target in degrees
    target_positional_angle - location of the target (-179, 180)
    target_positional_angle_error - standard deviation of
                                    target_positional_angle
    """
    upper_bound = weapon_angle + target_angular_size
    lower_bound = weapon_angle - target_angular_size
    return (probability_hit_within(upper_bound, target_positional_angle_error,
                                   spread_angle)
            - probability_hit_within(lower_bound, target_positional_angle_error,
                                     spread_angle))
                         
                                           
def total_probable_weapon_score_at_angles(
        weapons: tuple,
        minimum_means: tuple,
        maximum_means: tuple,
        target_width: float,
        distance: float,
        standard_deviation: float) -> tuple:                                       
    #now, for angles -359 to 360 (all possible signed angles)
    target_positional_angles = tuple(i for i in range(-179, 180))
    target_angular_size = arc_to_deg(target_width, distance) / 2
    target_positional_angle_error = arc_to_deg(standard_deviation, distance)
    weapon_scores = tuple(WEAPON_SCORES[weapon["size"], weapon["pd"]]
                          for weapon in weapons)
    total_probable_weapon_score_at_angles = []
    for target_positional_angle in target_positional_angles:
        total_probable_weapon_score = 0
        for i, weapon in enumerate(weapons):
            weapon_angle = (0 if weapon.slot["arc"] == 360
                            else transformed_angle(target_positional_angle,
                                              minimum_means[i],
                                              maximum_means[i]))
            probability = probability_hit_at_angle(weapon_angle,
                weapon["spread"], target_angular_size, target_positional_angle,
                target_positional_angle_error)
            total_probable_weapon_score += probability * weapon_scores[i]
        total_probable_weapon_score_at_angles.append(
            total_probable_weapon_score)
    return total_probable_weapon_score_at_angles


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, 5) 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 optimum_angle(
        weapons: tuple,
        minimum_means: tuple,
        maximum_means: tuple,
        target_width: float,
        distance: float,
        standard_deviation: float) -> float:
    #we use a separate vector to keep track of angle, since vector
    #index 1 corresponds to angle -179 now
    x_axis = range(-179, 180)
    optimum_angle_index = middle_index_of_approximate_maxima(
        total_probable_weapon_score_at_angles(weapons, minimum_means,
        maximum_means, target_width, distance, standard_deviation))
    return x_axis[optimum_angle_index]
   
   
def distribution(
        spread: float,
        minimum_mean: float,
        maximum_mean: float,
        target_positional_angle: float,
        bounds: tuple,
        distance: float,
        standard_deviation: float) -> tuple:
    """
    Return the probability of a weapon of this spread to hit between each
    pair of the bounds of an armor row positioned at this angle and
    distance
   
    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.
   
    spread - of the weapon
    minimum_mean - minimum_mean of this weapon's probable score at this
                    angle
    maximum_mean - maximum_mean of this weapon's probable score at this
                    angle
    bounds - of the armor grid cells of the target
    distance - range to target
    standard deviation - of target position
    """
    spread_distance = deg_to_arc(weapon["spread"], distance)
    angle_difference = (0 if weapon.slot["arc"] == 360 else transformed_angle(
        target_positional_angle, minimum_mean, maximum_mean))
    adjustment = deg_to_arc(angle_difference, distance)
    adjusted_bounds = tuple(bound + adjustment for bound in bounds)
    distribution = hit_distribution(adjusted_bounds, standard_deviation,
                                    spread_distance)
    return distribution
   

if __name__ == "__main__":
    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, cells_across: int, cell_size):
            self.cells = [[i for i in range(cells_across)]]
            self.bounds = tuple(i * cell_size for i in range(cells_across))
           
    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, "LARGE", TestSlot(-10.0, 20.0)),
                    TestWeapon(5.0, False, "LARGE", TestSlot(10.0, 20.0)),
                    TestWeapon(0.0, False, "LARGE", TestSlot(-160.0, 20.0)),
                    TestWeapon(0.0, False, "LARGE", TestSlot(180.0, 20.0)),
                    TestWeapon(0.0, False, "LARGE", TestSlot(160.0, 20.0)),
                    TestWeapon(5.0, False, "LARGE", TestSlot(120.0, 90.0)),)

    test_target = TestTarget(220)
    test_target.armor_grid = TestArmorGrid(12, 10)
    standard_deviation = 50
    distance = 1000
    print("spread, arc, angle")
    for weapon in test_weapons:
        print(weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
    print()
    minimum_means, maximum_means = minimum_and_maximum_means(test_weapons)
    print("minimum and maximum means")
    for min_mean, max_mean in zip(minimum_means, maximum_means):
        print(min_mean, max_mean)
    print()
    optimum_angle = optimum_angle(test_weapons, minimum_means, maximum_means,
                                  test_target["width"], distance,
                                  standard_deviation)
    print("Optimum Angle:", optimum_angle)
    print()
    bounds = test_target.armor_grid.bounds
    print("Bounds")
    print(tuple(round(bound, 3) for bound in bounds))
    print()
    distributions = tuple(distribution(weapon["spread"], minimum_means[i],
                                       maximum_means[i], optimum_angle,
                                       test_target.armor_grid.bounds, distance,
                                       standard_deviation)
                         for i, weapon in enumerate(test_weapons))
    print("Distributions")
    for distribution in distributions:
        print(tuple(round(probability, 3) for probability in distribution))
[close]
Result
spread, arc, angle
5.0 20.0 -10.0
5.0 20.0 10.0
0.0 20.0 -160.0
0.0 20.0 180.0
0.0 20.0 160.0
5.0 90.0 120.0

minimum and maximum means
-17.5 -2.5
2.5 17.5
-170.0 -150.0
170.0 -170.0
150.0 170.0
77.5 162.5

Optimum Angle: 167

Bounds
(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110)

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)
(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)
(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.241, 0.045, 0.048, 0.05, 0.052, 0.052, 0.053, 0.052, 0.051, 0.049, 0.046, 0.043, 0.219)
(0.5, 0.053, 0.052, 0.05, 0.048, 0.046, 0.042, 0.038, 0.034, 0.03, 0.025, 0.021, 0.061)
(0.859, 0.03, 0.026, 0.021, 0.017, 0.013, 0.01, 0.007, 0.005, 0.004, 0.002, 0.002, 0.002)
[close]
« Last Edit: January 19, 2023, 04:44:30 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #436 on: January 19, 2023, 07:50:15 PM »

Here is what the functions do: transform hit coord - finds the mean of the shot distribution when the gun tracks the target
transformed angle - finds the signed angle from this mean to the weapon

So here might be more reasonable names for them:
transform hit coord -> mean when tracking
transformed angle -> angle from mean

Looks like the change broke something in your code because the result is no longer the same unfortunately. Also it occurs to me that there is another super nice exception to the exception: if somebody has specified a spread of 360 then a gun in a 360 arc mount still can't track. Or it might? Here is where I actually don't know what happens though: in the previous we have assumed the gun can never point outside its arc, leading to that a gun with spread > arc can't track (the recoil is more likely to kick it in the other direction if it tracks, re-aligning the mean with facing*). But if we have a gun with 360 arc and 360 recoil, and the target is to one side, can the recoil spin the gun around a full circle? If it can then no change should be made, the gun can in fact track, although it is irrelevant because shots are still completely random. If it cannot then the gun cannot track (recoil is more likely to kick it to the other side) and the exception that if spread>= tracking angle return facing should be moved above the step that if tracking angle ==360 return target angle. This change is not really necessary though unless we are using the mean angle for something, since shots are fully random with 360 spread.

*This follows from the additional assumption that the recoil/spread means "select a permissible angle for the shot from a uniform distribution with width equal to spread", to be clear. A physical force wouldn't work this way but would instead still permit tracking and instead stress the mount when it hits an impermissible angle.
« Last Edit: January 19, 2023, 08:35:59 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #437 on: January 19, 2023, 08:33:40 PM »

Here is what the functions do: transform hit coord - finds the mean of the shot distribution when the gun tracks the target
transformed angle - finds the signed angle from this mean to the weapon

I could use some more explanation about mean_when_tracking because I'm not sure what "finds the mean of the shot distribution when the gun tracks the target" means.

What is the difference between the results?

I wonder how this new exception would apply to, say, a gun with a 5 degree tracking arc and 15 degree spread, too.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #438 on: January 19, 2023, 09:22:45 PM »

Sorry, typo.

transformed angle - finds the signed angle from this mean to the weapon


Should read

transformed angle - finds the signed angle from this mean to the target

My bad

Anyway, so it all goes back to what we think about spread. I chose to assume spread is constant (else guns would be more accurate near the edge of their arc). Additionally I chose to assume guns can't go beyond their arc (else we could have "pseudo-tracking" beyond the maximum arc due to recoil repeatedly kicking the gun past its maximum angle).

Taken together these assumptions inevitably mean that the mean of the angle distribution of shots can't reach the edge of the gun's arc. Why? If it could go closer than spread/2, but the gun can't go over its arc, the sprrad would narrow at the edge.

Note that this is talking about the mean. If the mean cannot go closer than spread/2 then that means that with 0 sd individual shots can only go to the arc's edge and no further, exactly and if it were otherwise they could go beyond the arc.

So getting back to your gun with 5 arc and 15 spread: say that the arc is from 0 to 5. This touches on the special case that the spread is greater than the arc. In this case we must let go of one of the assumptions above. I relaxed the assumption that shots don't go beyond the arc (and kept that spread is constant). Now we have a choice of what to do. Should the gun pseudo-track in the opposite direction? Let's say target is at +10. In that case we would get a mean angle for the dist of -5. This seems unreasonable. So I kept what vestige we can of not going beyond the arc and say that this gun cannot track at all (its dist mean is entirely determined by spread but stays within the arc). So we get a mean of 2.5 and individual shots can go from +17.5 to -12.5.

Note that we could make different assumptions and end with different results. For example if we permit pseudo-tracking beyond the arc (shots can generally go arbitrarily far beyond it even with SD=0) then the dist mean can align with arc edge. Or, in the special case, if we held on to shots not going beyond the arc, then the spread would shrink to 5 deg for this gun in this mount instead.

I suppose that while means are hard to test in the game, the special case could be tested empirically. Mod in a gun with say a 180 deg spread. Put it in a turret mount with a 5 degree arc. Will its spread be 5 or 180? This would tell us what is the realistic thing to do here, since the above is based on reasoning only. This experiment could also rule in or out pseudo-tracking: give the gun a rapid rate of fire. Will the gun fire successive shots to one side (pseudo-tracking, the dist mean can go beyond the arc) or will they be randomly distributed to both sides with no runs of shots to the one side (the dist mean stays centered despite shots going over the arc). Liral, you know how to mod, can you try this? Ideally you would test the significance of the randomness with a runs test, I can do that for you if you want and feel it can't be estimated visually - just need the sequence of shots and which side they went to for statistics.

(I mean, I suppose the alternative would be to ask Alex or someone who knows how spread and turret arcs interact exactly)
« Last Edit: January 19, 2023, 10:07:36 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #439 on: January 19, 2023, 09:53:34 PM »

Thanks for explaining your concern about the over-spread weapon, and sure, I can try modding a weapon to have a 5 degree tracking arc and 180 degree spread.  I understand you want to know what the spread would be and whether the distribution of shots would stay centered--is that so?

Also, you missed two questions. 

1. What should the name and docstring of each function (currently named transformed_angle and transformed_coordinate) be?  I don't understand these short one-sentence descriptions you've written, and I think a follow-up paragraph could elaborate, but I don't know what to write in that paragraph because I still don't quite understand the purpose of either function.

2. How do my new results differ from yours?  The big change I made was to replace the call to the upper_bounds function with the armor grid bounds themselves, perhaps skewing the results.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #440 on: January 19, 2023, 11:09:33 PM »

Well, let's say we have a weapon. Its shots are spread over a range of angles with an associated probability distribution. This probability distribution will have a mean. Tracking the target means trying to align this mean with the target. Transform_hit_coord finds where the mean is, from our ship's perspective, as the gun does so. Transformed_angle finds the signed angle from this mean to the target (negative - mean is above target, positive - mean is below target).

I suggest the new names could be transform_hit_coord -> mean_while_tracking and transformed_angle -> angle_from_mean.

Your results are quite different in many dimensions -
Results now

spread, arc, angle
5.0 20.0 -10.0
5.0 20.0 10.0
0.0 20.0 -160.0
0.0 20.0 180.0
0.0 20.0 160.0
5.0 90.0 120.0

minimum and maximum means
-17.5 -2.5
2.5 17.5
-170.0 -150.0
170.0 -170.0
150.0 170.0
77.5 162.5

Optimum Angle: 167

Bounds
(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110)

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)
(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)
(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.241, 0.045, 0.048, 0.05, 0.052, 0.052, 0.053, 0.052, 0.051, 0.049, 0.046, 0.043, 0.219)
(0.5, 0.053, 0.052, 0.05, 0.048, 0.046, 0.042, 0.038, 0.034, 0.03, 0.025, 0.021, 0.061)
(0.859, 0.03, 0.026, 0.021, 0.017, 0.013, 0.01, 0.007, 0.005, 0.004, 0.002, 0.002, 0.002)


Our previous found common result:


spread, arc, angle
5.0 20.0 -10.0
5.0 20.0 10.0
0.0 20.0 -160.0
0.0 20.0 180.0
0.0 20.0 160.0
5.0 90.0 120.0

minimum and maximum means
-17.5 -2.5
2.5 17.5
-170.0 -150.0
170.0 -170.0
150.0 170.0
77.5 162.5

Optimum Angle: 167

Bounds
(-110.0, -91.667, -73.333, -55.0, -36.667, -18.333, 0.0, 18.333, 36.667, 55.0, 73.333, 91.667, 110.0)

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.008, 0.01, 0.018, 0.029, 0.043, 0.059, 0.073, 0.085, 0.092, 0.096, 0.095, 0.091, 0.082, 0.219)
(0.061, 0.041, 0.056, 0.071, 0.083, 0.092, 0.096, 0.096, 0.092, 0.083, 0.071, 0.056, 0.041, 0.061)
(0.338, 0.093, 0.096, 0.095, 0.09, 0.08, 0.067, 0.052, 0.037, 0.024, 0.014, 0.007, 0.004, 0.002)


It appears the bounds have become quite different here.

Quote
Thanks for explaining your concern about the over-spread weapon, and sure, I can try modding a weapon to have a 5 degree tracking arc and 180 degree spread.  I understand you want to know what the spread would be and whether the distribution of shots would stay centered--is that so?

Yes, that's exactly it. If the current theory is correct then 1) the spread will be 180, rather than 5 and 2) shots will be randomly to both sides without runs to either side (ie. will stay centered). If we deviate from this can modify theory.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #441 on: January 20, 2023, 05:57:58 AM »

I had changed the bounds formula to be the cell size times the cell number for the number of cells plus one.  Hardcoding the bounds to what they were before, I got the same result as before.
spread, arc, angle
5.0 20.0 -10.0
5.0 20.0 10.0
0.0 20.0 -160.0
0.0 20.0 180.0
0.0 20.0 160.0
5.0 90.0 120.0

minimum and maximum means
-17.5 -2.5
2.5 17.5
-170.0 -150.0
170.0 -170.0
150.0 170.0
77.5 162.5

Optimum Angle: 167

Bounds
(-110.0, -91.667, -73.333, -55.0, -36.667, -18.333, 0.0, 18.333, 36.667, 55.0, 73.333, 91.667, 110.0)

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)
(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)
(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.241, 0.045, 0.048, 0.05, 0.052, 0.052, 0.053, 0.052, 0.051, 0.049, 0.046, 0.043, 0.219)
(0.5, 0.053, 0.052, 0.05, 0.048, 0.046, 0.042, 0.038, 0.034, 0.03, 0.025, 0.021, 0.061)
(0.859, 0.03, 0.026, 0.021, 0.017, 0.013, 0.01, 0.007, 0.005, 0.004, 0.002, 0.002, 0.002)

What should the armor bounds formula be?  The difference between armor cell bounds seems to be 18 1/3, and that difference times the number of cells, 12, equals twice the rightmost number.  So, the position of each bound should be its index times the bound distance minus half the product of the bound distance and the number of cells.
Code
number_of_cells = 12
cell_width = 18 + 1/3
right_bound = number_of_cells * cell_width / 2
tuple(i * cell_width - right_bound for i in range(number_of_cells + 1)
>>(-109.99980000000001, -91.66646666666668, -73.33313333333334, -54.99980000000001, -36.66646666666668, -18.33313333333335, 0.00019999999999242846, 18.333533333333307, 36.66686666666665, 55.00019999999999, 73.3335333333333, 91.66686666666665, 110.00019999999999)
which, rounded to three decimal places, are our previous bounds within rounding error.
(-110.0, -91.666, -73.333, -55.0, -36.666, -18.333, 0.0, 18.334, 36.667, 55.0, 73.334, 91.667, 110.0)
Adding this code to the test and changing the armor cell width to 18 + 1/3 yields this result, which I believe to be the same as it was before.
spread, arc, angle
5.0 20.0 -10.0
5.0 20.0 10.0
0.0 20.0 -160.0
0.0 20.0 180.0
0.0 20.0 160.0
5.0 90.0 120.0

minimum and maximum means
-17.5 -2.5
2.5 17.5
-170.0 -150.0
170.0 -170.0
150.0 170.0
77.5 162.5

Optimum Angle: 167

Bounds
(-110.0, -91.667, -73.333, -55.0, -36.667, -18.333, 0.0, 18.333, 36.667, 55.0, 73.333, 91.667, 110.0)

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.008, 0.01, 0.018, 0.029, 0.043, 0.059, 0.073, 0.085, 0.092, 0.096, 0.095, 0.091, 0.082, 0.219)
(0.061, 0.041, 0.056, 0.071, 0.083, 0.092, 0.096, 0.096, 0.092, 0.083, 0.071, 0.056, 0.041, 0.061)
(0.338, 0.093, 0.096, 0.095, 0.09, 0.08, 0.067, 0.052, 0.037, 0.024, 0.014, 0.007, 0.004, 0.002)
If you agree that they are the same, then I will add a results-check to the test.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #442 on: January 20, 2023, 06:42:16 AM »

Yes, these results are identical.

The formula to generate upper bounds is:
First bound = -ship width / 2
Increment = ship width / number of cells
Number of bounds = number of cells + 1
Bound 2 = Bound 1 + Increment
Bound n = Bound n-1 + Increment

So, example: dominator
First bound = - 220/2 = -110
Increment = 220 / 12 = 55/3 = 18 and 1/3 (18.33333...)
Second bound = -110 + 18 and 1/3 = -91 and 2/3
Third bound = -91 and 2/3 + 18 and 1/3 = -73 and 1/3

and so forth. So you calculated it correctly.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #443 on: January 20, 2023, 11:06:18 AM »

Uh-oh, bad news.  That 10 pixel armor cell size we've used in testing isn't possible because the armor cell size formula is
Code
cell_size = 15 if height < 150 else height/10 if height < 300 else 30
You'll have to run your armor grid generator code again with the new formula and post the results.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #444 on: January 20, 2023, 11:37:52 AM »

Hm? The Dominator height is given as 180 px in dominator.ship. So its cells should be 18 px wide. Still, good catch, 18 px is not 18.333... px.

I guess I really don't know how it works after all, because look at this thing:


It has 12 cells across. This should not be possible if it is 220 wide, 180 tall, and therefore 1 cell is 18 px wide. Any clues how to interpret this?

Anyway the formula above to generate ship cells is saying that
First bound = -ship width / 2 #the first cell starts at the left edge of the ship
Increment = ship width / number of cells #if you divide the ship's width by how many cells it has across you get the width of one cell
Number of bounds = number of cells + 1 #every cell has one upper bound, and in addition a lower bound, which is not another cell's upper bound for one cell only. these are all the boundaries of the cells of the ship (in one row in the horizontal direction)
Bound 2 = Bound 1 + Increment #the next cell follows after the width of the previous cell
Bound n = Bound n-1 + Increment

Looking at that and then looking at the picture, I guess step #2 might be wrong because cells can be partial and in that case you would not get the width of one complete cell from the division. Then the algorithm should instead read like this:

Start at 0
If the ship has an odd number of cells, start with a vector containing two boundaries at half the size of one cell to either side. (e.g. [-9 9]), if an even number, with one containing 0 ([0])
While you are below shipwidth/2 (above -shipwidth/2) concatenate to right (to left) the value of the previous index +cell width (next index -cell width)
(e.g. [-99 -81 -63 -45 -27 -9 9 27 45 63 81 99])
Concatenate shipwidth/2 to right (-shipwidth/2 to left)
(e.g [-110 -99 -81 -63 -45 -27 -9 9 27 45 63 81 99 110])

However that does not explain what is going on here. By the formula there should be one extra partial cell on either side instead (applying the above algorithm we'd get [-110 -108 -90 -72 -54 -36 -18 0 18 36 54 72 90 108 110] for the Dommy. So let's figure that out before writing more code for a change. I was thinking maybe the game drops "sliver" cells only a few px wide but that doesn't seem to be the case (look closely at the image).
« Last Edit: January 20, 2023, 12:10:34 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #445 on: January 20, 2023, 12:56:48 PM »

If the cell is 18 pixels wide and ship 220, then the armor grid of the ship should be 220 / 18 = 12.2222 cells across.  My code rounds the cell count down to the nearest integer, yielding 12 as seen in-game.

Code
width, height = 220, 180
cell_size = 15 if height < 150 else height / 10 if height < 300 else 300
cells_across = int(width / cell_size)
print(cells_across)
12
« Last Edit: January 20, 2023, 01:05:59 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #446 on: January 20, 2023, 10:05:00 PM »

I have an alternative proposal based on close observation of the sprite. Note that the final cell is partial, not whole. This is consistent with the armor grid only extending to the collision bound (in the dominator's case -102, 103) rather than to the edge of the sprite. Sorry, I am a little short on time now, so not much time to expain. Here, however, is the code based on this idea.


generate_ship_upper_bounds <- function(left_collision_bound, right_collision_bound, length){
  #this operation centers the target to a whole pixel
  ship_lower_bound <- -floor((abs(left_collision_bound)+abs(right_collision_bound))/2)
  ship_upper_bound <- ceiling((abs(left_collision_bound)+abs(right_collision_bound))/2)
  cell_size <- 15
  #we assume there can be no fractions of a pixel, so floor
  if(length >= 150) cell_size <- floor(length / 10)
  if(length >= 300) cell_size <- 30
  #we have enough cells to reach exactly or go over the collision bounds, soceil
  no_cells <- ceiling((ship_upper_bound-ship_lower_bound)/cell_size)
  #the starting vector differs for odd and even counts
  if(no_cells %% 2 == 0) ub_vector <- c(0)
  #we add a floor and ceiling operation here to keep to integers - equivalent to us aiming at a whole pixel
  else ub_vector <- c(-floor(cell_size/2), ceiling(cell_size/2))
  #concatenate positive upper bounds
  while(ub_vector[length(ub_vector)] + cell_size < ship_upper_bound) ub_vector <- c(ub_vector, ub_vector[length(ub_vector)]+cell_size)
  ub_vector <- c(ub_vector, ship_upper_bound)
  #concatenate negative upper bounds
  while(ub_vector[1] - cell_size > ship_lower_bound) ub_vector <- c(ub_vector[1]-cell_size, ub_vector)
  ub_vector <- c(ship_lower_bound, ub_vector)
  return(ub_vector)
}


Output for parameters from dominator.ship


> generate_ship_upper_bounds(-102,103,180)
 [1] -102  -90  -72  -54  -36  -18    0   18   36   54   72   90  103


To-do: take a screenshot of the game at native resolution with the Dominator in the lower left horizontal and measure pixels to see if this is exactly correct.

Here is R code without loops that might be computationally advantageous but may not translate cleanly, however you can see if something similar can be done in Py. Provided we believe this is the correct method to generate bounds, that is.


generate_ship_upper_bounds <- function(left_collision_bound, right_collision_bound, length){
  #this operation centers the target to a whole pixel
  ship_lower_bound <- -floor((abs(left_collision_bound)+abs(right_collision_bound))/2)
  ship_upper_bound <- ceiling((abs(left_collision_bound)+abs(right_collision_bound))/2)
  cell_size <- 15
  #we assume there can be no fractions of a pixel, so floor
  if(length >= 150) cell_size <- floor(length / 10)
  if(length >= 300) cell_size <- 30
  #we have enough cells to reach exactly or go over the collision bounds, soceil
  no_cells <- ceiling((ship_upper_bound-ship_lower_bound)/cell_size)
  #the starting vector differs for odd and even counts
  if(no_cells %% 2 == 0) ub_vector <- c(0)
  #we add a floor and ceiling operation here to keep to integers - equivalent to us aiming at a whole pixel
  else ub_vector <- c(-floor(cell_size/2), ceiling(cell_size/2))
  #concatenate upper bounds
  ub_vector <- unique(c(ship_lower_bound,
  rev(-seq(-ub_vector[1],-ship_lower_bound,cell_size)),
  ub_vector,
  seq(ub_vector[length(ub_vector)],ship_upper_bound,cell_size),
  ship_upper_bound))
  return(ub_vector)
}
« Last Edit: January 20, 2023, 11:28:46 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #447 on: January 21, 2023, 12:22:07 AM »

Before trying to write partial cell code, we should ask Alex if partial cells exist.

CapnHector

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

Oh that's easy, just look at the in-game image I posted above. Doesn't it clearly have partial cells? Or do you mean that they can actually be hit like full cells despite looking like they're less than?

I sent a PM to Alex about whether I have it right here and how the partial cells can be hit.
« Last Edit: January 21, 2023, 12:59:47 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 #449 on: January 21, 2023, 04:36:18 AM »



So here are the pixel coordinates in this snapshot of the in-game interface according to MSPaint. Note that in this we have the ship width as 65 or 64 and 1 cell is 11 px wide except the most lateral is 9 px. If we scale this to the actual width we get a scaling factor of 102/64 or 103/65, so we get these boundaries:


64, scaling factor 102/64
> bound <- 64
> width <- 102
> round(c(seq(0,bound,11),bound)*width/bound,2)
[1]   0.00  17.53  35.06  52.59  70.12  87.66 102.00

65, scaling factor 102/65
> bound <- 65
> width <- 102
> round(c(seq(0,bound,11),bound)*width/bound,2)
[1]   0.00  17.26  34.52  51.78  69.05  86.31 102.00

64, scaling factor 103/64
> bound <- 64
> width <- 103
> round(c(seq(0,bound,11),bound)*width/bound,2)
[1]   0.00  17.70  35.41  53.11  70.81  88.52 103.00

65, scaling factor 103/65
> bound <- 65
> width <- 103
> round(c(seq(0,bound,11),bound)*width/bound,2)
[1]   0.00  17.43  34.86  52.29  69.72  87.15 103.00

64.5, scaling factor 102.5/64.5
> round(c(seq(0,bound,11),bound)*width/bound,2)
[1]   0.00  17.48  34.96  52.44  69.92  87.40 102.50


It appears that no matter which values we choose for this it won't line up with the results above. However, what if we base this on the length of the ship according to collision bounds rather than the stated length? According to dominator.ship the real ship length (that is the y distance between the highest and lowest points of the ship, from the gun placements to the pointy rear) appears to be 97-(-72)=169 rather than 180.

Plugging this into the upper bounds function above we get this

> generate_ship_upper_bounds(-102,103,169)
 [1] -102  -93  -76  -59  -42  -25   -8    9   26   43   60   77   94  103

Yeah that's obviously not right either.

We could obviously insert an arbitrary scaling factor other than 10, say we divide 180 by 10.3 or 169 by 9.5 to get the real values, but that is very confusing and likely to go wrong because a) we don't know which value to use and b) why would Alex choose a value like that?

Ideas?

If we do accept the version with a first bound of 17.7 then we are very close with
> generate_ship_upper_bounds(-102,103,180)
 [1] -102  -90  -72  -54  -36  -18    0   18   36   54   72   90  103
though

(the measured result there is

> bound <- 64
> width <- 103
> round(c(seq(0,bound,11),bound)*width/bound,2)
[1]   0.00  17.70  35.41  53.11  70.81  88.52 103.00
)
« Last Edit: January 21, 2023, 04:39:43 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge
Pages: 1 ... 28 29 [30] 31 32