Split The code is public; the app answers only to the crew.

A bot with an AWP fires it from the hip: no bot ever scopes, so the most accurate gun in the game sprays a 7.7° cone #233

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A bot holding the AWP now scopes it when the target is far enough away, then nearly stops and waits for its feet to settle before firing. Its shot goes from a 7.7° cone to 0.11°. `bun run check` passes (types plus 838 tests), and the new tests fail without the change.

shipped merged

opened this

A bot holding the AWP now scopes it when the target is far enough away, then nearly stops and waits for its feet to settle before firing. Its shot goes from a 7.7° cone to 0.11°. bun run check passes (types plus 838 tests), and the new tests fail without the change.

Where it lives. The work is done by a new snipe step in src/client/bots.ts. The issue put it in scriptedFight, but the game doesn't use that function: every game bot runs the learned policy, and scriptedFight is only the arena baseline. The arena's duels also only ever hand out an AK or an M4. So snipe runs after whichever fight made the decision, learned or scripted. It only reads and trims the input the bot sends, in the same way the reaction-time gate already overrides the policy. The policy has no scope action and its inputs are unchanged. The spread value it already sees now reads the scoped spread whenever the bot is zoomed, so the shipped weights mean what they were trained on. world.ts is unchanged.

The rule, as named constants:

  • Past SCOPE_RANGE (500 units) the bot asks for the scope. It only comes back out inside UNSCOPE_RANGE (350). The gap means an enemy stepping back and forth across one line can't flip the scope every tick against the bolt gate in applyInput.
  • While zoomed, the bot's forward and side are each held to SCOPED_MOVE (±0.15), and it doesn't fire until it is moving slower than STEADY_SPEED (60 u/s).
  • Below SCOPE_SKILL (0.3), which covers most of Easy, a bot never scopes.
  • A terrorist bot that is out of clock and running for the site doesn't scope, since scoping would slow it to a walk.

Why 500. I ran throwaway duels down one long open line: a skill-0.8 AWP bot against a skill-0.8 AK bot, 80–250 duels per range, scoping on vs off. Scoping never lost.

  • Scripted fight: level at 500, then +14 points at 700 and +40 at 1000.
  • Learned fight: level from 450 to 1100, then +14 at 1400. At those middle ranges the bot's own aim error of about 1.6° decides the duel more than the gun's spread does.

Round split, before → after:

  • scripts/duel.ts: shipped 170/32/38 (71% won) and scripted 94/76/70 (39% won), identical before and after, because the arena has no AWPs.
  • scripts/measure.ts, 12 matches (default seed): T 62% → 66%. That is past #154's ±2 points, but it is 90 rounds with 3 of them flipping.
  • 40 matches on seed 7: T 63% (180/284) → T 63% (186/293). The 12-match move was noise, and the split held.

Tests:

  • In bots.test.ts, for both the learned and the scripted bot: it asks for the scope at 1200 units but not at 200; a rifle is never scoped and an easy bot doesn't scope; the decision holds steady between the two ranges; a zoomed bot's side stays within ±0.15; and the one shot it takes over three seconds of ticks through applyInput is under 0.15°.
  • readme.test.ts checks that the README quotes the constants the code actually holds.

Not done: the scope doesn't narrow what a bot can see. Bots don't have a field of view, so the one cost of scoping for a bot is the walk.

The README's bots section now has a "A bot with the AWP scopes it" paragraph with the ranges and these measurements.

🤖 Generated with Claude Code

Closes #194

commented

A bot holding the AWP now scopes when the target is far enough away, then nearly stops and waits for its feet to settle before firing. Its shot goes from a 7.7° cone to 0.11°. The branch is merged with main, and bun run check passes (types plus 843 tests); the new tests fail without the change.

Where it lives. The work is done by snipe in src/client/bots.ts. The issue put it in scriptedFight, but the game doesn't use that function: every game bot runs the learned policy, and scriptedFight is only the arena baseline, whose duels only ever hand out an AK or an M4. So snipe runs after whichever fight made the decision, learned or scripted. It only reads and trims the input the bot sends, in the same way the reaction-time gate already overrides the policy. The policy has no scope action and its inputs are unchanged. The spread value it already sees now reads the scoped spread whenever the bot is zoomed, so the shipped weights mean what they were trained on. world.ts is unchanged.

The rule, as named constants:

  • Past SCOPE_RANGE (500 units) the bot asks for the scope. It only comes back out inside UNSCOPE_RANGE (350). The gap means an enemy stepping back and forth across one line can't flip the scope every tick against the bolt gate in applyInput.
  • While zoomed, the bot's forward and side are each held to SCOPED_MOVE (±0.15), and it doesn't fire until it is moving slower than STEADY_SPEED (60 u/s).
  • Below SCOPE_SKILL (0.3), which covers most of Easy, a bot never scopes.
  • A terrorist bot that is out of clock and running for the site doesn't scope, since scoping would slow it to a walk.

Why 500. Throwaway duels down one long open line: a skill-0.8 AWP bot against a skill-0.8 AK bot, 80–250 duels per range, scoping on vs off. Scoping never lost.

  • Scripted fight: level at 500, then +14 points at 700 and +40 at 1000.
  • Learned fight: level from 450 to 1100, then +14 at 1400. At those middle ranges the bot's own aim error of about 1.6° decides the duel more than the gun's spread does.

Round split, before → after. These were measured before merging main, which has since added bots topping up their magazines (plant rate 67% → 81% in that pull's own measurement).

  • scripts/duel.ts: shipped 170/32/38 (71% won) and scripted 94/76/70 (39% won), identical before and after, because the arena has no AWPs.
  • scripts/measure.ts, 12 matches: T 62% → 66%. That is past #154's ±2 points, but it is 90 rounds with 3 of them flipping.
  • 40 matches on seed 7: T 63% (180/284) → T 63% (186/293). The split held.

Tests:

  • In bots.test.ts, for both the learned and the scripted bot: it asks for the scope at 1200 units but not at 200; a rifle is never scoped and an easy bot doesn't scope; the decision holds steady between the two ranges; a zoomed bot's side stays within ±0.15; and the one shot it takes over three seconds of ticks through applyInput is under 0.15°.
  • readme.test.ts checks that the README quotes the constants the code actually holds.

Merge with main. The conflicts were two independent additions in the same spot, so both sides are kept: in the README the sniper paragraph now sits before the top-up paragraph, and in bots.test.ts the sniping tests sit before the reloading tests.

Not done: the scope doesn't narrow what a bot can see. Bots don't have a field of view, so the one cost of scoping for a bot is the walk.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs