Feature/command fully randomize - #170
Open
HansGR wants to merge 8 commits into
Open
Conversation
Turn -com into a meta flag. It still accepts the 13 two-digit command ids it always did, and now also accepts two full random modes: -com fr F.M.I fully random -com fru F.M.I fully random unique F, M and I are the percent chance (0-100) that each of the 12 characters with a special skill keeps the default Fight, Magic and Item commands. Every command slot not taken by one of those three is backfilled with a random skill, so -com fr 100.100.100 reproduces the all-random command ids and -com fr 0.0.0 gives every character four random skills. All four command slots in character initialization data ($ed7ca2-$ed7ca5) are written in menu order: fight -> skills -> magic -> item. fru snake drafts the skill slots from the available commands (random initial order, reversed every other round) and refills them when they run out, so commands stay unique for as long as there are commands to go around. In both modes a character never gets the same command twice, only one character can get Morph, the -rec1 through -rec6 exclusions are respected, and the suplex a train objective still guarantees Blitz. Also: - validate -com values instead of silently generating garbage commands - -scc shuffles whole command sets between characters in the random modes, which keeps each character's menu order intact - spoiler log lists every character's full command menu in the random modes - tests for the -com interface (no rom required)
Gau has no fight command in vanilla, so give him half everyone else's chance of rolling one: -com fr 80.50.50 gives him a 40% fight chance. This keeps "Gau doesn't always have fight" true even at a 100% chance while still letting him have it sometimes. The roll uses a doubled range rather than a halved percent so odd values stay exact (25% -> 12.5%, not 12%).
The snake draft refills the pool once nothing in it can legally go to the drafting character, which happens either because the pool is empty or because everything left in it is already on that character. The refill replaced the pool outright, so in the second case the leftover command was thrown away and lost its turn in the rotation. Refill around the leftovers instead, so a command that could not be handed out this turn is still waiting to be drafted later. Over 4000 drafts of 24 slots from a pool of 5, the spread between the most and least drafted command is now 1 in 82% of drafts, up from 64%. This cannot hand out morph twice: a refill only happens once every pooled command is already on the drafting character, so morph is never sitting in the pool at the moment a refill could add a second copy of it. Also adds a test covering the refill path: 24 slots drafted from a pool of 5 fills every slot, never repeats a command on a character, and never drafts morph more than once.
A random natural magic learner (-nm1/-nm2 random) could land on a character with no magic command, which is now easy to hit: the -com full random modes can leave a character without one, and vanilla Gau has never had one. Pick the random learner from the characters who can cast in battle, and fall back to any of them only when nobody can. X Magic opens the magic menu too, so it counts even without the magic command itself. An explicitly named learner is unchanged and still always gets the spells, whether or not they can cast in battle: their natural magic is still usable outside of battle for cure, life, warp and the like.
HansGR
requested review from
BriGuy7727,
bootherunner,
ff6wc and
wrjones104
as code owners
August 2, 2026 19:16
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
…gth) New flag -rec / --random-exclude-command-ids takes excluded command ids as one dot-separated value with an arbitrary number of entries (e.g. -rec 05.07.10.16.13), replacing the fixed six -rec1..-rec6 slots. The legacy -recN flags remain accepted as thin wrappers: their values fold into the same exclusion list (-rec entries first, then -recN in order), and the canonical flag string re-emits everything as a single two-digit-padded -rec value. Excluding every randomly-selectable command is rejected (it would empty the random pools). The Flags menu keeps its familiar shape: one row per exclusion padded with "None" rows to the usual six, so seeds without exclusions are byte-identical. Tests: three new -rec cases (dot-list parsing/canonicalization, legacy wrapper folding, invalid/exhaustive rejection); full suite passes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeosgZMF9VWGg4piL618LG
Vanilla gates the Skills submenu's Espers row with the same test as the Magic row: the enable routine at C3/4D3D compares the character's four command bytes against a per-row command-id table at C3/4D78, whose Espers entry is 02 -- the Magic command. A character without the Magic command therefore cannot open the esper equip screen at all (the row byte at $79 is both the draw color and the selection gate, checked at C3/208B). Gogo is covered by an explicit override; Umaro only incidentally, by never having Magic. With the -com fr/fru fully random command modes a character can be esper-capable yet lack Magic, losing esper level-up bonuses, spell learning, and out-of-battle casting. New menus/skills.py rewrites the routine in place -- byte-for-byte the same 59 bytes, zero bank C3 free space consumed: the command scan now indexes the row table from its Magic entry (rows 1-6, stored at $7A,X), and the freed tail enables the Espers row purely by character id (BCS on 0x0C greys Gogo, Umaro, and any special record above them). Unconditional: the coupling only made sense with fixed commands. Verified in-emulator on a '-cg -com fr 0.0.100' seed (no character has Magic): the Espers row renders enabled while Magic stays greyed, the screen opens, and Ramuh equips (character esper byte 0xff -> 0x00). Full test suite passes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeosgZMF9VWGg4piL618LG
The command-randomization styles are now separate flags that compose instead of exclusive -com meta-modes: - -com: traditional explicit ids only (99 random, 98 random unique, 97 none). Alone it behaves exactly as before, byte-identical. - -comfr F.M.I / -comfru F.M.I: Fight/Magic/Item percent chances (replacing the '-com fr/fru' syntax). - -compr IDS PERCENTS / -compru: per-command probability declarations, e.g. -compr 0.1.2.28 50.50.50.100 (97 declares a chance at an empty slot; ids are the 24 real menu commands). Composition semantics (data/commands.py mod_probability_random_commands): 1. explicit -com picks claim their slots first; 97 holds a slot empty; 99/98 mark slots for random/unique backfill 2. the declared probabilities (from -compr, plus -comfr folded in as the Fight/Magic/Item special case) roll into the remaining capacity, grouped by likelihood, most likely group first, random order within a group; a rolled command the character already holds claims nothing 3. backfill: 98 marks draft unique, 99 marks fill randomly, and leftover capacity uses the family default (unique for the -u variants) Rules: -comfr/-comfru are mutually exclusive, as are -compr/-compru; mixing unique and non-unique variants across families is an error; an id declared by both -comfr and -compr is an error; ids overlapping -rec are an error. The retired '-com fr/fru/pr/pru' syntax errors with a pointer to the new flag. Menu order: fight -> explicit skills -> rolled skills -> backfilled -> magic -> item -> empty slots. Full test suite passes (CLI coverage for composition/conflicts/migration plus behavioral invariant harnesses for the grouped roll, the four-slot cap, empty-slot declarations, morph uniqueness, and composed mode). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeosgZMF9VWGg4piL618LG
Community decision: Gau not having the Fight command is part of what makes him unique, so a declared Fight probability (from -comfr or -compr) simply never applies to him and his slot backfills instead (replacing the halved fight chance the fr mode used). An explicit Fight in Gau's traditional -com slots remains the player's choice. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeosgZMF9VWGg4piL618LG
wrjones104
requested changes
Aug 6, 2026
wrjones104
left a comment
Collaborator
There was a problem hiding this comment.
PR #170 Review — Feature/command fully randomize
1. PR Summary & Grade
- Overall Grade: B (Minor Tweaks Needed)
- Executive Summary: This PR adds probability-driven command randomization (
-comfr/-comfru/-compr/-compru), makes-comcomposable with those flags, consolidates the six-recNexclusion flags into a single dot-list-rec(keeping the legacy forms), and fixes the Skills/Espers submenu so esper-capable characters aren't locked out when they lack the Magic command. The implementation is well-structured, heavily commented, and backed by strong invariant tests; CI is green on Python 3.9–3.13. It earns a B rather than an A because declaring Morph as a probability command can hand Morph to multiple characters, violating the "only one character gets Morph" invariant the rest of the code enforces, and the PR description's Gau/Fight rule contradicts the shipped behavior.
2. Must Change (Critical / Blockers)
Morph can be assigned to multiple characters in probability mode
- Location: [data/commands.py](data/commands.py) —
roll_probability_commands(diff lines 536–566) called per-character in the loop atmod_probability_random_commands(diff lines 630–635). - Issue: Morph (id
3) is a valid probability command (PROBABILITY_EXCLUDED_NAMESdoesn't exclude it), androll_probability_commandsruns independently per character with no cross-character Morph guard. Themorph_placedlogic (diff 616–625) only removes Morph from the backfill pool (available); it does nothing to stop the per-character rolls from each producing Morph. So-compru 3 100gives every non-Gau character Morph, and-compru 3 50gives it to ~half the party. - Why:
data/characters_asm.update_morph_characterexplicitly# NOTE: this assumes only 1 character has morphand charges the morph gauge for only the first holder — every other Morph command is non-functional/undefined, exactly the mechanic the design says to avoid (PR note #5). This is a documented, tested feature path (PR_MORPH_INVARIANTSuses-compru 3 50), so it's user-reachable. - Note on the test gap:
PR_MORPH_INVARIANTSonly assertscmds.count(MORPH) <= 1per character — it never checks the party-wide total, which is why the bug passes CI. - Suggested Fix: Track Morph placement across the per-character roll loop and stop rolling it once placed. For example, thread a shared mutable flag into
roll_probability_commands:
# in mod_probability_random_commands, before the roll loop
morph_rolled = morph_placed # already-explicit morph counts as placed
for character in characters:
capacity[character] = (...)
rolled[character], morph_rolled = self.roll_probability_commands(
character, capacity[character], set(explicit[character]), morph_rolled)def roll_probability_commands(self, character, capacity, held, morph_rolled):
morph_id = name_id["Morph"]
...
if command == morph_id and morph_rolled:
continue
if random.randrange(100) < percent and command not in held:
rolled.append(command)
if command == morph_id:
morph_rolled = True
return rolled, morph_rolledThen extend PR_MORPH_INVARIANTS to assert sum(MORPH in chars[i].commands for i in c.full_random_characters()) <= 1.
3. Could Change (Low Impact / Optional)
- Gau/Fight description mismatch (please reconcile before merge). PR note #6 says "Gau's probability of getting Fight is always half that of the other characters," but the code (
roll_probability_commands, diff 561–562) fully skips Fight for Gau — he can never roll it. The test comment agrees with the code ("gau never gets fight from a probability roll, even at 100%"). This is almost certainly a stale description rather than a code bug, but it's user-facing, so either update the PR/README text or implement the halving. (Flagged here rather than as a blocker since the code is internally consistent and tested.) - Explicit
-comMagic/Item picks crash in composed mode. Inmod_probability_random_commands(diff 609–614), an explicit-comcommand that is Magic/Item (accepted by_process_character_commands, which only checkscommand in id_name) falls toraise ValueError, an uncaught exception during generation rather than a cleanparser.error. Consider validating explicit picks against the allowed set at parse time so the user gets a friendly message. options()can now emit more than sixrandom_exclude_commandNrows (diff 336–343) and newcommand_probability_*/Random Modekeys. Worth confirming the flag-selection UI/menu consumer tolerates >6 exclusion rows and the new keys; low impact for ROM generation itself.menus/skills.pyASM is not covered by the test suite (the unit tests parse args / use fake characters and never assemble the ROM). Since it's an in-place 59-byte rewrite gated byReserve(0x34d3d, 0x34d77, …), an overflow or logic error would only surface on a real build — see QA below.
4. General Notes & QA Check
-
Praise:
- Excellent test discipline — the
DRAFT_INVARIANTS/PR_*/COMPOSED_INVARIANTSscripts exercise the snake-draft refill, cap, empty-slot, and composition logic across thousands of trials without needing a ROM. That's exactly the right way to pin down probabilistic behavior. - The
menus/skills.pyrewrite is beautifully documented — the comment explains the vanilla routine, the exact bug (esper row keyed to the Magic command), and how the byte-for-byte in-place rewrite preserves the table. Refactoring the moogle logic out intomod_moogle_commandsfor reuse is clean.
- Excellent test discipline — the
-
Manual QA Testing Plan:
- Morph blast radius: Generate a seed with
-compru 3 50(and-compru 3 100), then inspect the spoilerCommandssection — confirm how many characters end up with Morph. This reproduces the blocker above. - Espers submenu fix: Seed with
-comfr 0.0.0(no Magic command for anyone), start a game, and verify a natural-magic learner can still open Skills → Espers and equip an esper, while Gogo and Umaro remain greyed out of the Espers row. Confirm the ASM fit in the reserved space (a build error would show at generation time). - Composition + menu order:
-com 05999999999999999999999999 -compr 28 100 -comfr 50.50.50— verify Terra keeps her explicit Steal, everyone holds Possess, Fight/Magic/Item appear in the correct menu order, and no character exceeds four slots. - Backward compatibility: Confirm a legacy flagstring using
-rec1 28 -rec2 27still round-trips (re-emitted as-rec 28.27) and produces an identical seed to before this PR for a plain-com …(no probability flags).
- Morph blast radius: Generate a seed with
-
Questions for Contributor:
- Is Morph intended to be declarable via
-compr/-compru? If yes, the multi-character issue must be fixed; if not, it should be added toPROBABILITY_EXCLUDED_NAMES. - For Gau + Fight: is the intended behavior "never" (as coded) or "half chance" (as documented in note #6)? Which one should the release notes reflect?
- Was the change of
-comtonargs="*"verified against every place that reloads/round-trips a saved flagstring (e.g. presets/seed replays), given it now yields a list beforeprocess()normalizes it back to a string?
- Is Morph intended to be declarable via
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This feature modifies the processing of the
-comflag to allow two new modes: fully random commands and fully random unique commands. These modes can replace the common commands Fight, Magic, and Item with skill commands (steal, rage, etc.) with a probability determined by the flags.-com fr F.M.I: Fully randomized commands.F,M, andIare integers in the range {0, 100} and correspond to the percent probability that any given character has the common commands Fight, Magic, and Item.-com fru F.M.I: Fully randomized unique commands. After rolling each characters' common commands, the algorithm distributes unique commands to the unfilled command slots via a snake draft, replenishing when it runs out.This enables several challenge modes: for example,
0.100.100makes all characters like Gau (no fight, two commands);33.33.33gives each character approximately one of {Fight, Magic, Item} on average, with the rest backfilled with skills;0.0.0would give characters skills only (no magic or items).Notes on the implementation:
-com fruis not truly unique: it is not possible to have truly unique commands as there are too many slots to fill. However, this minimizes repeats to the extent possible.