Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/mhfu-port/src/mhfu_port/brain/own.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]}",
Expand Down Expand Up @@ -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(
Expand All @@ -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]:
Expand Down
53 changes: 38 additions & 15 deletions packages/mhfu-port/tests/test_brute_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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):
Expand All @@ -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
3 changes: 1 addition & 2 deletions packages/mhfu-port/tests/test_fatigue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions packages/mhfu-port/tests/test_own.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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"}
Expand Down
24 changes: 7 additions & 17 deletions packages/mhfu-port/tests/test_own_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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


Expand All @@ -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
42 changes: 8 additions & 34 deletions ports/brute_tigrex.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" = [
Expand All @@ -288,10 +284,7 @@ entry = "base_12"
0,
21,
]
"1,15" = [
0,
27,
]
"1,15" = "own"
"2,3" = [
1,
2,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Loading