-
Notifications
You must be signed in to change notification settings - Fork 176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More detailed auto-selection of bombs #4164
Conversation
…te method previously used.
…modate (undocumented!) input requirements for setting bombs on Entity.
How will this interact with @Sleet01 ’s special munitions distribution system? |
@SuperStucco @IllianiCBT I think we can separate the two initially, then integrate them as necessary over the next release or two (after 0.49.20):
@SuperStucco I would say that moving code that you want to extract into a new class is better done sooner than later, so that you can set up unit tests early and validate basic functionality more easily. I'll do a full code review when I'm back on broadband, but based on a quick scan I don't think there's any reason our work couldn't be integrated into one seamless system for outfitting units. |
@SuperStucco There's a thread on the MegaMek Discord server where we discussed some of the stuff I was doing in my work: https://discord.com/channels/458705327911731231/1222035892965736539 |
I'm pleased to hear that, because I'm greedy and want both :D |
I'm taking a more 'eating the elephant' approach with more small bites, as I get up to speed. While this gets reviewed I'm looking at a couple of other things - having field gun infantry generation respect the availability values, and potentially generating marine/XCT infantry in place of standard infantry in hostile environment conditions. |
if (numBombers >= maxBombers) { | ||
break; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth adding a check here? I'm guessing that if the unit is unarmed it's not going to need any bombs.
if (isUnarmed) {
continue
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly relevant: The Boeing Jump Bomber is specifically designed to drop bombs, but has no internal weapons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Carried this logic forward from the previous test. If it's carrying bombs, it really should be loaded up as that's the only realistic contribution it can make. Lower down in the code, they are specifically excluded from counting towards the total number of loaded bombers.
If anything they would be prioritized over armed units. Would it be worth re-ordering the bomber subset List<> to put unarmed entities first so they always get loaded up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, if that's the case then it's likely fine how it is. I hadn't spotted the previous test where it was defined :)
randomThreshold = 80; | ||
break; | ||
default: | ||
randomThreshold = 99; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering whether we should throw an exception here, instead of assigning a default value to randomThreshold
. There shouldn't be any point in which quality isn't within the F-A* bounds, so having an exception thrown would mean we get alerted to the problem rather than it failing silently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the method is called correctly, but the quality parameter is a straight int type. So it could be called inappropriately with bad values (negative, large numbers, etc.).
So, using a
throw new IllegalArgumentException("Unrecognized rating value: " + quality);
statement, and let the thrown exception get handled upstream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, my thinking is that if we throw an exception we know something has gone wrong; if we silently handle it with a default value the only way we know there was a bad call is if someone happens to notice an unusually large quantity of rockets being handed out.
I was curious in seeing how everything worked, so I had a scroll through your code. I love the use of I did have a couple of points of feedback, nothing major, just thoughts that plonked into my noggin while I was reading. If Sleet comes along and disputes anything I've commented, assume they're right and I'm wrong. :D |
Should this be considered a draft, or is it ready to go. |
/** | ||
* Distribution of various external ordnance choices for non-pirate forces | ||
*/ | ||
private static final Map<String, Integer> bombMapGroundSpread = Map.ofEntries ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be useful to know what these numbers represent: relative weights, percentages, absolute counts?
MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
if (numBombers >= maxBombers) { | ||
break; | ||
((IBomber) curBomber).setBombChoices(generatedBombs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this construction:
- We're mixing bomb configuration and bomb setting; this should be separate for cleanliness and ease of parsing.
- We duplicate this call in two places, but
neither will get called ifactually, I see that setBombChoices() is outside the top test onhasGuided
is false so if there are no guided munitions, no bombs will get set on any Aero unit.hasGuided
, but this illustrates my point re: parsing. - We have a hard-coded set of guided munitions
that doesn't include Aero Guided Arrow IV Bomb munitions,(B_HOMING is Aero Homing AIV bomb) ; this should probably be a set or list that we declare up with the other constants. It looks like the second option erases all bombs for the current unit and just gives it one single TAG pod, which I guarantee is not what we want.Upon re-reading the code, I see that this is theelse
for the outer conditional, and only applies to ASFs without bombs. However, this seems to countermand the comments above stating that we are selecting a subset of ASF units to be bombers. I'm not sure I see a reason to track the number of bombers when we may add TAG to all non-selected bomber candidates anyhow.
I would actually suggest splitting the TAG-adding code out into a separate helper that just mutates a generatedBombs array based on the current bomb load on the bomber, the bomber's weight, and perhaps the bomber's loaded speed - that way we can avoid pushing a bomber from "fast enough" to "too slow" by adding the TAG pods only to units that have a slot to spare before dropping down one thrust rating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TAG part might be cleaned up a bit. The reasoning on that is, you can't use a TAG pod on the same round you're dropping bombs. So can't self-designate for laser/TAG guided bombs, and while you can self-designate for a homing Arrow that you dropped last round, the minimum movement of fixed wing aircraft means the chances they can be over the target zone the very next round is slim. So TAG designators get pushed to any fixed wing aircraft that did NOT get any external ordnance (*edit - or units which have ordnance but nothing guided), which happens from the random limit on how many units to load ordnance on to. That means units with external TAG will only have the one unit and will always have sufficient thrust; it also means that if all units are loaded with ordnance any guided ordnance will need ground designators or built-in TAG.
(generatedBombs[BombType.B_LG] > 0 || | ||
generatedBombs[BombType.B_HOMING] > 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment below; instead of two separate locations with different tests of hard-coded munitions, we should have a set or list of guided munitions that we can use to check for matches, and it should include ASF Homing Arrow IV bomb munitions as well (BombType.B_HOMING is ASF Homing AIV bombs).
double completeWeight = loadoutMap.values().stream().mapToDouble(curWeight -> Math.max(curWeight, 1.0)).sum(); | ||
double randomThreshold = (Compute.randomInt(100) / 100.0) * completeWeight; | ||
for (String curMap : loadoutMap.keySet()) { | ||
countWeight += Math.max(loadoutMap.get(curMap), 1.0); | ||
if (countWeight >= randomThreshold) { | ||
mapName = curMap; | ||
break; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This randomly chooses which entry loadout map type to apply to the unit by summing the relative weights in the loadout map, then checking a random value somewhere between 0.0 and (sum of weights) against a cumulative sum of the weights again? And this works?
Just when I thought I couldn't hate arbitrary relative weighting any more...
My gut feeling is that this isn't going to work as planned, because bare Maps don't guarantee insertion-order ordering of entries. So e.g. for bombMapPirateGroundSpread
, a randomThreshold of <= 3.0 is equally likely to produce "Normal" or "Firestorm", and a value of 7.1+ will either select "Firestorm" or "Normal" depending on the order in which entries are returned; the chance of returning "Firestorm" is actually higher than the relative weighting of 7 and 3 would suggest.
When a collection's ordering is not guaranteed it must be treated as if ordering is random, at least for modelling behaviour, so this approach seems unlikely to work correctly.
Unfortunately I'm not an expert in this field so I'd need to go do some research to figure out the ideal approach, but I would say that switching the maps to LinkedHashMaps... unfortunately this requires using something other than Map.ofEntries() to populate them. Perhaps a custom class that supports ofEntries()...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, looking at the code below that uses this selected string, I don't see why we couldn't just create array lists that give us weighted xBombLoad mappings, without the role names that are only used for the switch cases.
e.g., instead of bombMapGroundSpread
plus a bunch of complicated weighted-option-selecting code plus a switch on mapName
, just cut out the middleman and do:
public static ArrayList<Map<Integer, Integer>> createWeightedList(Map<Map<Integer, Integer>, Integer> weightMap) {
// For each map M, there will be N entries where N is the arbitrary weight (integer)
ArrayList<Map<Integer, Integer>> entriesByWeight = new ArrayList<Map<Integer, Integer>>();
for(Map.Entry<Map<Integer, Integer>, Integer> m: weightMap.entrySet()) {
// Create a sublist of size N = relative weight
ArrayList<Map<Integer, Integer>> subList = new ArrayList<Map<Integer, Integer>>(m.getValue());
for (int i=0;i<subList.size();i++) {
// Fill with references to the specified Map of BombType:Weight
subList.add(m.getKey());
}
// Add all entries
entriesByWeight.addAll(subList);
}
return entriesByWeight;
}
Pass it a Map of Maps and weights, like:
private static final Map<String, Integer> standardBombMapsMap = Map.ofEntries (
Map.entry(normalBombLoad, 6),
Map.entry(antiMekBombLoad, 3),
Map.entry(antiConvBombLoad, 2),
Map.entry(standoffBombLoad, 1),
Map.entry(strikeBombLoad, 2)
);
private static final ArrayList<Map<Integer, Integer>> standardBombWeightedList =
createWeightedList(standardBombMapsMap);
Then a randomized integer between 0 and the length of the resulting ArrayList can be used to randomly select one of the maps:
Map<Integer, Integer> curMap = standardBombWeightedList.get(Compute.randomInt(standardBombWeightedList.size());
Which is all kind of verbose, but now you can just get the maps and the probability of getting a given map equals (weight / sum of weights).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've stepped through the code several dozen times during the development and testing process, and it does seem to produce reliable results at least as far as is intended.
As for the proposed substitute, that's getting very complicated, to the point where I'm having trouble following it. The (switch) is wordier and less elegant but also less opaque.
Don't want to put too much effort into fiddling with this, as eventually those distribution numbers are going to get pushed out to external data so players can fiddle with it to suit their needs.
Map<Integer,Integer> bombMap; | ||
if (!isPirate) { | ||
if (!airOnly) { | ||
switch (mapName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can all be significantly simplified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not certain how. The test cannot be combined, as it needs to split between pirate and non-pirate loads, then it also needs to choose between air/ground and air/air loads - didn't feel that with only two choices of air/air loads, and the limited ordnance choices warranted an extra set of maps.
Granted, I could remove the negation, but the coding style is to put more likely choices at the top so when stepping through or manually reading code, large jumps to following lines are outliers.
completeWeight = workingBombMap.values().stream().mapToDouble(curWeight -> Math.max(curWeight, 1.0)).sum(); | ||
randomThreshold = (Compute.randomInt(100) / 100.0) * completeWeight; | ||
countWeight = 0.0; | ||
for (int curBomb : workingBombMap.keySet()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comments on similar block that selects specific maps from the map of roles and weights.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for making a bunch of corrections; I was re-acquainting myself with some of the base code while doing the review.
In general I like this approach, and it's similar to what I'm doing for generalized munition loadout creation. But I have some concerns and suggestions.
At a high level:
- the random selection system is suspect. I don't believe it will actually select items using the weights specified, due to vagaries of the Java Map class. I think we can get by with turning Maps of the various BombType: weight mappings into ArrayLists and selecting using random indexes.
- the mixing of bomb load mutation and application rubs me the wrong way; it should probably be two separate functions, or at least two separate and distinct blocks of code. There should really only be one call to
setBombChoices
for each unit. - some of the larger conditionals could be shrunk by creating collections of the Homing ammunition types, and checking for membership.
This looks like a significant improvement over the existing stuff, and I look forward to seeing it in action!
…ic list as constant.
… Push unarmed bombers to the front of the list so they always get loaded.
…an directly over Map key values. Ordnance tables now assigned directly rather than using an intermediate name. Some loop optimization.
OK, those last few commits should cover most of what is requested. Not going to get into anything fancier on the weighted random selection, it looks to be working as intended so optimization can be done later. |
@SuperStucco Apologies, I totally missed the re-review request. I will take a look as soon as I can. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Check failures are for Gradle version mismatch, should be good to go. |
Randomly generated units that are cleared to carry bombs currently do so in a somewhat blunt manner - pick a random bomb type, load up to maximum based on carrying capacity until it's just out of thrust. This PR adds more randomness to the selection process, puts in a few guards to maintain a minimum thrust level, and produces more role-focused selections. It's a bit chunkier than I would like but should be a good start.
The new process looks like this:
Differences from the current system:
Should help resolve #2674
Eventually, I think this should switch to reading values from an external data file rather than using hard-coded values, and probably be pulled into it's own class as well to clean up the current location. That can wait until after the general process/framework is validated.