Skip to content

Add Assemble a Force: partition loose lobby units into CamOps formations - #8691

Draft
HammerGS wants to merge 12 commits into
mainfrom
Implement-Assemble-A-Force
Draft

Add Assemble a Force: partition loose lobby units into CamOps formations#8691
HammerGS wants to merge 12 commits into
mainfrom
Implement-Assemble-A-Force

Conversation

@HammerGS

Copy link
Copy Markdown
Member

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

  • Assemble Force button in the lobby toolbar. Assembles the local player's and every local bot's
    loose units. Repeatable - units already in a force are never touched - so it is safe to press
    again after adding more.
  • On I'm Done, any still-loose units are assembled first, so no bot force reaches round one
    formationless. Un-readying leaves the formations in place and editable.
  • Force Organization setting in Player Settings, remembered as a client preference: Inner Sphere
    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

  • View Formation Rationale on the right-click menu explains a formation in plain language: the
    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.
  • Requirements table: one row per rulebook requirement, one column per unit, and a met/needed
    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.
  • Recomputed from the force as it stands, never stored, so it works on hand-built forces too and
    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 FormationType dominates, role concentration
comes 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 constraint
    with 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.
  • Faction-exclusive formations were being handed out blind. Hammer and Anvil are Free Worlds
    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, Organization
  • megamek/src/megamek/client/ui/dialogs/abstractDialogs/FormationRationaleDialog.java - the view

Main 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 setting
  • megamek/src/megamek/client/ratgenerator/FormationType.java - divide-by-zero guard, accessor
  • megamek/src/megamek/common/preference/ClientPreferences.java - the preference
  • megamek/resources/megamek/client/messages.properties - labels and report text

Tests

  • megamek/unittests/megamek/client/formation/FormationAssemblerTest.java - NEW: 26 tests

Testing

  • Unit tests: 26. The two design rosters are literal fixtures - a Clan ten must split into a
    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.
  • Manual testing in a live lobby found five defects, all fixed here and each pinned by a test:
    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.
  • Full suite, checkstyle and javadoc: all green, zero failures, including after Core Rules Phase 1
    (Core rules phase 1 #8656) merged underneath.

Out of scope

  • Sub-bots, one bot client per formation with behavior preset by formation type. The follow-up this
    is groundwork for.
  • Nested company trees. The server assigns force ids asynchronously, so a tree needs a round trip per
    level; formations are flat top-level forces, one packet each.

HammerGS and others added 11 commits August 10, 2026 15:22
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>
Copilot AI lite review requested due to automatic review settings August 12, 2026 01:08
@HammerGS
HammerGS requested a review from a team as a code owner August 12, 2026 01:08
@HammerGS HammerGS added Princess/AI Issues or PR that relate to the current Bot AI AI ready for Review Indicates that is has been in game tested and is ready for review as it can be AI Assisted Development This project/code contains AI use under the supervision of a human developer. With Human testing. labels Aug 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread megamek/src/megamek/client/formation/FormationAssembler.java
Comment thread megamek/src/megamek/client/formation/FormationAssembler.java
Comment thread megamek/src/megamek/client/formation/FormationAssembler.java Fixed
Comment thread megamek/src/megamek/client/formation/FormationAssembler.java Fixed
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>
@HammerGS
HammerGS marked this pull request as draft August 12, 2026 01:33
@HammerGS

Copy link
Copy Markdown
Member Author

Opened for Co-Pilot feedback, more testing and work needed before flipping live

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Assisted Development This project/code contains AI use under the supervision of a human developer. With Human testing. AI ready for Review Indicates that is has been in game tested and is ready for review as it can be Princess/AI Issues or PR that relate to the current Bot AI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants