Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
23da2ee
feat(hands): remove the ringside doctor stoppage
user1303836 Oct 17, 2026
55cc178
feat(hands): mega gore sprays, gore budgets and a soaked canvas
user1303836 Oct 17, 2026
447142e
feat(hands): faces and bodies bleed early and heavily
user1303836 Oct 17, 2026
aa4322f
feat(hands): mid-fight carnage: mist, flesh, teeth and a knockdown gush
user1303836 Oct 17, 2026
94d9e96
feat(hands): a gory finisher on every stoppage
user1303836 Oct 17, 2026
9a0d020
feat(hands): screen gore: blood on the lens, flashes, vignette and shake
user1303836 Oct 17, 2026
5d114d9
feat(hands): blood leaves the face it hits, and finishers tear wetly
user1303836 Oct 17, 2026
5c45f1f
perf(hands): find a full drop pool's oldest drops in batches
user1303836 Oct 17, 2026
5e8a81d
fix(hands): keep reduced blood to a tooth from only the biggest blows
user1303836 Oct 17, 2026
af9d495
docs(hands): say plainly what the stain and pool constants mean
user1303836 Oct 17, 2026
0855148
feat(hands): lightning-fast pacing, forgiving stamina, honest blockin…
user1303836 Oct 17, 2026
1c026c4
test(hands): pin a torn shoulder's pour to the shoulder and a burst's…
user1303836 Oct 17, 2026
6d7beb4
feat(hands): a running drop on the lens leaves a thinning, fading trail
user1303836 Oct 17, 2026
18ff5c3
feat(hands): an authoritative referee who can be hit, bled and killed
user1303836 Oct 17, 2026
1c0b271
feat(hands): grotesque blood volume: every solid punch a geyser
user1303836 Oct 17, 2026
ce3e7ed
feat(hands): grotesque finishers: geysers, brain, teeth and eyeballs
user1303836 Oct 17, 2026
9934ae3
feat(hands): grotesque skin and screen: drenched faces, a splattered …
user1303836 Oct 17, 2026
a49614c
feat(hands): a severed head or glove trails a stream of blood as it f…
user1303836 Oct 17, 2026
068aa0c
test(hands): pin the soak, the skin's extra blood, the geyser and cle…
user1303836 Oct 17, 2026
2bf834e
feat(hands): draw the referee from the engine, bloodied, knocked out …
user1303836 Oct 17, 2026
91a57b4
fix(hands): footwork keys that stick or cancel, and held input sent a…
user1303836 Oct 17, 2026
f72814d
feat(hands): a referee who runs out of health is dead, not out cold
user1303836 Oct 17, 2026
7f74429
perf(hands): paint each canvas stain in one draw from pre-tinted stamps
user1303836 Oct 17, 2026
5640b0c
Merge branch 'feature/hands-arcade-gore' into feature/hands-arcade
user1303836 Oct 17, 2026
c67ab09
feat(hands): the player's own punch at the server's pace from the press
user1303836 Oct 17, 2026
725ae2d
feat(hands): the referee spurts, pours and pools like the fighters
user1303836 Oct 17, 2026
fad7360
feat(hands): predict walking out of a punch's recovery and slipping o…
user1303836 Oct 17, 2026
901de26
feat(hands): guards and evasions snap into place
user1303836 Oct 17, 2026
981f48f
feat(hands): a punch that finds the referee lands on his head
user1303836 Oct 17, 2026
6204b0d
feat(hands): power punches you can see, hear and feel coming
user1303836 Oct 17, 2026
60f620b
feat(hands): show the player's own punch landing as soon as the serve…
user1303836 Oct 17, 2026
3f42ef7
fix(hands): turn with the opponent again as a punch recovers
user1303836 Oct 17, 2026
47ad094
refactor(hands): tidy the evasion-step note and drop an unused intent…
user1303836 Oct 17, 2026
80b65e7
fix(hands): predict walking while a press waits and after a slip cuts…
user1303836 Oct 17, 2026
e479c68
fix(hands): end a stoppage on the spot, and engine fixes from review
user1303836 Oct 17, 2026
1bafd50
Merge branch 'feature/hands-arcade-feel' into feature/hands-arcade
user1303836 Oct 17, 2026
f4efa2a
fix(hands): the referee's death plays inside the engine's pause, and …
user1303836 Oct 17, 2026
83c877c
fix(hands): a punch and then cover throws the punch
user1303836 Oct 17, 2026
2ef63e5
fix(hands): freeze only the punch a contact belongs to
user1303836 Oct 17, 2026
dda0dc4
fix(hands): a keyboard guard cancels a waiting press instead of sendi…
user1303836 Oct 17, 2026
07da983
fix(hands): a slip that never starts leaves the punch's recovery alone
user1303836 Oct 17, 2026
498cd1e
fix(hands): the POWER ARMED tag reads Option as the punch does
user1303836 Oct 17, 2026
0dbd090
fix(hands): the knockout replay lands a power punch as it was live
user1303836 Oct 17, 2026
6508a6c
test(hands): the held sends leave the presses their room under a hard…
user1303836 Oct 17, 2026
7ff9d4a
test(hands): cover the own punch's glow, early-started press and foot…
user1303836 Oct 17, 2026
5c20708
test(hands): a fresh press of a key whose release was lost becomes th…
user1303836 Oct 17, 2026
f38cfb9
fix(hands): presses keep their room in a round's first second
user1303836 Oct 17, 2026
2994337
Merge branch 'feature/hands-arcade-feel' into feature/hands-arcade
user1303836 Oct 17, 2026
5be47a4
build(hands): rebuild the Activity bundle for protocol 5
user1303836 Oct 17, 2026
84e5643
fix(hands): engine fixes from the final review
user1303836 Oct 17, 2026
084e39e
fix(hands): a guard rides out a press already made instead of cancell…
user1303836 Oct 17, 2026
367e2d2
fix(hands): a dead referee is not narrated, and his replay survives a…
user1303836 Oct 17, 2026
8d6e15d
fix(hands): the knockout replay shows a knockdown through the guard a…
user1303836 Oct 17, 2026
ebfa7bb
test(hands): a contact that names no punch never freezes one
user1303836 Oct 17, 2026
a04f698
test(hands): POWER ARMED stays off while input is off
user1303836 Oct 17, 2026
f854098
Merge branch 'feature/hands-arcade-feel' into feature/hands-arcade
user1303836 Oct 17, 2026
c674576
build(hands): rebuild the Activity bundle with the final review's fixes
user1303836 Oct 17, 2026
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
9 changes: 6 additions & 3 deletions scripts/hands_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

from intelstream.hands.cpu import CpuBrain, CpuLevel, cpu_style
from intelstream.hands.engine import AttackState, BoxingEngine, EngineConfig, FighterState
from intelstream.hands.rules import TICKS_PER_SECOND
from intelstream.hands.rules import GUARD_REARM, TICKS_PER_SECOND
from intelstream.hands.types import (
ActionKind,
DefensivePose,
Expand Down Expand Up @@ -214,7 +214,8 @@ def decide(self, engine: BoxingEngine) -> InputCommand | None:
self._next_jab = tick + 24 + (tick * 7919 + self._start) % 18
jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD)
return InputCommand(self._sequence, tick, move[0], move[1], actions=(jab,))
rest_arms = distance > 175
# Out of range, or with a guard worn too thin to hold up, the arms rest so it comes back.
rest_arms = distance > 175 or me.guard_broken or me.guard < GUARD_REARM
defense = DefensivePose.NONE if rest_arms else DefensivePose.GUARD_HIGH
return InputCommand(self._sequence, tick, move[0], move[1], defense)

Expand Down Expand Up @@ -285,7 +286,9 @@ def decide(self, engine: BoxingEngine) -> InputCommand | None:
wanted = self.WANTED_DISTANCE[self.kind]
toward = (round(dx / distance * 1000), round(dy / distance * 1000))
move = toward if distance > wanted else (0, 0)
guarded = self.kind in ("turtle", "brawler", "counter")
# The turtle holds his guard whatever it costs; the others let a worn one come back.
tiring = self.kind != "turtle" and (me.guard_broken or me.guard < GUARD_REARM)
guarded = self.kind in ("turtle", "brawler", "counter") and not tiring
defense = DefensivePose.GUARD_HIGH if guarded else DefensivePose.NONE
roll = (tick * 2654435761 + self._sequence + self._start) % 1000
free = me.attack is None and not me.pending_actions
Expand Down
77 changes: 70 additions & 7 deletions scripts/hands_parity_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
conditioning and body trauma near the fatigue steps;
- style-timing-table.json: the same, with the stamina price, for every other style;
- movement-traces.json: what the engine sent, tick by tick, while a fighter holding a direction
was parried, flinched mid-punch or finished a punch.
was parried, flinched mid-punch, finished a punch, pressed a punch during another's recovery or late in
a flinch, slipped, or slipped out of a punch's recovery.

uv run python scripts/hands_parity_fixtures.py # rewrite the files
uv run python scripts/hands_parity_fixtures.py --check # fail if they are out of date
Expand All @@ -27,10 +28,12 @@
from intelstream.hands.protocol import encode_snapshot
from intelstream.hands.rules import PUNCH_RULES, STYLE_RULES, style_punch_rule
from intelstream.hands.types import (
ActionKind,
DefensivePose,
FighterStyle,
Hand,
InputCommand,
MovementAction,
Power,
PunchAction,
PunchClass,
Expand All @@ -50,7 +53,7 @@
BODY_TRAUMA = (0, 900)


def _engine(styles: tuple[FighterStyle, FighterStyle], gap: int) -> BoxingEngine:
def _engine(styles: tuple[FighterStyle, FighterStyle], gap: int, centre: int = 0) -> BoxingEngine:
engine = BoxingEngine(
match_id="parity",
activity_instance_id="parity",
Expand All @@ -61,8 +64,8 @@ def _engine(styles: tuple[FighterStyle, FighterStyle], gap: int) -> BoxingEngine
config=EngineConfig(countdown_ticks=0, flash_ko_enabled=False),
styles=styles,
)
engine.fighter("one").x = -(gap // 2)
engine.fighter("two").x = gap - gap // 2
engine.fighter("one").x = centre - gap // 2
engine.fighter("two").x = centre + gap - gap // 2
return engine


Expand Down Expand Up @@ -135,8 +138,10 @@ def _trace(

def movement_traces() -> dict[str, list[dict[str, Any]]]:
back = InputCommand(0, 0, -1000, 0)
# Off to the side he backs away from, so 45 ticks of footwork never reach the ropes.
centre = 200
straight = PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)
parried = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 130)
parried = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 130, centre)
contact = 1 + PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.POWER)].startup

def parry(tick: int) -> dict[str, InputCommand]:
Expand All @@ -149,7 +154,7 @@ def parry(tick: int) -> dict[str, InputCommand]:

hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.HEAD, Power.NORMAL)
jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD, Power.NORMAL)
flinched = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 100)
flinched = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 100, centre)

def counter(tick: int) -> dict[str, InputCommand]:
# His hook is still winding up when the other man's quicker jab lands on it.
Expand All @@ -160,17 +165,75 @@ def counter(tick: int) -> dict[str, InputCommand]:
"two": InputCommand(tick, tick, actions=(jab,)),
}

finished = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300)
flinched_pressing = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 100, centre)

def press_late_in_flinch(tick: int) -> dict[str, InputCommand]:
# The jab he presses near the end of the flinch waits for it to wear off.
if tick == 5:
return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(jab,))}
return counter(tick)

finished = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300, centre)

def alone(tick: int) -> dict[str, InputCommand]:
if tick != 1:
return {}
return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(jab,))}

slip = MovementAction(ActionKind.SLIP_LEFT)
slipped = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300, centre)

def slip_away(tick: int) -> dict[str, InputCommand]:
if tick != 1:
return {}
return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(slip,))}

cut = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300, centre)
missed = PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.NORMAL)

def slip_out(tick: int) -> dict[str, InputCommand]:
# The straight misses, he walks out of its recovery, and the slip cuts the rest of it short.
action = {1: missed, 9: slip}.get(tick)
if action is None:
return {}
return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(action,))}

waited = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300, centre)

def press_during_recovery(tick: int) -> dict[str, InputCommand]:
# A hook and a jab are no combination: the jab waits in the buffer for the hook to end while
# he walks out of its recovery.
action = {1: hook, 8: jab}.get(tick)
if action is None:
return {}
return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(action,))}

long_cut = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300, centre)
uppercut = PunchAction(Hand.RIGHT, PunchClass.UPPERCUT, Target.HEAD, Power.POWER)

def slip_out_early(tick: int) -> dict[str, InputCommand]:
# The slip cuts a long recovery short as soon as the uppercut has missed; the snapshots go on
# presenting the uppercut past the end of the slip.
action = {1: uppercut, 12: slip}.get(tick)
if action is None:
return {}
return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(action,))}

return {
"parried while holding back": _trace(parried, back, parry),
"flinched mid-hook while holding back": _trace(flinched, back, counter),
"a jab pressed late in a flinch while holding back": _trace(
flinched_pressing, back, press_late_in_flinch
),
"a jab thrown holding back": _trace(finished, back, alone),
"a jab pressed during a hook's recovery while holding back": _trace(
waited, back, press_during_recovery
),
"a slip thrown holding back": _trace(slipped, back, slip_away),
"slipped out of a straight's recovery while holding back": _trace(cut, back, slip_out),
"slipped out of a power uppercut's recovery while holding back": _trace(
long_cut, back, slip_out_early
),
}


Expand Down
2 changes: 0 additions & 2 deletions scripts/hands_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ def lab_engine(seed: int) -> BoxingEngine:
rest_ticks=REST_TICKS,
countdown_ticks=0,
flash_ko_enabled=False,
doctor_cut_threshold=700,
doctor_swelling_threshold=820,
),
styles=STYLES,
)
Expand Down
Loading
Loading