Repository navigation
Hands: computer opponent, fighting styles, Fight Night mechanics, physics knockouts, gore and a broadcast presentation - #294
Conversation
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 1 of several: client app, network, audio, commentary, HUD (reviewed at 46d6ffd, re-anchored to 3ee9662)
This is an automated review that the repo owner asked for. The server/CPU opponent, ragdoll, gore/effects, scene/crowd/camera and newest-commit reviews are still running and will post as follow-up batches. Each inline comment was checked against 3ee9662, and anything already fixed there was dropped.
Checks run on 3ee9662 (CI doesn't run on this PR)
tsc --noEmitpasses.vitest runpasses: 46 files, 622 tests.ruff check,ruff format --checkandmypy src/are clean.hands_replay.py --checkis deterministic (sha256d236b12f…). It no longer matches the hash the description says is unchanged.- The committed
hands.js/hands.cssare byte-identical to a freshvite build.
Verified OK
- Discord avatars: ids and hashes are format-checked, no referrer is sent, the canvas is never read back, the letter fallback works, and the build scanners allow only the avatar URL prefix.
- Starting the opponent's punch on arrival lines up with the server's contact tick better than before: about 17 ms off instead of 50 ms with jitter up to 200 ms. Only freezes of 400 ms or more make it visibly early more often.
- Settings storage can't throw in Discord's iframe, haptics are guarded, corner instructions are fighter-only during the rest, and a human joining during a pending CPU request is handled. Player names never reach
innerHTML. - The merge of
feature/hands-realismat253b245kept the CPU rematch working (const rematching = …is captured before the reset). An auto-merge would have broken it silently, and253b245resolves it correctly.
Biggest issue in this batch: the engine emits a hit right after every block/perfect_block for the same punch. Only the renderer pairs the two, so CompuBox, the commentary and the audio all count blocked punches as landed. Ten fully blocked jabs show as 10/10 (100%).
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 2: ragdoll, gore/effects/injury, scene/camera/crowd (reviewed at 46d6ffd, re-anchored to 3ee9662)
Each finding was re-checked against 3ee9662 before posting. 20c019c already fixes the face shape of the severed head, so that comment now covers only the injuries and the material.
Top priorities
-
KnockoutRagdoll.blend()never blends.bone.quaternion.slerpQuaternions(from[i], bone.quaternion, w)copiesfrominto the bone before slerping toward itself (threeQuaternion.js:771-773), so the result is alwaysfrom. Every physics knockdown freezes for 0.1 s and then snaps the head about 26 cm in one frame, right at the knockout punch. Every get-up levitates the body flat and then snaps the chest about 140°. It's a one-line fix (slerp through a scratch quaternion), but it needs a test that checks rotation continuity; the current tests only check positions. -
Finishers vs. the real server. The room stops ticking on the result tick, so:
- a third-knockdown TKO decapitates or bursts before the punch is drawn;
- flash-KO and TKO finishers never get the slow-motion replay (
planKnockoutReplayneeds 12 ticks after the impact); - the finish close-up picks its side before the body falls.
The dev fixture keeps generating snapshots after the result, which hides all three. Either send a short tail of presentation snapshots after a stoppage, or have the client extend the plan itself.
-
Gore pacing vs. the MK/SoF/Dead Space bar.
- Ordinary hits over-trigger permanent canvas stains: about 58% of the centre is solid red by the end of round 1 with nobody cut.
- Finisher wounds stop bleeding after about 1.5 s and only pool if the engine says the fighter was cut.
- The finisher type ignores punch weight and target (a jab can decapitate, and a body shot severs a hand).
- Landed gore vanishes.
- The severed head loses its injuries.
-
The ragdoll is robust to bad dt but not at hand-offs. A fall after a rendering gap starts from a stale pose up to about 2 m from the engine spot. The replay diverges from the live fall when a decapitation changes the masses. Feet and skull limits fight the canvas, so toes jump 20-47 cm and heads flip 40-70° in one frame.
Verified OK
- The ragdoll stays finite and inside the ring with frame times of 0.5 s and 2 s, and allocates nothing per step (about 78 µs per step on desktop).
- Without decapitation or a moving opponent, the replay reproduces the live fall exactly.
- Look hashing over 100k snowflakes is uniform: every skin tint lands at 14.1-14.4%, hair is evenly spread, and neighbouring ids get independent looks.
- The decision-ceremony marks and the referee's raised wrist are correct for all stances.
- Disposal is clean for the crowd, ropes, officials' hands and the shadow material.
- The corner cut waits for the cutman.
- Blood Off/Reduced and reduced motion are honoured for finishers, drops, drips and mist (exceptions are commented inline).
- f510a2b already addresses the crowd and officials triangle counts and the per-frame instance uploads that the reviewer measured at
46d6ffd.
Not commented inline
- The ragdoll's
foldstyle has the same stiffness values ascrumple. - The get-up slides the hands and feet 0.1-0.75 m back to the engine spot.
- Some falls creep for 7 s or more before sleeping.
- The swelling cap after d11a142 (about 1 cm) reads puffy rather than grotesque. Now that the push no longer folds the mesh, the eyes and cheeks could go back toward 2 cm.
- The PR's 60 fps table was measured with a desktop GPU and a throttled CPU. A run on a real mid-range phone inside Discord is still needed, especially with the canvas-blood uploads and the discard in the skin shader.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 3: commits 46d6ffd..3ee9662 (realism merge, polishfix, fighter records; re-anchored to f9b85f9)
Verified OK
-
The merge of
feature/hands-realismat253b245lost nothing fromf7a989f:- the render clock restarts on
welcome/resumed; - a rematch retries on
room_closedbehind a single guarded timer; - instant sends have their own budget, and the corner choice goes through
transmit; constrainPrediction,compileForComposer,disposeComposer/releaseFighterGpuanddisposeSkeletonsare all present.
The one hand edit, capturing
rematchingbefore the reset, keeps the CPU rematch working. - the render clock restarts on
-
The fighter record is built on the server at join from stored results, so a client can't spoof it. Bouts against the computer aren't counted, draws count for both, and a rematch re-reads it after the result is saved.
-
The committed bundle at
3ee9662is byte-identical to a fresh build. -
f9b85f9addresses the CompuBox and commentary blocked-punch comments from batch 1. The follow-up review of the newest commits is checking it now.
Not tied to one line
- KO count: the stored KO count leaves out doctor stoppages, which a boxing record counts as TKOs. That rule predates this PR, but the count is now shown and read aloud.
- Old clients: an old client crashes on the new
recordfield. The PR already breaks old clients through the requiredcorner_choice, so this is no worse. Making a protocol mismatch reload the page would fix both (see theerrors.tscomment on #293). - Severed head: it still drops every injury. It's covered by the existing comment on
effects.ts, now confirmed with a test after20c019c: the baked geometry is identical to an unhurt boxer's.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 4: server, CPU opponent, engine additions (reviewed at 46d6ffd, re-checked against f9b85f9)
The blind-side finding was dropped because df51956 fixes it: the computer now checks _blind_to before slipping and works its corner. Each comment below was re-checked on f9b85f9. The review of the balance commits (75afb48, df51956, f9b85f9) is still running and will be the next batch.
Verified OK
- Cost: the computer brain runs inside the room tick at about 11 µs per tick on average (p99 45 µs), against about 610 µs for the engine step. 24 full bouts against a random human raised no exceptions; that human also sent fouls, clinches, corner actions and get-ups.
- No hidden state: it reads nothing a client can't already see in its own snapshot.
- Unrated: bouts against the computer are never persisted and show ratings unchanged, so rating farming isn't possible.
- One room per account: each account can hold one room and Discord verifies the instance. There's no global room cap, though, so each account can now run a full-speed bout alone.
- Cleanup and access: if the player disconnects, the computer pauses and the room is abandoned after the grace period. Spectators can't call the computer.
- New engine state: corner instructions, blind side, parry stagger and body knockdowns are deterministic and integer-only, their state is in the checksum, and they arrive through the existing rate-limited input pipeline, so only the fighter can send their own. Protocol changes are additive.
- Merge with #293:
git merge-tree 37a3df6 46d6ffdmergesengine.py,rooms.pyandrules.pycleanly with nothing lost. The test files conflict only because both sides appended tests at the end. With those resolved, 280 cpu/engine/rooms/protocol tests, 38 server/scoring tests and the golden check pass.
Windows-only test issues (not regressions from this PR)
test_a_rematch_reaches_a_new_room_while_the_old_one_is_still_closingfails, as commented on #293.test_server.py::test_authenticated_room_admission_is_not_part_of_first_frame_timeouthangs, at both37a3df6and46d6ffd, so a fullpytest tests/test_handsnever finishes on Windows.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 5: balance and CPU commits 3ee9662..f9b85f9 (75afb48, df51956, f9b85f9)
These were measured with a harness driving the real engine and the new hands_balance.py players over about 70 seeded full-length bouts. Throwaway scripts are noted per comment.
Verified OK
- CompuBox fix:
f9b85f9fixes the miscount on CompuBox, the result card, round summaries, combos, the takeover chant, unanswered counts and damage tallies.wasBlockedpairs byaction_id, tick and the reversed actor/target, and the committed bundle includes it. Audio and rumble still miss it (inline). - Engine and tests: the engine stays deterministic (
hands_replay.py --check). 231 engine, balance and CPU pytest pass, and 112 vitest pass (render, commentary, prediction, rocked). - Stun fields: the new fields reset at round end, at rest and on get-up. Neither the 45-tick cap nor the 24-tick clear window is off by one, apart from the rocked bypass in the inline comment.
- Punch timing: the timing table is unaffected, since punch timings and the fatigue formula didn't change.
- CPU visibility and cost: the CPU reads only what the HUD shows, and its per-tick work is bounded.
- Knockdowns: equal computer bouts still produce about 1.6-1.75 knockdowns per bout, but rookie v rookie produced none in 8.
- Turtling isn't dominant: the skilled player, jab spam and hook spam all beat the turtle.
The bigger picture for the AAA/gore goal: the balance pass fixes the round-one stun-lock, but it overcorrects on blood. With marking at 33-70% of the old rates and nothing from blocked punches:
- faces stay nearly clean through three rounds;
- doctor stoppages vanish;
- most client gore thresholds become unreachable:
injury.tscuts and eye closure, the commentary's cut and doctor lines, glove blood, and the CPU's corner and shut-eye play.
Combined with the passive-CPU trap, a dominant human gets a long, bloodless decision. That is the opposite of the Fight Night + MK brief. See the combat-manifest.json and cpu.py comments.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 6: HUD, overlay and flow fixes 8cf2611..ae716e6 (re-anchored to 23f886f)
Layout findings were measured by rendering the real drawHud, caption code and stylesheet in headless Edge at 320-412 px portrait, landscape phones and 1920-3440 px.
Verified fixed
- The compact name plates' detail lines overlapping on 360-390 px phones: no overlap from 320 to 412 px. Truncation now cuts the knockdowns, though (inline).
- The ring announcer card showing "RATED 1400" for the computer: it now says COMPUTER.
- The commentary calling blocked punches landed (
f9b85f9): still fixed.
Fixed in a way that causes a new problem: the spoken records running into round 1. The voice is now cut at the opening bell, so neither name is ever spoken (inline on app.ts).
Still open, not touched here
- The hurt vision and audio muffle start before the punch lands (
renderer.ts). - On #293, the compact scoreboard mini-bars still overlap by 36 px at 320 px.
- On #293, touch still has no slip, weave, pull or clinch. The new Controls panel has Keyboard, Between rounds and Controller sections but no Touch section.
Looks fine
- Corner panel: the server accepts any instruction once per rest, so hiding an empty option only prevents a wasted pick; it never blocks something the server would accept.
- Commentary expiry: only the "can he beat the count?" lines and the introduction cards can be dropped. Knockdowns, results and the verdict never are.
- Rematch flow: the waiting picker, CPU rematches and retries behave correctly.
- Smaller changes: the caption ghost fix, Shift-gated fouls and the extra measuring (one per plate per frame) are fine.
- Big screens: nothing overflows, and the smallest text is 15 px.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 7: client review fixes f9b85f9..8cf2611 (re-anchored to 23f886f)
tsc is clean and all 646 vitest tests pass at 8cf2611. Fixes were checked against throwaway reproductions of the original comments.
Verified fixed
- Over-the-shoulder camera lurch: the contact, teeth and gum-shield code now use their own vectors (
contactPoint,mouthPoint). - Flash-KO and TKO replays are now planned when the recording ends on the punch, and the TKO head no longer comes off before the punch lands.
- Finish close-up: it now picks its side after the replayed fall.
- Ragdoll: the opponent obstacle is recorded per step and replayed. Bodies come to rest, all asleep by 2.3 s in 96 instrumented knockouts.
- Canvas blood uploads: checked in real WebGL2 (headless Edge on a GTX 1080 Ti, and SwiftShader). All 10 mip levels and the rendered frames match the old full-upload path, the stains land the right way round, and there are no GL errors. Dirty-rect union, edge clamping, the 2048 limit and r186's
copyTextureToTexturepath are correct. - Rematch audio: the heartbeat, breathing and clapper start over. A forfeit clears the muffle and the hurt vision.
- Smaller fixes: muting stops the announcer mid-line; the corner pads are coloured where the fighters rest; a held controller button is no longer a corner pick; captions keep clear of the panel.
Partly fixed
settle()is capped at about 280 steps instead of 720, but still in one frame.- Officials' heads are one-sided again and closed fighter heads drop back faces early, but the sever
discardis still compiled into every skin shader, body included.
Still open (not claimed by these commits)
- The ragdoll
blend()slerp blocker (ragdoll.tsblend, unchanged). Every physics knockdown still freezes then snaps, and every get-up still snaps about 140°. It's a one-line fix: slerp through a scratch quaternion. - The ragdoll replay diverging after a decapitation, the stale fall start after a rendering gap, and the solveFeet/skull-cone/ankle pops.
- The blood plane's shader compiling mid-fight.
- The announcer speaking while the Activity is hidden.
- With no replay (mainly reduced motion), the wave-off and celebration still start before the punch lands.
Worth a device check: the region-only canvas upload was measured on desktop Chrome. Whether iOS WebKit copies only the region for a canvas source is unverified, so it's worth profiling on an iPhone.
Up close the only thing keeping two heads apart was a fixed offset from the engine's gap between the fighters, so a punch or a lean brought foreheads through each other on the close camera. A fighter whose head comes within 25 cm of the other's now leans his head and shoulders away from it, building the lean while they are too close and easing back once they are 32 cm clear.
The rookie used to be beatable by a beginner only because it went passive once its head was battered; with that fixed (and tired punches no longer refused, so a masher never stops to recover) a beginner pressing every punch button without guarding was stopped in the first round in 8 bouts of 8. The rookie now boxes like one: it attacks rarely (5% aggression, at least 70 ticks between attacks), stands admiring for 5 seconds after landing, never counters, and reads and guards less (15% each). Over 16 seeds of three-round bouts the masher now wins 10, loses 4 and draws 2, and a power-hook swinger wins 9 of 16; the rookie still loses every bout to the contender and champion. The balance harness gains the "mash" strategy used for this.
…phones - The knockout replay's close shot often had the referee's body across a third of it; the replay now isolates the two fighters and leaves the referee (and his shadow) out until it cuts back to live. - Over a count the close camera stayed on the gap between the fighters, so a man on the canvas nearer the camera lay along the bottom of the picture behind the plates. The shot now centres on the fighter who is down and looks lower. - On a phone held upright, the rest's wide shot from the side could not hold both corners and cut the blue corner in half. It now looks down the ring's diagonal from high behind the blue corner, the near fighter low in the picture and the far one above him.
…feree left it The referee's count ran on through the mandatory eight and the look before "Box!", and the snapshot kept showing it once the fight resumed: a fighter who rose at nine was shown a count of eleven, which the client refuses as out of bounds and drops the connection (found by sweeping every snapshot of 96 bouts against the client's bounds). The count now stops at the one the referee reached, eight or the count he rose at.
- Stains on the canvas were glossy (roughness 0.32), so under the ring lights their highlight turned them lavender; blood soaked into canvas is matte, and they now stay red. - Each stain was one flat, opaque disc with satellites. A splash is now a lumpy blot of overlapping lobes, thinner in the middle than at its rim, where blood dries darkest, with satellites of varied weight. - Hit spray was drawn as streaks up to four times a drop's size with a tail twice as long as the drop, which read as thin red needles. Drops now stretch to a little over twice their size and trail a short tail.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 8: core review fixes ae716e6..23f886f (d91d4d0, e262d77, 7962bf1; re-anchored to the current head)
Checked with throwaway engine and server scripts, hands_balance.py runs on the same seeds, and real engine snapshots fed through the client decoder and predictor. The requested Python tests pass (the 2 known Windows tests deselected), along with vitest, tsc, ruff, mypy and the golden replay check.
Verified fixed
- CPU request flood:
cpuframes now count against the frame limit, and the 4,000-frame test ends inrate_limited. - Single decode per frame: malformed and unknown frames, ticket acks, spectator frames, oversize frames and error codes behave as before.
- CPU "hurt" check: judged against the poise ceiling now. The contender throws 18-61 punches a round in rounds 2-3, where it used to throw 0-2.
- CPU reach: stepping in, follow-ups and approach use the reach its swollen eyes allow.
- Worn-guard parries:
perfectand the flash-KO check now follow fromblocked. A guard of 0, 50 or 59 lands, counts as landed and lets the queued straight cut in; 60 still parries. That also covers the #293 comment about a guard-broken fighter counting as parried. - Body-collapse freeze: a fighter walking into the shot drifts at most about 2 units, and his own screen holds him still.
- Stun movement parity: server and client agree to within 1 unit for stuns that don't interrupt a punch (see the inline comment for the ones that do).
- Smaller fixes: the bleeding stamina cap, the poise clamp at bout end, alternating simultaneous body collapses, and checksum coverage of the stun fields. The client only checks the checksum's format.
- Get-up limit: the client's maximum of 171 matches the engine's worst case.
Partly fixed
- The CPU's "stops fighting" trap: the head half is fixed, but the
_reserve()half remains (inline). - Stun movement parity: it holds for plain stuns but not for stuns that cut a punch or land mid-slip (inline).
Still open (not claimed by these commits)
- the CPU's follow-up stamina check using the discounted cost;
- a rocked stun restarting on the tick the last one ends;
- the champion's fixed 133 ms reaction;
perfect_percentbarely affecting perfect blocks;- a late friend locked into spectating;
- slow cut, eye and bleeding build-up (the faces-stay-clean comment);
- no cue when a worn guard stops blocking.
…er, opening introductions, foul separation, tired punches
After the mandatory eight the engine says when the fight resumes, and nothing on screen said so. The broadcast now shows BOX! and the commentary calls it, with the fighter who beat the count and the one looking to finish him.
The first cut of the styles let the boxer win nine bouts in ten against a balanced fighter at champion level and more than four in five against everyone else. Measured with every pair of styles boxing 48 bouts from both corners, no style now wins more than 61% against another at champion level; at contender level the swarmer still takes 64-67% from the balanced fighter and the boxer, and the counter-puncher 66% from the slugger, about one standard error above 60%. What changed for players: - Boxer: the jab comes back a tick sooner instead of landing almost before it can be seen; reach 102% (was 104%), punches at 92%. The extra stamina recovery is gone: at a few points a tick it rounded to nothing. - Slugger: 110% power and 108% poise damage (were 112%), takes 90% of the poise damage (was 88%), and is slower on his feet (96%). - Swarmer: full reach (was 96%), 110% foot speed (was 108%), a tick longer to slip or weave, and 96% power, with body shots still 115% of that. - Counter-puncher: counters 38% harder than a plain punch (was 45%) so they still beat a balanced fighter's 28%; a 3-tick longer counter window (was 6) and one extra tick of evasion (was 2). The computer boxes its styles a little less extremely: the boxer keeps less range and no longer has extra footwork, the slugger is slower on his feet, the swarmer presses closer than the balanced fighter but less deep and moves its head more, and the counter-puncher reads and counters a little less. scripts/hands_balance.py --style-matrix plays every style pair at a level and prints the win table.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 9: core fixes part 2 0da7263..73eb05a (standing eight, neutral corners, opening introductions, foul separation, tired punches, rebalance, "Box!")
Checked with throwaway engine scripts, hands_balance.py over 24 seeds per matchup, real-time commentary replays and HUD renders. 252 engine/cpu/balance tests and prediction.test.ts pass. 73eb05a ("Box!") has no problems of its own.
Verified fixed
- Stun stumble knockdowns: the half-speed stumble was doubling knockdowns of stunned fighters. After
ccca89c, champion v champion has 22 such knockdowns over 24 seeds, with 10 bouts going the distance; contender v contender has 10 and 7. At 50% those were 37 and 39. - Faces staying clean: largely fixed. Competitive CPU bouts now reach cuts of 437-440, with 4-5 shut eyes per 24 bouts, and one-sided bouts reach doctor stoppages. CPU v CPU bouts still never do.
New rules that check out
- Count and walk: the count, hold, round clock, three-knockdown TKO and checksum are correct, and the
592ca8edisplay fix works. Across 12,416 walk positions, every walk reaches the far neutral corner within 7.4 s and none leaves the ropes. - Opening countdown: it applies to round 1 only; the CPU stays idle and the glove touch still lines up.
- Tired punch: it can't flash-KO (it can still cut). Client timing matches the engine on 5,376 tired rows and 192 regenerated timing-table rows.
- Mashing: not a dominant strategy. It beats the rookie 9-2-1 but loses 0-16 to the contender and 1-15 to the champion.
Still open from earlier batches (not changed by these commits)
- a rocked stun restarting with no free tick (reproduced at the 45-tick cap);
- a mid-slip stun freezing the fighter;
- no cue for a worn guard (
guard_breakfired once in 480 bouts); - the CPU body reserve (400 vs a maximum stamina of 330);
- the CPU follow-up cost: in that band it now throws a tired arm punch instead of being refused;
- the champion's fixed reaction and
perfect_percent; - stun prediction parity when a stun cuts a punch;
- the result-hold input flood.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 10: 3D scene fixes 23f886f..0da7263 (severed heads, winner off the body, knockout ceremony, ragdoll spine limit, head lean, phone camera framing, blood stamps)
These were measured with two-graph and renderer-stub harnesses, a 128-fall ragdoll sweep, a 19.8k-case fuzz of the winner push, camera projection at 16:9 and 390x844, and raycasts against buildRing().
Holds up
- Spine limit (
c19bb47): 0 of 128 falls go past 45°, against 124 without it. Start poses stay at or under 17°, so the first step doesn't snap. It adds no new pops, and a warm step costs 79 µs against 76. - Canvas blood: still uploads by region (no full uploads came back), and the stamps are built once. Droplet counts and the Off/Reduced settings are unchanged.
- Severed head: comes to rest on an ear, within 5 mm of the canvas.
- Winner off the body: never left inside a body circle in 19.8k random cases.
- Arm raise: picks the winner's nearer glove, so it's right for both stances.
Still open: none of the earlier ragdoll and gore comments are fixed by these commits
- The
blend()slerp blocker is still there (ragdoll.ts, theslerpQuaternionsline).c85c122added a position lerp beside it, which works becauselerpVectorsis alias-safe, so during the blend-in the positions now slide while the rotations stay frozen, then snap at 0.1 s. It's a one-line fix:scratch.copy(bone.quaternion); bone.quaternion.copy(from[i]).slerp(scratch, w). - Ragdoll: the replay masses after a decapitation, the stale fall start after a rendering gap, the toe and skull pops (the 128-fall sweep shows the same spikes with and without the new solvers), and
settle()in one frame. - Severed head look: the material is still plain,
computeVertexNormalsstill creases the UV seams, andcloseCutstill gets noacross. The rigid bake only removes the skin spikes. - Gore and presentation:
- canvas stain triggers (unchanged);
- blocked-punch blood;
- the flash-KO drip;
- finisher wounds stopping too soon;
- landed gore vanishing;
- finisher choice ignoring the punch;
- close-up timing without a replay;
- chest-to-chest pops;
- the over-the-shoulder camera on portrait;
- the replay reusing the previous fall;
- the missing TKO knockdown feedback.
… one A new styles scenario has one fighter choose with the keyboard and the other by touch while a spectator watches: the spectator cannot pick, each fighter sees the other's style once it is settled, the bout starts when both have settled, and both remember their style for next time. Every other scenario, the computer and the rematches included, now settles on the offered style as soon as the pick appears instead of waiting out the deadline.
With the style added to the plate, the two plates on a phone wrote their lines over each other in the middle of the screen. On a narrow screen a plate now shows the style and the bout's own count (knockdowns, warnings, points taken), and the record and rating stay with the ring announcer's introduction; on any screen a line that is still too long is set smaller and then cut short inside its plate. Also tests the extra counter window a counter-puncher gets after a parry and the swarmer's extra tick to slip or weave.
…out, and a computer that fights to its style # Conflicts: # scripts/hands_balance.py # scripts/hands_e2e_scenarios.js # src/intelstream/hands/cpu.py # src/intelstream/hands/engine.py # src/intelstream/hands/rooms.py # tests/test_hands/test_cpu.py # tests/test_hands/test_protocol.py # web/hands/replays/golden.json # web/hands/src/app.ts # web/hands/src/prediction.ts # web/hands/src/protocol.test.ts # web/hands/src/render/commentary.ts # web/hands/src/render/hud.ts # web/hands/src/render/render.test.ts # web/hands/src/style.css
Two scenarios stopped testing what they claim: - reconnect: Chrome's offline mode stops new connections but leaves an open websocket alone, so the player who "dropped" never left and the opponent never saw the pause. The drop now also kills the socket without a close handshake, as a lost connection does; the opponent sees "Connection paused" and the dropped player comes back. - ko and tko: since the standing eight sends the puncher to a neutral corner, a puncher that only stepped right walked away from the man he had put down, and since tired punches are weak a puncher that never rested could not finish anyone. The scripted puncher now walks at his opponent and gets his breath back between flurries.
user1303836
left a comment
There was a problem hiding this comment.
Claude review, batch 11: fighting styles, client side (73eb05a..3db8394)
Checked with the real engine, the real room harness, drawHud with measured font metrics, and the app DOM rendered with style.css in headless Edge at 11 phone sizes. The four client test files pass (135 tests). The server-side review of the styles is still running and will be the next batch.
Verified fixed
- Name plates at 360-390 px (
7fe871f): the phone plate's second line now holds only the style and the knockdowns (at most 77 px), and the plates stay at least 36 px apart from 320 to 412 px. - Plates cutting the knockdowns: fixed in portrait only; landscape is inline on
hud.ts.
Checks out
- Prediction parity is exact. The regenerated 1,280-row style timing table is identical to the committed one, and 4,800 more cases match
predictedPunchTimingexactly. That covers all five styles, tired arm punches, and conditioning loss after the punch. - Footwork: predicted movement stays within about 2 units of the engine over 8 ticks for the swarmer.
- Reach, slips, counter and parry windows follow the snapshot, so nothing needs to match.
- Decoder headroom: the longest styled recovery is 39 ticks (a tired slugger's power uppercut) against the decoder's limit of 40.
- Pick input: input is off during the pick, so nothing leaks into the bout. Held keys don't repeat, so style frames can't flood. The picker's timer and listeners are cleaned up and it's reused across rematches without leaking. Spectators can't pick, and your own reconnect mid-pick restores your settled choice.
Still open from earlier batches
- the announcer cut at the bell (made worse by the style title, inline);
- stun prediction when a stun cuts a punch (
prediction.ts); - the mandatory-eight HUD telling the standing fighter he's down (
hud.ts); - struggle commentary after a fighter has risen.
… a clinch Two leftovers from the corner and clinch rules. - When a corner pad refused part of the push that parts two fighters, the separation started again from where they met and moved only the man with room. That kept the pressed fighter's own step off the pad every tick, so a pair pressing into a corner crept onto the pad and bounced off it every few ticks. In 2,000 random corner-pressing pairs, 2.6% still bounced back by 3-5 units after 320 ticks (the review saw about 1%, up to 6 units). The push now stops where the pad meets him, along the push and without sliding him along the pad, and the one with room gives way by what is still missing. The same 2,000 pairs: none bounces by more than 2 units (13% show a 1-unit rounding tick, from 20%). - A clinch still on at the bell was cleared with no break, so the fighters stood overlapped, 60 units apart, into the first tick of the rest. With no rest the clinch carried into the next round, and at the final bell the result showed them overlapped. The bell now breaks it as the referee does when a clinch runs its time (one shared _break_clinch, with its referee_break event). Tests: two corner cases that bounced by 6 and 4 units now come to rest; a seeded fuzz of 24 pairs, the pressed man walking out at the presser behind a high guard, settles without a bounce (the old code bounced by 4 units in 4 of them); the bell breaks a clinch with a rest, without one and at the final bell. The golden replay is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without the parries it used to get for free, the champion had fallen
below the contender. Over a 50 ms connection the scripted skilled
player beat it 92% of the time and the scripted counter-puncher 60%,
against 44% and 0% for the contender, which broke the counter-puncher
down with body work. The rookie also never led at a busy, open
opponent, so a button masher beat him every time.
- The champion now reads and perfect-blocks four punches in five (read
and perfect 80, were 66 and 45), keeps his guard up more (80, was
65) and goes to the body more (38%, was 22%).
- The contender goes to the body a little less (42%, was 45%).
- The rookie's wait became a chance per look, busy_lead_percent: he
leads at a man throwing punches with his hands free one look in
five. One in three let him beat the masher (the masher won 25-40%),
one in ten handed it to the masher (92-94%).
Over a 50 ms connection with a reaction of 6-9 ticks to each punch (48
seeds a cell, +-4-7), share of bouts the scripted player wins:
skilled counter-puncher button masher
rookie 100% 100% 59%
contender 57% 38% 0%
champion 36% 10% 0%
The scripted turtle beats the rookie 77% while the rookie lands about
12 clean punches a bout on him. These are measured on the engine
before its pending rule changes and are retuned on the final rules.
Tests: at 50 ms the scripted counter-puncher wins more of six bouts
against the contender than against the champion; the rookie leads at a
guard or a quiet man every look and at a busy, open one about one look
in five, and the contender always. Two sampled outcomes in the
champion test are re-pinned (the champion now wins those seeds); the
body-hook test gives its brain distinct read and perfect chances; the
battered computer test asks for 5 punches in 30 s (the champion, now
guarding more, throws 7; it threw none before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"OPPONENT RECONNECTING" was set mid-screen, where the touch pads stand on a phone; through a pause they stay up, dimmed. Measured in headless Edge with the real drawHud, stylesheet and touch controls, it ran under them at 16 of 17 phone sizes: at 320x568 under BODY, POWER, GUARD and LOW (92 x 32 px), at 390x844 under the SLIP and WEAVE row, and on every phone on its side under the moves and the punch pads. With the pads up the pause now takes the centre panels' place (topPanel), which keeps clear of them at every size; desktops keep it mid-screen. It is drawn over a countdown's or a rest's panel there, as the pause is the news. The caption follows it: under it like under the rest panel, and no introduction drawn over it during a paused countdown, so the renderer tells the caption slot when the bout is paused. Re-measured at all 17 sizes in the fight, the rest and the countdown: the pause panel and any caption with it clear the pads, the top bar and the round card. The new layout test draws them at those sizes and phases and fails on the old code at 320x568. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine branch's push() tells the movement prediction about each event (a parry roots the player's fighter), so the HUD branch's push stub needs it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rocked immunity and fixed-point conditioning into the balance branch The one conflict was the sampled outcomes in test_the_champion_can_be_outboxed_or_outcountered_but_not_mashed; this branch's pins are kept. Two computer tests fail on the merged rules until the next commit: under the perfect-block re-arm the computer's late guard no longer parries when it was holding its guard, so its parry rate and the champion's edge over the contender drop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rawn pressed flat on it The physics and the get-up lay a knocked-down body on spheres at its joints, but the meshes reach past them: the calf between knee and ankle, the loose trunks and the gloves' padding were drawn under the canvas, so a lying leg or glove cut visibly into the floor (the merge notes saw a knee and shin about 6 cm under). Every fighter material (and an official's, harmlessly) now lifts a posed vertex that is under the ring's floor onto it in the vertex shader, after skinning and the other shader patches move it and just before it is projected. Off the platform, where the floor is the arena's a metre lower, nothing changes. The patch composes with the injury, outfit, corner colour and look patches and gets a program cache key of its own. Before: across the get-up sweep the skinned meshes reached 10 cm (trunks), 6.4 cm (calf) and 5.5 cm (gloves) under the canvas. After: nothing is drawn under it. Checked in headless Edge (WebGL2 through SwiftShader): the fighters' full shader chain compiles with no errors or warnings, and a sphere half under the canvas is drawn only above it on the platform and whole off it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s gloves The engine now breaks a guard when a block wears it below the line at which it stops anything (guard.block_minimum, 60): under it a raised guard stops nothing until it has recovered past the line. The client gave no sign of it: the HUD's guard bar had no mark at 60 and showed a sliver like any low bar, and the fighter kept his gloves at his cheeks. The guard bar now has a gold mark at the line, standing proud of the bar (3 px in from its start on a 320 px phone's 40 px bar, 5 px on a desktop's 64 px one, from the right on the red corner's). Under the line the bar shows spent: a red frame, mark and label, a dimmed fill and red hatching across the whole bar, so even the phone's bar reads at a glance; the label keeps its text, so nothing moves at 320 px. Rendered in headless Edge at 320x568 and 1280x720 to check it. A fighter whose guard is under the line drops his gloves toward his chest, raised or not, so the opening shows: measured on the skinned fighter, a raised guard's gloves sit at 1.51 m with the guard at 80 or 60 and at 1.36 m at 59 or 40, under his chin (head 1.55 m, chest 1.14 m). The client reads the line from the manifest (manifest.ts GUARD_BLOCK_MINIMUM), the number the engine uses. Tests: the mark on both guard bars at the line and the spent bar's red label, mark and hatching on a desktop and a 320 px phone, with none at the line itself; and the gloves 0.1 m and more lower at guard 40 than at 80, raised or not, under the chin, and up at the line itself. Both fail on the old code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t guard shown Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two comments still described the old walk back after the get-up, which 34e3ca9 replaced with a step back to his place as he rises off his knee. Comments only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own long enough
Under the engine's re-arm rule a guard parries only when it is raised
after being let down for more than perfect_rearm_ticks. The computer
did not know: with a perfect-block roll it let its guard down until
the last moment and raised it again, though a guard it had held a
moment before only blocks, so it uncovered itself for nothing; and it
kept power back from a guard only just raised, which can no longer
parry a punch thrown after the raise.
- A parry is tried only when the late raise will be a re-armed one.
Otherwise the guard goes up at once and blocks, and a read that
leaves no time for a plain block is missed only when a raise now
would itself be a parry the roll did not give.
- Power is kept back only from a guard that is down, which can come up
fresh before the punch lands; a guard that is up only blocks it.
Its hands up, the champion now meets a body uppercut it reads with a
plain low block and never lets the guard down first. With its hands
down, parries of power body hooks still follow read x perfect.
Holding his guard, the champion now seldom parries, which cost him his
edge: over a 50 ms connection the scripted counter-puncher beat him
31% of the time, more than the contender (25%). He now goes to the body
a little more (43%, was 38%), the weapon that breaks that guard down.
At 50 ms with a reaction of 6-9 ticks (48 seeds a cell, +-5-7), share
of bouts the scripted player wins:
skilled counter-puncher button masher
rookie - - 62%
contender 54% 25% -
champion 27% 12% -
Tests: a guard held up blocks without ever being let down; power goes
at a guard just raised; the parry-rate test puts the computer's hands
down first. The 50 ms order test now uses the scripted skilled player,
whose gap is the clearer (7 of 8 bouts against the contender, 1 of 8
against the champion), and one sampled champion outcome is re-pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…footwork as the manifest now gives them With poise following power and the swarmer's footwork down to 105%, the bars the pick screen draws from the manifest's numbers (the pick branch's test derives each from them) change for two styles: the boxer hits for 92% of the damage and poise (power 1, was 2), and the swarmer for 96% with his body work (power 2, was 3) on quicker but not much quicker feet (footwork 4, was 5). The swarmer's reach bar is 3, his reach being the balanced fighter's, as the pick branch already has it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-up hand-off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # web/hands/src/render/graph.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: the items not tied to a lineEvery inline review thread on this PR has been answered on its thread and resolved. These are the review-summary items that weren't tied to a line, plus four layout problems found while fixing the others. This branch also now carries #293's review fixes (merged in 6333068), and #293 is merged into main. Ragdoll and gore (batch 2)
Injury thresholds (batch 5) (fe994b2): a cheek cut now opens fully at 750, short of the doctor's 800, and the forehead splits from swelling 380 instead of 520. HUD, audio and finish timing (batches 6 and 7)
Records, protocol and capacity (batches 3 and 4)
Found while fixing (phone layout)
|
…ng follows between punches The facing blended 35% of the way toward the opponent twice a tick (58% a tick), so a quarter turn took two ticks and circling could never take an angle: a full-speed circle at 120 units left the other man 2 degrees off. And it froze for as long as an attack was on, which in a cancel chain meant from the first punch to the end of the last, so every follow-up went along the first punch's line. - A real angular limit: a fighter turns toward the other man at no more than facing.turn_degrees_per_second, 75 (2.5 degrees a tick), replacing turn_percent_per_update. His own footwork turns him with it: the bearing change his own step makes is free, so a man who circles keeps the other in front of him while the other has to turn to follow. Standing still against a full-speed circle with the hands down he falls 24 degrees behind in a second at 120 units (41 at 100, 11 at 140), enough for his straight to go past; against a circler with his guard up (4.9 a tick) he keeps up beyond about 110. Stepping with the circler, or backing off, keeps him square. Standing, a quarter turn takes 1.2 s. - The facing holds still only through each punch's startup and active ticks, so slips and weaves are still judged against the punch's own line, and turns at the limit through the recovery: between the punches of a cancel chain it follows the other man. - The risen fighter turns to face the other man while the referee finishes the eight-count, and the winner's walk to the neutral corner turns with his steps, so nobody boxes on facing away. Measured with scripts/hands_balance.py's players over 36 held-out seeds, computer against computer, the old facing emulated against the new: champion v champion 10 decisions, 9 KOs, 14 TKOs and 2.44 knockdowns a bout before, 9, 11, 13 and 2.36 after, 14-15 clean punches a round either way; contender v contender 11/14/9 and 1.92 before, 12/16/6 and 2.06 after. Punches thrown off the line (a chained follow-up along the first punch's line) fall from 0.4-0.5 a round to none, and the share of in-range time one fighter stands outside the other's jab while square himself falls from about 1% to 0.1%: the computers never circle hard enough to outpace the turn. A scripted player who circles at full speed with his hands down gets the angle 3% of the time on the contender and 6% on the champion. The golden replay is regenerated: the winner's walk to the neutral corner turns with his steps. Tests: a standing fighter turns 2.5 degrees a tick; a second of circling at 120 puts a man who stands more than 15 degrees off, so his straight whiffs while the circler's lands, and one who steps with the circler stays square (the old rule left him 2 degrees off); a jab-straight chain holds the facing through each punch and follows between them (the old rule froze it through both). The rest-walk and turn-to-face tests wait for the turn at the new rate, and the manifest check refuses a turn of nothing or of a right angle a tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a one-sided bout must leave a mark The doctor stops a bout at a cut of 800 or swelling of 900 since 04f918d, but the commentary still called a cut getting worse at 420 and "the doctor could stop this" at 560 (60% and 80% of the old 700), and had the doctor look a fighter over at the bell from a cut of 450 or swelling of 650. And nothing checked that a one-sided bout marks the loser's face at all, which a retune had once made untrue. - The doctor's thresholds move into combat-manifest.json (doctor.cut 800, doctor.swelling 900) as the EngineConfig defaults, and the client reads them through manifest.ts (DOCTOR_CUT, DOCTOR_SWELLING). - The commentary's cut marks are 60, then 60% and 80% of the doctor's cut (480 and 640), and the doctor looks at a fighter at the bell from 70% of his cut (560) or 75% of his swelling (675). - A test plays skilled against the rookie at the default config on two seeds and checks the loser's face: worst cut 289 and 342 (the client draws a full brow cut from 220), the worse eye at 1000 (closing from 330) and swollen shut once and three times, and both go to the cards with no doctor's stoppage. Halving the eye and cut rates fails it (worst cut 111 and 126). Tests: the cut test calls "worse" at 60% of the doctor's cut and not a point below, and the doctor's line at 80%; the doctor looks at a cut of 70% and a swelling of 75% at the bell and not a point below (the old marks fail both). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one exchange on the x axis The golden replay never left the x axis and never cancelled, clinched, rested or touched a corner, so its hash locked in none of the rules added since: it stopped 40 ticks into its one knockdown. It now plays a boxer against a slugger through all of them, and the script stops with an error if any stops happening: - the opening exchange as before (jab_1 t71, jab_2 t99, the straight t113 and the knockdown t120 are unchanged), then two beats the count and takes the standing eight (up t156, box t420); - off the x axis, one circles the man who stands and takes an angle on him, so his straight goes past (40 degrees off, whiff t506), and a jab whose recovery the straight behind it cuts short (t528); - a power straight that rocks a hurt man (t558) and a straight landing inside the stun it started without starting another (t581); - a clinch the referee breaks after the hold (t609-t654), a low blow, the warning and the referee's separation (t666, resumed t726); - a clinch the bell breaks (t752, bell t762), the rest with both corners' work, the walk to the corners and the turn to face the ring (240 ticks, up from 10, so they arrive and sit before the bell); - in round two a power body hook on a man with his body broken down, who drops on the delayed body knockdown (collapse t1094, down t1104). The replay is 1,145 ticks (3.9 MB) instead of 161, and is written with LF line endings on every platform. Its hash will change with every rule change merged after this; regenerate it with scripts/hands_replay.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll's clinch break, doctor thresholds and golden coverage into the balance branch No conflicts. engine2's golden replay now plays a boxer against a slugger, and this branch's style numbers (poise following power, the slugger's single power number, the swarmer's footwork) change that bout, so web/hands/replays/golden.json is regenerated with scripts/hands_replay.py; every rule marker it checks still happens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions, corner and bell fixes, the doctor in the manifest and a golden replay of every rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # web/hands/replays/golden.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd select The room hands the seat over before the socket's writer has sent the spectator's fighter welcome and the fresh select, so checking the socket on the same tick as the room's player list raced the writer and failed on a slow CI runner (it saw the earlier welcome and waiting). It now waits for those messages to arrive, like the first fighter's check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ay it faces The engine judges a punch along the way its thrower faces, which now turns toward the other man at most 75 degrees a second and holds still through the punch's startup and active ticks. The computer judged range along the straight line to the other man, so a man circling it drew punches that went past him: against a scripted circler at full speed the contender threw 3.9 a round that whiffed off the line with him in range, and the champion 4.2. It now judges every punch along the facing it will throw it along: for a punch started now, its facing turned at most a tick's turn toward the other man; for a combination's next punch, the facing as far as it can turn between the end of the punch's active ticks and the cancel point. Against the same circler (12 bouts each) its off-the-line whiffs fall to 0.2 a round for the contender and 0.5 for the champion, and all its whiffs from 5.0 and 4.6 to 1.3 and 0.9 a round. Tests: a jab at a man who has stepped 40 degrees round the computer is not thrown (the engine whiffs it) until the computer faces him again, and no follow-up goes to a man 50 degrees off once a punch has landed. The one-round masher test becomes a three-round one, measuring what it claims: over 1-8 he out-lands the rookie every bout and wins more than he loses (two in three over 24 bouts, draws as half), though a man who never stops punching never gets his poise back and is dropped now and then. Two sampled outcomes are re-pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the final rules (the facing turn limit, and the computer judging reach along its facing) the styles were still far apart. Win % of the row's style against the column's, computer against computer, held-out seeds 101-124, 48 bouts a pair (+-5-7): contender bal box slu swa cou champion bal box slu swa cou balanced - 57 40 38 48 balanced - 68 44 35 39 boxer 43 - 16 17 26 boxer 32 - 33 16 42 slugger 60 84 - 47 62 slugger 56 67 - 23 51 swarmer 62 83 53 - 77 swarmer 65 84 77 - 82 counter 52 74 38 23 - counter 61 58 49 18 - The boxer's lighter hands lost nearly every exchange, and the swarmer won most of his on his feet and his body work. Between scripted players at 50 ms (reaction 6-9 ticks), with the skilled player and the brawler each taking both styles of a pair, the swarmer won 55-66% of every pair, and with two brawlers the slugger beat the boxer 81% and the counter-puncher 83%. Styles, in the manifest: - boxer: impact and poise damage 92 -> 98. A plain straight into a balanced fighter takes 56 poise and does 58 damage (balanced's own 58 / 60). His card's Power goes back from 1 to 2. - slugger: poise taken 90 -> 95 and stamina cost 110 -> 115, so his chin is still the hardest but his heavy hands tire him sooner. - swarmer: footwork 105 -> 103 (209 units in a second's walk to balanced's 202), body damage 115 -> 110, and conditioning loss 80 -> 85 on the fixed-point conditioning. His card is unchanged. The computer in a style: - its boxer jabs a little more than its level (+5, was +25) and leads as often (+4 aggression, was -6), and no longer gives up power punches and body shots; - its swarmer works 8 closer than its level (was 20) and moves his head a little more (+5, was +10). His body work is in his punches' body damage, so he picks body shots and leads as often as his level. After, on the same seeds (the champion here; the contender's own retune is the next commit): champion bal box slu swa cou balanced - 56 53 49 39 boxer 44 - 46 34 46 slugger 47 54 - 46 38 swarmer 51 66 54 - 56 counter 61 54 62 44 - Scripted players at 50 ms, 48 bouts a pair: skilled against brawler, every pair is now 46-54% (was 34-66%); brawler against brawler, 33-67% (was 17-83%), the slugger beating the boxer 67% and the counter-puncher 65% (62% and 56% over 96 bouts, seeds 101-148). The one pair still over 62% is the swarmer against the computer's boxer, 64% over 96 bouts at champion: the infighter who slips the jab and gets inside. Taking away his evasion tick brought it to 60-62% but took the quick head off his card, and the contender swarmer against balanced to 38%. The golden replay (boxer against slugger) is regenerated: its markers and their ticks are unchanged, and its damage, poise and stamina figures follow the new numbers. hands_parity_fixtures.py rewrites the client's style timing table for the slugger's stamina price. Tests: the style tests read the slugger's stamina price and the swarmer's footwork from the manifest, and the straight's order is swarmer = counter-puncher < boxer < balanced < slugger; the swarmer's styled profile keeps its level's body and aggression. Two sampled outcomes are re-pinned: the turtle test's seeds (seed 2's rookie, a slugger, now lands nothing in round two) and the counter-puncher's win over the champion boxer (seed 7 -> 3). The 50 ms test's docstring carries this state's figures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a newcomer who covers up
After the style tune, a scripted skilled player over a 50 ms
connection (reaction 6-9 ticks, 48 seeds a cell) beat the contender
68% of the time, much nearer the champion's 31% than the 40-55% a
middle level is for, while the scripted counter-puncher beat him only
19% (25-40% wanted). The rookie landed 5.9 clean punches a bout on a
newcomer who covers up and left 23 of 144 rounds with nothing landed,
and a button masher beat him only 59%.
- Contender: guard 60 -> 70%, power punches 40 -> 55%, body shots
42 -> 45%.
- Rookie: body shots 15 -> 25%, since a newcomer who covers up keeps
his guard high, and he leads at a busy, open masher 17% of looks
(was 20%).
At 50 ms, share of bouts the scripted player wins, before -> after:
skilled counter-puncher button masher
rookie - - 59% -> 79%
contender 68% -> 57% 19% -> 23% 2%
champion 31% 10% 0%
The rookie now lands 11.9 clean punches a bout on the turtle and
leaves 7 of 144 rounds blank; the turtle still wins 94% (was 100%).
At 100 ms the skilled script still beats the contender 35% and the
champion 23%.
The two contender figures stay a couple of points outside their
ranges, on opposite sides. Every contender setting tried that holds
off the skilled script (more body work or power) holds off the
counter script at least as much: at body 50 and power 50 the skilled
script falls to 45% but the counter script to 8%. Guard, reads,
perfect blocks, aggression and footwork barely moved the skilled
figure.
Computer against computer at contender (seeds 101-124, 48 bouts a
pair), after this and the style tune: every pair is 42-58% except the
swarmer over the boxer, 65% (62% over 96 bouts; was 83%), and the
slugger over the counter-puncher, 71% on these seeds but 52% over 96.
Before the style tune the boxer lost 74-84% to three styles.
Tests: the turtle test plays seed 2 again in place of seed 4, whose
rookie now lands nothing in round two (over seeds 1-8 he lands in
every round of six bouts, and the turtle wins all eight); the masher
and 50 ms docstrings carry the new figures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reads, reserve and reach, levels and styles retuned on the final rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # web/hands/src/styles.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… too The test waits for the server's transport to stay paused with a backlog past SNAPSHOT_BACKLOG_BYTES, which only happens once the kernel buffers between the two ends are full. The reader capped its receive buffer, but the server's send buffer was left to the system, and on a CI runner that sized it large the stall took longer than the test's 10 s and it timed out. It now caps the server side at 4 KB as well, so the stall builds in well under a second, and allows 30 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#293 is merged into main, and this PR now targets main. It carries #293's review fixes and the fixes for every review thread on this PR. Everything a player sees or does beyond the realism overhaul: a computer opponent, fighting styles, Fight Night's corner, count, blind-side, parry and body-shot mechanics, a rebalanced engine whose bouts go rounds, physics knockouts, graphic gore and dismemberment, a broadcast presentation with commentary and a ring announcer, camera choices, records and CompuBox stats.
What players will notice
Fight the computer
Fighting styles
Fight Night mechanics
Knockouts and gore
The broadcast
Server, protocol and security surface
client_outdatedin its own version and a Reload button. New in this release: thecpuandstyleclient frames;select,ready.players[].style,players[].recordandplayers[].cpu, and snapshotstyle/corner_choice;final.punches;waiting.open_seatsof 1 or 2; a spectator welcome that may list 0–2 fighters; and a second fighter welcome when a spectator is seated.select.service_busy.img-srcis limited tohttps://cdn.discordapp.com/avatars/.result_jsonstores both styles; doctor stoppages count as knockouts, and knockouts are recounted from stored bouts at startup.Measured
Apple M4 GPU through ANGLE Metal, headless Chrome, the dev fixture with a finisher in the bout:
Reviews
Every batch of work was reviewed by agents that did not write it, and every finding is fixed with a test that fails on the old code.
First review (first five commits): 8 findings, fixed in 89b895a.
Second review (whole branch): the decision walk crossing, rope straps, reduced-motion drift, severed parts losing their look and blood, weak-device costs (officials' hands 3,540 to 684 triangles, crowd uploads halved and shed at the low tier, per-frame allocations), a shader pre-compile that never matched the composer, and a test gap (11 of 13 deliberate breakages passed the suite because nothing tested the draw loop's wiring; a harness now runs the real
draw()).Core review (engine, server, computer): a fatal decode for the fighter stopped on a TKO (a field above the client's bound), the computer going passive once battered, unthrottled computer requests, the computer parking outside its reach with swollen eyes, clean hits through a worn guard marked as not landed, checksum gaps, a collapse that was not frozen, negative poise on a flash knockout.
Client review (rendering): the over-the-shoulder camera lurching on every contact, no knockout replay for a flash knockout or TKO against the real server and its finisher arriving before the punch, bodies that never came to rest and replays that fell differently, a silent heartbeat after a rematch, captions under the corner panel, a held button picking a corner instruction, the gum shield lying on the canvas during the replay, full-texture blood uploads, double-sided heads, muting not stopping the announcer, hurt vision surviving a forfeit.
Visual QA (a playtest of the whole build on desktop and phones): 27 findings, from colliding plates, banners behind the top bar and a dead-end rematch screen to a count with no recovery time, fighters resting in the neutral corners, heads passing through each other, broken ragdoll poses, hollow severed heads and close-ups of the back of the head. All but two cosmetic items are fixed (see below).
Final review round (113 inline threads in 12 batches, plus the summaries): every thread is answered with a fix and resolved; for device profiling, the answer is what was measured instead. Ten fix branches covered:
Each fix has a regression test that fails on the old code.
GPU memory: every rematch leaked about 15 MB in the shared WebGL context (also on Hands: realism overhaul (simulation feel, netcode, animation, injuries, presentation) #293); fixed, and the rematch-loop check counts live GL objects instead of
renderer.info.Verification
uv run ruff check .,uv run ruff format --check .,uv run mypy src/,uv run bandit -r src/,uv run pytest tests/test_hands tests/test_database.py(729 passed; CI runs the whole suite),uv run python scripts/hands_replay.py --check(1,145 ticks through every rule),uv run python scripts/hands_parity_fixtures.py --check(client timing tables and movement traces match the engine),npm run typecheck,npm run test:run(961 passed) andnpm run build(static scan passed). The committed bundle matches the build.styles(both fighters pick, the bout starts with both styles),cpu(offer, pick, bout, the computer's corner instruction, knockout, unrated result, rematch against the computer),ko(knockout replay 405 frames, no jumps),tko(three knockdowns, the bout ends on the punch and the replay still plays),reconnect(the opponent sees the pause; smooth rendering resumes at once), a beaten count (standing eight, the winner in the neutral corner, "Box!"), plus the earlierrest,rematch,rematchloop,latencyandresponse.Not verified / known gaps
rpc.activities.writeOAuth scope, which changes the consent prompt; left for a decision.Running it live
The live bot and its quick tunnel have been down since the host rebooted on 2026-09-29. To play this branch in Discord: stop any running bot (one token, one process), run
env -u ANTHROPIC_API_KEY uv run intelstreamfrom a worktree on this branch, startcloudflared tunnel --url http://127.0.0.1:8080, and point the Activity's URL mapping/in the Developer Portal at the new tunnel host.