Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  

News:

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

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

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

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #450 on: January 21, 2023, 09:51:54 AM »

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

I sent a PM to Alex about whether I have it right here and how the partial cells can be hit.

The latter!  I figure the partial cells might be 'padding' cells 'creeping' into the image.  Anyhow, Alex probably has some simple system because it's less effort.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #451 on: January 21, 2023, 10:52:31 AM »

Got a prompt reply from Alex. Can't help but admire that he takes the time!

Anyway it is as theorized. 1/10 sprite height, maximum 30, minimum 15 for armor cell size. There are enough cells to cover the sprite +2 on either side. Collision bounds determine how cells get hit with no unexpected interactions. So the bounds we are looking for for the dominator are in fact exactly

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

Because the cells are 18 wide, but the final cell can't be hit beyond the collision bound (-102/+103).
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #452 on: January 21, 2023, 11:39:30 AM »

Can ships have odd numbers of armor cells, or must the number be even?  If we need only the even case, we can write:
Code
height = 180
#"width" field in .ship file
sprite_width = 220
#calculated by subtracting leftmost from rightmost x coordinate
#in "bounds" field of .ship file
bounds_width = 206
cell_size = 15 if height < 100 else height / 10 if height < 300 else 30
distance = 0
bounds = [i * cell_size for i in range(int(sprite_width / 2 / cell_size) + 1)]
bounds[-1] = min(bounds[-1], bounds_width / 2)
bounds = tuple(-bound for bound in reversed(bounds[1:])) + tuple(bounds)
print(bounds)
(-103.0, -90.0, -72.0, -54.0, -36.0, -18.0, 0.0, 18.0, 36.0, 54.0, 72.0, 90.0, 103.0)
Please tell me whether my numbers and their sources are right; note that I don't get quite the same left bound as you do.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #453 on: January 21, 2023, 12:38:22 PM »

Yeah ships can have an odd number of bounds. However, in vanilla this appears to happen only for asymmetric designs as it seems like the cells start at the ship's center and extend both ways. This Vigilance has 5 armor cells, however, the 5th is only a sliver. Note that it is 15 up to 150 height, not 100.



I don't know why you get 206, the leftmost coordinate I can find in dominator.ship is -102 and the rightmost is 103, so it should be 205 wide.

Dominator

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

Vigilance

> generate_ship_upper_bounds(-27,24,101)
[1] -25 -13   2  17  26


The reason why my code puts the middle cell slightly off center is that we aim at the midpoint of the ship's collision bounds rather than at the midpoint of the collision bound coordinates in the case of this asymmetric ship.

Wait, that's only 4 cells for a Vigilance.  Nevermind we must be using midpoint of the sprite again. Alex did say the armor grid does not interact with the collision bounds in any other way other than determining when the cells are hit. So we need an extra parameter for sprite width.
incorrect code

generate_ship_upper_bounds <- function(left_collision_bound, right_collision_bound, length, width){
  #this operation centers the target to a whole pixel
  ship_lower_bound <- -floor((abs(left_collision_bound)+abs(right_collision_bound))/2)
  ship_upper_bound <- ceiling((abs(left_collision_bound)+abs(right_collision_bound))/2)
  midpoint <- ship_lower_bound + ceiling(width/2)
  cell_size <- 15
  #we assume there can be no fractions of a pixel, so floor
  if(length >= 150) cell_size <- floor(length / 10)
  if(length >= 300) cell_size <- 30
  #we have enough cells to reach exactly or go over the collision bounds, soceil
  no_cells <- ceiling((ship_upper_bound-ship_lower_bound)/cell_size)
  #start at the midpoint
  ub_vector <- c(midpoint)
  #concatenate positive upper bounds
  while(ub_vector[length(ub_vector)] + cell_size < ship_upper_bound) ub_vector <- c(ub_vector, ub_vector[length(ub_vector)]+cell_size)
  ub_vector <- c(ub_vector, ship_upper_bound)
  #concatenate negative upper bounds
  while(ub_vector[1] - cell_size > ship_lower_bound) ub_vector <- c(ub_vector[1]-cell_size, ub_vector)
  ub_vector <- c(ship_lower_bound, ub_vector)
  return(ub_vector)
}

[close]
Vigilance

> generate_ship_upper_bounds(-27,24,101, 63)
[1] -25 -23  -8   7  22  26

Yeah that's 5 cells like in the picture. However, this now gives incorrect results for the Dominator:


> generate_ship_upper_bounds(-102,103,180, 220)
 [1] -102 -100  -82  -64  -46  -28  -10    8   26   44   62   80   98  103


Damn. Well, maybe it's just a visual bug in the armor grid visualization. Going back to the previous method...

generate_ship_upper_bounds <- function(left_collision_bound, right_collision_bound, length){
  #this operation centers the target to a whole pixel
  ship_lower_bound <- -floor((abs(left_collision_bound)+abs(right_collision_bound))/2)
  ship_upper_bound <- ceiling((abs(left_collision_bound)+abs(right_collision_bound))/2)
 
  cell_size <- min(30,max(15,length/10))
  #number of cells
  #try to start at point 0
  midpoint <- ship_lower_bound+abs(left_collision_bound)

  ub_vector <- c(midpoint)
  #concatenate positive upper bounds
  while(ub_vector[length(ub_vector)] + cell_size < ship_upper_bound) ub_vector <- c(ub_vector, ub_vector[length(ub_vector)]+cell_size)
  ub_vector <- c(ub_vector, ship_upper_bound)
  #concatenate negative upper bounds
  while(ub_vector[1] - cell_size > ship_lower_bound) ub_vector <- c(ub_vector[1]-cell_size, ub_vector)
  ub_vector <- c(ship_lower_bound, ub_vector)
 
  return(ub_vector)
}


Dominator

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


Vigilance

> generate_ship_upper_bounds(-27,24,101)
[1] -25 -13   2  17  26
« Last Edit: January 21, 2023, 01:13:47 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #454 on: January 21, 2023, 02:20:47 PM »

Quote
Well, maybe it's just a visual bug in the armor grid visualization.

Let's ask Alex again to find out.  Maybe we've discovered a bug!  8)

CapnHector

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

It is not a bug. Alex replied and said this uses the midpoint defined in the .ship file. I will post corrected code shortly, I'll have time to write it on the bus.

I also asked about whether spread can kick the gun beyond its arc. Alex said he thinks so but unlike previous did not cite source code. So we are going to need to do some empirical science on this. I have the idea for another test. These will require a modded weapon with a spread of 180, a ship with a 5 degree (or any small nonzero) tracking arc mount and a ship with a 180 degree tracking arc mount.

Two tests:
1) mount the modded gun in the large tracking arc mount, position the target to one side (direct left or right, so it is at the edge of the arc) and fire at it. One of three things will happen:
1.1) the gun will fire randomly in a 180 degree spread centered on the target. This means tracking arcs are not inviolable and spread is constant.
1.2) the gun will fire randomly in a 90 degree spread from target to gun facing. This means tracking arc is constant and spread is not constant.
1.3) the gun will fire randomly in the 180 degree arc of the gun. This means tracking arc and spread are constant and is the model we have now.

2) mount the modded gun in the small tracking arc mount and position so that the target is directly to the left or right of the tracking arc (so 90 or -90 degrees from gun to target). Again we will get one of three things
2.1) fire is restricted to the 5 degree arc. This means tracking arcs are inviolable and spread is not constant.
2.2) fire is randomly within a 180 degree arc with the midpoint in the gun mounts arc. This means spread is constant at the cost of the tracking arc and is the model we have now.
2.3) fire is spread non-randomly beyond the 5 degree arc (likely either in the 90 degree arc towards or away from the target). This means the interaction between spread and arc leads to pseudo-tracking (the gun can rotate 5 degrees to track the target and the recoil can kick it beyond this arc, but it still continues to track, leading to complex behavior).

I know how to adjust the model in each case but can't decide which is correct without empirical data.

Edit to add: these tests apply to continuous fire only so the gun should preferably be relatively rapid firing and the mouse button should be held down for the duration of the test.
« Last Edit: January 22, 2023, 10:00:22 PM 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 #456 on: January 22, 2023, 10:20:44 PM »

Well, actually it probably is a visual bug because here is the code that does what Alex says it should:


generate_ship_upper_bounds <- function(left_collision_bound, right_collision_bound, length){
  #this operation centers the target to a whole pixel
  #we will fire at the center of the ship, so center = 0

  cell_size <- min(30,max(15,length/10))
  ub_vector <- c(0)
  #concatenate positive upper bounds
  while(ub_vector[length(ub_vector)] + cell_size < right_collision_bound) {
    ub_vector <- c(ub_vector, ub_vector[length(ub_vector)]+cell_size)
  }
  ub_vector <- c(ub_vector, right_collision_bound)
  #concatenate negative upper bounds
  while(ub_vector[1] - cell_size > left_collision_bound) ub_vector <- c(ub_vector[1]-cell_size, ub_vector)
  ub_vector <- c(left_collision_bound, ub_vector)
 
  return(ub_vector)
}

Vigilance

> generate_ship_upper_bounds(-27,24,101)
[1] -27 -15   0  15  24


Not the same as the image since this is 4 cells without a 1 px sliver. Probably doesn't matter since we know what the bounds are now.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #457 on: January 23, 2023, 07:57:58 AM »

Well, actually it probably is a visual bug because here is the code that does what Alex says it should:


generate_ship_upper_bounds <- function(left_collision_bound, right_collision_bound, length){
  #this operation centers the target to a whole pixel
  #we will fire at the center of the ship, so center = 0

  cell_size <- min(30,max(15,length/10))
  ub_vector <- c(0)
  #concatenate positive upper bounds
  while(ub_vector[length(ub_vector)] + cell_size < right_collision_bound) {
    ub_vector <- c(ub_vector, ub_vector[length(ub_vector)]+cell_size)
  }
  ub_vector <- c(ub_vector, right_collision_bound)
  #concatenate negative upper bounds
  while(ub_vector[1] - cell_size > left_collision_bound) ub_vector <- c(ub_vector[1]-cell_size, ub_vector)
  ub_vector <- c(left_collision_bound, ub_vector)
 
  return(ub_vector)
}

Vigilance

> generate_ship_upper_bounds(-27,24,101)
[1] -27 -15   0  15  24


Not the same as the image since this is 4 cells without a 1 px sliver. Probably doesn't matter since we know what the bounds are now.

I guess we should report it, then!  Meanwhile, I've translated it to Python.

Code
import numpy as np

left, right = -27, 24
height = 101
size = 15 if height < 150 else height / 10 if height < 300 else 30
bounds = np.array((left, *(i * size for i in range(int(left / size),
                                                   int(right / size) + 1)),
                   right))
print(bounds)
[-27 -15.    0.   15.   24. ]
« Last Edit: January 23, 2023, 08:09:30 AM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #458 on: January 23, 2023, 09:46:32 AM »

Oh, I think I got it. What we see in the image are not the collision bounds, but the sprite, which is wider, explaining the inconsistency. Alex may not want to show collision bounds as those are not visually as recognizable. So not a bug.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #459 on: January 23, 2023, 11:51:20 AM »

Wait, I thought you said you saw 'slivers' of armor cells around the edges of the ship.  ???

CapnHector

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

Yes. If you look at the image of the Vigilance I posted above you can see a 1 px wide partial armor cell to the left of its widest part. It displays a total of 5 horizontal armor cells with this partial one included. This does not correspond to our calculation of 4 armor cells. However, we are calculating within collision bounds, which indeed determine if the ship gets hit according to Alex. But that image is clearly displaying the outline of the Vigilance sprite, and armor grid bounds within the sprite, not the collision bounds of the Vigilance and armor grid within them. This is why we don't get 5 cells like the image: there aren't 5 hittable cells, unlike the image (the sprite is wider). But it is likely not a bug, since the collision bounds are a simplified polygon in the shape of the ship and wouldn't look good to the player. Hence it's more likely a deliberate decision by Alex to show the armor grid within the sprite rather than collision bounds which are usually close
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #461 on: January 24, 2023, 05:23:56 AM »

So, with all that settled, I guess we should decide what distributions to expect.  I can't quite get the same ones.

Code
Code: analysis.py
"""
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 tuple(minimum_means), tuple(maximum_means)
   

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


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


def 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 = (*(i for i in range(-179, 180)),)
    target_angular_size = arc_to_deg(target_width, distance) / 2
    target_positional_angle_error = arc_to_deg(standard_deviation, distance)
    weapon_scores = tuple(WEAPON_SCORES[weapon["size"], weapon["pd"]]
                          for weapon in weapons)
    total_probable_weapon_score_at_angles = []
    for target_positional_angle in target_positional_angles:
        total_probable_weapon_score = 0
        for i, weapon in enumerate(weapons):
            weapon_angle = (0 if weapon.slot["arc"] == 360
                            else transformed_angle(target_positional_angle,
                                              minimum_means[i],
                                              maximum_means[i]))
            probability = probability_hit_at_angle(weapon_angle,
                weapon["spread"], target_angular_size, target_positional_angle,
                target_positional_angle_error)
            total_probable_weapon_score += probability * weapon_scores[i]
        total_probable_weapon_score_at_angles.append(
            total_probable_weapon_score)
    return total_probable_weapon_score_at_angles


def middle_index_of_approximate_maxima(row: tuple) -> int:
    """
    Return the middle index of those indices where the row is nearly maximum.
   
    row - a row containing real numbers
    """
    rounded_row = tuple(round(element, 5) for element in row)
    indicies_of_approximate_maxima = (*(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 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, *(1 if bound < 0 <= bounds[i+1] else 0 for i, bound in
                     enumerate(bounds[:-1])))
    elif standard_deviation == 0: #return part of a box
        a = 2 * spread_distance
        return (min(1, max(0, (bounds[0] + spread_distance)) / a),
                *((min(1, max(0, (bounds[i+1] + spread_distance)) / a)
                   - min(1, max(0, (bound + spread_distance)) / a))
                  for i, bound in enumerate(bounds[:-1])),
                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]),
                *(cdf(bounds[i+1]) - cdf(bound) for i, bound in
                  enumerate(bounds[:-1])),
                1 - cdf(bounds[-1],))
    return (probability_hit_within(bounds[0], standard_deviation,
                                   spread_distance),
            *(probability_hit_within(bounds[i+1], standard_deviation,
                                     spread_distance)
              - probability_hit_within(bound, standard_deviation,
                                       spread_distance)
              for i, bound in enumerate(bounds[:-1])),
            1 - probability_hit_within(bounds[-1], standard_deviation,
                                       spread_distance))
   
   
def distribution(
        spread: float,
        arc: float,
        minimum_mean: float,
        maximum_mean: float,
        target_positional_angle: float,
        bounds: tuple,
        distance: float,
        standard_deviation: float) -> tuple:
    """
    Return the probability of a weapon of this spread to hit between each
    pair of the bounds of an armor row positioned at this angle and
    distance
   
    as well as of a miss due to hitting below ship's lowest bound in the
    first cell or above ship's highest bound in the last one.
   
    spread - of the weapon
    arc - of the weapon slot
    minimum_mean - minimum_mean of this weapon's probable score at this
                    angle
    maximum_mean - maximum_mean of this weapon's probable score at this
                    angle
    bounds - of the armor grid cells of the target
    distance - range to target
    standard deviation - of target position
    """
    angle_difference = (0 if arc == 360 else transformed_angle(
        target_positional_angle, minimum_mean, maximum_mean))
    adjustment = deg_to_arc(angle_difference, distance)
    adjusted_bounds = (*(bound + adjustment for bound in bounds),)
    distribution = hit_distribution(adjusted_bounds, standard_deviation,
                                    deg_to_arc(spread, distance))
    return distribution
Code: test_analysis.py
import analysis
import numpy as np

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

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

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

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

    class TestArmorGrid:
        def __init__(self, cells_across: int, cell_size: float):
            self.cells = [[i for i in range(cells_across)]]
            first_bound = - cells_across * cell_size / 2
            self.bounds = tuple(i * cell_size + first_bound for i in range(
                                cells_across + 1))
           
    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, 18 + 1/3)
    standard_deviation = 50
    distance = 1000
    #weapons
    print("spread, arc, angle")
    for weapon in test_weapons:
        print(weapon["spread"], weapon.slot["arc"], weapon.slot["angle"])
    print()
    #means
    minimum_means, maximum_means = analysis.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()
    assert minimum_means == (-17.5, 2.5, -170.0, 170.0, 150.0, 77.5)
    assert maximum_means == (-2.5, 17.5, -150.0, -170.0, 170.0, 162.5)
    #optimum angle
    optimum_angle = analysis.optimum_angle(test_weapons, minimum_means, maximum_means,
                                  test_target["width"], distance,
                                  standard_deviation)
    print("Optimum Angle:", optimum_angle)
    print()
    assert optimum_angle == 167
    #bounds
    bounds = test_target.armor_grid.bounds
    print("Bounds")
    print(tuple(round(bound, 3) for bound in bounds))
    assert tuple(round(bound, 3) for bound in 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)   
    print()
    #distributions
    distributions = np.array(tuple(analysis.distribution(weapon["spread"],
                                                weapon.slot["arc"],
                                                minimum_means[i],
                                                maximum_means[i],
                                                optimum_angle,
                                                test_target.armor_grid.bounds,
                                                distance,
                                                standard_deviation)
                          for i, weapon in enumerate(test_weapons)))
    print("Distributions")
    expected_distributions = np.array((
        (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)
    ))
    print("got")       
    print(np.round(np.array(distributions), 3))
    print("expected")
    print(expected_distributions)
    print("difference")
    print(np.round(np.array(distributions), 3) - expected_distributions)
    difference = distributions - expected_distributions
    assert difference.all() < 0.01
   
[close]
Result
Distributions
got
[[ 1.    -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.   ]
 [ 1.     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]]
expected
[[ 1.    -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.   ]
 [ 1.     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]]
difference
[[ 0.     0.     0.     0.     0.     0.     0.     0.     0.     0.
   0.     0.     0.     0.   ]
 [ 0.     0.     0.     0.     0.     0.     0.     0.     0.     0.
   0.     0.     0.     0.   ]
 [ 0.     0.     0.     0.     0.     0.     0.     0.     0.     0.
   0.     0.     0.     0.   ]
 [-0.007 -0.009 -0.014 -0.019 -0.021 -0.018 -0.004  0.016  0.037  0.048
   0.047  0.031  0.009 -0.095]
 [-0.047 -0.022 -0.018 -0.007  0.013  0.033  0.047  0.047  0.033  0.013
  -0.007 -0.018 -0.022 -0.047]
 [ 0.     0.     0.     0.     0.     0.     0.     0.     0.     0.
   0.     0.     0.     0.   ]]
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #462 on: January 24, 2023, 05:34:31 AM »

You shouldn't get the same ones, because the ship is now narrower (it used to be 220 px wide and is now 205 px wide). However, without the empirical test it's a little pointless to work on that code since it could very well be wrong, too. So I've sort of been waiting if you have the chance to create the modded weapon, alternatively you can tell me the right way to go about it and I'll test it.
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 #463 on: January 26, 2023, 02:47:58 AM »

Alright, so, to test this, I edited weapon_data.csv to give the Hephaestus a min and max spread of 180 (rather than 0 and 10). After taking the snapshot below, I also made it fire 25x faster (refire delay 0.01 rather than 0.25). And I edited the Conquest. ship file to give one of the left large guns a 5 degree and the other a 180 degree turret arc.



Here is how it works: Turret arc 5 degrees, weapon spread 180 degrees. The shots are random within 180 degrees:



Turret arc 180 degrees, weapon spread 180 degrees, firing at -90 degrees. The shots are random within 180 degrees and go past the turret arc. There is no noticeable "pseudo-tracking" from hypothetical gun rotation during fire, shots appear to be completely random.



What this means for the code (and in more general) is: turret arcs are not respected for weapon spread. The weapon shot distribution's mean can align with the endpoint of the turret arc. Will post fixed code shortly.
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 #464 on: January 26, 2023, 03:08:55 AM »

Here is the fixed code. The following changes must be made:
1) The ship object must contain left collision bound, right collision bound and height (in this code: length, but height is actually used in game files) information. Ship width where used should be = right collision bound - left collision bound.
2) The upper bounds code must be changed as we did above.
3) In min max mean code, the calculation is simply "facing + tracking arc / 2, facing -tracking arc / 2" (do still apply the wraparound)

And that's it.
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. The relevant parameters for this test are width (ship[5]), left bound(ship[6]), right bound(ship[7]) and length (ship[8])
ship <- c(14000, 500, 10000, 1500, 205, -102, 103, 180, 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){
    vec <- c(
      weapon[[3]]-weapon[[4]]/2,
      weapon[[3]]+weapon[[4]]/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)
}

#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(left_collision_bound, right_collision_bound, length){
  #this operation centers the target to a whole pixel
  #we will fire at the center of the ship, so center = 0
  cell_size <- min(30,max(15,length/10))
  ub_vector <- c(0)
  #concatenate positive upper bounds
  while(ub_vector[length(ub_vector)] + cell_size < right_collision_bound) {
    ub_vector <- c(ub_vector, ub_vector[length(ub_vector)]+cell_size)
  }
  ub_vector <- c(ub_vector, right_collision_bound)
  #concatenate negative upper bounds
  while(ub_vector[1] - cell_size > left_collision_bound) ub_vector <- c(ub_vector[1]-cell_size, ub_vector)
  ub_vector <- c(left_collision_bound, ub_vector)
 
  return(ub_vector)
}

#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
    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[6],ship[7],ship[8])
  print("bounds")
  print(upper_bounds)
 
  #calculate and report the distributions for weapons, round for human readability
  print("distributions")
  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(optimum_angle)
}
seq(0,110,18)
#sample runs
main(ship, 1000, 50, weapons[1:6,])
[close]

Output

[1] "bounds"
 [1] -102  -90  -72  -54  -36  -18    0   18   36   54   72   90  103
[1] "distributions"
[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.003 0.003 0.010 0.021 0.040 0.067 0.098 0.125 0.141 0.140 0.122 0.094 0.049 0.087
[1] "pd gun 3:"
 [1] 0.021 0.015 0.039 0.065 0.096 0.124 0.141 0.141 0.124 0.096 0.065 0.039 0.016 0.020
[1] "photon torpedo:"
 [1] 0.253 0.055 0.089 0.094 0.094 0.091 0.083 0.072 0.058 0.043 0.030 0.019 0.008 0.011
            name damage facing tracking_arc spread min_mean max_mean
1   right phaser      7    -10           20      5      -20        0
2    left phaser      7     10           20      5        0       20
3       pd gun 1      7   -160           20      0     -170     -150
4       pd gun 2      7    180           20      0      170     -170
5       pd gun 3      7    160           20      0      150      170
6 photon torpedo      7    120           90      5       75      165
[1] "optimum angle"
[1] 168

« Last Edit: January 26, 2023, 03:23:59 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge
Pages: 1 ... 29 30 [31] 32