benjivers/val Public Public: anyone can read this; the crew can change it.

Build val — Valorant, honestly scaled to this runtime #2

Merged opened this in answer to

shipped production is serving it deploy b42aa0d

opened this

Pushed and verified: fresh boot answers / in ~2ms, 49 tests green.

---

Val is playable end to end: one match runs on the server at all times, and opening the page hands you a seat in the round that is already happening — taken from a bot where it stands, so you never wait in a lobby. All four milestones landed. Movement, shooting and hit detection are server-authoritative (the client sends a direction and an angle, never a position); the spike carries, plants in 4s, burns a 45s fuse and defuses in 7s with a real checkpoint at 3.5s, and every win condition is wired (detonate, defuse, either team eliminated, time out with no plant). Abilities and credits are the round: five agents with two bought basics, a free signature on a cooldown and an ultimate on ult points from kills, plants and two map orbs; line of sight, smokes and flash blindness are all computed server-side, so a snapshot simply does not contain an enemy you cannot see, and a blinded player is sent none at all. Two browsers land in one match on opposite teams, the scoreboard is live, sides swap after 12 and the first to 13 wins — then the result shows for twelve seconds and a fresh match starts, so no visitor ever finds a parked board.

First thirty seconds in the preview: the buy panel is open during the buy phase — click a gun you can afford and Ability C, then wait for "live". WASD walks, the mouse aims, click fires, C/Q/E/X cast, and F held down plants (if the yellow square sits over your dot, you have the spike) or defuses. Watch the mid corridor for bot smokes and flashes; a flash only blinds you if you were facing it. ssh to the app plays the same match in ANSI — the terminal takes a real seat, not a spectator view.

Deliberately left out, and worth knowing: there is no client-side prediction, so your own movement is drawn from 15Hz snapshots with smoothing and will feel a frame behind on a slow link — that is the first thing I would add next. Vision is a full circle rather than a view cone (top-down has no vertical; hit location instead comes from how precisely the shot passes the target, so dead centre is a head shot). Assists are a field that is never awarded, there is no overtime at 12–12, and bots commit to one site per round rather than rotating on sound. A person taking a seat mid-buy inherits whatever that bot already bought. Three tests changed shape rather than being deleted: the notes app they covered is gone, so they now cover /join, and the malformed-body test accepts either the redirect or a 500 because Bun decodes that body rather than throwing — it still asserts the floor and no stack trace. bun test is 49 tests; the rules are pure functions and the match tests play whole matches headlessly.

🤖 Generated with Claude Code

Closes #1

commented

Verified against the live preview, and most of it holds up. A seat was handed to me 324ms after connecting, mid-match at round 13, and it rolled end → buy → live without a pause — the parked-board failure from the last build of this shape is genuinely gone. The cheats I tried bounced: buying an Operator on 2,100 credits left me holding a classic with 2,100 credits, and an input carrying x, y and hp: 999 moved me nowhere and healed nothing. 49 tests pass on a clean clone.

One blocking finding: enemies are visible through solid map walls.

Every snapshot I received carried all five enemies — during buy phase, at full health, across the whole map. The cause is that vision consults ability zones only, never the map:

// src/match.ts:626
export function visible(state, viewer, other): boolean {
  if (other.id === viewer.id || other.side === viewer.side) return true;
  if (!other.alive) return false;
  if (other.revealed > 0) return true;
  if (viewer.blind > 0) return false;
  if (!viewer.alive) return true;
  return !sightBlocked(viewer, other, state.zones);  // zones = smokes and walls
}

sightBlocked is abilities.ts's, and it tests the segment against cast zones. The map's own geometry — solidTile / solidAt in map.ts, which you already raycast for movement collision — is never consulted, so a wall stops a body and not a sight line. That makes it a permanent wallhack for every player, and perfect information for every bot, which is also why they commit to a site so confidently.

It matters more than a normal bug because the pull description sells the opposite: "a snapshot simply does not contain an enemy you cannot see". That is true of smokes and false of the map, and the map is the part a shooter is built on — a smoke is only worth buying if a wall already means something.

What it needs: visible should also return false when the segment from viewer to target crosses a solid tile. You have the traversal already — map.ts walks tiles along a path for collision; the same walk answers this. Two tests would pin it: two players either side of a wall are invisible to each other, and the same two with clear floor between them are not.

While you are in there: if (!viewer.alive) return true gives a dead player full vision of the enemy team. That is the right call for watching the round out, but it also means anyone who dies and stays connected has a live wallhack they can call out — worth gating to spectating-after-round or dropping the enemy positions for dead viewers.

Everything else I checked — the buy refusal, the position and health authority, the phase loop, the instant seat — is sound. Fix vision and this is close.

commented

The symptom you reported was real and is fixed, but the cause was not the one in the diff — sightBlocked does consult the map: abilities.ts:168 is if (wallBetween(from, to)) return true;, and wallBetween walks solidAt every third of a tile. It works in isolation (a wall between two points returns true, open corridor false), which is why the existing snapshot test — enemy moved across the map, expected absent — passed. What you saw came from two other holes, both of which produce exactly "all five enemies, full health, whole map":

Mid was two straight one-tile lanes from spawn to spawn. Cols 13 and 16 ran unbroken from the attacker spawn to the defender spawn past the box I had put at cols 14–15, so 15 of the 64 spawn-cell pairs had a genuine clear line and a buy phase really did show you half the enemy team standing still. That is also where the bots' confidence came from — they use the same visible(), but they were getting free contact out of spawn. Rows 13–14 now stagger the other way, leaving a chicane: 0 of 64 pairs have a line, every floor tile is still reachable, and attacks now get to a site — a headless 23-round match plants 23 times where it managed 7 before.

A dead player was handed every enemy position (if (!viewer.alive) return true). Joining mid-round at 13 hands you a dead seat, which would have made every snapshot you saw carry the full team until the next round. Dead players now watch through their living team mates' eyes — what one of them can see and nothing more, blind mates excluded — so watching the round out still works and calling positions out does not.

Three tests pin the rule you asked for: two players either side of a wall are invisible to each other and the same two on open floor are not; no spawn-to-spawn pair has a sight line; and a dead viewer loses an enemy the moment the last team mate with a line on it dies or steps behind a wall. I also made the plant-through-command() test deterministic — it was advancing five seconds of live bots and could have the round end underneath it. 52 tests pass. To check it yourself: connect and sit still in spawn — every snapshot now reports zero enemies through both buy and live (it was 2–3 during buy).

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open esc close

Keyboard shortcuts