Skip to content

Feature/command fully randomize - #170

Open
HansGR wants to merge 8 commits into
ff6wc:devfrom
HansGR:feature/command-fully-randomize
Open

Feature/command fully randomize#170
HansGR wants to merge 8 commits into
ff6wc:devfrom
HansGR:feature/command-fully-randomize

Conversation

@HansGR

@HansGR HansGR commented Aug 2, 2026

Copy link
Copy Markdown

This feature modifies the processing of the -com flag 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, and I are 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.100 makes all characters like Gau (no fight, two commands); 33.33.33 gives each character approximately one of {Fight, Magic, Item} on average, with the rest backfilled with skills; 0.0.0 would give characters skills only (no magic or items).

Notes on the implementation:

  1. -com fru is 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.
  2. These uses respect excluded commands.
  3. Command order is sorted to preserve the order: "Fight" --> "Skill(s)" --> "Magic" --> "Item".
  4. When Natural Magic User = random, the algorithm attempts to find a character who is capable of using magic. If there are none, the natural magic use is still granted (as spells can still be used out of battle so it may still be useful).
  5. Morph can only be granted to one character because of its mechanics.
  6. Gau's probability of getting Fight is always half that of the other characters (e.g. if F = 80, Gau's probability of having Fight is 40%). This custom handling is intended to preserve Gau's identity a bit.
  7. Gogo and Umaro are not affected. Gogo can equip any commands that the party has unlocked, as per usual.

claude added 4 commits August 2, 2026 15:53
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.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

claude added 4 commits August 3, 2026 03:25
…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 wrjones104 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 -com composable with those flags, consolidates the six -recN exclusion 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 at mod_probability_random_commands (diff lines 630–635).
  • Issue: Morph (id 3) is a valid probability command (PROBABILITY_EXCLUDED_NAMES doesn't exclude it), and roll_probability_commands runs independently per character with no cross-character Morph guard. The morph_placed logic (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 100 gives every non-Gau character Morph, and -compru 3 50 gives it to ~half the party.
  • Why: data/characters_asm.update_morph_character explicitly # NOTE: this assumes only 1 character has morph and 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_INVARIANTS uses -compru 3 50), so it's user-reachable.
  • Note on the test gap: PR_MORPH_INVARIANTS only asserts cmds.count(MORPH) <= 1 per 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_rolled

Then 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 -com Magic/Item picks crash in composed mode. In mod_probability_random_commands (diff 609–614), an explicit -com command that is Magic/Item (accepted by _process_character_commands, which only checks command in id_name) falls to raise ValueError, an uncaught exception during generation rather than a clean parser.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 six random_exclude_commandN rows (diff 336–343) and new command_probability_* / Random Mode keys. Worth confirming the flag-selection UI/menu consumer tolerates >6 exclusion rows and the new keys; low impact for ROM generation itself.
  • menus/skills.py ASM 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 by Reserve(0x34d3d, 0x34d77, …), an overflow or logic error would only surface on a real build — see QA below.

4. General Notes & QA Check

  • Praise:

    1. Excellent test discipline — the DRAFT_INVARIANTS / PR_* / COMPOSED_INVARIANTS scripts 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.
    2. The menus/skills.py rewrite 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 into mod_moogle_commands for reuse is clean.
  • Manual QA Testing Plan:

    1. Morph blast radius: Generate a seed with -compru 3 50 (and -compru 3 100), then inspect the spoiler Commands section — confirm how many characters end up with Morph. This reproduces the blocker above.
    2. 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).
    3. 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.
    4. Backward compatibility: Confirm a legacy flagstring using -rec1 28 -rec2 27 still round-trips (re-emitted as -rec 28.27) and produces an identical seed to before this PR for a plain -com … (no probability flags).
  • Questions for Contributor:

    1. Is Morph intended to be declarable via -compr/-compru? If yes, the multi-character issue must be fixed; if not, it should be added to PROBABILITY_EXCLUDED_NAMES.
    2. 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?
    3. Was the change of -com to nargs="*" verified against every place that reloads/round-trips a saved flagstring (e.g. presets/seed replays), given it now yields a list before process() normalizes it back to a string?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants