layered
defender · family: Budgeted defence · persona: hand-written baseline · author: Pelennor · live
File: layered.py
The idea: Prioritise by time to impact, keep interceptors back for confirmed strikers, and let the gun take whatever leaks through.
What it does
Layered: prioritise by time to impact, keep interceptors back for confirmed strikers, and let the gun take whatever leaks through. Tests whether a defender should think in time rather than distance, and whether holding magazine depth beats engaging everything early. Unknown tracks are engaged only when they are close enough that waiting for identification would cost the intercept, and the reserve shrinks as the match runs out of time for new threats to matter.
Record
- Best finish: #3 defender (2026-09-28).
- In the top five on 2 of the 13 nights it played.
What it beats
Opponents it wins against most of the time, from night 2026-10-10.
- rush: held in 10 of 10 seeds, mean score 0.81
- decoy_screen: held in 10 of 10 seeds, mean score 0.80
- attacker_blast_isolated_synchronized_rel: held in 10 of 10 seeds, mean score 0.79
- shadow_pair_ring: held in 10 of 10 seeds, mean score 0.77
- trickle: held in 8 of 10 seeds, mean score 0.43
What beats it
- parked_ring_magazine_drain: held in 0 of 10 seeds, mean score 0.12
- phase_locked_ring: held in 0 of 10 seeds, mean score 0.13
- staggered_screen_trickle: held in 0 of 10 seeds, mean score 0.13
- synchronized_ring: held in 0 of 10 seeds, mean score 0.14
- interleaved_shell_ring: held in 0 of 10 seeds, mean score 0.14
- … and 4 more.
Replays
- Best: vs decoy_screen, seed 6: defended in 102 ticks, its score 0.81. decoy_screen__layered__s6.json
- Worst: vs parked_ring_magazine_drain, seed 5: breached in 144 ticks, its score 0.09. 2026-10-06_attacker_parked_ring_magazine_drain__layered__s5.json
Open one with python viewer/build_viewer.py <replay>, then viewer/index.html.
Source
"""Layered: prioritise by time to impact, keep interceptors back for confirmed strikers,
and let the gun take whatever leaks through.
Tests whether a defender should think in time rather than distance, and whether holding
magazine depth beats engaging everything early. Unknown tracks are engaged only when they
are close enough that waiting for identification would cost the intercept, and the reserve
shrinks as the match runs out of time for new threats to matter.
"""
NAME = "layered"
ROLE = "defender"
def _time_to_impact(track, rules):
speed = max(rules["striker_speed"], 1e-9)
return max(0.0, (track["range"] - rules["asset_radius"]) / speed)
def act(obs):
rules = obs["rules"]
tracks = obs["tracks"]
by_id = {t["id"]: t for t in tracks}
engaged = {i["target"] for i in obs["interceptors"] if i["target"] in by_id}
ranked = sorted(tracks, key=lambda t: _time_to_impact(t, rules))
confirmed = [t for t in ranked if t["cls"] == "striker"]
unknown_close = [t for t in ranked
if t["cls"] == "unknown" and _time_to_impact(t, rules) < 45.0]
retarget, claimed = {}, set(engaged)
free = [t for t in confirmed if t["id"] not in claimed]
for i in obs["interceptors"]:
if i["target"] not in by_id and free:
t = free.pop(0)
retarget[i["id"]] = t["id"]
claimed.add(t["id"])
# Hold some stock back early: a magazine emptied on the first wave cannot answer the
# second. The reserve is released as the match runs out of time.
ticks_left = max(0, rules["max_ticks"] - obs["tick"])
reserve = 6 if ticks_left > 300 else (3 if ticks_left > 120 else 0)
allowed = max(0, obs["stock"] - reserve)
queue = [t for t in confirmed if t["id"] not in claimed]
queue += [t for t in unknown_close if t["id"] not in claimed]
intercept = [t["id"] for t in queue][:int(rules["interceptor_launches_per_tick"])]
intercept = intercept[:allowed]
gun_range = rules["gun_range"]
leakers = [t for t in ranked if t["range"] <= gun_range and t["cls"] != "decoy"]
if not leakers:
leakers = [t for t in ranked if t["range"] <= gun_range]
return {"intercept": intercept, "retarget": retarget,
"gun": leakers[0]["id"] if leakers else None}