Commit Graph
9 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5 4bbdf6357a The opponent can serve and fall, and there is a bench to judge it on
THE MISSING ART, generated by the whole-sheet restyle method: bouncing the ball
at the baseline, the toss with the ball leaving the fingertips, the strike at
full stretch — and the fall, which slice 4 has been waiting for: buckling, down
on the knees, flat on the court face down, then dragging itself back up. One
zombie is now complete as a tennis player rather than as a body that shuffles.
(Healthy only so far; the wound variants are the next sheet.)

THE SPRITE BENCH, in the back room. No ball, no scoring, no AI — one opponent
on an empty court, walking, at the size and speed it is actually played at,
because that is the only place a walk cycle can be judged. A sheet that looks
fine at 224 px on a contact sheet is a different animal at 30 px crossing a
lawn, and every sprite argument this project has had was really about that gap.

It steps through every state including the ones the game cannot reach yet, so
the serve and the fall could be looked at the day they were drawn instead of
the day they get wired up. It prints the FILENAME it is drawing and the hp the
wound stage was read from — the two numbers that separate "the art is wrong"
from "the wiring picked the wrong art" — and it says HEALTHY ONLY where a wound
variant does not exist rather than implying one does. The stride steps off
ground covered, like the real thing: a bench animating on a clock would flatter
exactly the sprites that foot-slide in play.

It owns the court like every other mode and every start* refuses while it runs.

FOUND BY IT, ten seconds in: `start()` applies the mode the setup screen picked,
so a remembered ZOMBIE TENNIS was taking the court before the back room could —
every back-room entry (weapons, bosses, drill, match, bench) had been silently
refusing since the mode select landed this morning. The back room passes null.

And a third calibration lesson: POSE MOVES A GLOBAL COLOUR STATISTIC. A body
face down shows a fraction of the shirt it shows standing, so `redness` fell 14
points across the fall sheet without a colour changing. The measure that
survives a pose change is the shorts HUE — mid-tone neutrals, present in every
frame in the same proportion — which is why it is the one that caught the drift
Oliver actually saw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:25:29 +02:00
wuwiandClaude Opus 5 c629ae1108 The ladder has names, the bodies play tennis, and the stats screen splits
THE SPRITES. The match opponent borrowed the turned CPU's art — a body drawn
to SHAMBLE at the camera with its racket hanging at its side, which is what
was playing tennis points against you. It has its own family now, 'r', drawn
off the LIVING opponent's poses: a ready stance, a shuffle pair and a real
forehand, at every wound stage, men's and women's — 20 frames, six batches.
'c' keeps its shamble untouched, because in the gore mode shambling is exactly
what the turned CPU should be doing. The pose comes from racketAI's own
`pose`, so the picture cannot drift from the behaviour, and a one-armed
opponent still plays tennis instead of reverting to a walk.

Two pipeline bugs found doing it, both fixed at the root in assets/:
- _fine.py keyed EVERY magenta pixel, not just the background, and the
  generator paints racket strings in the backdrop colour more often than not —
  so frame after frame installed a man holding an empty hoop. Background is now
  the region connected to the border; magenta left INSIDE a body is repainted
  pale grey rather than punched out.
- the flood fill silently did nothing, because Image.fromarray images are
  read-only and PIL's floodfill swallows the write error. .copy() plus a real
  error if it ever fills zero pixels.

THE LADDER (handoff slice 2). Rung N takes design N's name off the chapter
roster — the club's dead staff play you at tennis here and come for you in
gore, same joke twice — plus a personality that is DIALS, not new mechanics:
pace, width, flub and speed scalings multiplying numbers racketAI already
reads. A rung is a data row. Name plate on the walk-on, and the kill files
under the rung rather than under the generic body.

THE STATS SCREEN follows the mode split: BODY COUNT / THE LADDER / THE RECORD
behind a mode strip, each with its own three figures, its own roster heading
and its own footer. The weapon shelf and the roster hide in the modes that
have neither, rather than printing a line about something the mode cannot do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:34:26 +02:00
wuwiandClaude Opus 5 c89d4a3ff9 Three modes, picked up front — and CLASSIC TENNIS is one of them
The setup screen picks WHAT before WHO and WHERE. Three rows out of a table,
in the court row's own markup:

CLASSIC TENNIS is not the opener with the turn switched off and left there —
it is the mode the other two are built on, so it gets real match scoring: six
games with a two-game lead is a set, best of TUNE.classicSets is the match,
the board shows the sets column (hidden until now outside splatter) and the
bar under it says which set this is. The CPU stops rubber-banding and flubs
less: aiFlubLoss exists to get a new player PAST the opener, and a mode whose
whole promise is a real match cannot be won by an opponent that falls apart
the moment he goes ahead. No tiebreak — 6-6 plays on to 8-6. Marked ponytail.

That needed a second ENDING, the one with no corpse in it: G.done, the same
summary and gated tap as the death screen, its own words (GAME, SET, MATCH. /
BEATEN.) and the man still standing. The three endings now share one block
rather than three copies of it.

Caught by the first classic test: stats banked only the FIRST classic run of a
session. `banked` was cleared by the next KILL, and a classic match has no
kills in it at all — every later match was swallowed by the double-bank guard.
beginRun() is the honest reset and restartRun calls it.

ZOMBIE TENNIS from the menu is one line; the back room keeps its door.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:27:49 +02:00
wuwiandClaude Opus 5 66fdad3d74 A match run is a different kind of run, and stops polluting the gore stats
The two modes share no currency: gore counts kills, multi-kills and weapons;
zombie tennis counts opponents felled and has exactly one kill per opponent,
always credited to 'bomb' (matchGameWon's explode). Every one of those was
landing in splatterRuns as a gore run, on the roster as THE CLUB PRO, and on
the KILLS world board — where a five-body match would post a row next to a
300-kill massacre and mean something else entirely by it.

One character on the record fixes all of it: `m`, absent meaning gore (which
is what every existing record is, so no migration). read() takes a mode and
so does endRun, which now compares you only against your own mode and refuses
a name prompt from a match.

The record's two headline slots carry each MODE's two headline numbers — gore
g=games s=sets, match g=felled s=games — so `g` stays the thing everything
ranks on and the match best is derived from the record instead of living in a
second key that drifts. That retires the matchBest key from the last commit.

Found on the way, pre-existing and visible: endRun returned `{...best, kills,
games}` and the later key won, so G.best.games was the run that had just died.
The gore death screen has been printing "BEST <this run> GAMES · <the record>
SETS". The return now carries the best and nothing else.

And stats.check() — the module's own fixture check — was exported and called
by nothing. Wired into the suite, with the mode split added to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:58:04 +02:00
wuwiandClaude Opus 5 0620e97d6c Zombie tennis: one game per opponent, blood only
Triage failed the only test that matters — "i think the normal player does
not grasp this" — and the lesson across four drill designs is that every
invented rule needs teaching, while the rules the scene already implies need
none. So the new mode is the game's own oldest rule: tennis.

MATCH TEST in the back room (#match). One game per opponent, real 15/30/40
scoring. Built on top of the duel machinery: G.match runs a permanent
interlude, which is already what makes serving tennis-like, hides the rack,
quiets the spawn director and wakes racketAI. The one change is in
resolveInterlude — a settled point feeds matchPoint instead of ending
anything. About 90 lines for a whole mode, because the deepest system in the
repo was already built.

The score is the body: a quarter of its pool per point won, floored at 1, so
a love game walks the racket zombie through every wound stage and the game
point fells it. Its points draw your blood — hp--, eaten at zero, matchHeal 2
per game won is how you last. Regicide is inherited: kill the body mid-game
and the game banks by hand.

The timing is drawn, per Oliver's ask: an approach ring around the incoming
ball that kisses a fixed inner ring exactly at the sweet contact distance
(gold inside the band), and an apex band beside the toss spanning the same
0.12m keyPressed scores as perfect, with a tick riding the live ball. Both
cues use the mechanics' own numbers, so the picture cannot lie.

Verified by autopilot: 7 games in 90 simulated seconds, opponents 1..8, pools
4/6/8/10, wounds tracking points exactly.

36 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:19:37 +02:00
wuwiandClaude Opus 5 ef8a413031 You aim it yourself now, and no two rounds are dealt alike
Oliver played it: "the auto aim just mean no skills involved at all. this
should just go away.. there is no penalty at the moment.. make some
variations of the position on the court and the color".

The first two notes are one finding. With the shot locked to the called body
it was unmissable, so the fault could never fire — there was no penalty
because there was no way to earn one. Both snaps are deleted. What replaced
them is one line: aimTie is 0 during a drill. That is not auto-aim, it is
removing the guess — the ordinary magnet lets a nearer body inside the same
cone steal a shot aimed past it, which is right for a crowd you are mowing
down and exactly wrong where hitting the wrong body IS the failure. An
unaimed tap now lands on grass.

And the variation:
- six colours (purple, white) with the court dealt three to five of them, so
  which colours turn up and how many decoys there are are live questions. At
  four out of four every round dealt every colour and only the order moved.
- PLACEMENTS: fan, cluster, column, stagger, wing — weighted, mirrored per
  round, lanes shuffled against the bib order. column stacks bodies in one
  lane, and the ball hits the first thing it touches, so feet matter.
- pips are dice faces now, shared by bib and HUD chip. A row of five dots has
  to be counted; a die is recognised.

Measured over 30 deals: all five shapes, counts even, all six colours, and
the first target screened by another body in ~10% of rounds. Verified end to
end in the browser that FAULT and DOUBLE FAULT both fire.

26 tests, including a named regression for the auto-aim staying gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:16:25 +02:00
wuwiandClaude Opus 5 22df96cc1d The umpire reads a list, and the bibs come out to meet it
The late game is repetitive because the optimal move never changes: kill the
nearest thing, now. This is the first swing at a mode where it does — a
coaching drill where the umpire CALLS a colour sequence aloud while bibbed
bodies take their marks, and you must put them down in that order while they
walk at you. Uncalled colours are decoys you have to ignore. Two faults is a
double fault and the round is theirs.

Back room only (DRILL TEST, or #drill): this is a feel-test for one question —
tense, or just noisy? — so it keeps its own round count and touches neither
tennis scoring nor the run.

- voiceSeq: voice() drops a line that would start while the umpire is still
  talking, which is fatal for a list. Decode first, queue at accumulated
  offsets, and the anti-overlap rule is satisfied instead of fought.
- Four colours with PIPS as well as hue — red/green alone makes the mode
  unplayable for ~8% of men.
- drawBib is a ponytail-marked overlay that knowingly breaks the sprite rule;
  it is the thing to delete if the mode survives.
- BOTH target pickers snap (flickTarget and zombieTarget). Snapping only the
  slingshot left fifteen tap-serves in a row scoring nothing.
- Contact costs the point, not the run — permadeath stays in the gore mode.
- startDrill disarms the ordinary schedule; updateDrill redeals and logs an
  INVARIANT if anything empties its court.

Eight new tests (24 total) pin the ownership contract both ways, the call
beat, the win, the double fault, the un-winnable-round guard and the death.
PROJECT.md carries the four open questions for the playtest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:01:33 +02:00
wuwiandClaude Opus 5 5ea84f8511 Nothing arrives unannounced: the horde gets its breather, the boss gets a name
THEY KEEP COMING is a 2.4s 'over' beat again (warnHorde in beats.js) instead of a
flash raised on the same frame the wave lands, and the chapter boss walks on behind
an overlay — his name in red over ENTERED THE COURT — held for 3s by the new
self-dismissing modal clock (m.hold) rather than announced on a routine-set card.

startHorde returns a bool: if the court has been taken when the warning expires the
pendingHorde latch is re-armed rather than the set losing the horde it paid for. The
warning ticks on its own clock, not on the 'over' beat, so a beat that moves the
phase underneath it can't strand it (the mgT lock, again).

Three tests, 15 total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:38:29 +02:00
wuwiandClaude Fable 5 1f636530b9 The code moves into src/: index, server and the test harness keep the root
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:45:23 +02:00