diff --git a/packages/mhfu-port/src/mhfu_port/brain/own.py b/packages/mhfu-port/src/mhfu_port/brain/own.py index 60f56f05..0274c8ae 100644 --- a/packages/mhfu-port/src/mhfu_port/brain/own.py +++ b/packages/mhfu-port/src/mhfu_port/brain/own.py @@ -267,7 +267,7 @@ def _move(self, step: Step, after: str | None) -> Move: return Move( clip=self._clip(step.clip), attacks=self._attacks(step, until if length.loop else min(until, length.frames)), - steer=self._steer(step), + steer=self._steer(step, walls=length.loop), length=max(1, step.ticks) if cut else None, after=after, label=f"MHP3rd {self.pair[0]},{self.pair[1]}", @@ -321,9 +321,11 @@ def _attacks(self, step: Step, until: float) -> list[AttackWindow]: out = out[:MOVE_ATTACKS] return out - def _steer(self, step: Step) -> Steer: + def _steer(self, step: Step, walls: bool) -> Steer: + """The donor's turn; a wall ends only a step on a looping clip, a run (the donor ends no + move at a wall: a one-shot stop or blow plays out where it stands).""" if not step.turns: - return Steer() + return Steer(walls=walls) turn = max(step.turns, key=lambda t: t.window[1] - t.window[0]) if len(step.turns) > 1: self.find( @@ -336,7 +338,8 @@ def _steer(self, step: Step) -> Steer: self.find("info", "P3_OWN_MOTION", f"turn rate {rate} is out of range: the host's") rate = None lo = round(turn.window[0]) - return Steer(turn="hunter", rate=rate, window=(lo, max(lo + 1, round(turn.window[1])))) + window = (lo, max(lo + 1, round(turn.window[1]))) + return Steer(turn="hunter", rate=rate, walls=walls, window=window) def pairs(b: Brain) -> list[Pair]: diff --git a/packages/mhfu-port/tests/test_brute_brain.py b/packages/mhfu-port/tests/test_brute_brain.py index b51d6f1f..ec273495 100644 --- a/packages/mhfu-port/tests/test_brute_brain.py +++ b/packages/mhfu-port/tests/test_brute_brain.py @@ -2,10 +2,11 @@ # SPDX-FileCopyrightText: 2026 sp00ktober from pathlib import Path -from mhfu.em.script import build +from mhfu import addresses +from mhfu.em.script import build, ir from mhfu.em.script import vars as script_vars -from mhfu_port import layout, manifest, motion, slots -from mhfu_port.brain import p3, splice +from mhfu_port import layout, manifest, motion, moves, slots +from mhfu_port.brain import own, p3, splice PORT = Path(__file__).parents[3] / "ports" / "brute_tigrex.toml" @@ -17,10 +18,22 @@ def test_loads(): assert (m.brain.donor, m.brain.splice, m.brain.entry) == (23, "base_3", "base_12") -def test_charge_is_the_host_pair_of_the_donor_charge(): +OWN = [(1, 15), (2, 19), (2, 20), (2, 23), (7, 13), (7, 14), (7, 15)] + + +def test_moves_with_no_host_pair_are_derived(): m = manifest.load(PORT) + assert m.brain is not None and m.brain.fatigue + assert [p for p in own.pairs(m.brain)] == OWN + + +def spliced(data): + m, derived = own.expand(manifest.load(PORT), data) assert m.brain is not None - assert m.moves["charge"].pair == m.brain.host_pair((7, 13)) + host = build.lift_row(data.fu, m.port.host_species, m.port.host_species) + survey = script_vars.survey(host, data.fu.em(m.port.host_species)) + donor = p3.lift(data.p3rd, m.brain.donor, m.brain.slot) + return m, derived, host, splice.splice(donor, host, m.brain, survey, moves.slots(m)) def test_pins_land(data): @@ -36,20 +49,30 @@ def test_pins_land(data): def test_brain_splices_into_its_host(data): - m = manifest.load(PORT) - assert m.brain is not None - host = build.lift_row(data.fu, m.port.host_species, m.port.host_species) - survey = script_vars.survey(host, data.fu.em(m.port.host_species)) - s = splice.splice(p3.lift(data.p3rd, m.brain.donor, m.brain.slot), host, m.brain, survey) - assert not [f for f in s.findings if f.level == "error" or f.code == "P3_PAIR_UNUSED"] + m, derived, host, s = spliced(data) + found = [*derived, *s.findings] + assert not [f for f in found if f.level == "error" or f.code == "P3_PAIR_UNUSED"] assert build.Built.of(host, s.edits).ok + names = {own.own_name(p) for p in OWN} + assert names <= set(s.slots) and {n for _, n in s.uses if isinstance(n, str)} == names + + +def test_brain_enters_own_moves_and_reads_tired_from_gauge_a(data): + _, _, host, s = spliced(data) + p = build.Built.of(host, s.edits).program + insns = [n for r in p.routines() for n in ir.walk(r.body) if isinstance(n, ir.Insn)] + own_enters = { + n.args["sub"] + for n in insns + if n.op.name == "enter" and n.args["main"] == addresses.OWN_MOVE_MAIN + } + assert own_enters == set(s.slots.values()) + tests = [n for r in p.routines() for n in ir.walk(r.body) if isinstance(n, ir.Block)] + assert any(n.op.name == "if_gauge_a_low" for n in tests) def test_brain_keeps_its_call_history_in_the_first_free_variable(data): - m = manifest.load(PORT) + m, _, _, s = spliced(data) assert m.brain is not None and m.brain.history is None - host = build.lift_row(data.fu, m.port.host_species, m.port.host_species) - survey = script_vars.survey(host, data.fu.em(m.port.host_species)) - s = splice.splice(p3.lift(data.p3rd, m.brain.donor, m.brain.slot), host, m.brain, survey) (note,) = [f for f in s.findings if f.code == "P3_HISTORY"] assert note.level == "info" and "script variable 2" in note.text diff --git a/packages/mhfu-port/tests/test_fatigue.py b/packages/mhfu-port/tests/test_fatigue.py index ca7f4b67..bfded70f 100644 --- a/packages/mhfu-port/tests/test_fatigue.py +++ b/packages/mhfu-port/tests/test_fatigue.py @@ -355,8 +355,7 @@ def test_a_host_without_reactions_says_so(data): def test_the_moves_module_carries_the_table(data, tmp_path, capsys): path = tmp_path / "m.toml" - text = (PORTS / "brute_tigrex.toml").read_text() - path.write_text(text.replace("\n[brain]\n", "\n[brain]\nfatigue = true\n", 1)) + path.write_text((PORTS / "brute_tigrex.toml").read_text()) games = ["--data", str(data.fu.root), "--p3rd-data", str(data.p3rd.root)] assert main(["build", str(path), "-o", str(tmp_path / "out.bin"), *games]) == 0 module = (tmp_path / moves.module_name(manifest.load(path))).read_text() diff --git a/packages/mhfu-port/tests/test_own.py b/packages/mhfu-port/tests/test_own.py index 8abf539b..bbfda846 100644 --- a/packages/mhfu-port/tests/test_own.py +++ b/packages/mhfu-port/tests/test_own.py @@ -94,7 +94,7 @@ def test_a_step_is_a_move(): AttackWindow(7, 135, 165, label="donor 21"), AttackWindow(8, 138, 148, label="donor 28"), ] - assert mv.steer == Steer(turn="hunter", rate=546, window=(10, 40)) + assert mv.steer == Steer(turn="hunter", rate=546, walls=False, window=(10, 40)) assert "p3_61" not in m.clips and codes(found) == ["P3_OWN_ACTIVE", "P3_OWN_GROUP"] manifest.check(m) @@ -238,7 +238,7 @@ def test_brute_charge(derived): m, _ = derived charge = m.moves["p3_7_13"] assert charge.clip == "charge" and windows(m, "p3_7_13") == [(7, 135, 165), (8, 138, 148)] - assert charge.steer == Steer(turn="hunter", rate=546, window=(10, 40)) + assert charge.steer == Steer(turn="hunter", rate=546, walls=False, window=(10, 40)) def test_brute_rush_and_leap(derived): @@ -247,8 +247,9 @@ def test_brute_rush_and_leap(derived): assert windows(m, "p3_7_14") == [(15, 85, 144)] assert windows(m, "p3_7_14_2") == [(15, 0, 14), (7, 94, 124), (8, 96, 106)] assert m.moves["p3_7_14"].steer.rate == 364 and m.moves["p3_7_14"].length == 72 + assert m.moves["p3_7_14"].steer.walls and not m.moves["p3_7_14_2"].steer.walls assert windows(m, "p3_7_15") == [(4, 75, 100)] - assert m.moves["p3_7_15"].steer == Steer(turn="hunter", rate=273, window=(0, 20)) + assert m.moves["p3_7_15"].steer == Steer(turn="hunter", rate=273, walls=False, window=(0, 20)) left_out = [f for f in found if f.code == "P3_OWN_ATTACK"] assert left_out and all("record 26" in f.text for f in left_out) assert {f.item for f in found if f.code == "P3_OWN_BRANCH"} >= {"p3_7_14"} diff --git a/packages/mhfu-port/tests/test_own_cli.py b/packages/mhfu-port/tests/test_own_cli.py index f74f84ee..8a313e72 100644 --- a/packages/mhfu-port/tests/test_own_cli.py +++ b/packages/mhfu-port/tests/test_own_cli.py @@ -28,11 +28,7 @@ def brute() -> manifest.Manifest: - m = manifest.load(PORTS / "brute_tigrex.toml") - assert m.brain is not None - for key in ("7,13", "7,14", "7,15", "1,15", "2,19", "2,20", "2,23"): - m.brain.pairs[key] = manifest.OWN - return m + return manifest.load(PORTS / "brute_tigrex.toml") def test_the_cli_needs_an_own_pair(tmp_path, capsys): @@ -42,13 +38,11 @@ def test_the_cli_needs_an_own_pair(tmp_path, capsys): assert "no [brain.pairs] entry" in capsys.readouterr().err -def test_the_cli_lists_the_moves(data, capsys, tmp_path): - text = (PORTS / "brute_tigrex.toml").read_text() - path = tmp_path / "brute.toml" - path.write_text(text.replace('"7,13" = [\n 2,\n 8,\n]', '"7,13" = "own"')) +def test_the_cli_lists_the_moves(data, capsys): + path = PORTS / "brute_tigrex.toml" code = main(["own", str(path), "--data", str(data.fu.root), "--p3rd-data", str(data.p3rd.root)]) out = capsys.readouterr().out - assert code == 0 and "p3_7_13 (slot 0) clip 61 charge" in out + assert code == 0 and "p3_7_13 (slot 6) clip 61 charge" in out assert "donor 21 -> host record 7, frames 135-165" in out @@ -66,12 +60,8 @@ def test_the_moves_module_carries_the_derived_moves(data, tmp_path): assert 'after = "p3_7_14_2"' in text and any(f.code == "P3_OWN_ATTACK" for f in seen) -def test_the_splice_enters_the_derived_moves(data, tmp_path, capsys): - text = (PORTS / "brute_tigrex.toml").read_text() - path = tmp_path / "brute.toml" - own_pairs = text.replace('"7,13" = [\n 2,\n 8,\n]', '"7,13" = "own"') - path.write_text(own_pairs.replace('"7,14" = [\n 1,\n 9,\n]', '"7,14" = "own"')) +def test_the_splice_enters_the_derived_moves(data, capsys): args = ["--data", str(data.fu.root), "--p3rd-data", str(data.p3rd.root)] - assert main(["splice", str(path), *args]) == 0 + assert main(["splice", str(PORTS / "brute_tigrex.toml"), *args]) == 0 out = capsys.readouterr().out - assert "pair 7,13 -> own p3_7_13 (slot 0)" in out and "pair 7,14 -> own p3_7_14 (slot 1)" in out + assert "pair 7,13 -> own p3_7_13 (slot 6)" in out and "pair 7,14 -> own p3_7_14 (slot 7)" in out diff --git a/ports/brute_tigrex.toml b/ports/brute_tigrex.toml index 68bbf862..271c786e 100644 --- a/ports/brute_tigrex.toml +++ b/ports/brute_tigrex.toml @@ -265,15 +265,11 @@ source = 117 slot = 98 source = 118 -[moves.charge] -main = 2 -sub = 8 -clip = "charge" - [brain] donor = 23 splice = "base_3" entry = "base_12" +fatigue = true [brain.pairs] "1,1" = [ @@ -288,10 +284,7 @@ entry = "base_12" 0, 21, ] -"1,15" = [ - 0, - 27, -] +"1,15" = "own" "2,3" = [ 1, 2, @@ -312,22 +305,13 @@ entry = "base_12" 1, 5, ] -"2,19" = [ - 2, - 4, -] -"2,20" = [ - 1, - 10, -] +"2,19" = "own" +"2,20" = "own" "2,22" = [ 1, 4, ] -"2,23" = [ - 2, - 19, -] +"2,23" = "own" "3,2" = [ 2, 1, @@ -368,19 +352,9 @@ entry = "base_12" 3, 36, ] -"7,13" = [ - 2, - 8, -] -"7,14" = [ - 1, - 9, -] -"7,15" = [ - 3, - 37, -] +"7,13" = "own" +"7,14" = "own" +"7,15" = "own" [brain.when] if_target_state = true -if_tired = false