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: Planet Search Overhaul (07/13/24)

Pages: 1 ... 27 28 [29] 30 31 32

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

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #420 on: January 17, 2023, 04:00:57 AM »

You'll have no argument from me - if you are doing that, then that is the way to go. In fact when trying to model ships moving about (back on page 7, seems like an eternity ago) I used x and y coordinates myself. Here it just seemed more natural to do it this way for the angle finding algorithm because we are literally only considering angles so I didn't even think about this stuff or about modeling ships moving about more generally, but rather just did the first thing that came to mind like usual MO. Whether we'll include real ship movement is, I believe, an unresolved issue at this time, but yes, do it that way when the time comes.

We have a github now so also if you feel like programming some ship movement related stuff as a separate track to this thing then I believe that is/will be, at least soon, when Liral gets the py-version of the first draft together, possible?
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 726
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #421 on: January 17, 2023, 01:22:11 PM »

I don't know if I can translate this angle code because I'm slowly getting lost in the incremental edits to it.  My code yields a graph, which is flat except three narrow, high peaks at -60, 22, and 100 degrees; the peak at -60 degrees reverses on itself at its middle.  It also prints intermediate the results of minimum and maximum means, bounds, and the optimum angle.  Would you please compare your intermediate results to mine so I can find where my code is going wrong?

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_and_maximum_means(
        spread: float,
        angle: float,
        arc: float) -> tuple:
    """
    Return the minimum and maximum 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 - (spread + arc) / 2
    maximum_mean = angle + (spread - arc) / 2
    if minimum_mean > 180: minimum_mean -= 360
    elif minimum_mean < -179: minimum_mean += 360
    if maximum_mean > 180: maximum_mean -= 360
    elif maximum_mean < -179: maximum_mean += 360
    return minimum_mean, maximum_mean
   

def transformed_hit_coordinate(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    #weapon arc does not include 180 to  - 179
    if(maximum_mean >= minimum_mean):
        if(angle >= minimum_mean and angle <= maximum_mean): return(angle)
        if(min(abs(angle - maximum_mean),360 - abs(angle - maximum_mean))
           > min(abs(angle - minimum_mean),360 - abs(angle - minimum_mean))):
            return minimum_mean
        return maximum_mean
    #weapon arc includes 180 to  - 179 but does not cross 0
    if(maximum_mean <= 0 and minimum_mean >= 0):
        if(angle < 0 and angle < maximum_mean): return(angle)
        if(angle >= 0 and angle > minimum_mean): return(angle)
        if(angle >= 0 and angle <= minimum_mean):
            if(abs(angle - minimum_mean) >= angle - maximum_mean):
                return maximum_mean
            return minimum_mean
        if(angle < 0 and angle >= maximum_mean):
            if(abs(angle - minimum_mean) >= abs(angle - maximum_mean)):
                return maximum_mean
            return minimum_mean
    #weapon arc includes 0 and 180 to  - 179
    if(angle < 0 and angle <= minimum_mean and angle >= maximum_mean):
        if(abs(angle - maximum_mean) > abs(angle - minimum_mean)):
            return minimum_mean
        return maximum_mean
    if(angle >= 0 and angle <= minimum_mean and angle >= maximum_mean):
        if(angle - maximum_mean > abs(angle - minimum_mean)):
            return minimum_mean
        return maximum_mean
    return angle


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 - transformed_hit_coordinate(angle, minimum_mean, maximum_mean)


def upper_bounds(width: 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 - how many cells across the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * pi * distance
    ship_angle = width / c
    cell_angle = ship_angle / width
    angles = [-ship_angle / 2]
    for i in range(width): angles.append(angles[-1] + cell_angle)
    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_mean, maximum_mean = minimum_and_maximum_means(
                weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
            minimum_means.append(minimum_mean)
            maximum_means.append(maximum_mean)
        else:
            minimum_means.append(weapon.slot["angle"])
            maximum_means.append(weapon.slot["angle"])
    print()
    print("spread, arc, angle")
    for weapon in weapons:
        print(weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
    print()
    print("minimum and maximum means")
    for min_mean, max_mean in zip(minimum_means, maximum_means):
        print(min_mean, max_mean)
    print()
    #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(len(target.armor_grid.cells[0]), distance)
    print("Bounds")
    print(tuple(round(bound, 3) for bound in bounds))
    print()
    distributions = []
    for i, weapon in enumerate(weapons):
        spread_distance = deg_to_arc(weapon["spread"], distance)
        angle_difference = transformed_angle(optimum_angle,
                                             minimum_means[i],
                                             maximum_means[i])
        adjustment = deg_to_arc(angle_difference, distance)
        adjusted_bounds = tuple(bound + adjustment for bound in bounds)
        distributions.append(hit_distribution(adjusted_bounds,
                                              standard_deviation,
                                              spread_distance))
    return distributions

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, 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, "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)

    for distribution in distributions(test_weapons, test_target, 1000, 50):
        print(tuple(round(element, 3) for element 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
22.5 27.5
12.5 17.5
100.0 100.0
-70.0 -70.0
-60.0 -60.0
27.5 32.5

Optimum Angle: 22

Bounds
(-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)

(0.423, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.005, 0.514)
(0.839, 0.004, 0.003, 0.003, 0.003, 0.003, 0.003, 0.003, 0.003, 0.003, 0.003, 0.003, 0.003, 0.122)
(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)
(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.077, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.003, 0.003, 0.003, 0.003, 0.003, 0.893)
[close]

CapnHector

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

Sure. Here are the results using your weapon parameters. The graph has three symmetrical sharp peaks (in shape similar to previous - sorry, mobile, so saving and posting graph is a little tricky. I did get my computer back) in the middle, near +180 and near -179. We select the right peak which is highest.


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] 0.826 0.006 0.006 0.006 0.006 0.006 0.006 0.006 0.006 0.006 0.006 0.006
[13] 0.006 0.104
            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          120     90     75.0    105.0


Remember, only the transform hit coord function and the min and max means were changed, and in addition remove the sum operation at the end (-359 to 360 thing). This I think is still present in your code. You translated the logical and I will check it later, but I'd recommend translating the vector (inteinsic_parity's) version of the transform hit coord function since it is simpler to translate I think (less switches)

Here is the full code with the vector version of the calculation.
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, 120, 90, 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){
  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)
}
#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]

Remember you can run this at rdrr.io or other websites online yourself just copypasting it there to get any graphs and tests you like.
« Last Edit: January 18, 2023, 12:22:18 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 #423 on: January 17, 2023, 10:43:28 PM »

All right Liral, working with programiz.com to run Python online, I was able to fix some but not all bugs. First of all, you were passing incorrect parameters to the minimum and maximum mean function on line 263. Second, you had reversed the + and - operations when adding brackets to the min_max_mean function (adding brackets to try to simplify code is dangerous it seems!). So now it prints a "close but not right" output so there is likely something wrong in the distribution because I was unable to find any errors in the transform_hit_coordinate function. I had also parsed your test weapon incorrectly above, so here are corrected results for the R.

Here is the python, unfortunately I had to remove graphing code to use the online tool:
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_and_maximum_means(
        spread: float,
        angle: float,
        arc: float) -> tuple:
    """
    Return the minimum and maximum 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
    maximum_mean = angle + (arc - spread) / 2
    if minimum_mean > 180: minimum_mean -= 360
    elif minimum_mean < -179: minimum_mean += 360
    if maximum_mean > 180: maximum_mean -= 360
    elif maximum_mean < -179: maximum_mean += 360
    return minimum_mean, maximum_mean
   

