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