Thursday War ← Back to the editor

Write a Tactic

You command the four escorts by writing one JavaScript function. This page walks through Time to Impact, the tactic that holds the standoff raid, so you can read it, change a line and write your own. No prior JavaScript needed, the whole thing is one short function.

The idea

A tactic is a single function called plan. The simulator runs the engagement and calls your plan repeatedly, handing you the current picture each time. You return where each escort should go. That is the whole job: given what you can see right now, decide where the escorts should be.

You do not control firing. Each escort fires automatically at the nearest threat in range. Your only lever is positioning. A good tactic is one that puts the right escort in the right place at the right moment.

What you are given, what you return

Every tactic in the editor opens with this brief. It is the contract between your code and the simulator.

// BLUE C2 — command your interceptors.
// Implement: function plan(state) { ...; return orders }
//   state.hvu            { x, y, integrity }
//   state.interceptors[] { id, x, y, ready }   // ready=false while reloading
//   state.threats[]      { id, x, y }
//   state.bounds         { w, h }
// Return one waypoint per interceptor: [{ id, x, y }, ...]
// Interceptors auto-fire at the nearest threat in range.
// Optional: declare a PARAMS object to expose live sliders, then read them via plan(state, p).

So state is the picture: the ship (state.hvu), your escorts (state.interceptors, each with a ready flag that is false while it reloads), the inbound raid (state.threats) and the size of the arena (state.bounds). You return an array with one entry per escort, matched by id, each giving a point to steer toward.

Warm-up: the simplest tactic

Before Time to Impact, here is the shortest useful tactic. Send every escort at its nearest threat.

function plan(state){
  const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
  return state.interceptors.map(it => {
    let best = null, bd = Infinity;
    for (const t of state.threats) { const dd = d(it, t); if (dd < bd) { bd = dd; best = t; } }
    return { id: it.id, x: best ? best.x : state.hvu.x, y: best ? best.y : state.hvu.y };
  });
}

Read it from the inside out. d is a little distance helper. For each escort, the loop scans every threat and keeps the closest one. The escort is then ordered to that threat's position, or to the ship if the sky is clear. This is greedy targeting, and it teaches the shape of every tactic: map over the escorts, return one waypoint each. It also has a famous weakness. The escorts chase threats so far that they leave the ship open, which is why a leash on how far they may roam turns it from useless into effective.

Time to Impact, line by line

Here is the tactic that holds the standoff raid in full.

function plan(state){
  const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y), hvu = state.hvu;
  // most urgent threat = closest to the HVU
  const ranked = [...state.threats].sort((a, b) => d(a, hvu) - d(b, hvu));
  const free = [...state.interceptors], orders = [];
  for (const t of ranked) {
    if (!free.length) break;
    let bi = 0, bd = Infinity;
    free.forEach((it, i) => { const dd = d(it, t); if (dd < bd) { bd = dd; bi = i; } });
    const it = free.splice(bi, 1)[0];
    orders.push({ id: it.id, x: t.x, y: t.y });
  }
  // spare interceptors escort the HVU
  free.forEach((it, i) => orders.push({ id: it.id, x: hvu.x + 40, y: hvu.y + (i ? 60 : -60) }));
  return orders;
}

1 · Rank the threats by who strikes soonest

This is the whole idea. Deal with the threat that will reach the ship first. Because every threat moves at the same speed and heads straight for the ship, the one nearest the ship is the one that strikes soonest. So sorting the threats by distance to the HVU gives us their order of urgency. That ordering is the time to impact.

Worth noticing: distance is standing in for time here, and it only works because all the threats share a speed and a heading. The moment that stops being true, you would compute time to impact properly. That is the first thing to improve, and it is in the challenges below.

2 · Give each threat the nearest free escort

Now walk down the ranked list, most urgent first. For each threat, find the closest escort that has not already been assigned, order it to intercept, and take it out of the pool with splice so no escort is given two jobs. The most urgent threats get first pick of the closest escort. Less urgent threats take what is left.

3 · Park the spares

If there are more escorts than threats, the leftovers tuck in just ahead of the ship as a last line. Simple, and good enough for now.

Why it beats standoff

A fixed screen holds a ring and waits. Once the enemy can strike from outside that ring, the screen is finished. Time to Impact never holds a ring. It sends escorts out to meet the soonest-striking threats wherever they are, so they are killed before they reach their firing range. It defends against the threats, not a place. That is the difference between losing the convoy and getting it through.

Adding a dial

If you want a live slider, declare a PARAMS object and the editor draws it for you. Your plan then takes a second argument, p, holding the current values.

const PARAMS = {
  radius: { label: 'Deployment range (units from HVU)', min: 20, max: 140, step: 2, default: 110 }
};
function plan(state, p){
  const R = p.radius;          // R is the live slider value
  // ...use R to position the escorts...
}

That is how the surround screen exposes its deployment range. Anything you put in PARAMS becomes a control you can turn while the engagement runs.

Now make it better

Time to Impact is deliberately simple. Every shortcut in it is a door to a better tactic.

Those are exactly the questions that get hard in the next scenario. Which is the point.

Open the editor →

Choose Day 3, switch to Source, and you will find this exact code waiting. Change it, run it, see what happens.