def transformed_hit_coordinate(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    #weapon arc does not include 180 to  - 179
    if(maximum_mean >= minimum_mean):
        if(angle >= minimum_mean and angle <= maximum_mean): return(angle)
        if(min(abs(angle - maximum_mean),360 - abs(angle - maximum_mean))
           > min(abs(angle - minimum_mean),360 - abs(angle - minimum_mean))):
            return minimum_mean
        return maximum_mean
    #weapon arc includes 180 to  - 179 but does not cross 0
    if(maximum_mean <= 0 and minimum_mean >= 0):
        if(angle < 0 and angle < maximum_mean): return(angle)
        if(angle >= 0 and angle > minimum_mean): return(angle)
        if(angle >= 0 and angle <= minimum_mean):
            if(abs(angle - minimum_mean) >= angle - maximum_mean):
                return maximum_mean
            return minimum_mean
        if(angle < 0 and angle >= maximum_mean):
            if(abs(angle - minimum_mean) >= abs(angle - maximum_mean)):
                return maximum_mean
            return minimum_mean
    #weapon arc includes 0 and 180 to  - 179
    if(angle < 0 and angle <= minimum_mean and angle >= maximum_mean):
        if(abs(angle - maximum_mean) > abs(angle - minimum_mean)):
            return minimum_mean
        return maximum_mean
    if(angle >= 0 and angle <= minimum_mean and angle >= maximum_mean):
        if(angle - maximum_mean > abs(angle - minimum_mean)):
            return minimum_mean
        return maximum_mean
    return angle


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 - transformed_hit_coordinate(angle, minimum_mean, maximum_mean)


def upper_bounds(width: 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 - how many cells across the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * pi * distance
    ship_angle = width / c
    cell_angle = ship_angle / width
    angles = [-ship_angle / 2]
    for i in range(width): angles.append(angles[-1] + cell_angle)
    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_mean, maximum_mean = minimum_and_maximum_means(
                weapon["spread"], weapon.slot["angle"], weapon.slot["arc"])
            minimum_means.append(minimum_mean)
            maximum_means.append(maximum_mean)
        else:
            minimum_means.append(weapon.slot["angle"])
            maximum_means.append(weapon.slot["angle"])
    print()
    print("spread, arc, angle")
    for weapon in weapons:
        print(weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
    print()
    print("minimum and maximum means")
    for min_mean, max_mean in zip(minimum_means, maximum_means):
        print(min_mean, max_mean)
    print()
    #now, for angles -359 to 360 (all possible signed angles, calculate dps)
    target_positional_angles = tuple(i for i in range(-179, 180))
 
    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)
 
 
   
    #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(
        weapon_score_at_angles)
    optimum_angle = x_axis[optimum_angle_index]
    print("Optimum Angle:", optimum_angle)
    print()
    bounds = upper_bounds(len(target.armor_grid.cells[0]), distance)
    print("Bounds")
    print(tuple(round(bound, 3) for bound in bounds))
    print()
    distributions = []
    for i, weapon in enumerate(weapons):
        spread_distance = deg_to_arc(weapon["spread"], distance)
        angle_difference = transformed_angle(optimum_angle,
                                             minimum_means[i],
                                             maximum_means[i])
        adjustment = deg_to_arc(angle_difference, distance)
        adjusted_bounds = tuple(bound + adjustment for bound in bounds)
        distributions.append(hit_distribution(adjusted_bounds,
                                              standard_deviation,
                                              spread_distance))
    return distributions

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, 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, "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)

    for distribution in distributions(test_weapons, test_target, 1000, 50):
        print(tuple(round(element, 3) for element in distribution))
[close]

Output

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: 169
Bounds
(-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.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)
(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.32, 0.007, 0.007, 0.007, 0.007, 0.007, 0.007, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.591)
(0.452, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.008, 0.452)
(0.934, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.002, 0.045)


(correct output:)

[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.005 0.009 0.020 0.039 0.066 0.098 0.126 0.144 0.143 0.124 0.094 0.063
[13] 0.037 0.032
[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] 0.518 0.095 0.090 0.081 0.068 0.054 0.039 0.025 0.015 0.008 0.004 0.002
[13] 0.001 0.000
            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    120           90      5     77.5    162.5
[1] 169



Edit: I found another bug (or difference from my code). Your ship width function is incorrect. You must return the pixel bounds for the ship. Here is the correct output:

 [1] -1.100000e+02 -9.166667e+01 -7.333333e+01 -5.500000e+01 -3.666667e+01
 [6] -1.833333e+01  1.634938e-14  1.833333e+01  3.666667e+01  5.500000e+01
[11]  7.333333e+01  9.166667e+01  1.100000e+02


Your output was in degrees. However, if I ask it to convert deg to arc I still get the wrong output:


    return tuple(deg_to_arc(angle * c,distance) for angle in angles)

Output:

Bounds
(-104.72, -87.266, -69.813, -52.36, -34.907, -17.453, 0.0, 17.453, 34.907, 52.36, 69.813, 87.266, 104.72)


If I give it the correct information manually by writing

def upper_bounds(width: 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 - how many cells across the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * pi * distance
    ship_angle = 0.03501409
    cell_angle = 0.002917841
    angles = [-ship_angle / 2]
    for i in range(width): angles.append(angles[-1] + cell_angle)
    return tuple(angle * c for angle in angles)


Then it returns the correct output:

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)


But there is still something quite wrong with the complete output, though PD gun #3 is correct 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(-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)

(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.001, 0.001, 0.004, 0.01, 0.022, 0.041, 0.069, 0.101, 0.129, 0.144, 0.142, 0.122, 0.091, 0.124)
(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)
(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)


Anyway based on experience I feel like I should point out that it seems like the elegance gained from putting in brackets and small substitutions like c = 2*pi*distance may not be worth the time it may take to fix bugs if any occur, but this is of course personal preference.

Anyway, it seems like there is a problem somewhere in the optimum angle selection, because if in addition to the correct ship parameters I also give it the correct angle manually

    optimum_angle = 169


then the output is right.

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: 169

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)

(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.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)
(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)
(0.518, 0.095, 0.09, 0.081, 0.068, 0.054, 0.039, 0.025, 0.015, 0.008, 0.004, 0.002, 0.001, 0.0)
>


All right found one problem in the upper bounds function:

You wrote

def upper_bounds(width: 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 - how many cells across the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * pi * distance
    ship_angle = width / (c)
    cell_angle = ship_angle / width
    angles = [-ship_angle / 2]
    for i in range(width): angles.append(angles[-1] + cell_angle)
    return tuple(angle * c for angle in angles)


when the correct is divide by number of cells rather than width. However, this does not fix the code so there must be something else too.

Anyway so all in all it seems like the distribution and angle functions are in fact correct because it prints the correct output when manually fed the correct ship parameters and correct optimum angle, so it seems like the angle selection function (maybe the sum function?) has an error and the ship upper bounds function should probably be rewritten as it must have more than one error and is a relatively short one. Unfortunately I was unable to completely resolve these issues at this time.

Here is my final testing code:
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_and_maximum_means(
        spread: float,
        angle: float,
        arc: float) -> tuple:
    """
    Return the minimum and maximum 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
    maximum_mean = angle + (arc - spread) / 2
    if minimum_mean > 180: minimum_mean -= 360
    elif minimum_mean < -179: minimum_mean += 360
    if maximum_mean > 180: maximum_mean -= 360
    elif maximum_mean < -179: maximum_mean += 360
    return minimum_mean, maximum_mean
   

def transformed_hit_coordinate(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    #weapon arc does not include 180 to  - 179
    if(maximum_mean >= minimum_mean):
        if(angle >= minimum_mean and angle <= maximum_mean): return(angle)
        if(min(abs(angle - maximum_mean),360 - abs(angle - maximum_mean))
           > min(abs(angle - minimum_mean),360 - abs(angle - minimum_mean))):
            return minimum_mean
        return maximum_mean
    #weapon arc includes 180 to  - 179 but does not cross 0
    if(maximum_mean <= 0 and minimum_mean >= 0):
        if(angle < 0 and angle < maximum_mean): return(angle)
        if(angle >= 0 and angle > minimum_mean): return(angle)
        if(angle >= 0 and angle <= minimum_mean):
            if(abs(angle - minimum_mean) >= angle - maximum_mean):
                return maximum_mean
            return minimum_mean
        if(angle < 0 and angle >= maximum_mean):
            if(abs(angle - minimum_mean) >= abs(angle - maximum_mean)):
                return maximum_mean
            return minimum_mean
    #weapon arc includes 0 and 180 to  - 179
    if(angle < 0 and angle <= minimum_mean and angle >= maximum_mean):
        if(abs(angle - maximum_mean) > abs(angle - minimum_mean)):
            return minimum_mean
        return maximum_mean
    if(angle >= 0 and angle <= minimum_mean and angle >= maximum_mean):
        if(angle - maximum_mean > abs(angle - minimum_mean)):
            return minimum_mean
        return maximum_mean
    return angle


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 - transformed_hit_coordinate(angle, minimum_mean, maximum_mean)


def upper_bounds(width: 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 - how many cells across the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * pi * distance
    ship_angle = 0.03501409
    cell_angle = 0.002917841
    angles = [-ship_angle / 2]
    for i in range(width): angles.append(angles[-1] + cell_angle)
    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, 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 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_mean, maximum_mean = minimum_and_maximum_means(
                weapon["spread"], weapon.slot["angle"], weapon.slot["arc"])
            minimum_means.append(minimum_mean)
            maximum_means.append(maximum_mean)
        else:
            minimum_means.append(weapon.slot["angle"])
            maximum_means.append(weapon.slot["angle"])
    print()
    print("spread, arc, angle")
    for weapon in weapons:
        print(weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
    print()
    print("minimum and maximum means")
    for min_mean, max_mean in zip(minimum_means, maximum_means):
        print(min_mean, max_mean)
    print()
    #now, for angles -359 to 360 (all possible signed angles, calculate dps)
    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_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)
 
 
   
    #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(
        weapon_score_at_angles)
    optimum_angle = x_axis[optimum_angle_index]
    print("Optimum Angle:", optimum_angle)
    print()
    bounds = upper_bounds(len(target.armor_grid.cells[0]), distance)
    print("Bounds")
    print(tuple(round(bound, 3) for bound in bounds))
    print()
    distributions = []
    for i, weapon in enumerate(weapons):
        spread_distance = deg_to_arc(weapon["spread"], distance)
        angle_difference = transformed_angle(optimum_angle,
                                             minimum_means[i],
                                             maximum_means[i])
        adjustment = deg_to_arc(angle_difference, distance)
        adjusted_bounds = tuple(bound + adjustment for bound in bounds)
        distributions.append(hit_distribution(adjusted_bounds,
                                              standard_deviation,
                                              spread_distance))
    return distributions

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, 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, "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)

    for distribution in distributions(test_weapons, test_target, 1000, 50):
        print(tuple(round(element, 3) for element in distribution))
[close]
« Last Edit: January 17, 2023, 11:43:01 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 726
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #424 on: January 18, 2023, 04:54:10 AM »

I've fixed the upper_bounds function and replaced probability_hit_between calls with explicit differences of probability_hit_within calls.  If I set optimum_angle to 169, I get your result.  Also, the name of the upper_bounds function confuses me because I don't know what it's supposed to return.  It calculates angle bounds but then multiples them all by 2 * pi * distance right at the end, so I think those are bounds along the arc.

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 transformed_hit_coordinate(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    #weapon arc does not include 180 to  - 179
    if maximum_mean >= minimum_mean:
        if minimum_mean <= angle <= maximum_mean: return angle
        a, b = abs(angle - maximum_mean), abs(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(angle - minimum_mean) >= abs(angle - maximum_mean):
                return maximum_mean
            return minimum_mean
        if angle > minimum_mean: return angle
        if abs(angle - minimum_mean) >= angle - maximum_mean:
            return maximum_mean
        return minimum_mean
       
    #weapon arc includes 0 and 180 to  - 179
    if maximum_mean <= angle <= minimum_mean:
        if angle < 0:
            if abs(angle - maximum_mean) > abs(angle - minimum_mean):
                return minimum_mean
            return maximum_mean
        return (minimum_mean if angle - maximum_mean > abs(angle - minimum_mean)
                else maximum_mean)
    return angle


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 - transformed_hit_coordinate(angle, minimum_mean, maximum_mean)


def angle_bounds(
        cells_across: int,
        pixels_across: float,
        distance: float) -> tuple:
    """
    Return the angle of the upper bound of each cell of this armor
    grid 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.
   
    cells_across - how many cells wide the armor grid of the ship is
    pixels_across - how many pixels wide the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * math.pi * distance
    ship_angle = pixels_across / c
    cell_angle = ship_angle / cells_across
    angles = [-ship_angle / 2]
    for i in range(cells_across): angles.append(angles[-1] + cell_angle)
    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_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 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 distributions(
        weapons: tuple,
        target: tuple,
        distance: float,
        standard_deviation: float) -> tuple:
    """
    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"])
    print()
    print("spread, arc, angle")
    for weapon in weapons:
        print(weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
    print()
    print("minimum and maximum means")
    for min_mean, max_mean in zip(minimum_means, maximum_means):
        print(min_mean, max_mean)
    print()
    #now, for angles -359 to 360 (all possible signed angles, calculate dps)
    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_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])
            upper_angle = angle + target_angular_size
            lower_angle = angle - target_angular_size
            probability = (probability_hit_within(upper_angle,
                                                  standard_deviation,
                                                  weapon["spread"])
                           - probability_hit_within(lower_angle,
                                                    standard_deviation,
                                                    weapon["spread"]))
            weapon_score += (WEAPON_SCORES[weapon["size"], weapon["pd"]]
                             * probability)
        weapon_score_at_angles.append(weapon_score)
 
    #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(
        weapon_score_at_angles)
    optimum_angle = 169#x_axis[optimum_angle_index]
    print("Optimum Angle:", optimum_angle)
    print()
    bounds = upper_bounds(len(target.armor_grid.cells[0]), target["width"],
                          distance)
    print("Bounds")
    print(tuple(round(bound, 3) for bound in bounds))
    print()
    print("Distributions")
    distributions = []
    for i, weapon in enumerate(weapons):
        spread_distance = deg_to_arc(weapon["spread"], distance)
        angle_difference = transformed_angle(optimum_angle,
                                             minimum_means[i],
                                             maximum_means[i])
        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)
        print(tuple(round(probability, 3) for probability in distribution))
        distributions.append(distribution)
    return distributions

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):
            self.cells = [[i 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)

    distributions(test_weapons, test_target, 1000, 50)
       
[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: 169

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.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)
(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)
(0.518, 0.095, 0.09, 0.081, 0.068, 0.054, 0.039, 0.025, 0.015, 0.008, 0.004, 0.002, 0.001, 0.0)
[/spoiler=Result]
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #425 on: January 18, 2023, 05:01:15 AM »

Well, uh, apologies for my terrible function naming practices. Anyway let's say a ship is like this:

(-infinity,a)(a,b)(b,c)(c,d)(d,+infinity)

where those are coordinates along the x axis and corresponding intervals that represent cells. The first and last cell represent misses. The upper bounds function is supposed to return (a,b,c,d) (that is, all finite upper bounds of intervals) and then we calculate hit probabilities as: probability to miss below = probability coordinate less than a. Probability to hit cell 1 = probability coordinate less than b - probability coordinate less than a. etc. except for the last cell it is probability to miss above = 1 - probability coordinate less than d. Anyway that is why it is called upper bounds. Thinking about it I suppose you could just as well call it lower bounds, since picking the left bound of each interval where finite would give the same result. I thought about upper bounds specifically because we calculate the probability using a "less than" I guess.

This must be very close because you are getting the correct result when the optimum angle is right, so it's just a matter of figuring out why the optimum angle becomes wrong. it is not wrong - see below.

I found another thing: in this latest code on lines 292 and 295 it read standard_deviation, but it should read target_positional_angle_error (because that is in degrees, standard_deviation is in pixels)

Even after that, there is definitely something weird going on here. Here are your results for weapon score at indices 340:350


    print(weapon_score_at_angles[340:350])


[14.166934372136467, 14.891770867986182, 15.759365378507708, 16.55467822599273, 17.158811464253947, 17.498336565310417, 17.54829121987713, 17.33444568907221, 16.913947930559214, 16.347135239482807]
Optimum Angle: 167


Here are mine:

[341]  9.031166e+00  9.203635e+00  9.493293e+00  9.830988e+00  1.022583e+01
[346]  1.066577e+01  1.108756e+01  1.138932e+01  1.146283e+01  1.123129e+01


Now let's actually compute manually what is the correct weapon score at angle 169: it is, according to our distributions,

(1-(0.005+0.032))*7+(1-(0.014+0.014))*7+(1-(0.518))*7=16.919

And at weapon angle 167, which your code recommends:
(1-(0.125))*7+(1-(0.014+0.014))*7+(1-(0.340))*7=17.549


So, in fact, your code is right, not mine, here, and it's pointless to keep looking for more answers since that is the answer to the final missing degrees problem. It looks like my code has some issue here instead.

Because the manual calculations agree with your code's score I would declare it complete at this point. I have been going over it too and was unable to find any errors, that's why I turned to the possibility that the original code is wrong. Sorry about wasting your time with this stuff, although the end result is good because bugs were fixed and the code is double checked.
« Last Edit: January 18, 2023, 07:51:10 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 726
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #426 on: January 18, 2023, 12:05:34 PM »

Well, uh, apologies for my terrible function naming practices. Anyway let's say a ship is like this:

(-infinity,a)(a,b)(b,c)(c,d)(d,+infinity)

where those are coordinates along the x axis and corresponding intervals that represent cells. The first and last cell represent misses. The upper bounds function is supposed to return (a,b,c,d) (that is, all finite upper bounds of intervals) and then we calculate hit probabilities as: probability to miss below = probability coordinate less than a. Probability to hit cell 1 = probability coordinate less than b - probability coordinate less than a. etc. except for the last cell it is probability to miss above = 1 - probability coordinate less than d. Anyway that is why it is called upper bounds. Thinking about it I suppose you could just as well call it lower bounds, since picking the left bound of each interval where finite would give the same result. I thought about upper bounds specifically because we calculate the probability using a "less than" I guess.

Thanks for the explanation because I understand better now.  So, if the function returns the pixel distance across each cell, and if we have a formula for cell size, then why have we used this function to calculate it rather than sum cell sizes directly?
Code: Direct Sum
def bounds(cells_across: int, cell_size: float) -> tuple: return tuple(cell_size * i for i in range(cells_across + 1))

>> bounds(2, 5)
>> (0, 2, 4, 6, 8, 10, 12)
Quote
This must be very close because you are getting the correct result when the optimum angle is right, so it's just a matter of figuring out why the optimum angle becomes wrong. it is not wrong - see below.

I found another thing: in this latest code on lines 292 and 295 it read standard_deviation, but it should read target_positional_angle_error (because that is in degrees, standard_deviation is in pixels)

Even after that, there is definitely something weird going on here. Here are your results for weapon score at indices 340:350


    print(weapon_score_at_angles[340:350])


[14.166934372136467, 14.891770867986182, 15.759365378507708, 16.55467822599273, 17.158811464253947, 17.498336565310417, 17.54829121987713, 17.33444568907221, 16.913947930559214, 16.347135239482807]
Optimum Angle: 167


Here are mine:

[341]  9.031166e+00  9.203635e+00  9.493293e+00  9.830988e+00  1.022583e+01
[346]  1.066577e+01  1.108756e+01  1.138932e+01  1.146283e+01  1.123129e+01


Now let's actually compute manually what is the correct weapon score at angle 169: it is, according to our distributions,

(1-(0.005+0.032))*7+(1-(0.014+0.014))*7+(1-(0.518))*7=16.919

And at weapon angle 167, which your code recommends:
(1-(0.125))*7+(1-(0.014+0.014))*7+(1-(0.340))*7=17.549


So, in fact, your code is right, not mine, here, and it's pointless to keep looking for more answers since that is the answer to the final missing degrees problem. It looks like my code has some issue here instead.

Because the manual calculations agree with your code's score I would declare it complete at this point. I have been going over it too and was unable to find any errors, that's why I turned to the possibility that the original code is wrong. Sorry about wasting your time with this stuff, although the end result is good because bugs were fixed and the code is double checked.

Woah, I wasn't expecting that!  So, here's the latest version of the code and its results.
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(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    #weapon arc does not include 180 to  - 179
    if maximum_mean >= minimum_mean:
        if minimum_mean <= angle <= maximum_mean: return angle
        a, b = abs(angle - maximum_mean), abs(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(angle - minimum_mean) >= abs(angle - maximum_mean):
                return maximum_mean
            return minimum_mean
        if angle > minimum_mean: return angle
        if abs(angle - minimum_mean) >= angle - maximum_mean:
            return maximum_mean
        return minimum_mean
       
    #weapon arc includes 0 and 180 to  - 179
    if maximum_mean <= angle <= minimum_mean:
        if angle < 0:
            if abs(angle - maximum_mean) > abs(angle - minimum_mean):
                return minimum_mean
            return maximum_mean
        return (minimum_mean if angle - maximum_mean > abs(angle - minimum_mean)
                else maximum_mean)
    return angle


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 - transformed_hit_coordinate(angle, minimum_mean, maximum_mean)


def upper_bounds(
        cells_across: int,
        pixels_across: float,
        distance: float) -> tuple:
    """
    Return the angle of the upper bound of each cell of this armor
    grid 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.
   
    cells_across - how many cells wide the armor grid of the ship is
    pixels_across - how many pixels wide the armor grid of the ship is
    distance - range to the ship
    """
    c = 2 * math.pi * distance
    ship_angle = pixels_across / c
    cell_angle = ship_angle / cells_across
    angles = [-ship_angle / 2]
    for i in range(cells_across): angles.append(angles[-1] + cell_angle)
    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_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 = 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,
        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 weapons
    minimum_means - minimum_mean of this weapon's probable score at this
                    angle
    maximum_means - maximum_mean of this weapon's probable score at this
                    angle
    distance - range to target
    standard deviation - of target position
    """
    spread_distance = deg_to_arc(weapon["spread"], distance)
    angle_difference = transformed_angle(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):
            self.cells = [[i 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)
    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 = upper_bounds(len(test_target.armor_grid.cells[0]),
                              test_target["width"], distance)
    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, 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
(-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)
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #427 on: January 18, 2023, 07:21:45 PM »

Oh, probably because a) I tried to put everything in a function like you said and b) spaghetti code in the actual function (maybe it came from when I was going to write it using.degrees rather than px) but there is no real reason to do it using angles rather than pixels. Do it however you think is best, it's a very simple calculation.
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 #428 on: January 18, 2023, 09:41:38 PM »

I was able to find and fix the problem in my original code. It was one of those dumb, simple mistakes that seem to be able to hide in plain sight forever, until you look at the output carefully.

I had written

  shipwidth <- arc_to_deg(ship[5], range)/2

and

    ship_upper_bound <- transformed_angle(angle,weapons[i,])+shipwidth / 2
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth / 2


As a result, the ship width got divided by 2 two times, so we were using one-quarter ship width where it should have been one-half. When this is fixed (remove one of the /2:s) then the output is


[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.001 0.001 0.004 0.010 0.022 0.041 0.069 0.101 0.129 0.144 0.142 0.122
[13] 0.091 0.124
[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] 0.338 0.093 0.096 0.095 0.090 0.080 0.067 0.052 0.037 0.024 0.014 0.007
[13] 0.004 0.002
            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    120           90      5     77.5    162.5
[1] optimum angle: 167



And for the weapon score at angles we get

[341]  1.416693e+01  1.489177e+01  1.575937e+01  1.655468e+01  1.715881e+01
[346]  1.749834e+01  1.754829e+01  1.733445e+01  1.691395e+01  1.634714e+01


That is, we now get the exact same output. So I would call this section finished. Nice!

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, 120, 90, 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){
  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)
}
#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
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth
   
    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)
  print(optimum_angle)
