Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Pages: 1 ... 21 22 [23] 24 25 ... 32

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

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #330 on: December 27, 2022, 05:43:07 PM »

All right, getting back to this, here you go, this is in my approximation of programmer style going upward in levels of abstraction instead of in order of math.

Wow, this code is much better!  I can see more logical structure now. 

Quote
I feel like I don't understand my own code anymore, now that it is presented this way as opposed to the logical order.

Oh no, I hope I can help!  First a philosophical note: programs are developed, tested, and used extensively but understood only insofar as needed because they are only means to our ends rather than, as mathematical discoveries would be, ends in themselves. The number and scale of these ends entail programs so long and complicated that one person cannot understand them.  Therefore, we should write code with abstraction in mind from the beginning, whether writing from the 'bottom' of core functions to the 'top' of some interface or vice versa, by specifying what we want each method to do, then its arguments and return value, and only then implement the method itself.  Thereafter, we would rather neglect the implementation details to focus on the code in which we will call the method.

Quote
However, here it is and I even found a small error in the process (a lack of division by 2 for radius). I also included sample runs with randomly generated guns so that you can test that the implementation is correct. Code is here and also as an attachment. (edit: fix some typos and add underscore to a thing)

I have a question about the code because it declares weapon variables to which I cannot find any reference.  Also, these variables are declared outside the main method, so I'm worried about where they have ended up.  By the way, a dictionary more clearly stores such unique, unordered, heterogeneous data as the data of a weapon than an array does because each data point can be called by name rather than position.
 
Code
grad_student = {"age" : 29, "money" : 0, "grant" : "pending", "feeling" : "worried"}
if grad_student["age"] > 30: print("This degree is taking a while...")
if grad_student["money"] < 0: print("Outta cash!  I need a loan!")
if grad_student["grant"] == "denied": print("Gotta write another application.")
if instanceof(grad_student["grant"], int):
    print("Hooray!  I got a grant of $" + str(grad_student["grant"]) + "!")
    grad_student["feeling"] = "happy"
if grad_student["feeling"] == "happy":
    print("Did you finish grading those papers?")
    grad_student["feeling"] = "worried"

Also, when declaring elements that will become part of an array, just put them into the array right away rather than declaring and then adding them.

Code
grad_students = ({"age" : 23, "money" : 100, "grant" : "denied", "feeling" : "worried"}, 
                 {"age" : 25, "money" : -200, "grant" : "pending", "feeling" : "worried"},
                 {"age" : 29, "money" : 0, "grant" : "pending", "feeling" : "worried"})

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #331 on: December 27, 2022, 09:44:44 PM »

Oh no, I hope I can help!  First a philosophical note: programs are developed, tested, and used extensively but understood only insofar as needed because they are only means to our ends rather than, as mathematical discoveries would be, ends in themselves. The number and scale of these ends entail programs so long and complicated that one person cannot understand them.  Therefore, we should write code with abstraction in mind from the beginning, whether writing from the 'bottom' of core functions to the 'top' of some interface or vice versa, by specifying what we want each method to do, then its arguments and return value, and only then implement the method itself.  Thereafter, we would rather neglect the implementation details to focus on the code in which we will call the method.

Okay, I suppose that makes sense. If the program is documented well enough, and consistent, there is probably no logical obstacle to working on it without understanding the entire program. With my code the assumption might not be perfectly accurate  :D

Quote
I have a question about the code because it declares weapon variables to which I cannot find any reference.  Also, these variables are declared outside the main method, so I'm worried about where they have ended up.  By the way, a dictionary more clearly stores such unique, unordered, heterogeneous data as the data of a weapon than an array does because each data point can be called by name rather than position.

Please specify in more detail, don't know exactly what you mean. I flushed the R environment and re-ran, and it produced the results I posted, so I am certain the program does calculate everything it needs to.

If you are referring to this piece of code:

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

Then that works because the weapons are actually in a data frame, not a matrix. So the columns do, in fact, have names. I just mostly didn't use those, because these can only be used to call a column of the data frame in R, not a specific element, so it would have been more complicated to refer to columns by name rather than index. This would necessitate writing, for example, damage <- weapons[i,]$damage rather than damage <- weapons[i,2] in the sum_auc function. I assumed during writing that the way this code stores weapons should be just a placeholder anyway, as it should be passed weapons directly from the integrated code and use whatever structure they have there. You should generally replace references to the attributes of weapons with references to whatever is the equivalent element in your code.

By the way, here is one more test, using only those weapons whose indices are prime and this time with no standard deviation for the normal distribution. This revealed an error in my code. Specifically, when doing the math in my head, I had assumed that the width of the uniform distribution is =1 somehow, but of course it is not. Here is the fix.


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,1/2+z/2/b)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

Note: the change is on line 4:   if(a == 0 & b > 0) return(max(0,min(1,1/2+z/2/b)))


Test result (weapons 2,3,5,7,11,13,17 with sd 0):




> main(ship, 1000, 0, weapons[c(2,3,5,7,11,13,17),])
[1] "left phaser:"
 [1] 0.000 0.025 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.030 0.000 0.000
[1] "pd gun 1:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "bb gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "singularity projector:"
 [1] 0.374 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.374
[1] "left nullspace projector:"
 [1] 0 0 0 0 0 0 1 0 0 0 0 0 0 0
[1] "mini-collapsar rifle:"
 [1] 0.000 0.000 0.011 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.585
                      name damage facing tracking_arc spread min_mean max_mean
