-- Gravity Force (Kingsoft 1989), DOGFIGHT: a pilot for either seat.
--
-- Read from the game's own RAM (playbooks/gravityforce/battle.uss, measured
-- with probe bots in solo runs; units are thousandths of a pixel):
--   player records  0x37f30 (port 1, top view) and 0x37fd6 (port 0), 0xa6 apart:
--     +0x00 long  this player's view pointer into the cave bitmap
--     +0x14 long  x        +0x18 long  y   (y grows downwards; the ship's
--                                            centre is about (x+7, y+7) px)
--     +0x1c long  start x  +0x20 long  start y (its pad)
--     +0x28 word  heading, 0..71 in 5-degree steps clockwise from "right":
--                 54 is straight up
--   fuel            0x3b044 (port 1), 0x3b045 (port 0), 0..99
--   bullets         50 slots of 0x14 from 0x34c6a: +0 word frames to live
--                   (150 when fired), +2 x, +6 y, +0xa vx, +0xe vy (longs),
--                   +0x12 word: 0 a gunship's, 1 a player's (either:
--                   whose is told by where a new bullet appears)
--   gunships        records of 0x20 from 0x378f6 while the word at +0 is 4:
--                   +4 long x, +8 long y (pixels: the image is 46 x 29
--                   from x+2, y+1; touching it is within ~30 across and
--                   ~22 up or down of its centre). They patrol between y 264 and 336 and shoot at
--                   whoever comes near; their images are blitted into the
--                   bitmap, so the map holds their starting pictures
--   the cave        one 336 x 801 bitmap, 5 planes one after another from
--                   0x56000, 42 bytes a row, 33642 bytes a plane; world y is
--                   its row. Any set bit is rock (the ships are sprites).
-- Controls: LEFT/RIGHT turn one step a frame, FIRE thrusts (about 50 a
-- frame along the heading), UP shoots. Gravity is about 10.5 a frame
-- downwards and drag takes about 1% of the speed a frame.
--
-- The pilot: build a map of the cave, keep a distance field to the enemy
-- on 8x8 cells, fly along it with a velocity controller that keeps the
-- nose near up, and when the enemy is in sight and in range, turn to the
-- intercept angle and shoot. Rock ahead on the coasting path, or fuel low,
-- takes over from all of that.

local REC = { [1] = 0x37f30, [0] = 0x37fd6 }
local FUEL = { [1] = 0x3b044, [0] = 0x3b045 }
local BULLETS, BULLET_SLOTS, BULLET_SIZE = 0x34c6a, 50, 0x14
local slot_was, slot_mine = {}, {}         -- per bullet slot: last life, fired by us
local MAP, ROW, PLANE, ROWS, COLS = 0x56000, 42, 33642, 801, 42
local CELL = 8
local NCR = math.ceil(ROWS / CELL)        -- cell rows