print(dps_at_angles)
}

#sample runs
main(ship, 1000, 50, weapons[1:6,])
[close]
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 726
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #429 on: January 19, 2023, 02:49:49 AM »

Fantastic!  I'm glad you found the root cause of the error.  Now that we agree on the output for these inputs, we need a test that compares the output of this code to these correct answers and, if necessary, tests other inputs to ensure the code works for the general case.  What inputs might we need besides the ones we've tested?

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #430 on: January 19, 2023, 03:54:58 AM »

It's a tricky one if you want to make sure this is everywhere correct, because there is of course no way to compute every possible distribution manually. That shouldn't be necessary either, though, since this code is humanly understandable and if it is correct it is correct. If you want you can first try the set of all the random weapons alongside the normal ones. The correct output for that is (I've added the weapon score at degrees as the final part of the output)


[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 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.046 0.035 0.049 0.064 0.078 0.088 0.094 0.096 0.094 0.088 0.077 0.064
[13] 0.048 0.079
[1] "bb gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "space marine teleporter:"
 [1] 0.391 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018 0.018
[13] 0.018 0.391
[1] "turbolaser:"
 [1] 0.165 0.051 0.054 0.056 0.058 0.058 0.058 0.058 0.058 0.058 0.056 0.054
[13] 0.051 0.165
[1] "hex bolter:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "singularity projector:"
 [1] 0.384 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021
[13] 0.021 0.364
[1] "subspace resonance kazoo:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "left nullspace projector:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "telepathic embarrassment generator:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "perfectoid resonance torpedo:"
 [1] 0.462 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031
[13] 0.030 0.168
[1] "entropy inverter gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "mini-collapsar rifle:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "false vacuum tunneler:"
 [1] 0.342 0.026 0.026 0.026 0.026 0.026 0.026 0.026 0.026 0.026 0.026 0.026
[13] 0.026 0.342
                                 name damage facing tracking_arc spread
1                        right phaser      7    -10           20      5
2                         left phaser      7     10           20      5
3                            pd gun 1      7   -160           20      0
4                            pd gun 2      7    180           20      0
5                            pd gun 3      7    160           20      0
6                      photon torpedo      7    120           90      5
7                              bb gun      5   -150           11     20
8             space marine teleporter     78     69          173     29
9                          turbolaser     92    122          111      9
10                         hex bolter     24   -136           38     20
11              singularity projector     95     28          122     25
12           subspace resonance kazoo     68   -139           12      2
13           left nullspace projector     10     28           54      0
14 telepathic embarrassment generator     30    -31           35      8
15       perfectoid resonance torpedo     34     72           10     17
16               entropy inverter gun     78    -60           13     24
17               mini-collapsar rifle     27     28           16     13
18              false vacuum tunneler     32     78          157     20
   min_mean max_mean
1     -17.5     -2.5
2       2.5     17.5
3    -170.0   -150.0
4     170.0   -170.0
5     150.0    170.0
6      77.5    162.5
7    -150.0   -150.0
8      -3.0    141.0
9      71.0    173.0
10   -145.0   -127.0
11    -20.5     76.5
12   -144.0   -134.0
13      1.0     55.0
14    -44.5    -17.5
15     72.0     72.0
16    -60.0    -60.0
17     26.5     29.5
18      9.5    146.5
[1] optimum angle: 77
  [1]   8.05296159   8.80936083   9.75062973  10.79857489  11.85096854
  [6]  12.81922615  13.65814361  14.37104027  14.98992342  15.54467961
 [11]  16.03857849  16.44121778  16.70130719  16.77400454  16.65078057
 [16]  16.37587872  16.03765223  15.73719315  15.55274478  15.52375760
 [21]  15.66828262  16.02933960  16.73096388  18.01681178  20.24233271
 [26]  23.79890253  28.97314198  35.78562737  43.88729840  52.58557040
 [31]  60.94555674  68.08855516  73.45890747  76.84821685  78.33843279
 [36]  78.14325535  77.17493255  76.28345570  75.55417820  75.02159565
 [41]  74.66976280  74.45174482  74.31374762  74.21202373  74.11895633
 [46]  74.02127299  73.17732760  70.77688626  66.66781195  60.77988165
 [51]  53.29920462  44.75322831  35.94305686  27.74068220  20.84531841
 [56]  15.61434025  12.03622458   9.83140319   8.60813802   7.99566620
 [61]   7.71399671   7.58346127   7.50072518   7.40804323   7.27079097
 [66]   7.06660114   6.78278891   6.41689371   5.97632754   5.47564166
 [71]   4.93229804   4.36291444   3.78152145   3.20012848   2.63074501
 [76]   2.08740226   1.58672148   1.14618210   0.78041367   0.49714189
 [81]   0.29503168   0.16501050   0.09507160   0.07708525   0.11350153
 [86]   0.22181992   0.43485622   0.79538163   1.34542594   2.11337555
 [91]   3.10418956   4.29736017   5.65337928   7.12493410   8.66701458
 [96]  10.24162059  11.81622660  13.35830712  14.82986219  16.18588267
[101]  17.37906009  18.36990426  19.13797309  19.68843768  20.05028664
[106]  20.26705119  20.38477868  20.44251364  20.46800546  20.47814813
[111]  20.48192284  20.48377058  20.48645713  20.49357771  20.51236015
[116]  20.55722452  20.65364001  20.84048026  21.16823828  21.69141739
[121]  22.45661007  23.49155487  24.80160974  26.37653254  28.20382220
[126]  30.27981732  32.61033321  35.19884125  38.02788066  41.04340002
[131]  44.14974377  47.21695872  50.09632676  52.63795191  54.70622200
[136]  56.24723934  57.51247553  58.50184592  59.18736524  59.57513381
[141]  59.70421955  59.63674029  59.44327891  59.19015739  58.93391587
[146]  58.72407202  58.61014795  58.64594306  58.88511135  59.36727672
[151]  60.10070784  61.05169576  62.14866845  63.30111245  64.42461657
[156]  65.46003898  66.37918806  67.17791641  67.86393013  68.44713897
[161]  68.93609283  69.33910462  69.63379497  69.64052408  69.36069182
[166]  68.84799780  68.17459053  67.43017454  66.72059900  66.16312126
[171]  65.87350051  65.94193056  66.40298601  67.21475746  68.26384493
[176]  69.39976143  70.48230738  71.39379110  71.95902694  72.15338694
[181]  71.98673991  71.55553345  70.98368623  70.43413651  69.97464060
[186]  69.65643608  69.52676948  69.61803177  69.93937110  70.47460475
[191]  71.18748631  72.03166793  72.96060463  73.93338698  74.91526046
[196]  75.87452883  76.77919356  77.57457014  78.10246925  78.33649446
[201]  78.29940741  78.03676505  77.60749122  77.07330747  76.49077664
[206]  75.90714106  75.36063204  74.88939474  74.51037812  74.22506193
[211]  74.00971068  73.83555532  73.66289324  73.44589171  73.14073879
[216]  72.71389312  72.14860742  71.44737806  70.62913071  69.72246780
[221]  68.75838417  67.76597853  66.77291090  65.80987907  64.91631069
[226]  64.14342729  63.55150252  63.20112718  63.14287336  63.41292382
[231]  64.04034062  65.06397275  66.54796100  68.58168695  71.25722757
[236]  74.53486961  78.28718599  82.37887205  86.62209579  90.85396744
[241]  94.99829203  99.07726221 103.16730149 107.32587687 111.53068503
[246] 115.66108808 119.52508326 122.91170594 125.64010781 127.58390838
[251] 128.66709744 129.30179417 129.91122320 130.46140637 130.91607833
[256] 131.23843094 131.39498169 131.38137561 131.30892808 131.17553449
[261] 130.95285790 130.61410860 130.14144105 129.53164474 128.79718906
[266] 127.96199598 127.05402848 126.09792346 125.10992285 124.09548566
[271] 123.04885934 121.95415087 120.78827714 119.52628636 118.14836834
[276] 116.64623226 115.02601699 113.30637215 111.51296946 109.67260044
[281] 107.80997167 105.94863331 104.11531894 102.34519151 100.68448190
[286]  99.18731465  97.90579653  96.87630685  96.10813681  95.58040865
[291]  95.24900899  95.05987286  94.96221917  94.91677030  94.89775800
[296]  94.89062561  94.88823046  94.88751152  94.88731886  94.88727282
[301]  94.88726301  94.88726115  94.88726084  94.88726079  94.88726078
[306]  94.88726078  94.88726080  94.88726093  94.88726179  94.88726688
[311]  94.88729342  94.88741626  94.88792009  94.88975153  94.89565200
[316]  94.91250062  94.95514229  95.05079606  95.24097950  95.57613816
[321]  96.09966477  96.82450127  97.71401795  98.68156404  99.61427042
[326] 100.41070902 101.01171114 101.40815891 101.62480725 101.69251316
[331] 101.69223393 101.69133302 101.68876611 101.68232517 101.66814727
[336] 101.64091198 101.59556107 101.53058949 101.45079264 101.36508285
[341] 101.27575359 101.16156349 100.94329854 100.40401700  99.44375326
[346]  98.02651563  96.17712768  93.97613792  91.53504758  88.96616145
[351]  86.36037640  83.77905249  81.26006030  78.37804436  74.72792062
[356]  70.40704128  65.56577243  60.38875933  55.05553227  49.70296410




Then, here is a systematic test of the logic wrt. wraparounds:

weapon1 <- c("right phaser", 7, 0, 360, 0)
weapon2 <- c("left phaser", -7, 0, 350, 0)
weapon3 <- c("pd gun 1", 7, 0, 200, 0)
weapon4 <- c("pd gun 2", -7, 0, 190, 0)


Turn the SD off for this test. What is supposed to happen here: note that weapons 1 and 2 cancel each other out everywhere, except from 180...175 and -179...-175 degrees. There will be a spike of 7 where only weapon 1 is firing. There should be two additional spikes of 7 between 95...100 and -95...-100.


[1] "right phaser:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "left phaser:"
 [1] 0 0 0 0 0 0 1 0 0 0 0 0 0 0
[1] "pd gun 1:"
 [1] 0 0 1 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 2:"
 [1] 0 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      0          360      0      180      180
2  left phaser     -7      0          350      0     -175      175
3     pd gun 1      7      0          200      0     -100      100
4     pd gun 2     -7      0          190      0      -95       95
[1] 105
  [1] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
 [26] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
 [51] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7  0  0
 [76]  0  0  0 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[101] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[126] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[151] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[176] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[201] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[226] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[251] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[276] -7 -7 -7 -7 -7 -7  0  0  0  0  0 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[301] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[326] -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7 -7
[351] -7 -7 -7  0  0  0  0  0  0  0


We can tell we have found one more bug: the code thinks weapon 1 is fixed, because its max mean is identical to min mean, even though its tracking arc is 360!

We fix this by writing this to the start of the transform_hit_coord function:
  if(weapon$tracking_arc==360) return(angle)


This, however, does not fix the issue by itself. Now we get this:


[1] "right phaser:"
 [1] 0 0 0 0 0 0 1 0 0 0 0 0 0 0
[1] "left phaser:"
 [1] 0 0 0 0 0 0 1 0 0 0 0 0 0 0
[1] "pd gun 1:"
 [1] 0 0 0 0 0 0 0 0 1 0 0 0 0 0
[1] "pd gun 2:"
 [1] 0 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      0          360      0      180      180
2  left phaser     -7      0          350      0     -175      175
3     pd gun 1      7      0          200      0     -100      100
4     pd gun 2     -7      0          190      0      -95       95
[1] -102
  [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
 [38] 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 7
 [75] 7 7 7 7 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
[112] 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
[149] 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
[186] 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
[223] 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
[260] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 7 7 7 7 0 0 0 0 0 0 0 0 0 0
[297] 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
[334] 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



Why? Well, actually I have no idea. Geometry! *shakes fist at Euclid* I am starting to feel like I don't want to hear about angles for a while after this.

Edit: I wrote a long post here thinking about vectors and having to rewrite the code, and I even did rewrite it using vectors, but it didn't make a difference, but then I finally figured it out and it is not actually wrong what you see there. Here is the reason: these are weapons of spread 0. The ship's width is 12.6 degrees. We only consider whether we can hit target ship for weapon score, not how well we hit it. There is no effect at the edge of the ship BECAUSE the weapon worth +7 and -7 can both track it there! When the ship is at 180 degrees then it can still be hit from +175 degrees and we do not have a spread for this weapon so no consideration is made for how well it is hit.

So it is actually correct this way. And further, we can now with this knowledge assume that, if we give all the weapons a spread of 13 degrees then the boundaries at the edges will appear.


weapon1 <- c("right phaser", 7, 0, 360, 0)
weapon2 <- c("left phaser", -7, 0, 350, 0)
weapon3 <- c("pd gun 1", 7, 0, 200, 0)
weapon4 <- c("pd gun 2", -7, 0, 190, 0)


And so they do.

[1] "right phaser:"
 [1] 0.258 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.258
[1] "left phaser:"
 [1] 0.258 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.258
[1] "pd gun 1:"
 [1] 0.005 0.005 0.009 0.014 0.019 0.025 0.030 0.034 0.037 0.039 0.040 0.040 0.040 0.661
[1] "pd gun 2:"
 [1] 0.000 0.000 0.000 0.001 0.002 0.003 0.006 0.010 0.015 0.021 0.027 0.032 0.035 0.849
          name damage facing tracking_arc spread min_mean max_mean
1 right phaser      7      0          360     13   -173.5    173.5
2  left phaser     -7      0          350     13   -168.5    168.5
3     pd gun 1      7      0          200     13    -93.5     93.5
4     pd gun 2     -7      0          190     13    -88.5     88.5



Dang that was much ado about nothing.

fixed 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, 0, 360, 13)
weapon2 <- c("left phaser", -7, 0, 350, 13)
weapon3 <- c("pd gun 1", 7, 0, 200, 13)
weapon4 <- c("pd gun 2", -7, 0, 190, 13)
weapon5 <- c("pd gun 3",7, 160, 20, 0)
weapon6 <- c("photon torpedo", 7, 120, 90, 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$tracking_arc==360) return(angle)
  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
    ship_lower_bound <- transformed_angle(angle,weapons[i,])-shipwidth
   
    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)
  print(optimum_angle)
  print(dps_at_angles)
}

