Pay for a participant, carry the kills, and end the match on every machine (milestone 8) - #28
Merged
Merged
Conversation
Milestone 8, first half: who is paid for what, and a feed that says so on every machine. Attribution had two holes the code named and deferred: an unattributable kill fell back to seat 0, and shooting another participant paid nothing. Both were exactly right for one seat and arbitrary for two. Now: - Damage the arena inflicts -- a mine, a scrape -- carries a faction of its own, `FACTION_ENVIRONMENT`, instead of being blamed on "the other side" (`notMe`, gone). Who is *credited* is a rule in one place: the seat that last landed a hit on the victim, so a hostile chased onto a mine still scores for the chaser; in a match of one seat, that seat, which is the shipped scoreline bit for bit (the seeded baseline is untouched); with more seats and no last hitter, nobody. The star names the victim's own faction and is never a hit for anyone, as before. Arena damage pays points but not a hit: accuracy is bolts landed over bolts fired. - A participant's hull pays like any other: the hit to whoever landed it, the kill to whoever is owed it, at `PARTICIPANT_BOUNTY_MULT` (two) times the airframe's bounty, through the shooter's streak. Never the victim. - Every kill is a `KillEvent` -- killer, victim, hull, award, sequence -- and the snapshot carries the last four (`FEED_RING`). The host announces as it happens; a mirror announces each event once as it arrives, by sequence, which survives lost and reordered snapshots. Same text from the same event on every machine, with only YOU moving: the shooter reads "P2 DOWN +300", the victim "DOWNED BY P1", everyone else "P1 > P2 DOWN". Snapshot version 2. Checks: two Hornets on the autopilot with hulls the size of stations and an untouchable squadron fight to a death, every death is the other's kill, the award is a rung of the streak, and both seats' feeds read as above from the same events. Two mined waves: one untouched before anybody fires (pays nobody, the feed blames the arena; in a match of one, pays that seat), one worked over by a single shooter (pays the shooter, not the idle seat). The pinned "shooting a participant scores nothing yet" check is replaced. The sealed-loss check that relied on a dead seat 0 going on earning now pins the opposite. Over both wire tests the mirror's feed has exactly the host's line count, including at 30% loss. Simulation checks 567 -> 584, eleven new mutations. Not here yet: the match result -- a winner, a placing, a debrief on the mirror -- which is the second half. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
Milestone 8, second half: how a match ends when there is more than one seat, and telling the mirror. `sealMatch` is the rule, in one place. On a cleared squadron every seat still flying is paid for finishing intact (hull fraction x 1200) and fast (4000 - 25/s, floored at zero) -- for one seat exactly the win bonus the debrief always showed, so the single-player result is unchanged. Seats are then placed by score (`placeLines`): equal scores share a place, an eliminated seat places after every flying one whatever it scored, and the seats placed first and still flying have won -- a match that emptied the arena has a placing and nobody flying, so no winner. The time bonus is the same for everyone and cannot move the placing; the hull bonus can, and is meant to. `RunResult` carries the `MatchResult` when there was more than one seat. The host keeps the result (`Game.result`) and `createHost` sends it to every peer in a RESULT frame on the first tick after the match resolved and every half second after, for as long as it ticks: the wire drops frames, and a result that never arrived is a debrief the joiner never sees. No snapshot is sent on the resolving tick or after, since `finish` has cleared the roster and a snapshot of nobody is not a world. The mirror decodes it (short, long, foreign or mistyped frames refused with a RangeError), drops whatever snapshots it was still holding, and `Game.conclude`s: arena cleared, `onEnd` with its own seat's line; a repeat after that is ignored. The debrief shows the board on both machines -- place, seat, hull, score, kills/deaths/accuracy, the local seat lit -- and a seat that was alive and outscored reads OUTSCORED rather than HULL BREACH. Checks: the placing rules directly (ties, the eliminated, an emptied arena); the frame codec; and a two-seat match over the loopback where a mine dents the host's own hull before the squadron is cleared: both machines report the match ended with the same lines, the peer was told once, the dented host placed second and did not win, the gap between the two scores is exactly the hull the host was missing, and no snapshot followed the result. The same match over a wire that loses half of everything still concludes on the mirror, because the result was said again. The sealed-loss check reads the eliminated seat's line off the board: last, unpaid for finishing, not the winner. The stale sealResult commentary that deferred all of this is gone. The first cut of the mutation gate for this branch left four mutants alive, and each was a test gap rather than dead code: the arena's damage paying the sole seat points, a sole seat being paid for its own death, a scrape blamed on "the other side", and the eliminated being paid the finishing bonus. Each has a check now; a fifth mutant (`cleared &&` on the win) was equivalent, and the flag is gone. Simulation checks 584 -> 607, seven new mutations. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
… sealed loss reports Rebasing onto the merged #27 and running the gate on the whole branch found five mutations that no longer applied — the gate's "not testable", which it rightly fails over, and which the earlier per-commit runs had not seen because they predate the code moving: - "the host never tells its peers the match ended" targeted the first draft of the RESULT send, before it was made to repeat. Now it silences the resend branch itself. A sixth mutation joins it: a result said once and never again, which the 50%-loss wire check already pins. - "every hit is credited to seat 0" and "every kill is credited to seat 0" targeted the pre-M8 fallback-to-seat-0 code. Now they hit `onDamaged`'s direct credit and `bountyGoesTo`. - "finish waits only for flying seats" and "a teammate's win overwrites the drawn seat's sealed loss" targeted `finish(pendingResult ?? …)`, which became `resolveMatch`. The first now weakens the guard on `resolveMatch(false)`; the second drops the sealed-result branch. That last one was equivalent on everything the eliminated-seat check read: the board's line for an eliminated seat already says it lost, with the score it died with. What differs is the clock — a result sealed at death carries the time it died; the line read at resolution carries the time the match ended, seconds later. The check now pins that too: reported time within a tick of the death, and well before the resolution. Simulation checks 607 -> 608; 109 -> 110 mutations. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Milestone 8 of the multiplayer plan: who is paid for what when there is more than one seat, a kill feed that says so on every machine, and a match that ends with one scoreboard on both. Three commits, on top of the merged #27.
What changes
Attribution (
c02301e)Two holes the code named and deferred: an unattributable kill fell back to seat 0, and shooting another participant paid nothing. Right for one seat, arbitrary for two.
FACTION_ENVIRONMENTinstead of being blamed on "the other side" (notMeis gone). Credit is one rule inscores.ts: the seat that last landed a hit on the victim, so a hostile chased onto a mine still scores for the chaser; in a match of one seat, that seat (the shipped scoreline bit for bit, the seeded baseline check untouched); with more seats and no last hitter, nobody. Arena damage pays points but not a hit, so accuracy stays bolts landed over bolts fired.PARTICIPANT_BOUNTY_MULT(2) × the airframe's bounty through the shooter's streak. Never to the victim.KillEvent(killer, victim, hull, award, seq); the snapshot carries the lastFEED_RING(4), snapshot version 2. The host announces as it happens; the mirror announces each once by sequence, which survives lost and reordered snapshots. Same text from the same event, only YOU moves: shooterP2 DOWN +300, victimDOWNED BY P1, everyone elseP1 ▸ P2 DOWN.Match result (
0a24922)sealMatchis the rule in one place. On a cleared squadron every seat still flying is paid for finishing intact (hull fraction × 1200) and fast (4000 − 25/s, floored at 0); for one seat that is exactly the win bonus the debrief always showed, so single-player is unchanged.placeLinesplaces by score: ties share a place, an eliminated seat places after every flying one whatever it scored, first place and still flying wins. An emptied arena has a placing and no winner.FRAME.RESULT). The host sends it on the first tick after resolution and again everyHELLO_EVERY(30) ticks for as long as it ticks, because the channel is unreliable and a lost RESULT is a debrief the joiner never sees. No snapshot is sent on or after the resolving tick (the roster is cleared; a snapshot of nobody is not a world). The mirror drops its queue andGame.concludes; repeats are ignored. Short, long, foreign or mistyped frames are refused with aRangeError.Evidence
simcheck567 → 608,check:balanceholds, typecheck andnpm run buildclean, all at9c6da34(rebased onto main after #27 merged).check:mutants: 110 of 110 caught cleanly, 0 survived, 0 not testable, at9c6da34. Running the gate on the whole rebased branch found five earlier mutants that no longer applied (their target lines moved in M8);9c6da34re-points them and adds one more (a result said once and never again). Re-pointing "a teammate's win overwrites the sealed loss" showed it had become equivalent on everything the check read, so the check now also pins the clock: an eliminated seat's result carries the time it died, not the time the match ended. The first gate run on this branch left four real survivors, each a test gap not dead code (arena damage paying the sole seat, sole seat paid for its own death, scrape blamed on the other side, eliminated seat paid the finishing bonus). Each has a check now; a fifth (cleared &&on the win) was equivalent and the flag is gone.Not here
Milestone 9: lobby, seat backfill by AI, respawn policy. The host-side twin of #27's pacing (peer intents are still flown as they arrive).
🤖 Generated with Claude Code