local THRUST, GRAVITY, DRAG = 50, 10.5, 0.0105
local MUZZLE = 2500                        -- bullet speed relative to the ship
local SQRT = math.sqrt
local floor, abs, min, max = math.floor, math.abs, math.min, math.max
local band, rshift = function(a, b) return a & b end, function(a, b) return a >> b end
local function s32(v) if v >= 0x80000000 then return v - 0x100000000 end return v end
local GUNSHIPS, GUNSHIP_SIZE = 0x378f6, 0x20
local GUNSHIP_REACH = 85                   -- px from a gunship's centre
local function gunships()
  local out = {}
  for i = 0, 5 do
    local a = GUNSHIPS + i * GUNSHIP_SIZE
    if ram.u16(a) ~= 4 then break end
    out[#out + 1] = { s32(ram.u32(a + 4)) + 25, s32(ram.u32(a + 8)) + 15 }
  end
  return out
end
local turrets = {}                         -- gunships when the field was made
-- What the gunships cost a route, per cell, built up as they are seen:
-- their patrol lanes end to end and every cell one has been
-- in, with the cells around it (they turn between lanes at the ends).
local swept, lanes, extra = {}, {}, {}
local function remember(ships)
  local new = false
  for _, g in ipairs(ships) do
    local c = max(0, min(41, floor(g[1] / 8)))
    local r = max(0, min(100, floor(g[2] / 8)))
    local k = r * 42 + c
    if not swept[k] then
      swept[k] = true; new = true
      for dr = -3, 3 do for dc = -3, 3 do
        local r2, c2 = r + dr, c + dc
        if r2 >= 0 and r2 <= 100 and c2 >= 0 and c2 < 42 then
          local k2 = r2 * 42 + c2
          extra[k2] = min(60, (extra[k2] or 0) + 6)
        end
      end end
    end
    local ly = floor(g[2] / 4 + 0.5) * 4
    if not lanes[ly] then
      lanes[ly] = true; new = true
      -- a cell whose centre is within 22 of the lane's: a ship there
      -- touches a gunship passing; one row further is a gap it can use
      for r2 = max(0, floor((ly - 26) / 8)), min(100, floor((ly + 26) / 8)) do
        if abs(r2 * 8 + 4 - ly) < 25 then
          for c2 = 0, 41 do extra[r2 * 42 + c2] = (extra[r2 * 42 + c2] or 0) + 200 end
        end
      end
    end
  end
  return new
end

local port, me, them, fuel_at
local mask = {}                            -- mask[row] = string of 42 bytes, rock bits
local built = 0                            -- rows built so far
local solid = {}                           -- solid[cr*COLS + c] = true for rock cells
local near = {}                            -- near[cell] = rock within one cell
local clear = {}                           -- clear[cell] = cells to the nearest rock
local dist = {}                            -- distance field to the current goal
local goal_cell, field_age = -1, 0
local prev = {}                            -- last positions, for velocities
local stats = { deaths = 0, shots = 0, kills = 0 }
local shoot_cool = 0
local last_pos


local function rock(px, py)
  py = floor(py); px = floor(px)
  if py < 0 or py >= ROWS or px < 0 or px >= COLS * 8 then return true end
  local row = mask[py]
  if not row then return false end
  local b = row:byte(rshift(px, 3) + 1)
  return band(b, rshift(0x80, px & 7)) ~= 0
end

-- The cave map, a few dozen rows a frame: rows of all five planes OR'd.
local function build_some(rows)
  local stop = min(ROWS, built + rows)
  for y = built, stop - 1 do
    local acc = {}
    for i = 1, ROW do acc[i] = 0 end
    for p = 0, 4 do
      local s = ram.bytes(MAP + p * PLANE + y * ROW, ROW)
      for i = 1, ROW do acc[i] = acc[i] | s:byte(i) end
    end
    mask[y] = string.char(table.unpack(acc))
    local cr = y // CELL
    for c = 0, COLS - 1 do
      if acc[c + 1] ~= 0 then solid[cr * COLS + c] = true end
    end
  end
  built = stop
end

-- Once the rows are in: which cells are near rock, then every cell's
-- distance to rock (a breadth-first pass from all the rock at once), a
-- slice a frame, so each frame fits in the arena's cost cap.
local NEAR_ROWS, CLEAR_STEPS = 20, 900
local finished, near_row, cq, chead = false, 0, nil, 1
local function finish_some()
  if near_row < NCR then
    for cr = near_row, min(NCR, near_row + NEAR_ROWS) - 1 do
      for c = 0, COLS - 1 do
        local k = cr * COLS + c
        local n = solid[k]
        for dr = -1, 1 do for dc = -1, 1 do
          local r2, c2 = cr + dr, c + dc
          if r2 < 0 or r2 >= NCR or c2 < 0 or c2 >= COLS or solid[r2 * COLS + c2] then n = true end
        end end
        near[k] = n
      end
    end
    near_row = min(NCR, near_row + NEAR_ROWS)
    return
  end
  if not cq then
    cq = {}
    for k = 0, NCR * COLS - 1 do
      if solid[k] then clear[k] = 0; cq[#cq + 1] = k end
    end
    return
  end
  local stop = chead + CLEAR_STEPS
  while chead <= #cq and chead < stop do
    local k = cq[chead]; chead = chead + 1
    local r, c = k // COLS, k % COLS
    for dr = -1, 1 do for dc = -1, 1 do
      local r2, c2 = r + dr, c + dc
      if r2 >= 0 and r2 < NCR and c2 >= 0 and c2 < COLS then
        local k2 = r2 * COLS + c2
        if clear[k2] == nil then clear[k2] = clear[k] + 1; cq[#cq + 1] = k2 end
      end
    end end
  end
  if chead > #cq then finished = true; cq = nil end
end

local function cell_of(px, py)
  local c = max(0, min(COLS - 1, floor(px / CELL)))
  local r = max(0, min(NCR - 1, floor(py / CELL)))
  return r * COLS + c
end

-- Breadth-first distances from the goal over cells clear of rock by a cell
-- (the ship is two cells across); cells that are merely free are allowed
-- at a cost, so a tight passage is still a way through.
local function field(goal)
  local d = {}
  local queue, head = { goal }, 1
  d[goal] = 0
  while head <= #queue do
    local k = queue[head]; head = head + 1
    local r, c = k // COLS, k % COLS
    local base = d[k]
    for i = 1, 4 do
      local r2, c2 = r, c
      if i == 1 then r2 = r - 1 elseif i == 2 then r2 = r + 1 elseif i == 3 then c2 = c - 1 else c2 = c + 1 end
      if r2 >= 0 and r2 < NCR and c2 >= 0 and c2 < COLS then
        local k2 = r2 * COLS + c2
        if not solid[k2] and d[k2] == nil then
          local cl = clear[k2] or 9
          local cost = cl <= 1 and 20 or cl == 2 and 8 or cl == 3 and 3 or cl == 4 and 1.5 or 1
          local cx, cy = c2 * CELL + 4, r2 * CELL + 4
          for _, t in ipairs(turrets) do
            local tx, ty = cx - t[1], cy - t[2]
            if tx * tx + ty * ty < GUNSHIP_REACH * GUNSHIP_REACH then cost = cost + 25 end
          end
          cost = cost + (extra[k2] or 0)
          d[k2] = base + cost
          queue[#queue + 1] = k2
        end
      end
    end
  end
  return d
end

local function thin_line(x1, y1, x2, y2)
  local dx, dy = x2 - x1, y2 - y1
  local n = floor(SQRT(dx * dx + dy * dy) / 3)
  for i = 1, n do
    local t = i / (n + 1)
    if rock(x1 + dx * t, y1 + dy * t) then return false end
  end
  return true
end

-- A line the ship itself can fly: the centre line and two at its sides.
local function clear_line(x1, y1, x2, y2)
  local dx, dy = x2 - x1, y2 - y1
  local l = SQRT(dx * dx + dy * dy)
  if l < 1 then return true end
  local ox, oy = -dy / l * 7, dx / l * 7
  return thin_line(x1, y1, x2, y2) and thin_line(x1 + ox, y1 + oy, x2 + ox, y2 + oy)
     and thin_line(x1 - ox, y1 - oy, x2 - ox, y2 - oy)
end

local function ship(rec)
  return s32(ram.u32(rec + 0x14)) / 1000 + 7, s32(ram.u32(rec + 0x18)) / 1000 + 7, ram.u16(rec + 0x28) % 72
end

-- Does the ship, coasting from here, meet rock within `frames`?
local function coast_hits(x, y, vx, vy, frames)
  for t = 1, frames do
    vy = vy + GRAVITY / 1000; vx = vx * (1 - DRAG); vy = vy * (1 - DRAG)
    x = x + vx; y = y + vy
    if rock(x - 5, y - 5) or rock(x + 5, y - 5) or rock(x - 6, y + 6) or rock(x + 6, y + 6) or rock(x, y + 7) or rock(x, y - 7) or rock(x - 7, y) or rock(x + 7, y) then
      return t
    end
  end
  return nil
end

local function angle_err(h, want)
  local d = (want - h) % 72
  if d > 36 then d = 72 - d end
  return d
end

local DIRX, DIRY = {}, {}
for i = 0, 71 do DIRX[i] = math.cos(i * math.pi / 36); DIRY[i] = math.sin(i * math.pi / 36) end

-- Frames until rock, thrusting along `heading` for `burn` frames (none for
-- a nil heading) and coasting after, over `horizon` frames.
local function clear_for(x, y, vx, vy, heading, burn, horizon)
  for t = 1, horizon do
    if heading and t <= burn then
      vx = vx + THRUST / 1000 * DIRX[heading]; vy = vy + THRUST / 1000 * DIRY[heading]
    end
    vy = vy + GRAVITY / 1000; vx = vx * (1 - DRAG); vy = vy * (1 - DRAG)
    x = x + vx; y = y + vy
    if rock(x - 5, y - 5) or rock(x + 5, y - 5) or rock(x - 6, y + 6) or rock(x + 6, y + 6) or rock(x, y + 7) or rock(x, y - 7) or rock(x - 7, y) or rock(x + 7, y) then
      return t
    end
  end
  return horizon + 1
end

-- The way out when rock is coming: of the headings within reach, the one
-- whose burn keeps the ship clear longest (turning costs a frame a step,
-- so far-off headings start late).
local function escape(x, y, vx, vy, h)
  local best, bt = nil, clear_for(x, y, vx, vy, nil, 0, 40)
  for off = -24, 24, 3 do
    local hd = (h + off) % 72
    local late = abs(off)
    -- coast while turning, then burn
    local cx, cy, cvx, cvy = x, y, vx, vy
    local crashed = false
    for t = 1, late do
      cvy = cvy + GRAVITY / 1000; cvx = cvx * (1 - DRAG); cvy = cvy * (1 - DRAG)
      cx = cx + cvx; cy = cy + cvy
      if rock(cx, cy) then crashed = true; break end
    end
    if not crashed then
      local t = late + clear_for(cx, cy, cvx, cvy, hd, 12, 40 - late)
      -- prefer near-upright headings on ties: they can hover after
      if t > bt or (t == bt and best and angle_err(hd, 54) < angle_err(best, 54)) then best, bt = hd, t end
    end
  end
  return best, bt
end

local function heading_to(ax, ay)       -- angle of a vector as a heading index
  local a = math.atan(ay, ax)            -- y down: clockwise
  local h = floor(a / (math.pi / 36) + 0.5) % 72
  return h
end

local function turn_toward(h, want)       -- bits to turn from h to want
  local d = (want - h) % 72
  if d == 0 then return 0 end
  if d <= 36 then return bits.RIGHT end
  return bits.LEFT
end


-- The velocity (px/frame) to fly at: along the distance field towards the
-- goal, through the farthest cell in sight, slower near rock and near it.
local function waypoint(x, y)
  local k = cell_of(x, y)
  local best, bd = k, dist[k] or 1e9
  -- walk downhill a few cells, keep the farthest one still in sight
  local cur = k
  local wx, wy = nil, nil
  for step = 1, 12 do
    local r, c = cur // COLS, cur % COLS
    local nk, nd = nil, dist[cur] or 1e9
    for dr = -1, 1 do for dc = -1, 1 do
      if dr ~= 0 or dc ~= 0 then
        local r2, c2 = r + dr, c + dc
        if r2 >= 0 and r2 < NCR and c2 >= 0 and c2 < COLS then
          local k2 = r2 * COLS + c2
          local d2 = dist[k2]
          if d2 and d2 < nd then nk, nd = k2, d2 end
        end
      end
    end end
    if not nk then break end
    cur = nk
    local cx, cy = (cur % COLS) * CELL + CELL / 2, (cur // COLS) * CELL + CELL / 2
    if clear_line(x, y, cx, cy) then wx, wy = cx, cy else break end
  end
  return wx, wy
end

-- A push away from rock nearby: along 16 rays, the closer the rock, the
-- harder, in px/frame.
local RAYS = {}
for i = 0, 15 do RAYS[i] = { math.cos(i * math.pi / 8), math.sin(i * math.pi / 8) } end
local function repulsion(x, y)
  local px, py = 0, 0
  for i = 0, 15 do
    local dx, dy = RAYS[i][1], RAYS[i][2]
    for r = 8, 26, 3 do
      if rock(x + dx * r, y + dy * r) then
        local f = (26 - r) / 18
        px = px - dx * f; py = py - dy * f
        break
      end
    end
  end
  -- the strongest single push, not the sum: a floor under the ship is many
  -- rays and still only one floor
  local m = SQRT(px * px + py * py)
  local cap = 0.7
  if m > cap then px, py = px / m * cap, py / m * cap end
  return px, py
end

-- Thrust and turn to make the velocity `want` (px/frame), keeping the nose
-- within reach of up unless told it may point anywhere.
local function fly(h, vx, vy, wvx, wvy, upright)
  local tau = 12
  local ax = (wvx - vx) / tau
  local ay = (wvy - vy) / tau - GRAVITY / 1000
  -- thrust is all there is to push with: a thrust vector, gravity cancelled
  local mag = SQRT(ax * ax + ay * ay)
  local want = heading_to(ax, ay)
  if upright then
    -- never further than 60 degrees from up: a pilot who is upside down
    -- when it has to climb falls into the rock first
    local off = (want - 54 + 36) % 72 - 36
    if off > 12 then want = (54 + 12) % 72 elseif off < -12 then want = (54 - 12) % 72 end
  end
  local b = turn_toward(h, want)
  -- thrust only where it helps: along the nose as it is now, the push must
  -- go the way the ship needs to accelerate (clamped upright, a ship that
  -- needs to go down mostly lets gravity do it)
  local along = DIRX[h] * ax + DIRY[h] * ay
  if angle_err(h, want) <= 6 and along > 0.01 then b = b | bits.FIRE end
  return b
end

-- Where to aim to meet the enemy, or nil when no shot can get there.
local function intercept(x, y, vx, vy, ex, ey, evx, evy)
  local dx, dy = ex - x, ey - y
  local wx, wy = evx - vx, evy - vy
  local s = MUZZLE / 1000
  local a = wx * wx + wy * wy - s * s
  local b = 2 * (dx * wx + dy * wy)
  local c = dx * dx + dy * dy
  local t
  if abs(a) < 1e-6 then t = -c / b else
    local disc = b * b - 4 * a * c
    if disc < 0 then return nil end
    local r = SQRT(disc)
    local t1, t2 = (-b - r) / (2 * a), (-b + r) / (2 * a)
    t = (t1 > 0 and t2 > 0) and min(t1, t2) or max(t1, t2)
  end
  if not t or t <= 0 or t > 120 then return nil end
  return heading_to(dx + wx * t, dy + wy * t), t
end

-- The nearest enemy bullet that will pass within a ship's width soon.
-- Which bullets are ours: a slot that comes alive next to our ship.
local function sort_bullets(x, y, ex, ey)
  for i = 0, BULLET_SLOTS - 1 do
    local a = BULLETS + i * BULLET_SIZE
    local life = ram.u16(a)
    if life > 0 and (slot_was[i] or 0) == 0 then
      local bx, by = s32(ram.u32(a + 2)) / 1000, s32(ram.u32(a + 6)) / 1000
      local dm = (bx - x) ^ 2 + (by - y) ^ 2
      local de = (bx - ex) ^ 2 + (by - ey) ^ 2
      slot_mine[i] = ram.u16(a + 0x12) ~= 0 and dm < de
    end
    slot_was[i] = life
  end
end

local function threat(x, y, vx, vy)
  for i = 0, BULLET_SLOTS - 1 do
    local a = BULLETS + i * BULLET_SIZE
    local life = ram.u16(a)
    if life > 0 and life <= 200 and not slot_mine[i] then
      local bx, by = s32(ram.u32(a + 2)) / 1000, s32(ram.u32(a + 6)) / 1000
      local bvx, bvy = s32(ram.u32(a + 0xa)) / 1000, s32(ram.u32(a + 0xe)) / 1000
      -- our own bullets start at our nose going our way: skip those close
      -- behind us that are leaving
      local rx, ry = bx - x, by - y
      local rvx, rvy = bvx - vx, bvy - vy
      local v2 = rvx * rvx + rvy * rvy
      if v2 > 0.01 then
        local t = -(rx * rvx + ry * rvy) / v2
        if t > 0 and t < 25 then
          local cx, cy = rx + rvx * t, ry + rvy * t
          if cx * cx + cy * cy < 11 * 11 then return rvx, rvy, t end
        end
      end
    end
  end
  return nil
end

function init(p, players)
  port = p
  me = REC[p] or REC[1]
  them = (me == REC[1]) and REC[0] or REC[1]
  fuel_at = FUEL[p] or FUEL[1]
  print("gravityforce bot on port " .. p .. " of " .. players)
end

function frame(n)
  -- the map, a part a frame (the arena caps what a frame may cost)
  if built < ROWS then build_some(40); return 0 end
  if not finished then finish_some(); return 0 end
  local x, y, h = ship(me)
  local ex, ey = ship(them)
  local p = prev
  local vx = p.x and (x - p.x) or 0
  local vy = p.y and (y - p.y) or 0
  local evx = p.ex and (ex - p.ex) or 0
  local evy = p.ey and (ey - p.ey) or 0
  -- a jump back to the pad is a new life: no velocity across it
  if abs(vx) > 8 or abs(vy) > 8 then vx, vy = 0, 0; stats.deaths = stats.deaths + 1 end
  if abs(evx) > 8 or abs(evy) > 8 then evx, evy = 0, 0; stats.kills = stats.kills + 1 end
  -- an enemy that has not moved for three seconds is out of the game (or
  -- between lives): nothing to shoot or chase
  local still = (p.ex == ex and p.ey == ey) and (p.still or 0) + 1 or 0
  prev = { x = x, y = y, ex = ex, ey = ey, still = still, refuel = p.refuel }
  if n % 1000 == 0 then
    print(string.format("frame %d port %d deaths %d enemy-deaths %d shots %d fuel %d", n, port, stats.deaths, stats.kills, stats.shots, ram.u8(fuel_at)))
  end

  sort_bullets(x, y, ex, ey)
  local ships = gunships()
  if remember(ships) then field_age = min(field_age, 3) end
  local fuel = ram.u8(fuel_at)
  local home_x = s32(ram.u32(me + 0x1c)) / 1000 + 7
  local home_y = s32(ram.u32(me + 0x20)) / 1000 + 7
  local landed = abs(x - home_x) < 3 and abs(y - home_y) < 2 and abs(vy) < 0.05

  -- On the pad with little fuel: sit until it is filled.
  if landed and fuel < 90 and (p.refuel or fuel < 30) then
    prev.refuel = true
    return turn_toward(h, 54)
  end

  -- Resting on rock (a pad, or after a soft landing): a ship cannot turn
  -- there, so it lifts off nose up first.
  -- (within a few pixels of the ground it cannot turn either)
  local resting = abs(vx) < 0.3 and abs(vy) < 0.3 and (rock(x, y + 14) or rock(x - 4, y + 14) or rock(x + 4, y + 14))
    and angle_err(h, 54) <= 2
  if resting then return bits.FIRE end

  -- Rock on the coasting path soon: everything below flies slower and
  -- pushes off the rock harder.
  local hit = coast_hits(x, y, vx, vy, 18)

  -- An enemy bullet coming our way: thrust across its path.
  local tvx, tvy = threat(x, y, vx, vy)
  if tvx then
    local side = heading_to(-tvy, tvx)
    if angle_err(side, 54) > 18 then side = heading_to(tvy, -tvx) end
    if angle_err(side, 54) <= 24 then
      local b = turn_toward(h, side)
      if angle_err(h, side) <= 8 then b = b | bits.FIRE end
      return b
    end
  end

  -- Fuel low: go home.
  local gx, gy
  local going_home = fuel < 22
  if going_home then gx, gy = home_x, home_y - 18 else gx, gy = ex, ey end

  -- Enemy in sight and in range: shoot.
  local dx, dy = ex - x, ey - y
  local d = SQRT(dx * dx + dy * dy)
  shoot_cool = max(0, shoot_cool - 1)
  local function shot_line(x1, y1, x2, y2)
    local lx, ly = x2 - x1, y2 - y1
    local l = max(1, SQRT(lx * lx + ly * ly))
    local ox, oy = -ly / l * 3, lx / l * 3
    return thin_line(x1, y1, x2, y2) and thin_line(x1 + ox, y1 + oy, x2 + ox, y2 + oy) and thin_line(x1 - ox, y1 - oy, x2 - ox, y2 - oy)
  end
  local ehx = s32(ram.u32(them + 0x1c)) / 1000 + 7
  local ehy = s32(ram.u32(them + 0x20)) / 1000 + 7
  local enemy_home = (abs(ex - ehx) < 4 and abs(ey - ehy) < 4 and abs(evx) < 0.05 and abs(evy) < 0.05)
    or still > 150
  if not going_home and not enemy_home and d < 170 and shot_line(x, y, ex, ey) and vy < 1.2 and not hit and not coast_hits(x, y, vx, vy, 30) then
    local aim = intercept(x, y, vx, vy, ex, ey, evx, evy)
    if aim then
      local b = turn_toward(h, aim)
      if angle_err(h, aim) <= 1 and shoot_cool == 0 then
        b = b | bits.UP; shoot_cool = 6; stats.shots = stats.shots + 1
      end
      -- falling while aiming: a little thrust if the nose is high enough
      if vy > 0.5 and (h >= 36 and h <= 72) and angle_err(h, 54) <= 30 then b = b | bits.FIRE end
      return b
    end
  end

  -- Fly there: the distance field towards the goal, refreshed now and then.
  local gk = cell_of(gx, gy)
  field_age = field_age - 1
  if gk ~= goal_cell or field_age <= 0 then
    turrets = ships
    dist = field(gk); goal_cell = gk; field_age = 15
  end
  local wx, wy
  if clear_line(x, y, gx, gy) and (clear[cell_of(gx, gy)] or 0) >= 2 then wx, wy = gx, gy else wx, wy = waypoint(x, y) end
  if not wx then wx, wy = x, y - 10 end
  local ddx, ddy = wx - x, wy - y
  local dd = SQRT(ddx * ddx + ddy * ddy)
  local here = clear[cell_of(x, y)] or 4
  local speed = min(1.0, dd / 30, 0.15 + 0.17 * here)
  if going_home and dd < 30 then speed = min(speed, 0.4) end
  -- keep a standoff from the enemy when it is in sight
  if not going_home and d < 90 and thin_line(x, y, ex, ey) then speed = 0 end
  if hit then speed = min(speed, 0.4) end
  -- a gunship near the way ahead and not yet near us: wait for it to pass
  local function near_ship(px, py, r)
    for _, g in ipairs(ships) do
      local qx, qy = px - g[1], py - g[2]
      if qx * qx + qy * qy < r * r then return g end
    end
  end
  local g_here = near_ship(x, y, 55)
  local in_lane = false
  for ly in pairs(lanes) do if abs(y - ly) < 26 then in_lane = true end end
  if not g_here and not in_lane and near_ship(x + ddx * min(1, 40 / max(dd, 1)), y + ddy * min(1, 40 / max(dd, 1)), GUNSHIP_REACH) then
    speed = 0
  end
  -- in one's reach: away from it, fast
  if g_here then
    local qx, qy = x - g_here[1], y - g_here[2]
    local ql = max(1, SQRT(qx * qx + qy * qy))
    -- straight away from it, mostly up or down: they fly level
    ddx, ddy = qx / ql * 20, (qy >= 0 and 40 or -40)
    dd = SQRT(ddx * ddx + ddy * ddy)
    speed = max(speed, 0.9)
  end
  local wvx, wvy = 0, 0
  if dd > 0.5 then wvx, wvy = ddx / dd * speed, ddy / dd * speed end
  local rx, ry = repulsion(x, y)
  local push = hit and 1.0 or 0.6
  wvx, wvy = wvx + rx * push, wvy + ry * push
  local wm = SQRT(wvx * wvx + wvy * wvy)
  local top = max(speed, 0.5)
  if wm > top then wvx, wvy = wvx / wm * top, wvy / wm * top end
  -- the lanes are walls: below one, never rise within 30 of its centre;
  -- above one, never sink within 30 of it. Falling is slow (gravity only,
  -- unless the ship turns over), so the limits start early.
  for ly in pairs(lanes) do
    if y > ly and y < ly + 70 then
      local room = y - (ly + 28)
      -- rising, only as fast as gravity alone can stop in the room left
      if room < 0 then wvy = max(wvy, 0.5) elseif wvy < 0 then wvy = max(wvy, -0.6 * SQRT(2 * GRAVITY / 1000 * room)) end
    elseif y < ly and y > ly - 70 then
      local room = (ly - 28) - y
      if room < 0 then wvy = min(wvy, -0.5) elseif wvy > 0 then wvy = min(wvy, room / 25) end
    end
  end
  -- landing: straight down onto the pad, slowly
  if going_home and abs(x - home_x) < 6 and y < home_y and y > home_y - 40 then
    wvx, wvy = (home_x - x) / 20, 0.35
  end
  return fly(h, vx, vy, wvx, wvy, true)
end
