Add Assemble a Force: partition loose lobby units into CamOps formations - #8691
Add Assemble a Force: partition loose lobby units into CamOps formations#8691HammerGS wants to merge 12 commits into
Conversation
Players who build a force with the RAT generator get a pile of loose units; bots then fight it as a mob. This adds a deterministic assembler (new package megamek.client.formation) that partitions loose lobby units into Campaign Operations formations as forces in the force tree: C3 networks and battle armor with its ride fuse into atoms first, atoms split by combined-arms family (aerospace never mixes with ground), then every partition into equal-as-possible elements is enumerated and scored - a qualified CamOps FormationType dominates, role concentration second, speed spread and BV imbalance priced. Lobby: Assemble Force toolbar button (repeatable, never touches hand-built forces), an I'm Done hook so no bot force reaches round one formationless, and a Force Organization player setting (IS lances of 4, Clan stars of 5, ComStar/WoB Level IIs of 6; auto-detect default, bots always auto-detect). Also fixes a latent divide-by-zero in FormationType.qualifies(): the Order Lance registers group size 0 (one group of everything) and any caller sweeping all formation types crashed on it. 12 unit tests pin the two design rosters (role purity beats weight grouping; the C3 pair is never separated) and the structural rules (size ladder, determinism, aero separation, name collisions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
View Formation Rationale explains why a force holds the units it holds: the Campaign Operations type it qualified as, the shared role, the speed spread, what could not be split (C3 networks, carried units), and the three closest trades the search passed over, each priced. Ends with the existing FormationType qualification report. The rationale is recomputed from the force as it stands rather than remembered from assembly, so it works on hand-built forces too and stays honest after a player edits one. No new serialized state. Scoring is now defined once in aggregateScore and shared by the exhaustive search, the greedy search and the rationale, so the three cannot drift apart on what a better grouping means. A test caught a real defect while writing this: the alternatives pass offered to trade for half of a C3 pair and reported that the illegal swap scored better. Units fused into an atom are now excluded on both sides of a trade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rationale view embedded the raw FormationType qualification report, which lists criteria as prose and does not say which unit carries which requirement. It now scores each requirement against each unit and renders the result as a table: one row per rulebook requirement, one column per unit, and a met/needed total. The ideal role leads the section, because it is the rule that decides whether the rest of the table matters. Campaign Operations: if every unit holds the formation type ideal role, the other requirements are waived. The view states which way it went, and the table still shows a waived requirement honestly failing rather than hiding it. Verified against the rulebook example: four medium Brawlers qualify as a Battle Lance even though none is heavy, because Brawler is the Battle Lance ideal role. Pinned by a test. The unit types a formation admits are marked never waived. That gate runs before the ideal-role check in qualifies() and is part of what the formation is, not a requirement on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two defects from lobby testing. An element could exceed its own doctrine. The element count rounded to nearest, so twelve units under star doctrine became two stars of six rather than three stars of four. Campaign Operations allows an understrength element but not an oversize one, so the count now rounds up. This also affected lances: nine units produced a lance of five. Pinned by a sweep over every force size from three to fifteen. The requirement lines read as broken fragments, because the assembler was writing prose. It now carries the facts, a kind and the raw description, and the report composes the sentence, so the wording lives in one place and can be translated. The admitted unit types are named instead of implied. The requirements table also labelled its columns with model designations, which repeat inside a formation: a star with two Stalking Spider IIs had two columns headed the same. Members are now numbered and the columns use those numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lobby testing turned up the same two formation names over and over. Both causes are in the type picker. Hammer is Free Worlds League doctrine, and so are Anvil, Rifle for House Davion and Order for House Kurita. FormationType.qualifies does not test the exclusive faction on purpose: it answers whether the units fit the shape and leaves entitlement to the caller. Nothing asked, so Federated Suns lances were being named after a Free Worlds League formation. Faction-exclusive types are now skipped, since assembly does not know whose force it is. Adds the missing accessor. Ranger asks nothing at all beyond ground units of heavy weight or less, so every ground formation qualifies for it and it won on the alphabetical tie-break. A type that fits any group tells a player nothing. The pick now prefers a type whose ideal role every unit shares, then the type that demands the most of the units, then alphabetical order to stay deterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A player who picked ComStar or Word of Blake in the faction selector silently got lances of four. Auto-detect only counted tech base, and both powers field Inner Sphere equipment, so nothing about their units could ever reveal that they organize into Level IIs of six. The only way to get the right doctrine was to know to set Force Organization by hand. Auto-detect now asks the faction first and falls back to tech base only when the player never chose one. That also fixes a Clan faction fielding captured Inner Sphere salvage, which its own loot could talk it out of stars. The faction is matched whole, splitting on the dot for sub-commands. CSA, CSJ, CSR and CSV are Clan Star Adder, Smoke Jaguar, Snow Raven and Star Viper, so a prefix test would file four Clans as ComStar and hand them Level IIs. Pinned by a test. The faction comes from the team, which the player settings and the army generators already maintain. Nothing was reading it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR checklist pass: bare null, true and false in the JavaDoc this branch adds. A recurring review callout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR checklist pass: the right-click menu label and the met/not-met markers in the requirements table were hardcoded English. The rest of that popup hardcodes its labels, but new user-facing text goes through the message bundle. Also wraps the remaining JavaDoc literals in the two existing files this branch touches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR checklist pass: nested loop indices i and j over two different lists, and atomA/atomB for the two halves of a swap. Named for what they are. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a lobby-side “Assemble Force” feature that deterministically partitions loose lobby units into Campaign Operations-style formations (lances/stars/Level IIs), plus a rationale view explaining why each formation was chosen, and wires this into the lobby UI with a persisted “Force Organization” preference.
Changes:
- Introduces a formation assembly algorithm (with atom constraints like C3/transport, family pooling, exhaustive/greedy partitioning, and deterministic naming) and a rationale model for explaining results.
- Adds lobby UI actions: toolbar button + “I’m Done” hook to assemble, and a right-click “View Formation Rationale…” dialog for a force.
- Adds a client preference + player settings UI for choosing organization doctrine, plus extensive unit tests pinning algorithm behavior.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| megamek/unittests/megamek/client/formation/FormationAssemblerTest.java | New tests pinning assembly determinism, structural rules, and rationale reporting. |
| megamek/src/megamek/common/preference/ClientPreferences.java | Adds persisted FORCE_ORGANIZATION preference (stored as a string). |
| megamek/src/megamek/client/ui/panels/phaseDisplay/lobby/PlayerSettingsDialog.java | Adds “Force Organization” setting UI and saves/loads preference. |
| megamek/src/megamek/client/ui/panels/phaseDisplay/lobby/LobbyMekPopupActions.java | Routes new popup command to explain a formation. |
| megamek/src/megamek/client/ui/panels/phaseDisplay/lobby/LobbyMekPopup.java | Adds “View Formation Rationale…” context menu item for a single force. |
| megamek/src/megamek/client/ui/panels/phaseDisplay/lobby/LobbyActions.java | Implements assembleForces() and explainFormation() lobby wiring. |
| megamek/src/megamek/client/ui/panels/phaseDisplay/lobby/ChatLounge.java | Adds “Assemble Force” button and assembles on “I’m Done”. |
| megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java | New dialog rendering the rationale report. |
| megamek/src/megamek/client/ratgenerator/FormationType.java | Adds exclusive-faction accessor and guards grouping divide-by-zero. |
| megamek/src/megamek/client/formation/Organization.java | New doctrine enum with AUTO resolution (incl. ComStar/WoB by faction). |
| megamek/src/megamek/client/formation/FormationRationale.java | New rationale record used by the UI report. |
| megamek/src/megamek/client/formation/FormationAssembler.java | New assembler implementation and rationale computation. |
| megamek/src/megamek/client/formation/AssemblyUnit.java | New immutable unit snapshot for assembly (Entity -> AssemblyUnit). |
| megamek/src/megamek/client/formation/AssembledFormation.java | New assembled-formation container (name/type/units). |
| megamek/resources/megamek/client/messages.properties | Adds UI strings for Assemble Force, organization, and rationale report. |
Suppressed comments (5)
megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java:235
- Unit display names are inserted into the HTML report without escaping. Unit names can include user-controlled text, which can break the report layout or render as HTML.
for (AssemblyUnit unit : rationale.units()) {
report.append("<tr><td>").append(number++).append("</td><td>")
.append(unit.displayName()).append("</td><td>")
.append(roleName(unit.role())).append("</td><td>")
.append(EntityWeightClass.getClassName(unit.weightClass())).append("</td><td>")
megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java:297
- Binding descriptions are inserted into the HTML list without escaping. Since bindings include unit display names, they should be escaped before being appended to the HTML report.
for (String binding : rationale.bindings()) {
report.append("<li>").append(binding).append("</li>");
}
megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java:315
- Alternative-swap rows interpolate unit/formation names into an HTML list without escaping. These fields originate from formation/unit names and should be HTML-escaped to avoid breaking the report rendering.
report.append("<li>").append(Messages.getString(key, swap.unitName(), swap.otherUnitName(),
swap.otherFormation(), Math.round(Math.abs(swap.cost())))).append("</li>");
megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java:178
- Requirement descriptions are written into the HTML table without escaping. Formation data (and any future localized strings) may contain characters like
</&, which can corrupt the HTML rendering.
for (FormationRationale.Requirement requirement : rationale.requirements()) {
report.append("<tr><td>").append(describe(requirement));
if (!requirement.waivable()) {
megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java:258
- The list of units unknown to the catalog is joined and inserted into the HTML report without escaping. If a custom unit name contains
</&, it can break the rendered report.
} else if (!rationale.unknownToCatalog().isEmpty()) {
report.append("<li>").append(Messages.getString("FormationRationale.why.noTypeUnknown",
String.join(", ", rationale.unknownToCatalog()))).append("</li>");
} else {
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot review on PR 8691, five comments, all valid. The formation catalog was copied and sorted on every element evaluation. It is memoized per element, but the exhaustive path evaluates a few hundred distinct elements and each re-sorted the same 41 entries. The catalog never changes after it is filled, so one sorted view now serves every caller. Copy as Text put the HTML source on the clipboard. It now converts to text, with cells as tabs and blocks as line breaks so the requirements grid pastes as readable columns. The grouping requirement could report needing zero units. This is the same normalization already added to qualifies() in this branch and not mirrored into the report: a group size of zero means one group of everything, so the Order Lance, which requires every unit to be the same model, read as asking for nothing. Resolved the way qualifies() resolves it, counting only the units the rule applies to. Force and unit names are player-typed and reached the HTML report unescaped. Escaped at every insertion point, and message arguments are escaped before formatting so the templates stay intact. PoolSolver uses nothing from the enclosing instance, so it is static. Also fixes GUI scaling missed in the report pane: an HTML pane uses its own fixed font and ignores both the scale and the theme, so the report stayed small and dark on dark while the dialog scaled. Scroll increment and HTML cell padding are pixel values too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Opened for Co-Pilot feedback, more testing and work needed before flipping live |
Summary
A player who builds a force with the RAT generator gets a pile of loose units, and the bots then
fight it as a mob. This adds a deterministic assembler that partitions loose lobby units into
Campaign Operations formations (CamOps p. 60+) - lances, stars or Level IIs - as forces in the force
tree, plus a view that explains why each formation holds the units it holds.
About a quarter of the diff is tests: 640 lines pinning the two design rosters and the structural
rules. It reads as three separate pieces, in this order: the assembler, the view that explains it,
and the lobby wiring that makes either reachable.
Groundwork for per-formation sub-bots, which is the follow-up this enables.
Assemble it
loose units. Repeatable - units already in a force are never touched - so it is safe to press
again after adding more.
formationless. Un-readying leaves the formations in place and editable.
lances of 4, Clan stars of 5, ComStar and Word of Blake Level IIs of 6. Auto-detect reads the
player's faction first and falls back to tech base. ComStar and Word of Blake field Inner Sphere
equipment, so their units can never reveal how they organize - only the faction says it.
Understand it
type it qualified as, the shared role, the speed spread, what could not be split apart (C3
networks, carried units), and the three closest trades the search passed over, each priced.
total. The ideal role leads it, because that rule decides whether the rest of the table matters -
if every unit holds the type's ideal role, the other requirements are waived (CamOps). A waived
requirement still shows itself failing rather than being hidden, so a waiver cannot be mistaken
for a bug.
stays honest after a player edits one. No new serialized state.
How it groups units
Units that must stay together fuse into atoms first - members of a C3 network, battle armor with its
ride - so no partition that splits them is ever considered. Atoms then split by combined-arms family,
and aerospace never mixes with ground. Within a family every partition into equal-as-possible
elements is enumerated and scored: a qualified CamOps
FormationTypedominates, role concentrationcomes second, speed spread and battle-value imbalance are charged, spreading ECM is credited.
Exhaustive to 15 units per pool, greedy with a swap pass above that. The same roster always gives
the same formations.
An element may be understrength, which Campaign Operations allows, but never oversize.
Fixes to existing code
FormationType.qualifies()divided by zero. The Order Lance registers a grouping constraintwith group size 0, meaning one group of everything, and the arithmetic did not expect it. Latent
on main: only a caller that sweeps every formation type can reach it, which nothing did until now.
League, Rifle is House Davion, Order is House Kurita.
qualifies()does not test that on purpose -it answers whether the units fit the shape and leaves entitlement to the caller - and there was no
accessor for it. Added one, and the picker now skips them.
Files Changed
New source
megamek/src/megamek/client/formation/-FormationAssembler,AssemblyUnit,AssembledFormation,FormationRationale,Organizationmegamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java- the viewMain edits
.../lobby/LobbyActions.java- assembleForces() and explainFormation().../lobby/ChatLounge.java- the button and the I'm Done hook.../lobby/LobbyMekPopup.java,LobbyMekPopupActions.java- the menu item.../lobby/PlayerSettingsDialog.java- the Force Organization settingmegamek/src/megamek/client/ratgenerator/FormationType.java- divide-by-zero guard, accessormegamek/src/megamek/common/preference/ClientPreferences.java- the preferencemegamek/resources/megamek/client/messages.properties- labels and report textTests
megamek/unittests/megamek/client/formation/FormationAssemblerTest.java- NEW: 26 testsTesting
unanimous-Striker star and a heavy battle star, and an Inner Sphere twelve must keep its C3 pair
and its three-boat fire core together. The Campaign Operations ideal-role example is pinned
directly: four medium Brawlers qualify as a Battle Lance even though the Heavy+ requirement
genuinely fails, and the test asserts both halves of that. Plus determinism, the size ladder swept
over every force size from 3 to 15, aerospace separation, transport atoms, name collisions, and a
unit missing from the unit cache still assembling.
oversize elements (twelve units under star doctrine became two stars of six), broken requirement
wording, duplicate table column headers, faction-only formation names, and a formation type that
demands nothing winning on a tie-break.
(Core rules phase 1 #8656) merged underneath.
Out of scope
is groundwork for.
level; formations are flat top-level forces, one packet each.