1              left phaser    100     10           20      5      2.5     17.5
2                 pd gun 1     30   -160           20      0   -170.0   -150.0
3                 pd gun 3     30    160           20      0    150.0    170.0
4                   bb gun      5   -150           11     20   -150.0   -150.0
5    singularity projector     95     28          122     25    -20.5     76.5
6 left nullspace projector     10     28           54      0      1.0     55.0
7     mini-collapsar rifle     27     28           16     13     26.5     29.5


Same, but with "smoothing" from applying a normal distribution with sd 1 px:


> main(ship, 1000, 1, weapons[c(2,3,5,7,11,13,17),])
[1] "left phaser:"
 [1] 0.000 0.025 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.030 0.000 0.000
[1] "pd gun 1:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "bb gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "singularity projector:"
 [1] 0.374 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.374
[1] "left nullspace projector:"
 [1] 0.0 0.0 0.0 0.0 0.0 0.0 0.5 0.5 0.0 0.0 0.0 0.0 0.0 0.0
[1] "mini-collapsar rifle:"
 [1] 0.000 0.000 0.011 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.585
                      name damage facing tracking_arc spread min_mean max_mean
1              left phaser    100     10           20      5      2.5     17.5
2                 pd gun 1     30   -160           20      0   -170.0   -150.0
3                 pd gun 3     30    160           20      0    150.0    170.0
4                   bb gun      5   -150           11     20   -150.0   -150.0
5    singularity projector     95     28          122     25    -20.5     76.5
6 left nullspace projector     10     28           54      0      1.0     55.0
7     mini-collapsar rifle     27     28           16     13     26.5     29.5


Our normal smoothing of 50 px:


> main(ship, 1000, 0, weapons[c(2,3,5,7,11,13,17),])
[1] "left phaser:"
 [1] 0.000 0.025 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.030 0.000 0.000
[1] "pd gun 1:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "bb gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "singularity projector:"
 [1] 0.374 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.374
[1] "left nullspace projector:"
 [1] 0 0 0 0 0 0 1 0 0 0 0 0 0 0
[1] "mini-collapsar rifle:"
 [1] 0.000 0.000 0.011 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.040 0.585
                      name damage facing tracking_arc spread min_mean max_mean
1              left phaser    100     10           20      5      2.5     17.5
2                 pd gun 1     30   -160           20      0   -170.0   -150.0
3                 pd gun 3     30    160           20      0    150.0    170.0
4                   bb gun      5   -150           11     20   -150.0   -150.0
5    singularity projector     95     28          122     25    -20.5     76.5
6 left nullspace projector     10     28           54      0      1.0     55.0
7     mini-collapsar rifle     27     28           16     13     26.5     29.5
> main(ship, 1000, 50, weapons[c(2,3,5,7,11,13,17),])
[1] "left phaser:"
 [1] 0.079 0.048 0.064 0.077 0.088 0.094 0.096 0.094 0.088 0.078 0.064 0.049 0.035 0.046
[1] "pd gun 1:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "pd gun 3:"
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
[1] "bb gun:"
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
[1] "singularity projector:"
 [1] 0.374 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.374