#sample runs
main(ship, 1000, 50, weapons[1:4,])
[close]
« Last Edit: January 19, 2023, 08:08:39 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 726
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #431 on: January 19, 2023, 09:57:50 AM »

So... should I change anything about the code, and what are the test results that I should expect for a broad range of weapons?  :o

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #432 on: January 19, 2023, 10:12:34 AM »

Well, I'll try to fix one more thing which is the asymmetry you see in the output full of -7s. That is, I'm not sure a weapon fixed at 180 deg with 0 spread is hitting a wide ship at -179 correctly. For now you should add the

if(weapon$tracking_arc==360) return(angle)

clause since that is one thing we genuinely missed.

I am honestly not sure what you mean by what kind of test results you should expect for a broad range of weapons? A broad range, I suppose. Specify a bit what you have in mind? Remember what we have here is a machine to print quite complex graphs based on an arbitrary set of numbers, then find the maximum of that graph and then output a probability distribution based on that, so there are in fact literally infinite possible outputs.

Edit: having thought it through the correct way to go here is likely the vector math since this fixes the ship wraparound without any further exceptions and is conceptually simpler and generally less vulnerable to errors. So this requires changing only one function, like so


transform_hit_coord <- function(angle, weapon){
  if(weapon$tracking_arc==360) return(angle)
  if(weapon$spread > weapon$tracking_arc) return(weapon$facing)
  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(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)
}


That is to explain: there is an issue in the previous code that we are not wrapping the ship around at the edge correctly (a gun at 180 that cannot aim at all should still hit a 12.6 angle wide ship that is situated at -179 degrees). This could be fixed with one more special case about wrapping around but that is not necessary with vectors, and the vector math is also simpler and therefore more robust so I recommend just switching this function to it. By vectors, I mean we are using the formula


Here is the step by step explanation:


#Find the angle the distribution mean will take when the weapon tracks the target
transform_hit_coord <- function(angle, weapon){
#if the weapon can track 360 degrees it can always align itself with target, so return target's angle
  if(weapon$tracking_arc==360) return(angle)
#if the weapon has more recoil than tracking ability it cannot track even if slotted in a turret with a tracking arc
  if(weapon$spread > weapon$tracking_arc) return(weapon$facing)
  angle_rad <- angle * pi/180
  facing_rad <- weapon$facing * pi / 180
#else, calculate the angle from weapon to target
  angle_to_weapon <- acos(sin(angle_rad)*sin(facing_rad)+cos(angle_rad)*cos(facing_rad))*( 180 / pi )
#if we are within target arc, the distribution can be aligned: return target angle
  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
#else, calculate the angle to the minimum and maximum means
  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 we are closer to minimum return minimum
  if(angle_to_max >= angle_to_min) return(weapon$min_mean)
#if none of the above apply, return maximum
  return(weapon$max_mean)
}


