Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  

News:

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

Pages: 1 ... 18 19 [20] 21 22 ... 32

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

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #285 on: December 16, 2022, 11:59:55 AM »

Our code can already test many individual weapons, or small weapon groups, against a brick wall of armor, and that is already impressive.  With just a little work, it could even give the brick wall a shield.  A whole mod to do this manually already exists (Practice Targets) and we could largely automate and widen that process.  I call that progress made in just a month.  Now for the rest?  If we have the time and inclination, sure, we kinda could do at least some of it, but as CapnHector points out, the bigger the goal, the more work it would take and more misleading a small mistake could be.

As for user demand, I have some unrelated modder-labor-saving statistical code that I have wanted to publish for a while but did not because it would have been too small a mod on its own.  What if we bundled them into a nice little mod called StatSector? :D

intrinsic_parity

  • Admiral
  • *****
  • Posts: 3071
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #286 on: December 16, 2022, 12:53:55 PM »

IMO, return fire is a necessity if you want to consider shields/shield damage, and also if you want to evaluate loadouts as a whole. Raw damage output is not really that useful without the context of the rest of the loadout, ship and enemy. For instance, the Mjolnir and Gauss look great in your earlier results, because you don't consider flux at all. Both are very flux expensive and somewhat inefficient. IMO, those weapons are frequently unusable on smaller ships because of the flux cost, so I would say they are not top choices for the average ship.

To test a weapon, what I would want to do is something like:
Choose some set of ships and loadouts that you consider to be balanced, and test them against some 'reasonable' enemies to determine a baseline, then modify the loadouts to incorporate your new weapon, and see how the results compare. Just shooting a target dummy doesn't tell you enough to a balance a weapon IMO.

In a perfect world, I would want a mod which basically takes some input files to determine the ships and loadouts, and then runs the in-game combat simulator with no graphics and returns combat results (damage values, times, etc.). I view what we are doing as building towards something like that, but maybe it would be easier to just do that IDK. I don't have enough experience with JAVA to consider attempting that without investing way more time than I have.

Also, return fire is very trivial to add? It's all the same code as your own ships, so any bugs would also appear in your own ship. And as I mentioned earlier, the computational cost is really not that significant. I think your original code was just really slow/poorly optimized giving you a false impression of how expensive the computations are :P.
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #287 on: December 16, 2022, 07:58:31 PM »

As for user demand, I have some unrelated modder-labor-saving statistical code that I have wanted to publish for a while but did not because it would have been too small a mod on its own.  What if we bundled them into a nice little mod called StatSector? :D

Sounds good, you have my go-ahead!

Also, return fire is very trivial to add? It's all the same code as your own ships, so any bugs would also appear in your own ship. And as I mentioned earlier, the computational cost is really not that significant. I think your original code was just really slow/poorly optimized giving you a false impression of how expensive the computations are :P.

Oh no doubt. It was total spaghetti written in R. This new code will be at least 150x faster. The question is just what happens when you have a large number of weapons and layouts. For example if you have 20 different weapons and should choose 5 out of those, and we are also using realistic ships so slots are not interchangeable, then that will already result in 20^5 so  3 200 000 combats, about 30 times as many as I did, since you can choose the same weapon more than once. And that is a vast understatement of how many weapons are available if you are trying to balance mods. Or how many weapon slots ships have, for that matter.

Won't realistic return fire at least double computations, since it's running damage, shot spread etc. for our ship also, with a possibility of losing? Or would it just be limited to shields? In the latter case, isn't it just a modifier to flux? And open to a lot of criticism about unrealistic assumptions. Also, when explaining the results you, must then say these are the results given this kind of return fire. Whether that's good depends on the context of whether people would prefer to know base weapon effectiveness or a more realistic combat model.

On the other hand adding flux to our ship seems quite reasonable and easy to do. Just keep track of it and delay fire until it is low enough. Unfortunately this necessitates re-writing the firing sequence code to be able to delay shots. If return fire exists then in fact the firing sequence must be re-computed at each timepoint, since we do not know in advance what the return fire will be, given it's another system with feedback loops when both ships have flux and guns. It's not just an extend sequence by adding 0s operation either, because ammo will still regenerate when not firing. With no return fire we could pre-compute the sequence as we do now, but there is a question of how the ship should prioritize firing weapons. Should it save flux in some circumstances?

There is also another very simple solution to consider flux without any of this: compute flux to kill in addition to time to kill. This should be quite easy since we are already keeping track of how many shots/beam ticks have been fired. Then in addition to ranking weapons by time to kill, also compute a flux to kill rank. This is non-trivial and interesting information because usually very hard hitting weapons consume a lot of flux/sec, but also kill faster, so it's not necessarily true they take more flux to kill.

I would advocate for the last option for the time being.
« Last Edit: December 16, 2022, 08:14:44 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #288 on: December 16, 2022, 08:20:08 PM »

Now we have a dictionary of all ship and weapon specs, comprising the relevant .csv and .ship values, each typed appropriately albeit with some tweaking still needed on instantiation depending on weapon type, indexed as database[source]["weapons" or "ships"][weapon_id or ship_id].

database.py
Code
"""
Database of all ship and weapon specs, divided into one
section for each mod alongside one for vanilla.
"""
import os
import csv
import json
from sys import platform
import decimal
from decimal import Decimal


class Database:
    """
    Provides the data needed to instantiate the Ship and Weapon classes.

    Loads every weapon_data.csv, ship_data.csv, and .ship file from the
    vanilla folder and mods folder and returns the necessary data as a
    flat dictionary.

    methods:
    - weapon_data
    - ship_data
    """
    _IDS_OF_MODS_CAUSING_ERRORS = (
        'armaa',
        'gundam_uc',
        'SCY'
    )

    _WEAPON_DATA_CSV_COLUMN_DATA_TYPES = {
        "name" : str,
        "id" : str,
        "tier" : int,
        "rarity" : Decimal,
        "base value" : int,
        "range" : Decimal,
        "damage/second" : Decimal,
        "damage/shot" : Decimal,
        "emp" : Decimal,
        "impact" : Decimal,
        "turn rate" : Decimal,
        "OPs" : int,
        "ammo" : int,
        "ammo/sec" : Decimal,
        "reload size" : int,
        "type" : str,
        "energy/shot" : Decimal,
        "energy/second" : Decimal,
        "chargeup" : Decimal,
        "chargedown" : Decimal,
        "burst size" : Decimal,
        "burst delay" : Decimal,
        "min spread" : Decimal,
        "max spread" : Decimal,
        "spread/shot" : Decimal,
        "spread decay/sec" : Decimal,
        "beam speed" : Decimal,
        "proj speed" : Decimal,
        "launch speed" : Decimal,
        "flight time" : Decimal,
        "proj hitpoints" : int,
        "autofireAccBonus" : Decimal,
        "extraArcForAI" : Decimal,
        "hints" : str,
        "tags" : str,
        "groupTag" : str,
        "tech/manufacturer" : str,
        "for weapon tooltip>>" : str,
        "primaryRoleStr" : str,
        "speedStr" : str,
        "trackingStr" : str,
        "turnRateStr" : str,
        "accuracyStr" : str,
        "customPrimary" : str,
        "customPrimaryHL" : str,
        "customAncillary" : str,
        "customAncillaryHL" : str,
        "noDPSInTooltip" : bool,
        "number" : Decimal
    }

    _SHIP_DATA_CSV_COLUMN_DATA_TYPES = {
        "name" : str,
        "id" : str,
        "designation" : str,
        "tech/manufacturer" : str,
        "system id" : str,
        "fleet pts" : int,
        "hitpoints" : int,
        "armor rating" : int,
        "max flux" : int,
        "8/6/5/4%" : Decimal,
        "flux dissipation" : int,
        "ordnance points" : int,
        "fighter bays" : int,
        "max speed" : Decimal,
        "acceleration" : Decimal,
        "deceleration" : Decimal,
        "max turn rate" : Decimal,
        "turn acceleration" : Decimal,
        "mass" : int,
        "shield type" : str,
        "defense id" : str,
        "shield arc" : Decimal,
        "shield upkeep" : Decimal,
        "shield efficiency" : Decimal,
        "phase cost" : Decimal,
        "phase upkeep" : Decimal,
        "min crew" : int,
        "max crew" : int,
        "cargo" : int,
        "fuel" : int,
        "fuel/ly" : Decimal,
        "range" : Decimal,
        "max burn" : int,
        "base value" : int,
        "cr %/day" : Decimal,
        "CR to deploy" : int,
        "peak CR sec" : Decimal,
        "CR loss/sec" : Decimal,
        "supplies/rec" : int,
        "supplies/mo" : Decimal,
        "c/s" : Decimal,
        "c/f" : Decimal,
        "f/s" : Decimal,
        "f/f" : Decimal,
        "crew/s" : Decimal,
        "crew/f" : Decimal,
        "hints" : str,
        "tags" : str,
        "rarity" : Decimal,
        "breakProb" : Decimal,
        "minPieces" : int,
        "maxPieces" : int,
        "travel drive" : str,
        "number" : Decimal
    }
    def __init__(self):
        decimal.places = 6
        if platform == "darwin":
            self._vanilla_path = "Contents/Resources/Java"
        #elif platform == "linux":
            #self._vanilla_path = linux path
        #elif platform == "win32":
            #self._vanilla_path = windows path
        self._sources = self._load_sources()

    def __getitem__(self, source_id: str) -> dict:
        """
        Returns a dictionary of the source with this id.

        source_id - vanilla for the vanilla files and the relevant
                    mod_id for mod files
        """
        return self._sources[source_id]

    def _is_mod_causing_errors() -> bool:
        if 'mod_info.json' not in os.listdir(os.getcwd()):
            return True
        with open('mod_info.json') as f:
            lines = f.readlines()
        for line in lines:
            if '"id"' in line:
                for ID in Database._IDS_OF_MODS_CAUSING_ERRORS:
                    if ID in line: return True
        return False

    def _subdirectory_paths(path: str) -> tuple:
        """
        Return a list of the relative paths of the
        directories within this one.

        path - a path
        """
        return tuple(f.path for f in os.scandir(path) if f.is_dir())

    def _csv_dictionary(file_name: str) -> dict:
        """
        Return a .csv as a flat dictionary keyed by row, with elements
        typed by column.
        """
        types = (Database._WEAPON_DATA_CSV_COLUMN_DATA_TYPES
                 if file_name == "weapon_data.csv" else
                 Database._SHIP_DATA_CSV_COLUMN_DATA_TYPES)
        with open(file_name) as f:
            rows = tuple(row for row in csv.reader(f))
        column_names = rows[0]
        dictionary = {}
        for row in rows[1:]:
            ID = row[1]
            if ID == "": continue
            dictionary[ID] = {}
            for i, value in enumerate(row):
                if value == "": continue
                column_name = column_names[i]
                data_type = types[column_name]
                if data_type == str: value = data_type(value)
                dictionary[ID][column_name] = value
        return dictionary

    def _source_dictionary(path):
        """
        Return the data of the ships and weapons of this
        source, organized by origin.
        """
        source = {}
        directories = [path.split("/")[-1] for path in
                          Database._subdirectory_paths(os.getcwd())]
        if 'data' in directories:
            os.chdir('data')
            directories = [path.split("/")[-1] for path in
                           Database._subdirectory_paths(os.getcwd())]
        else: return source
        if 'weapons' in directories:
            os.chdir('weapons')
            if 'weapon_data.csv' in os.listdir(os.getcwd()):
                source['weapons'] = Database._csv_dictionary('weapon_data.csv')
            os.chdir('..')
        if 'hulls' in directories:
            os.chdir('hulls')
            if 'ship_data.csv' in os.listdir(os.getcwd()):
                source['ships'] = Database._csv_dictionary('ship_data.csv')
                for ship_id in source["ships"]:
                    if ship_id + '.ship' in os.listdir(os.getcwd()):
                        with open(ship_id + '.ship') as f:
                            source["ships"][ship_id].update(json.load(f))
            os.chdir('..')
        return source

    def _load_sources(self):
        """
        Consolidate the ship and weapon data of vanilla (and every mod
        selected by the user) into one dictionary for simulation and
        calculation use.

        The structure of this dictionary is

        {
            "ships":{
                "shipId":{
                    "attributeId":value
                }
            },
            "weapons" : {
                "weaponId":{
                    "attributeId":value
                }
            }
        }

        where every shipId, weaponId, and attributeId is taken from the
        game or mod files.
        """
        all_data = dict()
        os.chdir("..")
        os.chdir("..")
        for path in Database._subdirectory_paths(os.getcwd()):
            os.chdir(path)
            if Database._is_mod_causing_errors():
                print(("WARNING: mod at", path, "not loaded because its"
                      ".csv or .ship files cause errors."))
                os.chdir('..')
                continue
            all_data[path] = Database._source_dictionary(path)
            os.chdir("..")
        os.chdir("..")
        os.chdir("..")
        os.chdir(self._vanilla_path)
        all_data['vanilla'] = Database._source_dictionary(self._vanilla_path)
        return all_data
[close]

intrinsic_parity

  • Admiral
  • *****
  • Posts: 3071
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #289 on: December 16, 2022, 10:27:40 PM »

A factor of 2 in compute time should not be a big deal IMO.

Also, you can definitely get a good idea of balance with a small set of tests. The only time you need to do really exhaustive searches IMO, is if you are trying to find the 'optimal' builds completely numerically (without any prior knowledge or intuition). If you start with some idea of some builds with existing (balanced) weapons, that are good, then you really only need to compare your new stuff to the existing stuff to see if it is an outlier. Basically, a smart testing methodology should preclude the need for exhaustive searches.

Also, it really shouldn't be an issue to have multiple ships firing at once.

If the simulation time step is the same as the game so that shots always happen exactly at simulation time steps, then for normal weapons (no burst/charge up/charge down) it's just some straightforward modular arithmetic to check if a weapon is firing or not at any time step, given the time of the first shot. If you know in advance when weapons with start/stop firing, you can do all the checks in advance, but there is also a good argument for supporting some dynamic decision making for when to fire or not, since that is how the game actually works.

For burst/charge up/charge down weapons, what I would do is precompute the firing profile of a single burst with reference to the start of the burst, then do modular arithmetic to determine when the burst starts and execute the pre computed burst sequence at those times.
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #290 on: December 17, 2022, 07:36:48 AM »

Well, we'll cross that bridge when we get there. Turning to the math, I'm not done here yet.

math



[close]

But I did happen to derive the equations for simulating the probability distribution of hits on the enemy ship when rotating our ship.

Here are some graphs. AUC=probability of hits on ship times damage (damage is 1 where not specified), angle=angle of enemy ship to our target (0 degrees = exactly to the right, 90 degrees = exactly in front), offset = offset of guns' facing from 0 degrees, positive for one gun and negative for the other (ie offset 30=one is facing +30 and one -30 degrees). Range 1000px. Enemy ship width 220 px (Dominator).
Test 1. A simple plot of the 1 gun solution. Vertical lines are a and b. Note that the distribution's top turns flat in the area where the gun can follow the target due to tracking.


Test 2. 2 guns with a 20 degree turn range with variable offset, gun 1 has offset +offset and gun 2 has offset -offset.

The correct choice, when the dists do not overlap, is to select one of the guns to use, and rotate so that the target is in the area where the gun can track it.

Test 3. 1 gun fixed to the right of the ship dealing 3 damage with a 30 degree spread but unable to rotate, 2 guns with variable offset one dealing 2 damage and one dealing 1 damage. The correct solution is to overlap guns 2 and 1 when possible, when not you can use either gun 1 or gun 2 (roughly).


If I can't get the equations to come together (I will, eventually, for sure, but) then there is always the option of just brute forcing it by testing the 360 possible 1 degree facings for which produces the most DPS. This is a relatively small computation compared to the main damage calculation, it's comparable to creating the hit dists or firing sequences.

Code:
Spoiler
Code
library(ggplot2)
library(reshape2)

n <- 50/1000
w <- 110/1000
G <- function(y) return(y*pnorm(y)+dnorm(y))
Fz <- function(z, u) return(n/2/u*(G(z/n+u/n)-G(z/n-u/n)))
h <- function(x,a,b) return(min(a,max(x,b)))

u1 <- 10 * pi/180
u2 <- 2.5 * pi/180
u3 <- 0

AUC <- function(t,a,b,u) return(Fz(t+w-h(t,a,b),u)-Fz(t-w-h(t,a,b),u))
                             
#test 1
df <- data.frame()
offset1 <- 60 * pi/180
a1 <- offset1+u1
b1 <- offset1-u1
for (angle in 1:360){
  t <- angle*pi/180
  df <- rbind(df,c((AUC(t,a1,b1,a1-b1)),t))
}
colnames(df) <- c("auc","angle")
df$angle<-df$angle*180/pi
df
ggplot(df,aes(y=auc,x=angle))+
  geom_line()+
  geom_vline(xintercept=a1*180/pi)+
  geom_vline(xintercept=b1*180/pi)


#test 2 - 2 similar guns dealing equal damage
df <- data.frame()
for (offset in seq(1,180,2)){
  roffset <- offset*pi/180
  a1 <- roffset+u1
  b1 <- roffset-u1
  a2 <- -roffset+u1
  b2 <- -roffset-u1
  for (angle in seq(-180,180,2)){
    t <- angle*pi/180
    print(c(t,offset,AUC(t,a1,b1,a1-b1)+AUC(t,a2,b2,a2-b2)))
    df <- rbind(df,c((AUC(t,a1,b1,a1-b1)+AUC(t,a2,b2,a2-b2)),t,roffset))
  }
}

colnames(df) <- c("auc","angle","offset")
df$angle <- df$angle*180/pi
df$offset <- df$offset*180/pi
ggplot(df, aes(color=auc))+
  geom_tile(aes(x=angle,y=offset))+
  scale_color_viridis_c()

#test 3 -1 gun with rather heavy damage fixed to the front of the ship, and 2 guns with variable offset, one with double damage

df <- data.frame()
for (offset in seq(1,180,2)){
  roffset <- offset*pi/180
  a1 <- roffset+u1
  b1 <- roffset-u1
  a2 <- -roffset+u1
  b2 <- -roffset-u1
  a3 <- 0
  b3 <- 0
  for (angle in seq(-180,180,2)){
    t <- angle*pi/180
    df <- rbind(df,c(AUC(t,a1,b1,a1-b1)+2*AUC(t,a2,b2,a2-b2)+3*AUC(t,a3,b3,30*pi/180),t,roffset))
  }
}

colnames(df) <- c("auc","angle","offset")
df$angle <- df$angle*180/pi
df$offset <- df$offset*180/pi
ggplot(df, aes(color=auc))+
  geom_tile(aes(x=angle,y=offset))+
  scale_color_viridis_c()

[close]

Well, another possibility is to drop the normal distribution from the calculation which is probably how the AI does it considering the real game doesn't have a normal distribution on top. Then the problem becomes just one of finding the highest among stacked boxes, so almost trivial (it is find all overlaps of guns' firing arcs when applicable and then count which of the overlaps or single guns has highest dps and you are done).
« Last Edit: December 17, 2022, 09:25:04 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #291 on: December 17, 2022, 11:59:52 PM »

Thinking about what Liral said, that brute force is sometimes faster, I decided to apply brute force since I think the best I can do without a few more analysis courses is present a piecewise solution (since the continuous replacement functions for the piecewise I figured out were pretty non-trivial).

So here is the brute-force solution in hopefully human-readable code using our previous functions, slightly augmented.

Code

#create a vector containing angles from -180 to 180
aucvector <- seq(-180,180)


#standard things
#dominator, hullhp, shieldregen, shieldmax, startingarmor, widthinpixels, armorcells, shieldwidth, shieldefficacy, shieldupkeep
ship <- c(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)

#engagementrange
range <- 1000

#fudge factor
errorsd <- 0.05
#the fudge factor should be a function of range (more error in position at greater range), but not a function of weapon firing angle, and be expressed in terms of pixels
error <- errorsd*range
G <- function(y) return(y*pnorm(y) + dnorm(y))
#a is the SD of the normal distribution and b is the parameter of the uniform distribution
#we add the special cases to this function
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(min(1,b-abs(z)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

#compute maximum and minimum mean coordinates
#weapons: damage, facing (deg), tracking range (deg), spread
weapon1 <- c(10,-30,60,10)
weapon2 <- c(10,60,30,5)
weapon3 <- c(20,0,60,0)
weapon4 <- c(20,90,60,0)
weapon5 <- c(5,0,180,0)

#data frame of weapons
weapons <- data.frame()
for (i in 1:5) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
colnames(weapons) <- c("damage","facing","trackingrange","spread")
weapons

#now compute maximum and minimum mean by computing facing+trackingrange/2-spread/2 and facing-trackingrange/2+spread/2
weapons <- cbind(weapons, maxmean=(weapons$facing+weapons$trackingrange/2-weapons$spread/2))
weapons <- cbind(weapons, minmean=(weapons$facing-weapons$trackingrange/2+weapons$spread/2))

#function to transform hit coordinates so mean is aligned with 0, meaning we can use our hit dist fucntion
transform_hit_coord <- function(angle, weapon) return(max(weapon$minmean, min(weapon$maxmean,angle)))

#necessary transformations
segment_to_deg <- function(seg) return(seg/range*360/pi)
deg_to_segment <- function(deg) return(deg*range/360*pi)

#now we have all that we need, so compute the expected auc for degree facings

transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))

sumauc <- function(angle) {
  summed_auc <- 0
  shipwidth <- segment_to_deg(ship[5])/2
  derror <- segment_to_deg(error)

  for (i in 1:length(weapons[,1])){
    summed_auc <- summed_auc + hit_probability_coord_lessthan_x(deg_to_segment(transformed_angle(angle,weapons[i,])+shipwidth),error,deg_to_segment(weapons[i,4]))-
      hit_probability_coord_lessthan_x(deg_to_segment(transformed_angle(angle,weapons[i,])-shipwidth),error,deg_to_segment(weapons[i,4]))
  }
  return(summed_auc)
}


applied <- sapply(aucvector,FUN=sumauc)
plot(sapply(aucvector,FUN=sumauc),xlab="angle",ylab="dps")
abline(v=median(which(applied == max(applied))))


(vertical line = choice of rotation)

The main idea is transform coordinates of the enemy ship to the coordinates of the dist median (ie. enemy ship angle from angle wrt our ship to angle from dist median such that angle of dist median = 0), which you find by considering it is equal to target's center when in tracking range, max turret turn angle - spread/2 when above tracking range, and min turret turn angle + spread/2 when below tracking range, since the mean of the uniform dist is also the mean of the convolved dist. Then you select the median rotation of those which correspond to max dps values.

In this case we had 5 guns, with the following parameters

#weapons: damage, facing (deg), tracking range (deg), spread
weapon1 <- c(10,-30,60,10)
weapon2 <- c(10,60,30,5)
weapon3 <- c(20,0,60,0)
weapon4 <- c(20,90,60,0)
weapon5 <- c(5,0,180,0)


To continue from here, you then save the angle of choice and use the normal hit distribution function, but passing the cells' boundary angles through the transform function for each weapon to get the final hit distribution over the enemy ship from our guns at that angle.

Another plot: this ship has 11 guns that can track over 90 degree intervals and are spaced at 36 degree intervals. (note: the x axis is fixed here, was not centered in last).


#weapons: damage, facing (deg), tracking range (deg), spread
weapon1 <- c(10,-180,90,0)
weapon2 <- c(10,-144,90,0)
weapon3 <- c(10,-108,90,0)
weapon4 <- c(10,-72,90,0)
weapon5 <- c(10,-36,90,0)
weapon6 <- c(10,0,90,0)
weapon7 <- c(10,180,90,0)
weapon8 <- c(10,144,90,0)
weapon9 <- c(10,108,90,0)
weapon10 <- c(10,72,90,0)
weapon11 <- c(10,36,90,0)



This shows that taking the naive median of the max points is actually not satisfactory. Instead we should prefer to choose specifically the middle one (or lower middle, if two exist). Should also apply a little rounding. There is also a problem that the angle is not considered properly, in that angles over 180 do not map to -180+(angle-180) etc. That requires some rewriting paying attention to the cyclical nature of angles but does not change the basic idea. Here is one way to do it.

Code
aucvector <- seq(-360,360)

angles <- seq(-180,180)
applied <- sapply(aucvector,FUN=sumauc)
for (i in 1:360) applied[i] <- applied[i]+applied[i+360]
applied <- applied[1:361]
applied <- c(tail(applied,181),head(applied,180))
plot(applied,x=angles,xlab="angle",ylab="dps")
abline(v=angles[which(round(applied,3) == round(max(applied),3))[ceiling(length(which(round(applied,3) == round(max(applied),3)))/2)]])




For a final test some completely random weapons

weapon1 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon2 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon3 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon4 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon5 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon6 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon7 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon8 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon9 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))
weapon10 <- c(runif(1,0,10),runif(1,-180,180),runif(1,0,360),runif(1,0,30))




Edit: fixed wraparound, also I'm adding the fixed script as an attachment to this.

[attachment deleted by admin]
« Last Edit: December 18, 2022, 03:48:49 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #292 on: December 18, 2022, 02:51:30 PM »

I have had to struggle for about an hour to translate the attached code because of its format and organization, and I still can't translate all of it.  I imagine that you wrote it as you would have written an informal proof you were explaining to someone on a whiteboard: starting from the beginning, defining functions and variables as needed, and using the shortest variable names possible.  Had we been chatting at the whiteboard, I would have understood perfectly, but code is read by oneself, so alas I had to struggle.  Had you organized your code as follows, I could have translated it in minutes (besides the dataframe and graphing parts, which would be long and difficult regardless).

  • All global constants two blank lines below the import statements, named in capital letters
  • All functions two blank lines after the global constants, separated by two blank lines from one another
  • All 'driver' code at the bottom, in one function called main, which is called after being defined.
  • Every variable and function named with one or more words rather than an acronym or single letter
  • Repetitive variables "weapon1 = ... , weapon2 = ..., weapon 3 =..." written as a list or tuple assembled by a for-loop
  • Every line 79 characters long or shorter
Code
import this_library
import that_library


GLOBAL_CONSTANT_NAME = some_value
OTHER_GLOBAL_CONSTANT_NAME = other value


hereIsAFunction = function() { it_does_something }


hereIsAnotherFunction = function() {it_does_something_else }


main = function() {
    make_the_program_go
    someArrayName = c()
    for (i in 1:10) {
        cbind(c, somethingToAdd)
    }
}
main()

Think of code as a textbook chapter proof, of which the interleaved lines and paragraphs of natural natural language have been combined into the letters and squiggles themselves.  For example, an elementary physics textbook author might write

Quote
Consider an object a distance x0 from the origin of an axis, along which it is moving with velocity v0, at time t0.  Were acceleration a uniformly applied to the object for a duration delta_t, the object would afterward be a displacement delta_x from the origin.
delta_x = x0 + v0 * delta_t + 1 / 2 * a * delta_t^2


whereas a Python coder would write

Code
def displacement_under_uniform_acceleration(
        initial_position: float,
        initial_velocity: float,
        acceleration: float,
        acceleration_duration: float) -> float:
    """
    Return the displacement of an object uniformly-accelerated along its velocity axis.

    initial_position - starting displacement of the object from its velocity axis origin
    initial_velocity - starting velocity of the object along its velocity axis
    acceleration - acceleration uniformly applied to the object for some duration
    acceleration_duration - time over which the object is accelerated
    """
    return (initial_position
             + initial_velocity * acceleration_duration
             + 1 / 2 * acceleration * acceleration_duration ** 2)

I would be happy to answer any questions you might have.
Code
Code
#library(ggplot2)
#library(reshape2)
import math
import random


N = 50 / 1000#QUESTION: What is this variable?
W = 110 / 1000#QUESTION: What is this variable?


def G(y): return y * pnorm(y) + dnorm(y)


def Fz(z,  u): return N / 2 / u * (G((z + u) / N) - G((z - u) / N))


def h(x, a, b): return min(a, max(x, b))


def AUC(t, a, b, u):
    """
    QUESTION: What does this function do, and why?
    """
    return Fz(t + W - h(t, a, b), u) - Fz(t - W - h(t, a, b), u)


def hit_probability_coord_lessthan_x(x: float, a: float, b: float) -> float:
    """
   
    Includes special cases.
   
    a - standard deviation of the normal distribution
    b - parameter of the uniform distribution
    """
    if a > 0 or b > 0:
        if a == 0: return min(1, b - abs(x))
        return (pnorm(x, 0, a) if b == 0
               else a / 2 / b * (G((x + b) / a) - G((x - b) / a)))
    return 0 if x < 0 else 1
   

def random_weapon_damage() -> float:
    """
    Return a random damage amount.
    """
    return random.random() * 10
   
   
def random_weapon_facing() -> float:
    """
    Return a random facing in degrees.
    """
    return random.random() * 360 - 180
   
   
def random_weapon_tracking_range() -> float:
    """
    Return a random tracking range in degrees.
    """
    return random.random() * 360
   
   
def random_weapon_spread() -> float:
    """
    Return a random spread in degreees.
    """
    return random.random() * 30   
           
           
def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return hit coordinates transformed to align mean with 0.
   
    Lets us use our hit dist fucntion.
    """
    return max(minimum_mean,  min(maximum_mean, angle))
   
   
def transformed_angle(angle: float, minimum_mean: float, maximum_mean: float):
    """Return the expected auc for degree facings."""
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)
   

def segment_to_deg(seg: float) -> float:
    """Return the degree angle corresponding to a segment."""
    return seg / range * 360 / math.pi
   
   
def deg_to_segment(deg: float) -> float:
    """Return the segment corresponding to a degree angle."""
    return deg * range / 360 * math.pi
   
   
def sum_auc(
        ship: object,
        minimum_means: tuple,
        maximum_means: tuple,
        spreads: tuple,
        angle: float,
        error: float) -> float:
    """
    QUESTION: What does this function do, and why?
    """
    summed_auc = 0
    shipwidth = segment_to_deg(ship[5]) / 2
    derror = segment_to_deg(error)

    for i, _ in enumerate(spreads):
        spread_distance = deg_to_segment(spreads[i])
        middle_angle = transformed_angle(angle, minimum_means[i],
                                         maximum_means[i])
        left_segment = deg_to_segment(middle_angle + shipwidth)
        right_segment = deg_to_segment(middle_angle - shipwidth)
        summed_auc += (hit_probability_coord_lessthan_x(left_segment, error,
                                                        spread_distance)
                       - hit_probability_coord_lessthan_x(right_segment, error,
                                                          spread_distance))
    return summed_auc
   
   
def test_1(u: float):
    offset = 60 * math.pi / 180
    u = 10 * math.pi / 180
    a = offset + u
    b = offset - u
    angles = tuple(angle for angle in range(360))
    auc = tuple((AUC(angle * math.pi / 180, a, b, a - b)) for angle in angles)
    #ggplot(df, aes(y=auc, x=angle))
    #           + geom_line()
    #           + geom_vline(xintercept=a1 * 180 / math.pi)
    #           + geom_vline(xintercept=b1 * 180 / math.pi)


def test_2(u: float):
    """
    2 similar guns dealing equal damage
    """
    degree_angles = tuple(angle for angle in range(-180, 180, 2))
    degree_offsets = tuple(offset for offset in range(0, 180, 2))
    radian_angles = tuple(angle * math.pi / 180 for angle in degree_angles)
    radian_offsets = tuple(offset * math.pi / 180 for offset in degree_offsets)
    aucs = []
    for degree_offset, radian_offset in zip(degree_offsets, radian_offsets):
        a1 = radian_offset + u
        b1 = radian_offset - u
        a2 = u - radian_offset
        b2 = u - radian_offset
        for degree_angle, radian_angle in zip(degree_angles, radian_angles):
            auc = (AUC(radian_angle, a1, b1, a1 - b1)
                   + AUC(radian_angle, a2, b2, a2 - b2))
            print(degree_angle, degree_offset, auc)
            aucs.append(auc)
    #ggplot(df, aes(color=auc))
    #           + geom_tile(aes(x=angle, y=offset))
    #           + scale_color_viridis_c()


def test_3(u: float):
    """
    1 high-damage gun fixed to the front of the ship and 2 guns with
    variable offset, one with double damage
    """
    degree_offsets = tuple(offset for offset in range(0, 180, 2))
    degree_angles = tuple(angle for angle in range(-180, 180, 2))
    radian_offsets = tuple(offset * math.pi / 180 for offset in degree_offsets)
    radian_angles = tuple(angle * math.pi / 180 for angle in degree_angles)
   
    aucs = []
    for degree_offset, radian_offset in zip(degree_offsets, radian_offsets):
        a1 = radian_offset + u
        b1 = radian_offset - u
        a2 = u - radian_offset
        b2 = u - radian_offset
        a3 = 0
        b3 = 0
        for angle in radian_angles:
            aucs.append((AUC(t, a1, b1, a1 - b1)
                         + 2 * AUC(t, a2, b2, a2 - b2)
                         + 3 * AUC(t, a3, b3, 30 * math.pi / 180), t, roffset))
        #ggplot(df, aes(color=auc)
        #           + geom_tile(aes(x=angle, y=offset))
        #           + scale_color_viridis_c())
   

def main():
    runtests = False
    if runtests:
        u = 10 * math.pi / 180
        test_1(u)
        test_2(u)
        test_3(u)
   
    #dominator
    ship = (14000,#hullhp
            500,#shieldregen
            10000,#shieldmax
            1500,#startingarmor
            220,#widthinpixels
            12,#armorcells
            440,#shieldwidth
            1.0,#shieldefficacy
            200)#shieldupkeep
   
    engagement_range = 1000
   
    #fudge factor
    errorsd = 0.05
    #the fudge factor should be a function of range (more error in
    #position at greater range),  but not a function of weapon firing
    #angle,  and be expressed in terms of pixels
    error = errorsd * engagement_range
   
    ship_count = 10

    damage = tuple(random_weapon_damage() for _ in range(ship_count))
    spread = tuple(random_weapon_spread() for _ in range(ship_count))
    facing = tuple(random_weapon_facing() for _ in range(ship_count))
    tracking_range = tuple(random_weapon_tracking_range() for _ in
                           range(ship_count))
    maximum_means = tuple(facing[i] + (tracking_range[i] - spread[i]) / 2 for i
                          in range(ship_count))
    minimum_means = tuple(facing[i] - (tracking_range[i] + spread[i]) / 2 for i
                          in range(ship_count))
   
    angles = tuple(i for i in range(-180, 181))
    #applied = sapply(angles, FUN=sum_auc)
    #plot(sapply(aucvector, FUN=sumauc), x=aucvector, xlab="angle", ylab="dps")
    #abline(v=aucvector[which(round(applied, 3) == round(max(applied), 3))[ceiling(length(which(round(applied, 3) == round(max(applied), 3))) / 2)]])
main()
[close]
« Last Edit: December 18, 2022, 03:22:58 PM by Liral »
Logged

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #293 on: December 18, 2022, 05:26:58 PM »

Oh, sorry! But on the plus side I think your explanation of how code should be written makes perfect sense and is very succinct and good. I'll try to adhere to it. Your imagining of how I thought about code was also quite on the spot. Also the code included the previous test I did too - that really shouldn't have been to avoid confusion, but I didn't realize you would end up translating it. Anyway, let's comment this

Code
#library(ggplot2)
#library(reshape2)
import math
import random


N = 50 / 1000#QUESTION: What is this variable?
W = 110 / 1000#QUESTION: What is this variable?

#Answer: these refer to the latex math. The first part was a direct translation of math to code and wasn't meant to be translated. Other than G(y), which we need defined, but should already exist with its own name in Py, ignore everything between this comment and next.

def G(y): return y * pnorm(y) + dnorm(y)


def Fz(z,  u): return N / 2 / u * (G((z + u) / N) - G((z - u) / N))


def h(x, a, b): return min(a, max(x, b))


def AUC(t, a, b, u):
    """
    QUESTION: What does this function do, and why?
    """
    return Fz(t + W - h(t, a, b), u) - Fz(t - W - h(t, a, b), u)

# A: Part of testing - ignore

def hit_probability_coord_lessthan_x(x: float, a: float, b: float) -> float:
    """
   
    Includes special cases.
   
    a - standard deviation of the normal distribution
    b - parameter of the uniform distribution
This needs an additional correction in the line about 0 sd, so minimum is 0 and also it should in fact be abs(b/2+x), but we do not need the abs here when the lower bound is 0 anyway. That means calculating the part of the box of area 1 from -b/2 to b/2 that is below x. Whoops!

A version of this should already exist with its own name in py. It is the CDF of the hit distribution.
    """
    if a > 0 or b > 0:
        if a == 0: return max(0,min(1, b/2+x))
        return (pnorm(x, 0, a) if b == 0
               else a / 2 / b * (G((x + b) / a) - G((x - b) / a)))
    return 0 if x < 0 else 1
   
*

def random_weapon_damage() -> float:
    """
    Return a random damage amount.
    """
    return random.random() * 10
   
   
def random_weapon_facing() -> float:
    """
    Return a random facing in degrees.
    """
    return random.random() * 360 - 180
   
   
def random_weapon_tracking_range() -> float:
    """
    Return a random tracking range in degrees.
    """
    return random.random() * 360
   
   
def random_weapon_spread() -> float:
    """
    Return a random spread in degreees.
    """
    return random.random() * 30   
           
           
def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle of the mean of the hit distribution, when the gun attempts to track the target.
   
    Lets us use our hit dist fucntion.
    """
    return max(minimum_mean,  min(maximum_mean, angle))
   
   
def transformed_angle(angle: float, minimum_mean: float, maximum_mean: float):
    """Return the angle between the target and the mean of the hit distribution, with sign.

"""
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)
   

def segment_to_deg(seg: float) -> float:
    """Return the degree angle corresponding to a segment."""
    return seg / range * 360 / math.pi
   
   
def deg_to_segment(deg: float) -> float:
    """Return the segment corresponding to a degree angle."""
    return deg * range / 360 * math.pi
   
   
def sum_auc(
        ship: object,
        minimum_means: tuple,
        maximum_means: tuple,
        spreads: tuple,
        angle: float,
        error: float) -> float:
    """
    QUESTION: What does this function do, and why?
    A: for all weapons, sum the probability to hit target ship given target ship angle. That is, CDF (ship's angle from mean of distribution + width/2) - CDF (ship's angle from mean of distribution - width/2). This corresponds to an integral of the probability distribution from the ship's left edge to right from our perspective for each gun. Additionally, this should be scaled by dps so that higher dps guns weigh more in decisions about rotation. If that was missing from my code, it is a mistake. The summand should be dps*probability for each gun (since dps from gun = dps * probability of hitting)

We have previously written the probability distribution in terms of pixels of target (ignoring curvature). However, it is convenient to refer to angles to describe gun placement. Hence the need for seg to deg and deg to seg translation. Derror was a vestige of an alternative formulation that we translate the sd to degrees. Whichever way works so long as all parameters are either angle or distance when summing, transforming or passing to hit distribution function.

    """
    summed_auc = 0
    shipwidth = segment_to_deg(ship[5]) / 2

    for i, _ in enumerate(spreads):
        spread_distance = deg_to_segment(spreads[i])
        middle_angle = transformed_angle(angle, minimum_means[i],
                                         maximum_means[i])
        left_segment = deg_to_segment(middle_angle + shipwidth)
        right_segment = deg_to_segment(middle_angle - shipwidth)
        summed_auc += (hit_probability_coord_lessthan_x(left_segment, error,
                                                        spread_distance)
                       - hit_probability_coord_lessthan_x(right_segment, error,
                                                          spread_distance))
    return summed_auc
   
#do not translate part between this comment and next
   
def test_1(u: float):
    offset = 60 * math.pi / 180
    u = 10 * math.pi / 180
    a = offset + u
    b = offset - u
    angles = tuple(angle for angle in range(360))
    auc = tuple((AUC(angle * math.pi / 180, a, b, a - b)) for angle in angles)
    #ggplot(df, aes(y=auc, x=angle))
    #           + geom_line()
    #           + geom_vline(xintercept=a1 * 180 / math.pi)
    #           + geom_vline(xintercept=b1 * 180 / math.pi)


def test_2(u: float):
    """
    2 similar guns dealing equal damage
    """
    degree_angles = tuple(angle for angle in range(-180, 180, 2))
    degree_offsets = tuple(offset for offset in range(0, 180, 2))
    radian_angles = tuple(angle * math.pi / 180 for angle in degree_angles)
    radian_offsets = tuple(offset * math.pi / 180 for offset in degree_offsets)
    aucs = []
    for degree_offset, radian_offset in zip(degree_offsets, radian_offsets):
        a1 = radian_offset + u
        b1 = radian_offset - u
        a2 = u - radian_offset
        b2 = u - radian_offset
        for degree_angle, radian_angle in zip(degree_angles, radian_angles):
            auc = (AUC(radian_angle, a1, b1, a1 - b1)
                   + AUC(radian_angle, a2, b2, a2 - b2))
            print(degree_angle, degree_offset, auc)
            aucs.append(auc)
    #ggplot(df, aes(color=auc))
    #           + geom_tile(aes(x=angle, y=offset))
    #           + scale_color_viridis_c()


def test_3(u: float):
    """
    1 high-damage gun fixed to the front of the ship and 2 guns with
    variable offset, one with double damage
    """
    degree_offsets = tuple(offset for offset in range(0, 180, 2))
    degree_angles = tuple(angle for angle in range(-180, 180, 2))
    radian_offsets = tuple(offset * math.pi / 180 for offset in degree_offsets)
    radian_angles = tuple(angle * math.pi / 180 for angle in degree_angles)
   
    aucs = []
    for degree_offset, radian_offset in zip(degree_offsets, radian_offsets):
        a1 = radian_offset + u
        b1 = radian_offset - u
        a2 = u - radian_offset
        b2 = u - radian_offset
        a3 = 0
        b3 = 0
        for angle in radian_angles:
            aucs.append((AUC(t, a1, b1, a1 - b1)
                         + 2 * AUC(t, a2, b2, a2 - b2)
                         + 3 * AUC(t, a3, b3, 30 * math.pi / 180), t, roffset))
        #ggplot(df, aes(color=auc)
        #           + geom_tile(aes(x=angle, y=offset))
        #           + scale_color_viridis_c())
 
#tests end
#with the tests using direct translations from the math gone, the rest of the code describes a test of what we have written using 10 random weapons.

def main():
   
   
    #dominator
    ship = (14000,#hullhp
            500,#shieldregen
            10000,#shieldmax
            1500,#startingarmor
            220,#widthinpixels
            12,#armorcells
            440,#shieldwidth
            1.0,#shieldefficacy
            200)#shieldupkeep
   
    engagement_range = 1000
   
    #fudge factor
    errorsd = 0.05
    #the fudge factor should be a function of range (more error in
    #position at greater range),  but not a function of weapon firing
    #angle,  and be expressed in terms of pixels
    error = errorsd * engagement_range
   
    ship_count = 10

    damage = tuple(random_weapon_damage() for _ in range(ship_count))
    spread = tuple(random_weapon_spread() for _ in range(ship_count))
    facing = tuple(random_weapon_facing() for _ in range(ship_count))
    tracking_range = tuple(random_weapon_tracking_range() for _ in
                           range(ship_count))
    maximum_means = tuple(facing[i] + (tracking_range[i] - spread[i]) / 2 for i
                          in range(ship_count))
    minimum_means = tuple(facing[i] - (tracking_range[i] + spread[i]) / 2 for i
                          in range(ship_count))

# the next part describes one lazy way of handling the whole computations
   
#define all possible angles for the target ship. This should actually probably be -179 to 180 for consistency

    angles = tuple(i for i in range(-180, 181))

#for each angle, perform the sum described above.
#note that this version will incorrectly not wrap the angle around!
# i fixed this by calculating from -360 to 360, then summing -360 and 0, -359 and 1, etc. which covers all possibilities because guns are placed between -180 (should be -179) so minimum trackable angle possible is -360 (-359), corresponding to 0, and maximum+360). Then got the final result for angles -180 to 180 by a sub-vector selection and rotation operation. This can really be done any way you please, so long as you take note of that a gun at -179 with 30 tracking range should also be able to fire at a target at +151 degrees, etc.

    #applied = sapply(angles, FUN=sum_auc)
    #plot(sapply(aucvector, FUN=sumauc), x=aucvector, xlab="angle", ylab="dps")

# in my version we now find the angle of choice by listing the degrees where dps == maximum, with rounding to avoid errors due to precision, and select the angle corresponding to the middle of the list (list length/2 rounded up)
#use any similar method hers
#for the demo, we plot a vertical line at this point using abline
# in the real code, use this as the angle of target to ship for highest dps.

    #abline(v=aucvector[which(round(applied, 3) == round(max(applied), 3))[ceiling(length(which(round(applied, 3) == round(max(applied), 3))) / 2)]])
main()

Incidentally this might be a fun function in itself: show a ship variant's dps profile at around it at angles to visually find weak and strong points.
« Last Edit: December 18, 2022, 08:13:06 PM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Liral

  • Admiral
  • *****
  • Posts: 717
  • Realistic Combat Mod Author
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #294 on: December 19, 2022, 11:13:15 AM »

I don't think these results are right.  Also, would you please tell me what auc stands for?
Code
Code
#library(ggplot2)
#library(reshape2)
import math
import random
import statistics


def G(x, normal_distribution):
    return x * normal_distribution.cdf(x) + normal_distribution.pdf(x)
   
   
def segment_to_deg(seg: float, distance: float) -> float:
    """Return the degree angle corresponding to a segment."""
    return seg / distance * 360 / math.pi
   
   
def deg_to_segment(deg: float, distance: float) -> float:
    """Return the segment corresponding to a degree angle."""
    return deg * distance / 360 * math.pi
           
           
def transformed_hit_coord(
        angle: float,
        minimum_mean: float,
        maximum_mean: float) -> float:
    """
    Return the angle of the mean of the hit distribution,
    when the gun attempts to track the target.
   
    Lets us use our hit dist fucntion.
    """
    return max(minimum_mean,  min(maximum_mean, angle))
   
   
def transformed_angle(angle: float, minimum_mean: float, maximum_mean: float):
    """
    Return the angle between the target and the mean of the hit
    distribution, with sign.
    """
    return angle - transformed_hit_coord(angle, minimum_mean, maximum_mean)
   
   
def hit_probability_coord_lessthan_x(
        x: float,
        standard_deviation: float,
        uniform_parameter: float) -> float:
    """
   
    Includes special cases.
   
    a - standard deviation of the normal distribution
    b - parameter of the uniform distribution
   
    This needs an additional correction in the line about 0 sd,
    so minimum is 0 and also it should in fact be abs(b/2+x),
    but we do not need the abs here when the lower bound is 0 anyway.
    That means calculating the part of the box of area 1 from
    -b/2 to b/2 that is below x. Whoops!

    A version of this should already exist with its own name in py.
    It is the CDF of the hit distribution.
    """
    if standard_deviation > 0 or uniform_parameter > 0:
        if standard_deviation == 0:
            return max(0, min(1, x + uniform_parameter / 2))
        normal_distribution = statistics.NormalDist(0, standard_deviation)
        if uniform_parameter == 0: return normal_distribution.cdf(x, 0, a)
        a = standard_deviation / 2 / uniform_parameter
        b = uniform_parameter / standard_deviation
        c = x / standard_deviation
        return a * (G((b + c), normal_distribution)
                    - G((b - c), normal_distribution))
    return x < 0


def auc(middle_angle: float,
        spread_angle: float,
        ship_angle: float,
        distance: float,
        error_distance: float) -> float:
    """
    Return the probability for a weapons to hit the target ship given
    target ship angle.
   
    probability = (
        CDF (ship's angle from mean of distribution + width/2)
        - CDF (ship's angle from mean of distribution - width/2))
       
    This difference corresponds to an integral of the probability
    distribution from the ship's left edge to right from our
    perspective for the weapon.
   
    We have previously written the probability distribution in terms
    of pixels of target, ignoring curvature, but describing gun
    placement by angle is convenient. Hence the need for seg to deg
    and deg to seg translation. Derror was a vestige of an alternative
    formulation that we translate the sd to degrees. Whichever way
    works so long as all parameters are either angle or distance when
    summing, transforming or passing to hit distribution function.
    """
    spread_distance = deg_to_segment(spread_angle, distance)
    left_segment = deg_to_segment(middle_angle + ship_angle, distance)
    right_segment = deg_to_segment(middle_angle - ship_angle, distance)
    return (hit_probability_coord_lessthan_x(left_segment, error_distance,
                                             spread_distance)
            - hit_probability_coord_lessthan_x(right_segment, error_distance,
                                               spread_distance))


def random_weapon_damage() -> float:
    """
    Return a random damage amount.
    """
    return random.random() * 10
   
   
def random_weapon_facing() -> float:
    """
    Return a random facing in degrees.
    """
    return random.random() * 360 - 180
   
   
def random_weapon_tracking_range() -> float:
    """
    Return a random tracking range in degrees.
    """
    return random.random() * 360
   
   
def random_weapon_spread() -> float:
    """
    Return a random spread in degreees.
    """
    return random.random() * 30


def main():
    """
    Test what we have written using 10 random weapons.
    """
   
    #dominator
    ship = (14000,#hullhp
            500,#shieldregen
            10000,#shieldmax
            1500,#startingarmor
            220,#widthinpixels
            12,#armorcells
            440,#shieldwidth
            1.0,#shieldefficacy
            200)#shieldupkeep
   
    engagement_range = 1000
   
    #fudge factor
    errorsd = 0.05
    #the fudge factor should be a function of range (more error in
    #position at greater range),  but not a function of weapon firing
    #angle,  and be expressed in terms of pixels
    error = errorsd * engagement_range
   
    ship_count = 10

    damages = tuple(random_weapon_damage() for _ in range(ship_count))
    spreads = tuple(random_weapon_spread() for _ in range(ship_count))
    facings = tuple(random_weapon_facing() for _ in range(ship_count))
    tracking_ranges = tuple(random_weapon_tracking_range() for _ in
                            range(ship_count))
    maximum_means = tuple(facings[i] + (tracking_ranges[i] - spreads[i]) / 2 for
                          i in range(ship_count))
    minimum_means = tuple(facings[i] - (tracking_ranges[i] + spreads[i]) / 2 for
                          i in range(ship_count))

    #the next part describes one lazy way of handling the whole
    #computations
   
    #define all possible angles for the target ship. This should
    #actually probably be -179 to 180 for consistency note that this
    #version will incorrectly not wrap the angle around! I fixed this
    #by calculating from -360 to 360, then summing -360 and 0,
    #-359 and 1, etc. which covers all possibilities because guns are
    #placed between -180 (should be -179) so minimum trackable angle
    #possible is -360 (-359), corresponding to 0, and maximum+360).
    #Then got the final result for angles -180 to 180 by a sub-vector
    #selection and rotation operation. This can really be done any way
    #you please, so long as you take note of that a gun at -179 with
    #30 tracking range should also be able to fire at a target at +151
    #degrees, etc.

    #for each angle, perform the sum described above.
    applied = tuple(sum(auc(transformed_angle(facing, minimum_mean, maximum_mean
        ), spread, ship_angle, engagement_range, error) for minimum_mean,
        maximum_mean, spread, facing in zip(minimum_means, maximum_means,
        spreads, facings)) for ship_angle in tuple(i for i in range(-180, 181)))
       
    for element in applied: print(element)
   
       
    #plot(sapply(aucvector, FUN=sumauc), x=aucvector, xlab="angle", ylab="dps")

    #in my version we now find the angle of choice by listing the
    #degrees where dps == maximum, with rounding to avoid errors due
    #to precision, and select the angle corresponding to the middle
    #of the list (list length/2 rounded up)
    #use any similar method hers
    #for the demo, we plot a vertical line at this point using abline
    #in the real code, use this as the angle of target to ship for highest dps.

    #abline(v=aucvector[which(round(applied, 3) == round(max(applied), 3))[ceiling(length(which(round(applied, 3) == round(max(applied), 3))) / 2)]])
main()
[close]
Results
-213.6526924937567
-212.47788171395493
-211.30289120034305
-210.12772135322342
-208.9523725812908
-207.77684530162963
-206.60113993971098
-205.42525692938887
-204.24919671289598
-203.0729597408388
-201.89654647219203
-200.7199573742928
-199.5431929228337
-198.36625360185585
-197.18913990374105
-196.0118523292033
-194.83439138727996
-193.65675759532192
-192.47895147898376
-191.3009735722128
-190.1228244172379
-188.94450456455743
-187.76601457292682
-186.58735500934543
-185.40852644904285
-184.22952947546472
-183.05036468025767
-181.87103266325383
-180.69153403245508
-179.51186940401618
-178.33203940222745
-177.15204465949702
-175.97188581633264
-174.79156352132225
-173.6110784311146
-172.43043121039932
-171.2496225318856
-170.06865307628115
-168.88752353227028
-167.70623459649144
-166.5247869735136
-165.34318137581312
-164.1614185237493
-162.97949914553934
-161.79742397723294
-160.61519376268643
-159.43280925353594
-158.2502712091705
-157.06758039670393
-155.88473759094654
-154.70174357437645
-153.51859913711004
-152.3353050768717
-151.15186219896322
-149.96827131623277
-148.78453324904308
-147.60064882523875
-146.41661888011396
-145.23244425637856
-144.04812580412408
-142.8636643807894
-141.67906085112529
-140.49431608715892
-139.30943096815764
-138.124406380592
-136.93924321809862
-135.75394238144204
-134.56850477847658
-133.3829313241071
-132.19722294024973
-131.01138055579162
-129.8254051065504
-128.6392975352332
-127.45305879139474
-126.26668983139544
-125.08019161835848
-123.89356512212687
-122.70681131921941
-121.51993119278656
-120.33292573256574
-119.14579593483602
-117.95854280237226
-116.77116734439905
-115.58367057654354
-114.39605352078864
-113.20831720542483
-112.02046266500213
-110.83249094028139
-109.64440307818474
-108.45620013174626
-107.26788316006163
-106.07945322823737
-104.89091140733981
-103.70225877434352
-102.51349641207908
-101.32462540918087
-100.13564686003367
-98.94656186471964
-97.75737152896416
-96.5680769640816
-95.37867928692063
-94.18917961980895
-92.99957909049765
-91.80987883210513
-90.62007998306078
-89.43018368704791
-88.24019109294652
-87.05010335477553
-85.85992163163466
-84.66964708764598
-83.47928089189489
-82.28882421837072
-81.09827824590724
-79.90764415812235
-78.71692314335776
-77.52611639461794
-76.33522510950924
-75.1442504901778
-73.9531937432481
-72.7620560797603
-71.57083871510774
-70.37954286897391
-69.18816976526911
-67.99672063206674
-66.80519670153919
-65.61359920989369
-64.42192939730751
-63.23018850786294
-62.03837778948213
-60.84649849386134
-59.65455187640507
-58.46253919615995
-57.27046171574808
-56.07832070130035
-54.88611742238936
-53.69385315196204
-52.50152916627209
-51.30914674481202
-50.11670717024508
-48.92421172833681
-47.73166170788647
-46.539058400658035
-45.346403101311225
-44.153697107331965
-42.96094171896291
-41.768138239133606
-40.575287973390466
-39.38239222982651
-38.18945231901093
-36.99646955391849
-35.8034452498586
-34.610380724404344
-33.4172772973213
-32.22413629049606
-31.030959027864768
-29.837746835341324
-28.644501040745567
-27.45122297373115
-26.257913965713445
-25.064575349797163
-23.871208460703887
-22.6778146346995
-21.48439520952144
-20.29095152430588
-19.097484919514745
-17.90399673686271
-16.71048831924394
-15.51696101065895
-14.323416156141144
-13.12985510168348
-11.936279194164888
-10.742689781276736
-9.549088211449149
-8.355475833777342
-7.161853997947817
-5.968224054164585
-4.774587353075299
-3.5809452456973805
-2.387299083344086
-1.1936502175505876
0.0
1.1936502175505876
2.387299083344086
3.5809452456973805
4.774587353075299
5.968224054164585
7.161853997947817
8.355475833777342
9.549088211449149
10.742689781276736
11.936279194164888
13.12985510168348
14.323416156141144
15.51696101065895
16.71048831924394
17.90399673686271
19.097484919514745
20.29095152430588
21.48439520952144
22.6778146346995
23.871208460703887
25.064575349797163
26.257913965713445
27.45122297373115
28.644501040745567
29.837746835341324
31.030959027864768
32.22413629049606
33.4172772973213
34.610380724404344
35.8034452498586
36.99646955391849
38.18945231901093
39.38239222982651
40.575287973390466
41.768138239133606
42.96094171896291
44.153697107331965
45.346403101311225
46.539058400658035
47.73166170788647
48.92421172833681
50.11670717024508
51.30914674481202
52.50152916627209
53.69385315196204
54.88611742238936
56.07832070130035
57.27046171574808
58.46253919615995
59.65455187640507
60.84649849386134
62.03837778948213
63.23018850786294
64.42192939730751
65.61359920989369
66.80519670153919
67.99672063206674
69.18816976526911
70.37954286897391
71.57083871510774
72.7620560797603
73.9531937432481
75.1442504901778
76.33522510950924
77.52611639461794
78.71692314335776
79.90764415812235
81.09827824590724
82.28882421837072
83.47928089189489
84.66964708764598
85.85992163163466
87.05010335477553
88.24019109294652
89.43018368704791
90.62007998306078
91.80987883210513
92.99957909049765
94.18917961980895
95.37867928692063
96.5680769640816
97.75737152896416
98.94656186471964
100.13564686003367
101.32462540918087
102.51349641207908
103.70225877434352
104.89091140733981
106.07945322823737
107.26788316006163
108.45620013174626
109.64440307818474
110.83249094028139
112.02046266500213
113.20831720542483
114.39605352078864
115.58367057654354
116.77116734439905
117.95854280237226
119.14579593483602
120.33292573256574
121.51993119278656
122.70681131921941
123.89356512212687
125.08019161835848
126.26668983139544
127.45305879139474
128.6392975352332
129.8254051065504
131.01138055579162
132.19722294024973
133.3829313241071
134.56850477847658
135.75394238144204
136.93924321809862
138.124406380592
139.30943096815764
140.49431608715892
141.67906085112529
142.8636643807894
144.04812580412408
145.23244425637856
146.41661888011396
147.60064882523875
148.78453324904308
149.96827131623277
151.15186219896322
152.3353050768717
153.51859913711004
154.70174357437645
155.88473759094654
157.06758039670393
158.2502712091705
159.43280925353594
160.61519376268643
161.79742397723294
162.97949914553934
164.1614185237493
165.34318137581312
166.5247869735136
167.70623459649144
168.88752353227028
170.06865307628115
171.2496225318856
172.43043121039932
173.6110784311146
174.79156352132225
175.97188581633264
177.15204465949702
178.33203940222745
179.51186940401618
180.69153403245508
181.87103266325383
183.05036468025767
184.22952947546472
185.40852644904285
186.58735500934543
187.76601457292682
188.94450456455743
190.1228244172379
191.3009735722128
192.47895147898376
193.65675759532192
194.83439138727996
196.0118523292033
197.18913990374105
198.36625360185585
199.5431929228337
200.7199573742928
201.89654647219203
203.0729597408388
204.24919671289598
205.42525692938887
206.60113993971098
207.77684530162963
208.9523725812908
210.12772135322342
211.30289120034305
212.47788171395493
213.6526924937567
[close]

CapnHector

  • Admiral
  • *****
  • Posts: 1056
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #295 on: December 19, 2022, 11:50:24 AM »

Auc = area under (the) curve.

Those results certainly don't seem likely. It certainly shouldn't be the case that expected dps is linearly dependent on angle and possibly negative. I wonder if something is getting summed incorrectly somewhere? But I couldn't immediately find it. Tell you what, I'll write an actually clear and properly annotated version of the original tomorrow.
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Salter

  • Commander
  • ***
  • Posts: 213
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #296 on: December 19, 2022, 04:00:03 PM »

My experience with missile conquest has largely been to give it a locust SRM to overwhelm/swarm a ship's point defense then follow it up with a harpoon/hurricane MIRV missiles. Point defense AI targets the closest thing to it and this gives you the opportunity to slip the real missiles past its defenses. Harpoons and MIRV's pack enough punch that they will inflict significant flux if they hit shields and inflict significant damage if they hit the ship, if nothing else.

Its a ship where having an officer with missile spec and all the related missile mod's installed makes it a top-tier pick in many situations. Even against remnant ordo's and the elite redacted enemies that hang around the coronal hypershunt.
Logged

CapnHector

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

My experience with missile conquest has largely been to give it a locust SRM to overwhelm/swarm a ship's point defense then follow it up with a harpoon/hurricane MIRV missiles. Point defense AI targets the closest thing to it and this gives you the opportunity to slip the real missiles past its defenses. Harpoons and MIRV's pack enough punch that they will inflict significant flux if they hit shields and inflict significant damage if they hit the ship, if nothing else.

Its a ship where having an officer with missile spec and all the related missile mod's installed makes it a top-tier pick in many situations. Even against remnant ordo's and the elite redacted enemies that hang around the coronal hypershunt.

Agreed. The Conquest is quite suitable for Remnant farming. If you field a fleet of only Conquests or Conquests with frigates, then I don't think enemy PD is really much of a problem. The sheer volume of incoming missiles will overwhelm point defenses. I should give Locusts a try in Remnant farming though, just to see if it does improve things. I would probably replace the Hurricane, so Squall-Locust, since this was also the best combination in testing.
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 #298 on: December 20, 2022, 12:03:19 AM »

All right Liral, here is commented code that I hope that you will find more helpful. Also as an attachment and with reasonable sample output. This is, of course, what I should have written in the first place - apologies. I also fixed the problem with dps not being considered and made the wraparound more accessible to human cognition, although the process is equivalent but no longer involves rotating a vector. I added the ability to compute hit distributions.

Code
#Objective: calculate the optimum angle to place the enemy ship, with regard to our ship
#Definition: angle exactly to the right is 0 degrees, so the ship is heading towards +90 degrees.
#Possible angles are integers, from -179 to 180.
#Assumptions: our ship is pointlike. The enemy ship is at a constant range. The enemy ship's orientation
#is tangential to the circle defined by our ship's position and range. The enemy ship is represented
#by a single line of hittable armor cells.
#Additionally, we assume sufficient range that we do not need to compensate for curvature (that range
#to the enemy ship's cells varies) In short: we assume that it is the case that the secant such that
#the average range is range is approximately the same as the arc and that inaccuracies that result
#from using the average instead of cell-specific range are insignificant.

#Section 1. general considerations
#We will test against a Dominator. We wish to integrate this code with the other modules eventually,
#so we will use the full ship definition, even though only the width is needed.

ship <- c(14000, 500, 10000, 1500, 220, 12, 440, 1.0, 200)
range <- 1000

#For testing, we will give these parameters for weapons:
#damage, facing (deg), tracking range (deg), spread
#collect them in a data frame

#for this test we will add two weapons that are slightly angled away from each other and able to overlap
#in the exact middle
weapon1 <- c(100, -10, 20, 5)
weapon2 <- c(100, 10, 20, 5)
# and some rear mounted point defense weapons to make sure the wraparound, extreme angles are correct
weapon3 <- c(30, -160, 20, 0)
weapon4 <- c(30, 180, 20, 0)
weapon5 <- c(30, 160, 20, 0)
weapon6 <- c(120, 90, 0, 5)
#lastly a weapon fixed at the ship front that has higher damage by itself, to test angle choice logic
#the correct choice should be to pick the overlap of the two weapons
noweapons <- 6
weapons <- data.frame()
for (i in 1:noweapons) weapons <- rbind(weapons,get(paste("weapon",i,sep="")))
colnames(weapons) <- c("damage","facing","trackingrange","spread")


#We will use the same parameter for the normal distribution as we do in the other modules.
#Note that this results in the SD of the normal distribution being in pixels.

errorsd <- 0.05
error <- errorsd*range

#We need the helper function G used to define the cumulative distribution function of the convolved
#normal distribution.

G <- function(y) return(y*pnorm(y) + dnorm(y))

#The next function will give the cumulative distribution function of the hit distribution.
#Note that this is equal to integral from -inf to z of the probability distribution, so, applying
#the fundamental theorem of calculus, we can say that the integral from ship lower edge to ship upper edge
#is CDF(upper edge)-CDF(lower edge). We have used the variable z out of convention, but the argument is
#a real number, hence we refer to x in the function title.

#the first argument of the function is a real number. The second argument is the SD of the normal
#distribution. The third argument is the parameter of the uniform distribution.

#We note the following special cases: In case the parameter of the uniform distribution is 0,
#no convolution happens and the distribution is normal. In case the parameter of the normal distribution
#is 0, the distribution is uniform and no convolution happens. In case both are 0, the distribution is
#trivial (the CDF is equal to a step function going from 0 to 1 at z=0)

#Summary: Input: z, a real number, a, sd of normal distribution, b, radius of uniform distribution
#Output: CDF of z. For random variable Z from U(-b,b)+N(0,a): Probability that Z < z.

hit_probability_coord_lessthan_x <- function(z, a, b){
  if(a > 0 & b > 0) return(a/2/b*(G(z/a+b/a)-G(z/a-b/a)))
  if(a > 0 & b == 0) return(pnorm(z,0,a))
  if(a == 0 & b > 0) return(max(0,min(1,b/2+z)))
  if(a == 0 & b == 0) {
    if(z < 0) return(0) else return(1)
  }
}

#It is convenient to refer to the width of the enemy ship in pixels, but to the placement and
#tracking ability of turrets as angles. Therefore we will need to be able to convert between the two.
#Note that by assumption we assume we can use the arc to describe the enemy ship.

arc_to_deg <- function(arc) return(arc/range*360/pi)
deg_to_arc <- function(deg) return(deg*range/360*pi)

#Now note that since weapons can never go beyond their maximum turn angle, then if spread stays constant,
#it must be the case that the maximum mean point of the probability distribution of the weapon is given
#by maximum turn angle - spread / 2 and likewise minimum turn angle + spread/2.
#When a weapon tracks the target, it tries to align the mean of the probability distribution with the
#target. Therefore we can find the signed angle of the target from the mean of the distribution by

#first, find the mean point, that is, max mean, when target is above max mean, target, when target
#is between max mean and min mean, min mean, when target is below min mean.

#We will now compute the minimum and maximum means for each weapon and find the median of the
#hit distribution ("transform hit coord" for coordinates of mean of modified dist in
#terms of original dist )

weapons <- cbind(weapons, maxmean=(weapons$facing+weapons$trackingrange/2-weapons$spread/2))
weapons <- cbind(weapons, minmean=(weapons$facing-weapons$trackingrange/2+weapons$spread/2))

transform_hit_coord <- function(angle, weapon) return(max(weapon$minmean, min(weapon$maxmean,angle)))

#then calculate the angle between the mean and target by subtraction. We refer to this as "transforming"
#since we are moving from expressing the angle wrt. ship to wrt. weapon.
transformed_angle <- function(angle, weapon) return(angle-transform_hit_coord(angle,weapon))

#Now to orient the ship we wish to find the orientation that has the largest amount of DPS hitting the
#target. This means maximizing dps * probability to hit the target.
#therefore we will calculate the sum of areas under curve of probability distribution over target
#times damage. This is equivalent to taking the integral of ship lower angle to ship greater angle
#over the probability distribution times damage for each weapon. We use the CDF we described above.

#We refer to auc (area under curve, ie. the curve of damage times probability dist,
#giving total damage) for short.

#Input: angle of target ship
#Output: total damage to target ship
sumauc <- function(angle) {
  summed_auc <- 0
  #convert the ship's width from segment to degrees
  shipwidth <- arc_to_deg(ship[5])/2

  #for each weapon, calculate CDF(shipupperbound)
  for (i in 1:length(weapons[,1])){
    #angle of the ship's upper bound, in coordinates of the distribution mean
    #note that this is weapon specific
    shipupperbound <- deg_to_arc(transformed_angle(angle,weapons[i,])+shipwidth/2)
    shiplowerbound <- deg_to_arc(transformed_angle(angle,weapons[i,])-shipwidth/2)
    #we have defined spread in degrees, so we must convert it to pixels to be consistent
    pxspread <- deg_to_arc(weapons[i,4])
   
    damage <- weapons[i,1]
    #now we calculate the sum
    summed_auc <- summed_auc + damage*(
      hit_probability_coord_lessthan_x(shipupperbound, error, pxspread) -
        hit_probability_coord_lessthan_x(shiplowerbound, error, pxspread)
    )
  }
 
  return(summed_auc)
}


#Now that we have all the pieces, we can perform the calculation.
#A special consideration is that we are using signed angles for the probability distritbutions, but
#in reality, the ship's guns wrap around, so we must devise a way to map
#-359 to 1, etc.

#So first, we define a vector from -360 to 360. These all possible angles of fire, since a gun can
#only have a position from -180 to 180 degrees on the ship, and can only track 360 degrees at most.

