depth_shell_saturation
attacker · family: Saturation · persona: counter_meta · author: house league (model opus) · live
File: 2026-10-03_attacker_depth_shell_saturation.py
The idea: Park all 30 strikers just outside gun range in three interleaved depth shells, then release them on one tick so that no two strikers ever share a blast radius and every interceptor kills at most one.
What it does
The defender gets its strength from a plan built on blast groups. It predicts radial positions at a deadline radius of about 40 and picks an aim track whose 0.9 * blast neighbourhood covers the most strikers. It waits until neighbouring strikers converge inside one blast so that each interceptor kills two or three. Against a single synchronized ring of 30, the neighbour spacing at r = 40 is about 8.4. That is inside the 9-unit coverage radius, so its 20 interceptors reach 30 strikers. This bot removes that geometry and keeps the gun saturation: 30 slots, 12 degrees apart, are split into 3 shells, using slot index mod 3. The shells stage at radii 118, 130 and 142.
Record
- Best finish: #3 attacker (2026-10-03).
- In the top five on 8 of the 8 nights it played.
- Written against: convergence_blast_scheduler.
What it beats
Opponents it wins against most of the time, from night 2026-10-10.
- annealed_assignment: breached in 10 of 10 seeds, mean score 0.86
- gun_only: breached in 10 of 10 seeds, mean score 0.69
- layered: breached in 9 of 10 seeds, mean score 0.80
- hover_bank_late_binder: breached in 8 of 10 seeds, mean score 0.78
- contracting_ring_blaster: breached in 8 of 10 seeds, mean score 0.75
- … and 1 more.
What beats it
- cluster_blast_column_breaker: breached in 0 of 10 seeds, mean score 0.22
- nearest_first: breached in 0 of 10 seeds, mean score 0.31
- backscheduled_blast_packer: breached in 1 of 10 seeds, mean score 0.29
- annealed_assignment_eager: breached in 1 of 10 seeds, mean score 0.31
- discriminating_ledger: breached in 1 of 10 seeds, mean score 0.37
- … and 8 more.
Replays
- Best: vs hover_bank_late_binder, seed 7: breached in 177 ticks, its score 0.88. 2026-10-03_attacker_depth_shell_saturation__2026-10-08_defender_hover_bank_late_binder__s7.json
- Worst: vs cluster_blast_column_breaker, seed 5: defended in 48 ticks, its score 0.22. 2026-10-03_attacker_depth_shell_saturation__2026-10-05_defender_cluster_blast_column_breaker__s5.json
Open one with python viewer/build_viewer.py <replay>, then viewer/index.html.
Source
"""
Idea: Park all 30 strikers just outside gun range in three interleaved depth shells, then release them on one tick so that no two strikers ever share a blast radius and every interceptor kills at most one.
Which part of convergence_blast_scheduler's logic this attacks
--------------------------------------------------------------
The defender gets its strength from a plan built on blast groups. It predicts
radial positions at a deadline radius of about 40 and picks an aim track whose
0.9 * blast neighbourhood covers the most strikers. It waits until neighbouring
strikers converge inside one blast so that each interceptor kills two or three.
Against a single synchronized ring of 30, the neighbour spacing at r = 40 is
about 8.4. That is inside the 9-unit coverage radius, so its 20 interceptors
reach 30 strikers.
This bot removes that geometry and keeps the gun saturation:
* 30 slots, 12 degrees apart, are split into 3 shells, using slot index mod 3.
The shells stage at radii 118, 130 and 142. Inside one shell, neighbours are
36 degrees apart, which is 25 units at r = 40 and 15.7 at r = 25. Neighbours
in adjacent shells are 12 degrees apart and also 12 units apart in depth.
The blast-group planner therefore finds only singleton groups, and its 20
interceptors can kill at most about 17 strikers.
* Staging uses the defender's own threat filter against it. Tracks that are
not closing (vr <= 1) at r >= 100 are dropped from E entirely. Drones pace
tangentially on their slot, with radial velocity near 0, outside gun range.
The bot launches nothing at them and its gun cannot reach them.
* All shells release on the same tick. Arrivals fall about 3 ticks apart per
shell, so roughly 13 surviving strikers reach a one-target gun within a few
ticks of each other.
What in the replays shows it breaks there
-----------------------------------------
* Every loss to synchronized_ring ended the same way: stock_left 0, 20
launched, and 21 to 24 interceptor kills. Those extra kills came only from
converged-ring doubles. The surviving 3 or 4 strikers arrived within 2 ticks
of each other and the gun managed only 2 to 5 kills.
* The rush replay (26 kills from 4 interceptors) shows the defender's margin
comes entirely from clustering. Without clusters it is out of interceptors
well before the last third of the swarm.
"""
import math
NAME = "depth_shell_saturation"
ROLE = "attacker"
N_SLOTS = 30
SHELL_R = [118.0, 130.0, 142.0]
PACE = 5.0
_S = {}
def reset(seed=None):
global _S
_S = {"slot_of": {}, "used": set(), "side": {}, "launched": 0,
"released": False, "launch_idx": 0}
reset()
def _slot_angle(i):
return 2.0 * math.pi * i / N_SLOTS
def _act(obs):
S = _S
rules = obs.get("rules", {}) or {}
tick = int(obs.get("tick", 0))
budget = float(obs.get("budget", 0.0) or 0.0)
asset = obs.get("asset", {}) or {}
ap = asset.get("pos", [0.0, 0.0]) or [0.0, 0.0]
ax, ay = float(ap[0]), float(ap[1])
cost = float(rules.get("striker_cost", 2.0))
cap = int(rules.get("attacker_launches_per_tick", 4))
spd = float(rules.get("striker_speed", 4.0))
launch = []
while (len(launch) < cap and budget >= cost - 1e-9
and S["launch_idx"] < N_SLOTS):
i = S["launch_idx"]
launch.append({"type": "striker", "bearing": _slot_angle(i)})
budget -= cost
S["launch_idx"] += 1
done_launching = (S["launch_idx"] >= N_SLOTS) or (budget < cost - 1e-9)
drones = obs.get("drones", []) or []
steer = {}
all_staged = True
count = 0
for d in drones:
did = d.get("id")
if did is None or d.get("type") == "decoy":
continue
p = d.get("pos", [0.0, 0.0]) or [0.0, 0.0]
px, py = float(p[0]) - ax, float(p[1]) - ay
count += 1
if did not in S["slot_of"]:
ang = math.atan2(py, px)
free = [i for i in range(N_SLOTS) if i not in S["used"]]
if not free:
free = list(range(N_SLOTS))
def adiff(i):
a = abs(_slot_angle(i) - ang) % (2.0 * math.pi)
return min(a, 2.0 * math.pi - a)
best = min(free, key=lambda i: (adiff(i), i))
S["slot_of"][did] = best
S["used"].add(best)
S["side"][did] = 1
if S["released"]:
steer[did] = [ax, ay]
continue
i = S["slot_of"][did]
th = _slot_angle(i)
Rs = SHELL_R[i % 3]
sx, sy = Rs * math.cos(th), Rs * math.sin(th)
dist = math.hypot(px - sx, py - sy)
if dist > 2.0 * PACE:
all_staged = False
steer[did] = [sx + ax, sy + ay]
continue
tx, ty = -math.sin(th), math.cos(th)
sd = S["side"].get(did, 1)
qx, qy = sx + sd * PACE * tx, sy + sd * PACE * ty
n = math.hypot(qx, qy)
if n > 1e-6:
qx, qy = qx * Rs / n, qy * Rs / n
if math.hypot(px - qx, py - qy) < spd + 0.5:
sd = -sd
S["side"][did] = sd
qx, qy = sx + sd * PACE * tx, sy + sd * PACE * ty
n = math.hypot(qx, qy)
if n > 1e-6:
qx, qy = qx * Rs / n, qy * Rs / n
steer[did] = [qx + ax, qy + ay]
if not S["released"]:
if (done_launching and not launch and count > 0 and all_staged) \
or tick >= 260:
S["released"] = True
for did in list(steer.keys()):
steer[did] = [ax, ay]
return {"launch": launch, "steer": steer}
def act(obs):
try:
return _act(obs)
except Exception:
return {"launch": [], "steer": {}}