Full code:
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, 0, 360, 0)
weapon2 <- c("left phaser", -7, 0, 350, 0)
weapon3 <- c("pd gun 1", 7, 0, 200, 0)
weapon4 <- c("pd gun 2", -7, 0, 190, 0)
weapon5 <- c("pd gun 3",7, 160, 20, 0)
weapon6 <- c("photon torpedo", 7, 90, 120, 90, 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]
    ))
  }
}

#Output the angle between two vectors angled a and b using a dot b = |a||b|cos(angle) and noting these are
#unit vectors



#Given an angle and a weapon, return the mean of the distribution when the weapon tries to track the target
transform_hit_coord <- function(angle, weapon){
  if(weapon$tracking_arc==360) return(angle)
  if(weapon$spread > weapon$tracking_arc) return(weapon$facing)
 
  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(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)
}
#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
    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)
  print(dps_at_angles)
}

#sample runs
main(ship, 1000, 0, weapons[1:4,])
[close]

This should seriously be the final update, we are already down to minuscule bugs.
« Last Edit: January 19, 2023, 11:12:54 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 726
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #433 on: January 19, 2023, 11:11:09 AM »

Ok, where should I add this line?

Code
if(weapon$tracking_arc==360) return(angle)

Sorry, by a broad range I meant the long list of weapons you added plus any others you might like to add because they would test edge cases.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #434 on: January 19, 2023, 11:14:05 AM »

I posted a recommendation to re-writing the transform hit coord function above, that also shows where that line should go. I think with this we are done with the testing of edge cases, can't think of more! I'll add that I ran the previous two test with it too; results matched. Sorry about writing this one more time though, it's been quite a laborious thing this angle thingy.
« Last Edit: January 19, 2023, 11:18:32 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge
Pages: 1 ... 27 28 [29] 30 31 32