[1] "left nullspace projector:"
 [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 0.019 0.014
[1] "mini-collapsar rifle:"
 [1] 0.018 0.013 0.019 0.025 0.030 0.034 0.037 0.039 0.040 0.040 0.040 0.040 0.040 0.585
                      name damage facing tracking_arc spread min_mean max_mean
1              left phaser    100     10           20      5      2.5     17.5
2                 pd gun 1     30   -160           20      0   -170.0   -150.0
3                 pd gun 3     30    160           20      0    150.0    170.0
4                   bb gun      5   -150           11     20   -150.0   -150.0
5    singularity projector     95     28          122     25    -20.5     76.5
6 left nullspace projector     10     28           54      0      1.0     55.0
7     mini-collapsar rifle     27     28           16     13     26.5     29.5
« Last Edit: December 27, 2022, 09:57:24 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 #332 on: December 27, 2022, 11:54:49 PM »

Also, it occurs to me that we can make a mildly diverting graph of how SD affects damage output in a more interesting way than before using this code.

Add a return function to main(), so
  return(dps_at_angles)


and then write

dmatrix <- matrix(0,101*21,3)
for(i in 0:20){
  dmatrix[(i*101+1):(i*101+101),1] <- main(ship, 1000, i*10, matrix(c(1,2,100,100,-20,20,10,10,0,0),2,5))[130:230]
  dmatrix[(i*101+1):(i*101+101),2] <- seq(-50,50)
  dmatrix[(i*101+1):(i*101+101),3] <- i
}
df <- data.frame(dps=dmatrix[,1],angle=dmatrix[,2],sdmult=dmatrix[,3])
library(ggplot2)
ggplot(df,aes(y=dps,x=angle,group=sdmult,color=sdmult*10))+
  geom_line()+
  theme_classic()+
  labs(colour = "SD")+
  scale_color_viridis_c()


To get DPS as a function of angle, with SD increasing in increments of 10 from 0 to 200, for two weapons situated at angles -20 and 20, dealing 100 damage each, with 10 tracking arc each and no spread:


Note that the value we use normally is 50, so 6th line from the top.

Edit: I had some spare computing time while doing other things so here is a prettier graph:


Same thing with 10 spread and no tracking:

dmatrix <- matrix(0,101*21,3)
for(i in 0:20){
  dmatrix[(i*101+1):(i*101+101),1] <- main(ship, 1000, i*10, matrix(c(1,2,100,100,-20,20,0,0,10,10),2,5))[130:230]
  dmatrix[(i*101+1):(i*101+101),2] <- seq(-50,50)
  dmatrix[(i*101+1):(i*101+101),3] <- i
}
df <- data.frame(dps=dmatrix[,1],angle=dmatrix[,2],sdmult=dmatrix[,3])
library(ggplot2)
ggplot(df,aes(y=dps,x=angle,group=sdmult,color=sdmult*10))+
  geom_line()+
  theme_classic()+
  labs(colour = "SD")+
  scale_color_viridis_c()





5 spread and 10 tracking range:



Note that it is theoretically possible for there to be a new maximum in the middle, if it should happen that the normal distribution becomes so dominant and overlapping that the middle is near the peak of both normal distributions for some definition of near. If SD is movement then this would correspond to the enemy ship moving so much that it is better to try to catch it in the middle of the widest possible field of fire rather than aiming guns directly at it. This seems to require more extreme values of the SD for these reasonable guns than 4x of what we are using, though.
« Last Edit: December 28, 2022, 02:03:30 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #333 on: December 28, 2022, 12:30:56 PM »

Okay, I suppose that makes sense. If the program is documented well enough, and consistent, there is probably no logical obstacle to working on it without understanding the entire program. With my code the assumption might not be perfectly accurate  :D

Restructuring a project comprehensible only by reading and understanding every line to be understandable by reading its abstract organization saves the effort of reading the whole project for every bug, change, upgrade, documentation, or discussion.

Quote
Please specify in more detail, don't know exactly what you mean. I flushed the R environment and re-ran, and it produced the results I posted, so I am certain the program does calculate everything it needs to.

I am referring to Section 0
Quote
Code
#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", 100, -10, 20, 5)
weapon2 <- c("left phaser", 100, 10, 20, 5)
weapon3 <- c("pd gun 1", 30, -160, 20, 0)
weapon4 <- c("pd gun 2",30, 180, 20, 0)
weapon5 <- c("pd gun 3",30, 160, 20, 0)
weapon6 <- c("photon torpedo", 120, 90, 0, 5)

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

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

I can't find any reference to its variables (weapons1, weapons2, weapons3, ... weapons18) elsewhere in the code but do see weapons mentioned in the calls to main().  I think you meant for weapons to be a test dataset, and if so, I figure you have one there because R is designed presuming the user will make an all-in-one-file project file rather than keep a separate file of test routines and data.  Please designate test data by putting it just before the main function, rather than the top of the file as an CONSTANT_USED_IN_CODE would be, and prefixing the names of these constants with 'test_'.

Code
CONSTANT_USED_IN_CODE = ...


def a_function(data):
    some_variable = CONSTANT_USED_IN_CODE + 1
    ...


def another_function(data):
    some_other_variable = CONSTANT_USED_IN_CODE * 2
    ...


test_data = ...


def main():
    a_function(test_data)
    another_function(test_data)

Edit: Also I have noticed that you have used various tricks to try to save keystrokes and characters in the documentation.  Such tricks are unnecessary because programs can be as long as we like and because each section of documentation should be understandable by itself rather than by reference.

Code
def square(number):
    """
    Return the square of this number.

    number - float

    return - float
    ''''
    ...


def cube(number):
    """
    Return the cube of this number.

    number - float

    return - float
    """
    ...

And so on.  We want documentation we can later scroll to and immediately, explicitly understand.
« Last Edit: December 28, 2022, 05:49:42 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #334 on: December 28, 2022, 06:45:28 PM »

Will do next time! I'll admit I didn't think much about the test data because I assumed that wouldn't be its final form.

What that section of code does is 1. creates vectors called weapon1...weapon18 as manually specified, then

#generate data frame called "weapons"
weapons <- data.frame()
#for i from 1 to the manually specified number of weapons, add a row to the data frame containing the vector whose name is paste("weapon", i) with no separator (e.g. weapon1)
for (i in 1:no_weapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
#define column names for the data frame
colnames(weapons) <- c("name","damage","facing","trackingrange","spread")

That is where "weapons" comes from. Then pass it to main() by giving the rows corresponding to weapons you want to use, e.g. main(ship, range, sd, weapons[1:10, ])

In retrospect this was a poor way of doing things since it results in the columns having type character (string), which we must fix later in main (or should have here). Doing it via a matrix for example would have been better.
« Last Edit: December 28, 2022, 06:53:49 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #335 on: December 28, 2022, 11:00:07 PM »

Will do next time! I'll admit I didn't think much about the test data because I assumed that wouldn't be its final form.

What that section of code does is 1. creates vectors called weapon1...weapon18 as manually specified, then

#generate data frame called "weapons"
weapons <- data.frame()
#for i from 1 to the manually specified number of weapons, add a row to the data frame containing the vector whose name is paste("weapon", i) with no separator (e.g. weapon1)
for (i in 1:no_weapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
#define column names for the data frame
colnames(weapons) <- c("name","damage","facing","trackingrange","spread")

That is where "weapons" comes from. Then pass it to main() by giving the rows corresponding to weapons you want to use, e.g. main(ship, range, sd, weapons[1:10, ])

In retrospect this was a poor way of doing things since it results in the columns having type character (string), which we must fix later in main (or should have here). Doing it via a matrix for example would have been better.

Thanks, now I understand.  A list of dictionaries, each holding every relevant value by name for each weapon, would be clearest.  Please write function documentation with the following format:
Code
#"Return [what the function returns]" or "[Verb] ..." if it has an
#effect rather than a return value, in one sentence.
#
#Paragraph elaborating on this sentence if need be.  Skip
#implementation details unless they cannot be understood
#in the code; e.g., an integral formula.
#
# first_argument - description
# second_argument - description
# third_argument - description
function_name <- function(first_argument, second_argument, third_argument) {
    ...
}

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #336 on: December 29, 2022, 02:17:24 AM »

Sure. For now though, see any further obstacles to translation?
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #337 on: December 29, 2022, 11:35:49 AM »

Here's one:
Code
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)]]

Could this line be expanded into several?  I can't wrap my head around it.

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #338 on: December 29, 2022, 11:45:39 AM »

Can do next time I'm at the computer! However, let me explain

optimum_angle = #is
x_axis[ #the element of x axis, where
which( #the element of dps_at_angles is
round(dps_at_angles,3) == round(max(dps_at_angles),3)) #of the sub-vector where dps_at_angles is equal to the maximum value within the elements of the vector dps_at_angles,

                        [ceiling(length(which(round(dps_at_angles,3) == round(max(dps_at_angles),3)))/2)]#the element such that you take the length of the sub-vector above mentioned, divide it by two and take the ceiling
]#end

In English, it reads "the optimum value is the element of the vector x_axis at the index that corresponds to the index that is the index within the vector dps_at_angles of the element of the sub-vector of those elements that are equal to the maximum element in the vector that is at the index of that sub-vector corresponding to the ceiling of the length of the sub-vector divided by two".

I'll write a replacement, I admit it is a little more complex in writing than it was in my head.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #339 on: December 29, 2022, 03:13:01 PM »

This code yields the same graph as yours does.
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


#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


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
    a - standard deviation of the normal distribution N(0,a),
    b - parameter of the symmetric uniform distribution (-b/2,b/2)
    """
    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, uniform_distribution_width / 2 + x))
    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 deg * 2 * radius / 360 * pi


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


