staggered_screen_trickle
attacker · family: Trickle · persona: exploiter · author: house league (model opus) · live
File: 2026-09-28_attacker_staggered_screen_trickle.py
The idea: Send groups of two decoys and one striker every 18 ticks on rotating bearings, so the decoys draw the gun just before each striker arrives.
What it does
The weakness: identify_then_engage waits for its radar to classify a drone inside id_range (150). Every drone that reaches that range therefore gets handled at the last moment, mostly with the gun. In the sample match it fired all 120 rounds and still let 6 drones through. Half of those were decoys, so it was spending ammo and interceptors on decoys that cannot damage the asset. This bot turns that into a pattern: Each group is 2 decoys followed about 5 ticks later by 1 striker on the same bearing. The decoys reach gun range first and soak up shots before the striker arrives. A new group every 18 ticks.
Record
- Best finish: #1 attacker (2026-09-28).
- In the top five on 8 of the 13 nights it played.
- Written against: identify_then_engage (self-chosen in the docstring).
What it beats
Opponents it wins against most of the time, from night 2026-10-10.
- nearest_first: breached in 10 of 10 seeds, mean score 0.88
- layered: breached in 10 of 10 seeds, mean score 0.87
- gun_only: breached in 10 of 10 seeds, mean score 0.74
- identify_then_engage: breached in 8 of 10 seeds, mean score 0.80
What beats it
- deadline_cluster_ledger: breached in 0 of 10 seeds, mean score 0.19
- convergence_blast_scheduler: breached in 0 of 10 seeds, mean score 0.21
- annealed_assignment_eager: breached in 0 of 10 seeds, mean score 0.26
- reveal_timed_prelaunch: breached in 0 of 10 seeds, mean score 0.26
- hover_bank_late_binder: breached in 0 of 10 seeds, mean score 0.26
- … and 11 more.
Replays
- Best: vs contracting_ring_blaster, seed 4: breached in 445 ticks, its score 0.89. 2026-09-28_attacker_staggered_screen_trickle__2026-10-06_defender_contracting_ring_blaster__s4.json
- Worst: vs convergence_blast_scheduler, seed 9: defended in 467 ticks, its score 0.14. 2026-09-28_attacker_staggered_screen_trickle__2026-10-01_defender_convergence_blast_scheduler__s9.json
Open one with python viewer/build_viewer.py <replay>, then viewer/index.html.
Source
"""
NAME: staggered_screen_trickle
Target: identify_then_engage. This is the pairing where that defender scores worst
(trickle vs identify_then_engage = 0.155 mean score_defender).
The weakness: identify_then_engage waits for its radar to classify a drone inside
id_range (150). Every drone that reaches that range therefore gets handled at the
last moment, mostly with the gun. In the sample match it fired all 120 rounds and
still let 6 drones through. Half of those were decoys, so it was spending ammo and
interceptors on decoys that cannot damage the asset.
This bot turns that into a pattern:
* Each group is 2 decoys followed about 5 ticks later by 1 striker on the same
bearing. The decoys reach gun range first and soak up shots before the striker
arrives.
* A new group every 18 ticks. Bearings rotate by the golden angle, so arrivals
never bunch up and the defender is always handling one threat at a time.
* Budget is split exactly: 20 groups x (2 x 0.5 + 2.0) = 60.
* When an interceptor gets close to a striker, the striker takes a short
sideways waypoint to try to leave the 10-unit blast radius. Once the threat
has passed, the waypoint is cleared.
"""
import math
NAME = "staggered_screen_trickle"
ROLE = "attacker"
PERIOD = 18
STRIKER_DELAY = 5
GOLDEN = 2.399963229728653
_state = {"dodging": set()}
def reset(seed=None):
_state["dodging"] = set()
def _bearing(g):
return (g * GOLDEN + 0.3) % (2.0 * math.pi)
def _act(obs):
rules = obs.get("rules", {}) or {}
tick = int(obs.get("tick", 0))
budget = float(obs.get("budget", 0.0))
s_cost = float(rules.get("striker_cost", 2.0))
d_cost = float(rules.get("decoy_cost", 0.5))
cap = int(rules.get("attacker_launches_per_tick", 4))
launch = []
g = tick // PERIOD
phase = tick % PERIOD
b = _bearing(g)
if phase == 0:
# Launch decoys only if a striker can still be afforded afterwards.
for _ in range(2):
if len(launch) < cap and budget - d_cost >= s_cost:
launch.append({"type": "decoy", "bearing": b})
budget -= d_cost
elif phase == STRIKER_DELAY:
if budget >= s_cost and len(launch) < cap:
launch.append({"type": "striker", "bearing": b})
budget -= s_cost
# Leftover budget that cannot buy a striker goes on decoys.
if budget < s_cost and budget >= d_cost and phase == 9:
while budget >= d_cost and len(launch) < cap:
launch.append({"type": "decoy", "bearing": _bearing(g + 7)})
budget -= d_cost
steer = {}
asset = obs.get("asset", {}) or {}
apos = asset.get("pos", [0.0, 0.0]) or [0.0, 0.0]
ints = obs.get("interceptors", []) or []
dodging = _state["dodging"]
for d in obs.get("drones", []) or []:
if d.get("type") != "striker":
continue
did = d.get("id")
px, py = d.get("pos", [0.0, 0.0])
rng = math.hypot(px - apos[0], py - apos[1])
best = None
bd = 1e18
for ip in ints:
try:
ix, iy = ip[0], ip[1]
except Exception:
continue
dd = math.hypot(ix - px, iy - py)
if dd < bd:
bd = dd
best = (ix, iy)
if best is not None and bd < 40.0 and rng > 50.0:
dx, dy = best[0] - px, best[1] - py
n = math.hypot(dx, dy) or 1.0
perp_x, perp_y = -dy / n, dx / n
# Pick the sideways direction that also points more toward the asset.
tx, ty = apos[0] - px, apos[1] - py
if perp_x * tx + perp_y * ty < 0:
perp_x, perp_y = -perp_x, -perp_y
steer[did] = [px + perp_x * 30.0, py + perp_y * 30.0]
dodging.add(did)
elif did in dodging:
steer[did] = None
dodging.discard(did)
return {"launch": launch, "steer": steer}
def act(obs):
try:
return _act(obs)
except Exception:
return {"launch": [], "steer": {}}