angles <- seq(-359,360)

#Then, we calculate the expected damage at each angle.
#Note that the R command sapply is equal to performing the function indicated by FUN
#at each index with the value at each index, returning a vector containing the output, so equal to a
#for loop.

dps_at_angles <- sapply(angles,FUN=sumauc)

#next, we go back to one loop of the ship by noting that
#-180 corresponds to +180
#-181 corresponds to +179 etc, so 360 + index for indices 1:180

for (i in 1:180) dps_at_angles[i+360] <- dps_at_angles[i+360]+dps_at_angles[i]

#and +360 corresponds to 0, +359 to -1 etc. so index - 360 for indices 540:720

for (i in 540:720) dps_at_angles[i-360] <- dps_at_angles[i-360]+dps_at_angles[i]

#finally, to get angles -179 to 180, we select indices 181 to 540 of the new vector.
dps_at_angles <- dps_at_angles[181:540]
#note that vector indices no longer correspond to angles, rather vector index 1 corresponds to -179.
#to get a correct plot add this

xaxis <- seq(-179,180)

#the problem is solved. Now we can find the optimum angle quite simply by selecting the midmost maximum
optimumangle <- xaxis[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)]]

#plot the result
plot(dps_at_angles,x=xaxis)
abline(v=optimumangle)

#now to calculate the hit distributions that our weapons will have at this angle we will simply add
#the segment from the old angle to the new to the weapon's angle.

#we get the location of the weapon's mean as it tries to track the target from
#transformed_angle
#so

weaponadjustment_px <- function(weapon){
  angle_difference <- transformed_angle(optimumangle,weapon)
  arc <- angle_difference/360 * 2*pi*range
  return(arc)
}
#include our normal hit distribution function here


hit_distribution <- function(upperbounds, standard_deviation, spread){
  vector <- vector(mode="double", length = length(upperbounds)+1)
  if (standard_deviation == 0){
    if (spread == 0){
      vector[1] <- 0
      for (j in 2:(length(upperbounds))) {
        #if both spread and standard deviation are 0 then all shots hit 1 cell. this should be so even if
        #the ship has an even number of cells to prevent ships with even no. cells appearing tougher which is not
        #the case in the real game most likely
        if ((upperbounds[j] >= 0) & (upperbounds[j-1] < 0)) vector[j] <- 1
      }
      #return part of a box
    } else {
      vector[1] <- min(1,max(0,(upperbounds[1]+spread))/(2*spread))
      for (j in 2:(length(upperbounds))) vector[j] <- min(1,max(0,(upperbounds[j]+spread))/(2*spread)) - min(1,max(0,(upperbounds[j-1]+spread))/(2*spread))
      vector[length(upperbounds)+1] <- 1-min(1,max(0,(upperbounds[length(upperbounds)]+spread))/(2*spread))
    }
  } else {
    if (spread != 0){
      vector[1] <- hit_probability_coord_lessthan_x(upperbounds[1], standard_deviation, spread)
      for (j in 2:(length(upperbounds))) vector[j] <- (hit_probability_coord_lessthan_x(upperbounds[j], standard_deviation, spread)-hit_probability_coord_lessthan_x(upperbounds[j-1], standard_deviation, spread))
      vector[length(upperbounds)+1] <- (1-hit_probability_coord_lessthan_x(upperbounds[length(upperbounds)], standard_deviation, spread))
    } else {
      #if spread is 0 but standard deviation is not 0 we have a normal distribution
      vector[1] <- pnorm(upperbounds[1],0,standard_deviation)
      for (j in 2:(length(upperbounds))) vector[j] <- pnorm(upperbounds[j],0,standard_deviation) - pnorm(upperbounds[j-1], mean=0, sd=standard_deviation)
      vector[length(upperbounds)+1] <- 1-pnorm(upperbounds[length(upperbounds)], mean=0, sd=standard_deviation)
    }
   
  }
  return(vector)
}

#the usual - calculate ship cell upper bound angles
shipangle <- ship[5]/(2* pi *range)
cellangle <- shipangle/ship[6]
anglerangevector <- vector(mode="double", length = ship[6]+1)
anglerangevector[1] <- -shipangle/2
for (i in 1:(length(anglerangevector)-1)) anglerangevector[i+1] <- anglerangevector[i]+cellangle
#convert to pixels
pxupperboundvector <- anglerangevector*2*pi*range
pxupperboundvector
#now to get the hit distribution for weapons we do

hit_distribution_at_optimum_angle <- function(weapon){
  #convert spread to pixels
  pxspread <- deg_to_arc(weapon[,4])
  #adjust upper bound vector
  adj_ubs <- pxupperboundvector + weaponadjustment_px(weapon)
  return(hit_distribution(adj_ubs,error,pxspread))
}

#print results
hit_distribution_at_optimum_angle(weapons[1,])
hit_distribution_at_optimum_angle(weapons[2,])
hit_distribution_at_optimum_angle(weapons[3,])
hit_distribution_at_optimum_angle(weapons[4,])
hit_distribution_at_optimum_angle(weapons[5,])
hit_distribution_at_optimum_angle(weapons[6,])


Sample output:
Our weapons are

#for this test we will add two weapons that are slightly angled away from each other and able to overlap
#in the exact middle
weapon1 <- c(100, -10, 20, 5)
weapon2 <- c(100, 10, 20, 5)
# and some rearleft mounted point defense weapons to make sure the wraparound, extreme angles are correct
weapon3 <- c(30, -160, 20, 0)
weapon4 <- c(30, 180, 20, 0)
weapon5 <- c(30, 160, 20, 0)
weapon6 <- c(120, 90, 0, 5)
#lastly a weapon fixed at the ship front that has higher damage by itself, to test angle choice logic
#the correct choice should be to pick the overlap of the two weapons
noweapons <- 6


Our visual output is:


Our printed output is

> #print results
> hit_distribution_at_optimum_angle(weapons[1,])
 [1] 0.118751068 0.077981550 0.102383867 0.121027732 0.129001152 0.124055991 0.107596198 0.084057103
 [9] 0.059030566 0.037172868 0.020934021 0.010514600 0.004698446 0.002794840
> hit_distribution_at_optimum_angle(weapons[2,])
 [1] 0.002794840 0.004698446 0.010514600 0.020934021 0.037172868 0.059030566 0.084057103 0.107596198
 [9] 0.124055991 0.129001152 0.121027732 0.102383867 0.077981550 0.118751068
> hit_distribution_at_optimum_angle(weapons[3,])
 [1] 1 0 0 0 0 0 0 0 0 0 0 0 0 0
> hit_distribution_at_optimum_angle(weapons[4,])
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
> hit_distribution_at_optimum_angle(weapons[5,])
 [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1
> hit_distribution_at_optimum_angle(weapons[6,])
 [1] 8.351879e-250 1.799421e-244 3.390000e-239 5.584509e-234 8.044357e-229 1.013261e-223 1.116033e-218
 [8] 1.074884e-213 9.052691e-209 6.666963e-204 4.293542e-199 2.417929e-194 1.190735e-189  1.000000e+00

(note: Hitting the first or last cell is a miss, so this means only weapons 1 and 2 can hit the target - as it should be)



Now here is a question: do we want to also model weapon lateral offset?

It should be quite easy with this code, just a matter of geometry (you project the coordinates of the enemy ship to the different circles for the upper bounds vector and the ship edges). But here is the problem: if we no longer assume our ship is pointlike, then it is no longer valid to imagine the enemy ship as a tangential line, either. See illustration. That means adding a depth to the armor matrix, to model hitting the side of the ship, which of course will mean a greater computational cost.


 
The question is, is the function worth the added computational cost, when in most cases we are probably fine with the assumption?

I will add that there should have been a picture of an explosion on top of the Dominator, but MSPaint doesn't support transparency so it gets to live for now.

[attachment deleted by admin]
« Last Edit: December 20, 2022, 02:51:22 AM by CapnHector »
Logged
5 ships vs 5 Ordos: Executor · Invictus · Paragon · Astral · Legion · Onslaught · Odyssey | Video LibraryHiruma Kai's Challenge

Salter

  • Commander
  • ***
  • Posts: 213
    • View Profile
Re: Optimizing the Conquest: a Mathematical Model of Space Combat
« Reply #299 on: December 20, 2022, 04:32:19 AM »

My experience with missile conquest has largely been to give it a locust SRM to overwhelm/swarm a ship's point defense then follow it up with a harpoon/hurricane MIRV missiles. Point defense AI targets the closest thing to it and this gives you the opportunity to slip the real missiles past its defenses. Harpoons and MIRV's pack enough punch that they will inflict significant flux if they hit shields and inflict significant damage if they hit the ship, if nothing else.

Its a ship where having an officer with missile spec and all the related missile mod's installed makes it a top-tier pick in many situations. Even against remnant ordo's and the elite redacted enemies that hang around the coronal hypershunt.

Agreed. The Conquest is quite suitable for Remnant farming. If you field a fleet of only Conquests or Conquests with frigates, then I don't think enemy PD is really much of a problem. The sheer volume of incoming missiles will overwhelm point defenses. I should give Locusts a try in Remnant farming though, just to see if it does improve things. I would probably replace the Hurricane, so Squall-Locust, since this was also the best combination in testing.

Going a missile heavy fleet is not a terrible idea vs Rem's. Last fleet I ran nearly every ship had harpoons or reaper torpedo's. Mixed in two Harbingers and two Afflictors which is an extremely potent combo vs battlestations cause they will pop the stations shields and most missiles will make it through before the stations shields are back up again. You will need strong forward cruisers though to draw attention from the stations guns, or at least alot of smaller but tougher and faster ships. Omen's work great too as both escort ships and to EMP a target.

The more ships you have, the easier the fight gets really though casualties are inevitable and you might have to play at it a few times. The Conquest makes a great finisher ship, but you really need a fleet comp centered around EMP'ing shields and weapons off.
« Last Edit: December 20, 2022, 04:35:09 AM by Salter »
Logged
Pages: 1 ... 18 19 [20] 21 22 ... 32