def minimum_mean(weapon: tuple) -> float:
    """
    Return the minimum mean hit probability this weapon over its
    tracking arc.
   
    weapon - a tuple of facing, spread and tracking_range at
             [3],[4],[5]
    """
    return weapon[2] - (weapon[3] + weapon[4]) / 2


def maximum_mean(weapon:tuple) -> float:
    """
    Return the minimum mean hit probability this weapon over its
    tracking arc
   
    weapon - a tuple of facing, spread and tracking_range at
             [3],[4],[5]
    """
    return weapon[2] + (weapon[3] - weapon[4]) / 2


def transform_hit_coord(angle: float, weapon: tuple) -> float:
    """
    Return the facing of this weapon when aiming at this target.
   
    weapon - a tuple ending with min_mean and max_mean
    angle - angle of the target relative to our ship
    """
    return max(weapon[5], min(weapon[6], angle))


def transformed_angle(angle: float, weapon: tuple) -> 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 - transform_hit_coord(angle,weapon)


def upper_bounds(ship: tuple, 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.
   
    ship - a tuple with width at [4] and number of cells stored at [5]
    distance - range to the ship
    """
    ship_angle = ship[4] / (2 * pi * distance)
    cell_angle = ship_angle / ship[5]
    angles = [-ship_angle / 2]
    for i in range(ship[5]): angles[i+1] = angles[i] + cell_angle
    return angles * 2 * pi * distance


def weaponadjustment_px(weapon, optimum_angle, distance):
    """
    #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
    """
    angle_difference = transformed_angle(optimum_angle,weapon)
    return deg_to_arc(angle_difference, distance)


def hit_distribution(
        bounds: tuple,
        standard_deviation: float,
        spread: 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 - a parameter of a symmetric uniform distribution
             (-spread/2, spread/2)
    """
    if standard_deviation == 0 and spread == 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
        return (min(1, max(0, (bounds[0] + spread)) / a),
                + tuple((min(1, max(0, (bounds[j] + spread)) / a)
                         - min(1, max(0, (bounds[j-1] + spread)) / a))
                        for j in range(1, len(bounds)))
                + (1 - min(1, max(0, (bounds[-1] + spread)) / a)),)
    elif spread == 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),
            + tuple((probability_hit_within(bounds[j], standard_deviation,
                                            spread)
                     - probability_hit_within(bounds[j-1], standard_deviation,
                                              spread))
                    for j in range(1, len(bounds)))
            + (1 - probability_hit_within(bounds[-1], standard_deviation,
                                          spread)),)


def hit_distribution_at_angle(
        angle: float,
        sd: float,
        upper_bounds: tuple,
        weapon: float,
        distance: float) -> tuple:
    """
    Return the hit distribution at this angle.
   
    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.
   
    angle -
    sd - standard error in pixels
    weapon - tuple with spread at index 5
    upper_bounds - tuple of upper bounds
    """
    #convert spread to pixels
    px_spread = deg_to_arc(weapon[4], distance)
    #adjust upper bound vector
    adj_ubs = upper_bounds + weaponadjustment_px(weapon, angle,distance)
    return(hit_distribution(adj_ubs, sd , px_spread))


def hit_probability_at_angle(
        weapon: tuple,
        target_angular_size: float,
        target_positional_angle: float,
        target_positional_angle_error: float) -> float:
    """
    Return the sum of expected damage per second from this weapon
    to a target.
   
    weapons - a tuple of information about a weapon
    target_positional_angle -
    target_angular_size - angle, centered on weapons, from lower to
                          upper target bound
    target_positional_angle_error -
    """
    #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
    angle = transformed_angle(target_positional_angle, weapon)
    upper_bound = angle + target_angular_size
    lower_bound = angle - target_angular_size
    spread = weapon[4]
    return (probability_hit_within(upper_bound, target_positional_angle_error,
                                   spread)
            - probability_hit_within(lower_bound, target_positional_angle_error,
                                     spread))


def main(weapons: tuple, target: tuple, distance: float, standard_deviation: float):
    """
    Print 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
    """
    for weapon in weapons:
        weapon += ([minimum_mean(weapon), maximum_mean(weapon)]
                    if(weapon[4] < weapon[3]) else [weapon[2], weapon[2]])

    #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(ship[4], distance) / 4
    target_positional_angle_error = arc_to_deg(standard_deviation, distance)
    dps_at_angles = [
        sum(weapon[1] * hit_probability_at_angle(weapon,
                                                 target_angular_size,
                                                 target_positional_angle,
                                                 target_positional_angle_error)
            for weapon in weapons)
        for target_positional_angle in target_positional_angles]
 
    #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): dps_at_angles[i+360] += dps_at_angles[i]
    #likewise note that angle 360 is just angle 0, angle 359 is angle
    #-1, and so on
    for i in range(540,720): dps_at_angles[i-360] += dps_at_angles[i]
 
    #having summed, select angles -179 to 180
    dps_at_angles = dps_at_angles[181:540]
   
    #we use a separate vector to keep track of angle, since vector
    #index 1 corresponds to angle -179 now
    x_axis = range(-179,180)
   
    import matplotlib.pyplot as plt
    plt.scatter(x_axis, dps_at_angles)
    """
    #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
    bounds = upper_bounds(target, distance)

    #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_angled(optimum_angle, standard_deviation, bounds, weapons[i,], distance),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)
    """

#Section 0. Test ship and weapons.

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

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

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

#sample runs
main(simple_weapons, ship, 1000, 50)

main(random_weapons, ship, 1000, 50)
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #340 on: December 29, 2022, 10:28:43 PM »

Excellent, that means we are almost done here. There is no way that a bunch of random integers will produce the correct graphs unless it is the right code.

There is still the math error that was present in my original code on line 43:
        return max(0, min(1, uniform_distribution_width / 2 + x)) 


This is incorrect if the width of the uniform distribution is not 1, which is an error that I made when originally doing the math in my head. The correct version is, and I think I can write this directly in Python,
Comment:

If the user has chosen to disable the normal distribution, then weapon hit distribution is represented by a rectangle of area 1 and base from -b, b,
and the CDF is equal to the integral from -infinity to x over this box. So, it is 0, when x < -b, and 1, when x > b. When -b <= x <= b, it is
(x-(-b)/2b = 1/2 + x/(2b).


Note that I had also incorrectly stated the parameter of the uniform distribution in the comments. Per our main equation the uniform distribution is U(-b,b) not U(-b/2, b/2). This is a "bug" in the comments only as the code is actually correct (see lines 169-175) but I had forgotten what we are doing in the interval after writing it and didn't bother returning to Willink (who in fact defined it using (-b,b) - see https://www.cambridge.org/core/books/abs/measurement-uncertainty-and-probability/sum-of-normal-and-uniform-variates/DECD19AD9E3D68916CC4B04532418F58 ). So line 38 should read (    b - parameter of the symmetric uniform distribution (-b,b)). It is easy to check that we are doing this correctly by running the graph with SD set to exactly 0 and SD set to a low number, say 0.1 - the graphs must be almost exactly equal if the uniform distribution equation is correct, which is the case with this fix.

So, change line 43 to read
Code
        return max(0, min(1, 1 / 2 + x / uniform_distribution_width / 2))

Then, there is the middle maximum. I'm going to write a separate function for it that does the computations piece by piece.

Code
#Given a vector, return the index of the middle cell among those cells where the vector has its maximum value.
#
#Resolve fractions using a ceiling function. (e.g. in a vector with 7 cells we select cell 3.5 -> cell 4, the middle).
#Note that when porting this to a language with vectors indexed from 0, fractions should be resolved using floor
#(e.g. in a vector with 7 cells we select cell 3.5 -> cell 3, the middle).
#
#In languages with vectors indexed from 1, and with an even number of elements in the vector this will
#result in selecting the lower middle, while in others the upper middle. This has no real effect on the code.
#
#Use rounding to avoid selecting incorrect cells due to floating point arithmetic
#
#vector - a vector containing real numbers
middle_max <- function(vector) {
  vector <- round(vector, 3)
  maxima <- which(vector==max(vector))
  selection <- ceiling((length(maxima))/2)
  return(maxima[selection])
}

Now you can get the optimum angle by

Code
  optimum_angle <- x_axis[middle_max(dps_at_angles)]

I have checked that this produces exactly the same output as the original code.

Attached is a new version with these changes.

Edit: clarified the notes about ceiling and floor and added recommendation to use floor in Py.

[attachment deleted by admin]
« Last Edit: December 29, 2022, 10:41:11 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #341 on: December 30, 2022, 01:07:08 AM »

My translation code produces the same graphs but some strange all-on-one distributions, and I don't know why.
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


#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


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


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


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


def minimum_mean(weapon: tuple) -> float:
    """
    Return the minimum mean hit probability this weapon over its
    tracking arc.
   
    weapon - a tuple of facing, spread and tracking_range at
             [3],[4],[5]
    """
    return weapon[2] - (weapon[3] + weapon[4]) / 2


def maximum_mean(weapon:tuple) -> float:
    """
    Return the minimum mean hit probability this weapon over its
    tracking arc
   
    weapon - a tuple of facing, spread and tracking_range at
             [3],[4],[5]
    """
    return weapon[2] + (weapon[3] - weapon[4]) / 2


def transform_hit_coord(angle: float, weapon: tuple) -> float:
    """
    Return the facing of this weapon when aiming at this target.
   
    weapon - a tuple ending with min_mean and max_mean
    angle - angle of the target relative to our ship
    """
    return max(weapon[5], min(weapon[6], angle))


def transformed_angle(angle: float, weapon: tuple) -> 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 - transform_hit_coord(angle,weapon)


def upper_bounds(ship: tuple, 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.
   
    ship - a tuple with width at [4] and number of cells stored at [5]
    distance - range to the ship
    """
    ship_angle = ship[4] / (2 * pi * distance)
    cell_angle = ship_angle / ship[5]
    angles = [-ship_angle / 2]
    for i in range(ship[5]): angles.append(angles[-1] + cell_angle)
    return tuple(angle * 2 * pi * distance 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 - a parameter of a symmetric uniform distribution
             (-spread, spread)
    """
    if standard_deviation == 0 and spread == 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 hit_probability_at_angle(
        weapon: tuple,
        target_angular_size: float,
        target_positional_angle: float,
        target_positional_angle_error: float) -> float:
    """
    Return the sum of expected damage per second from this weapon
    to a target.
   
    weapons - a tuple of information about a weapon
    target_positional_angle -
    target_angular_size - angle, centered on weapons, from lower to
                          upper target bound
    target_positional_angle_error -
    """
    angle = transformed_angle(target_positional_angle, weapon)
    return probability_hit_between(angle - target_angular_size,
                                   angle + target_angular_size,
                                   target_positional_angle_error,
                                   weapon[4])


def index_of_maximum(row: tuple):
    """
    Return the index of the cell where the vector has its maximum value.
   
    vector - a vector containing real numbers
    """
    maximum = row[0]
    maximum_index = 0
    for i, cell in enumerate(row):
        if cell > maximum:
            maximum = cell
            maximum_index
    return maximum_index


def main(
        weapons: tuple,
        target: tuple,
        distance: float,
        standard_deviation: float):
    """
    Print 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
    """
    for weapon in weapons:
        weapon += ([minimum_mean(weapon), maximum_mean(weapon)]
                    if(weapon[4] < weapon[3]) else [weapon[2], weapon[2]])

    #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(ship[4], distance) / 4
    target_positional_angle_error = arc_to_deg(standard_deviation, distance)
    dps_at_angles = []
    for target_positional_angle in target_positional_angles:
        damage_per_second = 0
        for weapon in weapons:
            angle = transformed_angle(target_positional_angle, weapon)
            probability = probability_hit_between(
                angle - target_angular_size,
                angle + target_angular_size,
                target_positional_angle_error,
                weapon[4])
            damage_per_second += weapon[1] * probability
        dps_at_angles.append(damage_per_second)
 
    #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): dps_at_angles[i+360] += dps_at_angles[i]
    #likewise note that angle 360 is just angle 0, angle 359 is angle
    #-1, and so on
    for i in range(540,720): dps_at_angles[i-360] += dps_at_angles[i]
 
    #having summed, select angles -179 to 180
    dps_at_angles = dps_at_angles[181:540]
   
    #we use a separate vector to keep track of angle, since vector
    #index 1 corresponds to angle -179 now
    x_axis = range(-179,180)
   
    import matplotlib.pyplot as plt
    plt.scatter(x_axis, dps_at_angles)
   
    optimum_angle = x_axis[index_of_maximum(dps_at_angles)]
    bounds = upper_bounds(target, distance)
    for weapon in weapons:
        print(weapon)
        spread_distance = deg_to_arc(weapon[4], distance)
        angle_difference = transformed_angle(optimum_angle,weapon)
        adjustment = deg_to_arc(angle_difference, distance)
        adjusted_bounds = tuple(bound + adjustment for bound in bounds)
        dist = hit_distribution(adjusted_bounds, standard_deviation,
                                spread_distance)
        print(tuple(round(x, 3) for x in dist))
        print()

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

#Test ship and weapons.

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

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

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

#sample runs
main(simple_weapons, ship, 1000, 50)

main(random_weapons, ship, 1000, 50)
[close]
Result
['right phaser', 100.0, -10.0, 20.0, 5.0, -22.5, -2.5]
(0.0, 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)

['left phaser', 100.0, 10.0, 20.0, 5.0, -2.5, 17.5]
(0.0, 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)

['pd gun 1', 30.0, -160.0, 20.0, 0.0, -170.0, -150.0]
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.002, 0.005, 0.013, 0.026, 0.048, 0.078, 0.827)

['pd gun 2', 30.0, 180.0, 20.0, 0.0, 170.0, 190.0]
(0.0, 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)

['pd gun 3', 30.0, 160.0, 20.0, 0.0, 150.0, 170.0]
(0.0, 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)

['photon torpedo', 120.0, 90.0, 0.0, 5.0, 90.0, 90.0]
(0.0, 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)

['bb gun', 5, -150, 11, 20, -150, -150]
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.002, 0.003, 0.993)

['space marine teleporter', 78, 69, 173, 29, -32.0, 141.0]
(0.0, 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)

['turbolaser', 92, 122, 111, 9, 62.0, 173.0]
(0.0, 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)

['hex bolter', 24, -136, 38, 20, -165.0, -127.0]
(0.025, 0.014, 0.018, 0.021, 0.023, 0.025, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.692)

['hex bolter', 24, -136, 38, 20, -165.0, -127.0]
(0.025, 0.014, 0.018, 0.021, 0.023, 0.025, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.692)

['singularity projector', 95, 28, 122, 25, -45.5, 76.5]
(0.0, 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)

['subspace resonance kazoo', 68, -139, 12, 2, -146.0, -134.0]
(0.0, 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)

['left nullspace projector', 10, 28, 54, 11, -4.5, 49.5]
(0.0, 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)

['telepathic embarrassment generator', 30, -31, 35, 8, -52.5, -17.5]
(0.0, 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)

['perfectoid resonance torpedo', 34, 72, 10, 17, 72, 72]
(0.0, 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)

['entropy inverter gun', 78, -60, 13, 24, -60, -60]
(0.0, 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)

['mini-collapsar rifle', 27, 28, 16, 13, 13.5, 29.5]
(0.0, 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)

['false vacuum tunneler', 32, 78, 157, 20, -10.5, 146.5]
(0.0, 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)
[close]
« Last Edit: December 30, 2022, 01:26:58 AM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #342 on: December 30, 2022, 02:07:32 AM »

Well, does it also print/graph the correct optimum angle, if you ask it to? These are 0 for the "simple weapons" run and 73 for the "random weapons" run (pictured, weapons 7 to 18; note that your code actually has two copies of the "hex bolter"). If so, then there are only two places where the error can be: either in the adjusted cell upper bounds, or the hit distribution function.

Here is the whole "random weapons" run:

 main(ship, 1000, 50, weapons[7:18,])
[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 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 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.374 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.021 0.374
[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.344 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.031 0.285
[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 0.026 0.342
                                 name damage facing tracking_arc spread min_mean max_mean
1                              bb gun      5   -150           11     20   -150.0   -150.0
2             space marine teleporter     78     69          173     29     -3.0    141.0
3                          turbolaser     92    122          111      9     71.0    173.0
4                          hex bolter     24   -136           38     20   -145.0   -127.0
5               singularity projector     95     28          122     25    -20.5     76.5
6            subspace resonance kazoo     68   -139           12      2   -144.0   -134.0
7            left nullspace projector     10     28           54      0      1.0     55.0
8  telepathic embarrassment generator     30    -31           35      8    -44.5    -17.5
9        perfectoid resonance torpedo     34     72           10     17     72.0     72.0
10               entropy inverter gun     78    -60           13     24    -60.0    -60.0
11               mini-collapsar rifle     27     28           16     13     26.5     29.5
12              false vacuum tunneler     32     78          157     20      9.5    146.5
[1] 73

The last line is where I asked it to print the optimum angle.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 718
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #343 on: December 31, 2022, 10:36:09 AM »

It didn't, but I have fixed some bugs, and now it does.  I had flipped a sign in the minimum means code and implemented a simple index of maximum search rather than your middle index of approximate maxima search.
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


#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


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


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


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


def minimum_mean(weapon: tuple) -> float:
    """
    Return the minimum mean hit probability this weapon over its
    tracking arc.
   
    weapon - a tuple of facing, spread and tracking_range at
             [3],[4],[5]
    """
    return weapon[2] - (weapon[3] - weapon[4]) / 2


def maximum_mean(weapon: tuple) -> float:
    """
    Return the minimum mean hit probability this weapon over its
    tracking arc
   
    weapon - a tuple of facing, spread and tracking_range at
             [3],[4],[5]
    """
    return weapon[2] + (weapon[3] - weapon[4]) / 2


def transform_hit_coord(angle: float, weapon: tuple) -> float:
    """
    Return the facing of this weapon when aiming at this target.
   
    weapon - a tuple ending with min_mean and max_mean
    angle - angle of the target relative to our ship
    """
    return max(weapon[5], min(weapon[6], angle))


def transformed_angle(angle: float, weapon: tuple) -> 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 - transform_hit_coord(angle,weapon)


def upper_bounds(ship: tuple, 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.
   
    ship - a tuple with width at [4] and number of cells stored at [5]
    distance - range to the ship
    """
    ship_angle = ship[4] / (2 * pi * distance)
    cell_angle = ship_angle / ship[5]
    angles = [-ship_angle / 2]
    for i in range(ship[5]): angles.append(angles[-1] + cell_angle)
    return tuple(angle * 2 * pi * distance 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 - a parameter of a symmetric uniform distribution
             (-spread, spread)
    """
    if standard_deviation == 0 and spread == 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 hit_probability_at_angle(
        weapon: tuple,
        target_angular_size: float,
        target_positional_angle: float,
        target_positional_angle_error: float) -> float:
    """
    Return the sum of expected damage per second from this weapon
    to a target.
   
    weapons - a tuple of information about a weapon
    target_positional_angle -
    target_angular_size - angle, centered on weapons, from lower to
                          upper target bound
    target_positional_angle_error -
    """
    angle = transformed_angle(target_positional_angle, weapon)
    return probability_hit_between(angle - target_angular_size,
                                   angle + target_angular_size,
                                   target_positional_angle_error,
                                   weapon[4])


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 main(
        weapons: tuple,
        target: tuple,
        distance: float,
        standard_deviation: float):
    """
    Print 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
    """
    for weapon in weapons:
        weapon += ([minimum_mean(weapon), maximum_mean(weapon)]
                    if(weapon[4] < weapon[3]) else [weapon[2], weapon[2]])

    #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(ship[4], distance) / 4
    target_positional_angle_error = arc_to_deg(standard_deviation, distance)
    dps_at_angles = []
    for target_positional_angle in target_positional_angles:
        damage_per_second = 0
        for weapon in weapons:
            angle = transformed_angle(target_positional_angle, weapon)
            probability = probability_hit_between(
                angle - target_angular_size,
                angle + target_angular_size,
                target_positional_angle_error,
                weapon[4])
            damage_per_second += weapon[1] * probability
        dps_at_angles.append(damage_per_second)
 
    #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): dps_at_angles[i+360] += dps_at_angles[i]
    #likewise note that angle 360 is just angle 0, angle 359 is angle
    #-1, and so on
    for i in range(540,720): dps_at_angles[i-360] += dps_at_angles[i]
 
    #having summed, select angles -179 to 180
    dps_at_angles = dps_at_angles[181:540]
   
    #we use a separate vector to keep track of angle, since vector
    #index 1 corresponds to angle -179 now
    x_axis = range(-179,180)
   
    import matplotlib.pyplot as plt
    plt.scatter(x_axis, dps_at_angles)
   
    optimum_angle_index = middle_index_of_approximate_maxima(dps_at_angles)
    optimum_angle = x_axis[optimum_angle_index]
    bounds = upper_bounds(target, distance)
    for weapon in weapons:
        print(weapon)
        spread_distance = deg_to_arc(weapon[4], distance)
        angle_difference = transformed_angle(optimum_angle,weapon)
        adjustment = deg_to_arc(angle_difference, distance)
        adjusted_bounds = tuple(bound + adjustment for bound in bounds)
        dist = hit_distribution(adjusted_bounds, standard_deviation,
                                spread_distance)
        print(tuple(round(x, 3) for x in dist))
        print()
    print("optimum angle:", optimum_angle)

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

#Test ship and weapons.

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

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

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

#sample runs
#main(simple_weapons, ship, 1000, 50)

main(random_weapons, ship, 1000, 50)
[close]
Result
['bb gun', 5, -150, 11, 20, -150, -150]
(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)

['space marine teleporter', 78, 69, 173, 29, -3.0, 141.0]
(0.391, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.018, 0.391)

['turbolaser', 92, 122, 111, 9, 71.0, 173.0]
(0.165, 0.051, 0.054, 0.056, 0.058, 0.058, 0.058, 0.058, 0.058, 0.058, 0.056, 0.054, 0.051, 0.165)

['hex bolter', 24, -136, 38, 20, -145.0, -127.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)

['singularity projector', 95, 28, 122, 25, -20.5, 76.5]
(0.374, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.021, 0.374)

['subspace resonance kazoo', 68, -139, 12, 2, -144.0, -134.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)

['left nullspace projector', 10, 28, 54, 0, 1.0, 55.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)

['telepathic embarrassment generator', 30, -31, 35, 8, -44.5, -17.5]
(1.0, 0.0, -0.0, 0.0, -0.0, 0.0, 0.0, -0.0, 0.0, -0.0, 0.0, -0.0, 0.0, 0.0)

['perfectoid resonance torpedo', 34, 72, 10, 17, 72, 72]
(0.344, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.031, 0.285)

['entropy inverter gun', 78, -60, 13, 24, -60, -60]
(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)

['mini-collapsar rifle', 27, 28, 16, 13, 26.5, 29.5]
(1.0, 0.0, 0.0, 0.0, -0.0, 0.0, -0.0, 0.0, 0.0, 0.0, -0.0, -0.0, 0.0, -0.0)

['false vacuum tunneler', 32, 78, 157, 20, 9.5, 146.5]
(0.342, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.026, 0.342)

optimum angle: 73
[close]
« Last Edit: December 31, 2022, 10:39:59 AM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #344 on: December 31, 2022, 12:19:17 PM »

All right, great job! Seems like we now have the tools to import and analyze ships, at least to a degree of precision. By the way, and you probably know this but just to be sure, the code is in no way fixed to right = 0 degrees. It can be anything so long as all the ships and guns use the same reference point, for example if 0 is straight ahead in game files (I have no idea) then that is fine so long as it is the same for all ships.

Want to wander into projective geometry to implement more complex shapes or proceed with this?

Happy new year!

P.s. attached is how I imagine you might go about it. (If it should turn out to be difficult to formulate rules for which index to use from which direction using just 1 cell index, the alternative computation is just calculate cell distance from gun and hit nearest when the boundaries of multiple cells overlap. It might be that we have to do that because the rules for cell selection could be unclear for guns at the middle. (Edit: removed idea about using modular arithmetic and integers for this as euclidean distance might be more practical later if we want to e.g. add range)). The indices are a little wrong in the picture but the idea is that you would number cells from back to front and from right to left if the gun is to the left, and left to right if gun is to the right. Then you would find the corners of all cells projected to the circle. Then you find all overlaps. Then for all overlaps you select the highest cell number as the cell which will be hit. Then you calculate the probability distribution over the resulting line of cell bounds and map the hits to the ship armor grid. And the mathematical question is does this procedure always result in hitting the nearest cell in terms of euclidean distance among those whose projections to the circle overlap.  (I should think so when the ship is to the side, because let's say the enemy ship is rectangular. Then the vector from our ship to cell 1 is (x, y). Then let's say that the number of a particular cell is ar+c, where a is the number of rows, r is the number of cells per row, and c is the column index ie. the remainder of cell number / r. Then, the vector from cell 1 to that cell is (-sa, -sc) if s is the side of an armor cell. Without loss of generality we can say s=1. Then the euclidean distance to the cell from our ship is sqrt((x-a)^2+(y-c)^2). Now let's say that a cell has a shorter euclidean distance but a lower index. Then it must be the case that either a or c is lower (or both). But then it follows that the euclidean distance is greater. Then the question is does this apply to ships of an arbitrary shape. Most likely so, because we might conceptualize of the arbitrary shape as a rectangle but with cells that can't be hit, and if a cell can't be hit then the task is to find the highest index in the rectangle behind it. However, how to define the index when the enemy ship is not to one side but in the middle of our field of fire?)

But, as stated, I don't really think this is necessary to publish for now and it might lead to some work, so your call. It does seem like it will be pretty computationally intensive compared to what we have now.

[attachment deleted by admin]
« Last Edit: January 01, 2023, 03:29:49 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge
Pages: 1 ... 21 22 [23] 24 25 ... 32