Skip to content
GameGenresEspañol
Strategy

Artillery game

A usually turn-based game where you and your rivals fire at each other from fixed positions, choosing the angle and power of each shot. The challenge is working out the trajectory, allowing for distance, terrain and sometimes wind.

Also known as: artillery, artillery video game

Defining mechanics

  • Choosing the angle and power of each shot; that pair of numbers is the whole of your turn.
  • A parabolic trajectory under gravity, often bent by a wind that changes between turns or rounds.
  • Terrain of hills or buildings that blocks the line of fire and is deformed or destroyed by impacts.
  • Alternating turns against another person or the computer, sometimes with an arsenal of weapons bought between rounds.

Gameplay loop

Read the distance, wind and terrain, set the angle and power, fire, see where the shot lands and correct it on the next turn.

Our demo

Controls. Left and right arrows (or A/D) for the angle, up and down (or W/S) for the power, space or Enter to fire. With a mouse or on mobile, drag from your tank towards where you want to aim and let go. On the title screen, space or a click on the left plays against the AI, and the 2 key or a click on the right plays with two people.

Open full screen

Two tanks take turns firing at each other over randomly generated hills. You choose the angle and power, allow for the wind, and every shot digs a crater that changes the terrain. You can play against an AI that corrects its aim or against another person.

Made with Kaplay, in about 189 lines of JavaScript.

How it's made

  1. Setup and constants

    Virtual buttons are defined for angle, power and firing, and they work the same with a keyboard or with the touch buttons. Gravity, maximum launch speed and wind strength are the numbers that decide how each shot feels; the crater radius decides how much the terrain changes.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [24, 30, 64],
      buttons: {
        angleUp: { keyboard: ["left", "a"] },
        angleDown: { keyboard: ["right", "d"] },
        powerUp: { keyboard: ["up", "w"] },
        powerDown: { keyboard: ["down", "s"] },
        fire: { keyboard: ["space", "enter"] },
      },
    });
    touchControls(k, [
      { button: "angleUp", label: "↺" },
      { button: "angleDown", label: "↻" },
      { button: "powerDown", label: "−", side: "right" },
      { button: "powerUp", label: "+", side: "right" },
      { button: "fire", label: "●", side: "right" },
    ]);
    
    const W = 640;
    const H = 360;
    const GRAVITY = 240; // px/s² pulling the shell down
    const MAX_POWER = 420; // launch speed at 100 % power
    const WIND_MAX = 60; // px/s² of horizontal push, at most
    const CRATER = 26; // crater radius in px
    const COL = 4; // terrain column width in px
  2. The terrain as a height map

    The terrain is a list of numbers, one per 4-pixel column, storing the height where the ground begins. It is generated by adding sine waves with random phases, so each game has different hills. Deforming it is just changing numbers: the crater walks over the columns near the impact and lowers each one to the bottom edge of a circle. That is why the terrain can sink but cannot form caves or overhangs.

    // The terrain is a height map: one number per column (the y of the ground top).
    function makeTerrain() {
      const a = rand(0, 6), b = rand(0, 6);
      return Array.from({ length: W / COL }, (_, i) => {
        const x = i / (W / COL);
        return 230 - 50 * Math.sin(x * 3.1 + a) - 25 * Math.sin(x * 9 + b) - 60 * Math.sin(x * Math.PI);
      });
    }
    const groundAt = (ground, x) => ground[Math.max(0, Math.min(ground.length - 1, Math.floor(x / COL)))];
    
    // A crater removes the dirt inside a circle: each column's top drops to the circle's bottom edge.
    function digCrater(ground, cx, cy) {
      for (let i = 0; i < ground.length; i++) {
        const dx = i * COL + COL / 2 - cx;
        if (Math.abs(dx) < CRATER) ground[i] = Math.max(ground[i], cy + Math.sqrt(CRATER ** 2 - dx ** 2));
      }
    }
  3. The parabolic shot

    This is the genre's mechanic. The shell leaves with a velocity equal to the power in the direction of the angle. At every moment gravity adds downward speed and the wind adds sideways speed, and the position moves with that velocity: the result is a parabola, tilted by the wind. The path is computed in small steps so a fast shell cannot pass through a thin hill without touching it. A 45° angle reaches furthest on flat ground; higher angles help to clear a hill.

    // One step of a parabolic flight: gravity bends the path down, wind pushes it sideways.
    function step(p, v, d) {
      v.y += GRAVITY * d;
      v.x += wind * d;
      p.x += v.x * d;
      p.y += v.y * d;
    }
    
    function fire() {
      if (shell) return;
      const tk = tanks[turn];
      shell = { p: muzzle(tk), v: launchVel(tk) };
    }
    
    onUpdate(() => {
      if (!shell) return;
      for (let i = 0; i < 4; i++) {
        // small sub-steps so a fast shell cannot skip through a thin hill
        step(shell.p, shell.v, dt() / 4);
        if (shell.p.x < 0 || shell.p.x > W) return land(false);
        const enemy = tanks[1 - turn];
        if (shell.p.dist(tankPos(enemy).add(0, -6)) < 14) return land(true);
        if (shell.p.y >= groundAt(ground, shell.p.x)) return land(false);
      }
    });
  4. Impact, crater and turns

    When the shell touches the ground or a tank, a crater opens and the screen shakes. Any tank inside the blast radius loses, yours too if you fire too close. If nobody falls, the turn passes and the wind shifts a little, so repeating the last shot guarantees nothing.

    function land(hit) {
      const at = shell.p.clone();
      shell = null;
      if (at.x >= 0 && at.x <= W) {
        digCrater(ground, at.x, at.y);
        add([circle(CRATER), pos(at), anchor("center"), color(255, 200, 80), opacity(0.8), lifespan(0.25, { fade: 0.2 })]);
        shake(hit ? 12 : 4);
      }
      // Any tank within the blast radius is destroyed (including your own!).
      const caught = tanks.findIndex((tk) => tankPos(tk).add(0, -6).dist(at) < CRATER);
      const loser = hit ? 1 - turn : caught;
      if (loser >= 0) return wait(0.6, () => go("end", 1 - loser, vsAI));
      if (turn === 1 && vsAI) aiMemory.lastLanding = at.x;
      nextTurn();
    }
    
    function nextTurn() {
      turn = 1 - turn;
      // Variable wind: it drifts a little every turn, so no shot can be repeated blindly.
      wind = Math.max(-WIND_MAX, Math.min(WIND_MAX, wind + rand(-20, 20)));
      if (!isHuman()) wait(0.9, aiShot);
    }
  5. An AI that learns by approximation

    The AI does what a person does: it tries, sees where the shot fell and corrects. Its first shot comes from the flat-ground range formula, with a deliberate error. After that, since on flat ground the range grows with the square of the speed, it multiplies the power by the square root of «distance wanted / distance reached». If the shot falls far short because it hit a hill, it raises the angle instead of firing harder.

    // The AI fixes its angle and corrects the power by successive approximations.
    // On flat ground the range grows with the square of the speed, so it scales the
    // power by √(wanted distance / distance reached) and gets closer every turn.
    function aiShot() {
      const me = tanks[1];
      const target = Math.abs(me.x - tanks[0].x);
      me.angle = aiMemory.angle;
      if (aiMemory.power === null) {
        const rad = (180 - me.angle) * (Math.PI / 180);
        aiMemory.power = Math.sqrt((target * GRAVITY) / Math.sin(2 * rad)) / MAX_POWER;
        aiMemory.power *= rand(0.8, 1.2); // a deliberately rough first guess
      } else {
        const reached = Math.max(20, me.x - aiMemory.lastLanding);
        // A shell that stops very short hit a hill: aim higher instead of harder.
        if (reached < target / 3 && aiMemory.angle > 105) aiMemory.angle -= 10;
        else aiMemory.power *= Math.sqrt(target / reached) * rand(0.97, 1.03);
      }
      me.power = Math.max(0.15, Math.min(1, aiMemory.power));
      fire();
    }
  6. Aiming with keys or by dragging

    With the keyboard, holding an arrow changes the angle or power little by little. With a mouse or on a touch screen, the vector from the barrel to the pointer gives both at once, its direction being the angle and its length the power, and letting go fires.

    // Keyboard: angle and power in small steps. Mouse or touch: drag from the tank.
    onUpdate(() => {
      if (shell || !isHuman()) return;
      const tk = tanks[turn];
      if (isButtonDown("angleUp")) tk.angle = Math.min(180, tk.angle + 60 * dt());
      if (isButtonDown("angleDown")) tk.angle = Math.max(0, tk.angle - 60 * dt());
      if (isButtonDown("powerUp")) tk.power = Math.min(1, tk.power + 0.4 * dt());
      if (isButtonDown("powerDown")) tk.power = Math.max(0.1, tk.power - 0.4 * dt());
      if (aiming) {
        const d = mousePos().sub(muzzle(tk));
        tk.angle = Math.max(0, Math.min(180, -d.angle()));
        tk.power = Math.max(0.1, Math.min(1, d.len() / 160));
      }
    });
    onButtonPress("fire", () => isHuman() && fire());
    onMousePress(() => (aiming = !shell && isHuman()));
    onMouseRelease(() => {
      if (aiming) fire();
      aiming = false;
    });
  7. Drawing, trajectory and wind

    Everything is drawn every frame with simple shapes. The preview uses the same shot function to simulate only the first stretch of the path, which is enough to orient yourself but not to know where the shell will land; showing all of it would remove the challenge. The arrow at the top shows which way the wind blows and how hard.

    onDraw(() => {
      ground.forEach((y, i) => drawRect({ pos: vec2(i * COL, y), width: COL, height: H - y, color: rgb(80, 150, 90) }));
      tanks.forEach((tk) => {
        const p = tankPos(tk);
        drawRect({ pos: p.add(-12, -10), width: 24, height: 10, color: tk.colour });
        drawLine({ p1: muzzle(tk), p2: muzzle(tk).add(Vec2.fromAngle(-tk.angle).scale(18)), width: 4, color: tk.colour });
      });
      // Preview of the first part of the path: enough to aim, not enough to be sure.
      if (!shell && isHuman()) {
        const p = muzzle(tanks[turn]);
        const v = launchVel(tanks[turn]);
        for (let i = 0; i < 14; i++) {
          for (let j = 0; j < 3; j++) step(p, v, 1 / 60);
          drawCircle({ pos: p, radius: 2, color: rgb(255, 255, 255), opacity: 1 - i / 14 });
        }
      }
      if (shell) drawCircle({ pos: shell.p, radius: 4, color: rgb(255, 240, 160) });
      // Wind arrow: length and direction show its strength.
      const c = vec2(W / 2, 22);
      const tip = c.add(wind, 0);
      drawLine({ p1: c, p2: tip, width: 3, color: rgb(200, 220, 255) });
      drawTriangle({ p1: tip.add(Math.sign(wind) * 8, 0), p2: tip.add(0, -5), p3: tip.add(0, 5), color: rgb(200, 220, 255) });
      const tk = tanks[turn];
      const who = isHuman() ? t({ es: `Jugador ${turn + 1}`, en: `Player ${turn + 1}` }) : t({ es: "IA", en: "AI" });
      hud.text = `${who} · ${t({ es: "Ángulo", en: "Angle" })} ${Math.round(tk.angle)}° · ${t({ es: "Potencia", en: "Power" })} ${Math.round(tk.power * 100)} %`;
    });
  8. Scenes

    The game has three scenes, title, game and end. The game scene receives whether you play against the AI or between two people, and go() jumps between them, generating new terrain for each game.

    // On the title, the left half of the screen means "vs AI" and the right half "two players".
    function screen(title, subtitle, next) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
      add([
        text(subtitle, { size: 15, font: FONT, align: "center", width: 560 }),
        pos(center().add(0, 25)),
        anchor("center"),
        color(200, 210, 255),
      ]);
      onButtonPress("fire", () => next(true));
      onKeyPress("2", () => next(false));
      onClick(() => next(mousePos().x < W / 2));
    }
    
    scene("title", () =>
      screen(
        t({ es: "Artillería", en: "Artillery" }),
        t({
          es: "Por turnos: ←/→ cambian el ángulo, ↑/↓ la potencia, espacio dispara.\nO arrastra desde tu tanque y suelta. Mira el viento (flecha de arriba).\nEspacio o clic a la izquierda: contra la IA. Tecla 2 o clic a la derecha: dos jugadores.",
          en: "Turn-based: ←/→ change the angle, ↑/↓ the power, space fires.\nOr drag from your tank and let go. Watch the wind (arrow at the top).\nSpace or click on the left: against the AI. Key 2 or click on the right: two players.",
        }),
        (vsAI) => go("game", vsAI),
      ),
    );
    
    scene("end", (winner, vsAI) =>
      screen(
        vsAI && winner === 1 ? t({ es: "Ganó la IA", en: "The AI wins" }) : t({ es: `¡Gana el jugador ${winner + 1}!`, en: `Player ${winner + 1} wins!` }),
        t({ es: "Espacio o clic para volver al título.", en: "Space or click to go back to the title." }),
        () => go("title"),
      ),
    );
    
    go("title");

What we simplified

  • There is a single kind of shell, and a direct or nearby hit destroys the tank; there is no health, money or weapon shop.
  • Tanks do not move, and the dirt removed by craters simply vanishes; it does not fall or slide.
  • The AI almost always keeps the same angle and only corrects its power; it does not reason about the wind.

To modify it

  • Give each tank 100 health points and make the damage depend on the distance from the centre of the blast.
  • Make the AI take the wind into account too. Hint, compare where its shot landed with where it would have landed without wind.
  • Add a second shell that adds dirt instead of digging a crater, for example to shelter behind a wall.
Full code
// Artillery demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, touchControls, pixelDensity } from "../common.js";

// #region setup
const k = kaplay({
  width: 640,
  height: 360,
  letterbox: true, pixelDensity,
  background: [24, 30, 64],
  buttons: {
    angleUp: { keyboard: ["left", "a"] },
    angleDown: { keyboard: ["right", "d"] },
    powerUp: { keyboard: ["up", "w"] },
    powerDown: { keyboard: ["down", "s"] },
    fire: { keyboard: ["space", "enter"] },
  },
});
touchControls(k, [
  { button: "angleUp", label: "↺" },
  { button: "angleDown", label: "↻" },
  { button: "powerDown", label: "−", side: "right" },
  { button: "powerUp", label: "+", side: "right" },
  { button: "fire", label: "●", side: "right" },
]);

const W = 640;
const H = 360;
const GRAVITY = 240; // px/s² pulling the shell down
const MAX_POWER = 420; // launch speed at 100 % power
const WIND_MAX = 60; // px/s² of horizontal push, at most
const CRATER = 26; // crater radius in px
const COL = 4; // terrain column width in px
// #endregion

// #region terrain
// The terrain is a height map: one number per column (the y of the ground top).
function makeTerrain() {
  const a = rand(0, 6), b = rand(0, 6);
  return Array.from({ length: W / COL }, (_, i) => {
    const x = i / (W / COL);
    return 230 - 50 * Math.sin(x * 3.1 + a) - 25 * Math.sin(x * 9 + b) - 60 * Math.sin(x * Math.PI);
  });
}
const groundAt = (ground, x) => ground[Math.max(0, Math.min(ground.length - 1, Math.floor(x / COL)))];

// A crater removes the dirt inside a circle: each column's top drops to the circle's bottom edge.
function digCrater(ground, cx, cy) {
  for (let i = 0; i < ground.length; i++) {
    const dx = i * COL + COL / 2 - cx;
    if (Math.abs(dx) < CRATER) ground[i] = Math.max(ground[i], cy + Math.sqrt(CRATER ** 2 - dx ** 2));
  }
}
// #endregion

scene("game", (vsAI) => {
  const ground = makeTerrain();
  const tanks = [
    { x: 64, angle: 45, power: 0.6, colour: rgb(120, 200, 255) },
    { x: W - 64, angle: 135, power: 0.6, colour: rgb(255, 150, 90) },
  ];
  let turn = 0;
  let wind = rand(-WIND_MAX, WIND_MAX);
  let shell = null; // the projectile in flight, if any
  let aiming = false; // is the mouse/finger dragging an aim?
  const aiMemory = { angle: 130, power: null, lastLanding: null };
  const hud = add([text("", { size: 15, font: FONT }), pos(12, 10), z(10)]);
  const tankPos = (tk) => vec2(tk.x, groundAt(ground, tk.x));
  const muzzle = (tk) => tankPos(tk).add(0, -10);
  const launchVel = (tk) => Vec2.fromAngle(-tk.angle).scale(tk.power * MAX_POWER);
  const isHuman = () => !(vsAI && turn === 1);

  // #region projectile
  // One step of a parabolic flight: gravity bends the path down, wind pushes it sideways.
  function step(p, v, d) {
    v.y += GRAVITY * d;
    v.x += wind * d;
    p.x += v.x * d;
    p.y += v.y * d;
  }

  function fire() {
    if (shell) return;
    const tk = tanks[turn];
    shell = { p: muzzle(tk), v: launchVel(tk) };
  }

  onUpdate(() => {
    if (!shell) return;
    for (let i = 0; i < 4; i++) {
      // small sub-steps so a fast shell cannot skip through a thin hill
      step(shell.p, shell.v, dt() / 4);
      if (shell.p.x < 0 || shell.p.x > W) return land(false);
      const enemy = tanks[1 - turn];
      if (shell.p.dist(tankPos(enemy).add(0, -6)) < 14) return land(true);
      if (shell.p.y >= groundAt(ground, shell.p.x)) return land(false);
    }
  });
  // #endregion

  // #region impact
  function land(hit) {
    const at = shell.p.clone();
    shell = null;
    if (at.x >= 0 && at.x <= W) {
      digCrater(ground, at.x, at.y);
      add([circle(CRATER), pos(at), anchor("center"), color(255, 200, 80), opacity(0.8), lifespan(0.25, { fade: 0.2 })]);
      shake(hit ? 12 : 4);
    }
    // Any tank within the blast radius is destroyed (including your own!).
    const caught = tanks.findIndex((tk) => tankPos(tk).add(0, -6).dist(at) < CRATER);
    const loser = hit ? 1 - turn : caught;
    if (loser >= 0) return wait(0.6, () => go("end", 1 - loser, vsAI));
    if (turn === 1 && vsAI) aiMemory.lastLanding = at.x;
    nextTurn();
  }

  function nextTurn() {
    turn = 1 - turn;
    // Variable wind: it drifts a little every turn, so no shot can be repeated blindly.
    wind = Math.max(-WIND_MAX, Math.min(WIND_MAX, wind + rand(-20, 20)));
    if (!isHuman()) wait(0.9, aiShot);
  }
  // #endregion

  // #region ai
  // The AI fixes its angle and corrects the power by successive approximations.
  // On flat ground the range grows with the square of the speed, so it scales the
  // power by √(wanted distance / distance reached) and gets closer every turn.
  function aiShot() {
    const me = tanks[1];
    const target = Math.abs(me.x - tanks[0].x);
    me.angle = aiMemory.angle;
    if (aiMemory.power === null) {
      const rad = (180 - me.angle) * (Math.PI / 180);
      aiMemory.power = Math.sqrt((target * GRAVITY) / Math.sin(2 * rad)) / MAX_POWER;
      aiMemory.power *= rand(0.8, 1.2); // a deliberately rough first guess
    } else {
      const reached = Math.max(20, me.x - aiMemory.lastLanding);
      // A shell that stops very short hit a hill: aim higher instead of harder.
      if (reached < target / 3 && aiMemory.angle > 105) aiMemory.angle -= 10;
      else aiMemory.power *= Math.sqrt(target / reached) * rand(0.97, 1.03);
    }
    me.power = Math.max(0.15, Math.min(1, aiMemory.power));
    fire();
  }
  // #endregion

  // #region aiming
  // Keyboard: angle and power in small steps. Mouse or touch: drag from the tank.
  onUpdate(() => {
    if (shell || !isHuman()) return;
    const tk = tanks[turn];
    if (isButtonDown("angleUp")) tk.angle = Math.min(180, tk.angle + 60 * dt());
    if (isButtonDown("angleDown")) tk.angle = Math.max(0, tk.angle - 60 * dt());
    if (isButtonDown("powerUp")) tk.power = Math.min(1, tk.power + 0.4 * dt());
    if (isButtonDown("powerDown")) tk.power = Math.max(0.1, tk.power - 0.4 * dt());
    if (aiming) {
      const d = mousePos().sub(muzzle(tk));
      tk.angle = Math.max(0, Math.min(180, -d.angle()));
      tk.power = Math.max(0.1, Math.min(1, d.len() / 160));
    }
  });
  onButtonPress("fire", () => isHuman() && fire());
  onMousePress(() => (aiming = !shell && isHuman()));
  onMouseRelease(() => {
    if (aiming) fire();
    aiming = false;
  });
  // #endregion

  // #region draw
  onDraw(() => {
    ground.forEach((y, i) => drawRect({ pos: vec2(i * COL, y), width: COL, height: H - y, color: rgb(80, 150, 90) }));
    tanks.forEach((tk) => {
      const p = tankPos(tk);
      drawRect({ pos: p.add(-12, -10), width: 24, height: 10, color: tk.colour });
      drawLine({ p1: muzzle(tk), p2: muzzle(tk).add(Vec2.fromAngle(-tk.angle).scale(18)), width: 4, color: tk.colour });
    });
    // Preview of the first part of the path: enough to aim, not enough to be sure.
    if (!shell && isHuman()) {
      const p = muzzle(tanks[turn]);
      const v = launchVel(tanks[turn]);
      for (let i = 0; i < 14; i++) {
        for (let j = 0; j < 3; j++) step(p, v, 1 / 60);
        drawCircle({ pos: p, radius: 2, color: rgb(255, 255, 255), opacity: 1 - i / 14 });
      }
    }
    if (shell) drawCircle({ pos: shell.p, radius: 4, color: rgb(255, 240, 160) });
    // Wind arrow: length and direction show its strength.
    const c = vec2(W / 2, 22);
    const tip = c.add(wind, 0);
    drawLine({ p1: c, p2: tip, width: 3, color: rgb(200, 220, 255) });
    drawTriangle({ p1: tip.add(Math.sign(wind) * 8, 0), p2: tip.add(0, -5), p3: tip.add(0, 5), color: rgb(200, 220, 255) });
    const tk = tanks[turn];
    const who = isHuman() ? t({ es: `Jugador ${turn + 1}`, en: `Player ${turn + 1}` }) : t({ es: "IA", en: "AI" });
    hud.text = `${who} · ${t({ es: "Ángulo", en: "Angle" })} ${Math.round(tk.angle)}° · ${t({ es: "Potencia", en: "Power" })} ${Math.round(tk.power * 100)} %`;
  });
  // #endregion
});

// #region scenes
// On the title, the left half of the screen means "vs AI" and the right half "two players".
function screen(title, subtitle, next) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
  add([
    text(subtitle, { size: 15, font: FONT, align: "center", width: 560 }),
    pos(center().add(0, 25)),
    anchor("center"),
    color(200, 210, 255),
  ]);
  onButtonPress("fire", () => next(true));
  onKeyPress("2", () => next(false));
  onClick(() => next(mousePos().x < W / 2));
}

scene("title", () =>
  screen(
    t({ es: "Artillería", en: "Artillery" }),
    t({
      es: "Por turnos: ←/→ cambian el ángulo, ↑/↓ la potencia, espacio dispara.\nO arrastra desde tu tanque y suelta. Mira el viento (flecha de arriba).\nEspacio o clic a la izquierda: contra la IA. Tecla 2 o clic a la derecha: dos jugadores.",
      en: "Turn-based: ←/→ change the angle, ↑/↓ the power, space fires.\nOr drag from your tank and let go. Watch the wind (arrow at the top).\nSpace or click on the left: against the AI. Key 2 or click on the right: two players.",
    }),
    (vsAI) => go("game", vsAI),
  ),
);

scene("end", (winner, vsAI) =>
  screen(
    vsAI && winner === 1 ? t({ es: "Ganó la IA", en: "The AI wins" }) : t({ es: `¡Gana el jugador ${winner + 1}!`, en: `Player ${winner + 1} wins!` }),
    t({ es: "Espacio o clic para volver al título.", en: "Space or click to go back to the title." }),
    () => go("title"),
  ),
);

go("title");
// #endregion

Design challenges

  • Deciding how much information to give the player (a trajectory preview, the exact wind value) so that aiming is a calculation and not a lottery, but not trivial either.
  • Balancing randomness (generated terrain, wind) with skill, so that whoever best corrects their shots wins.
  • An AI that misses believably and gradually homes in, instead of always hitting on the first try.
  • Representing destructible terrain (a height map or a pixel mask) and keeping turns brisk when several people play.

History

  1. 1969
    Origin

    Potshot (1969), by Arthur Luehrmann, was perhaps the first version with graphical output, on a plotter; there were also text-only variants in the early 1970s.

    Artillery game (Wikipedia)
  2. 1980

    Super Artillery and Artillery Simulator, for the Apple II, bring the genre to graphics screens and add wind, visible terrain and tanks.

    Artillery game (Wikipedia)
  3. 1982
    Artillery Duel

    Artillery Duel brings to consoles the duel between two cannons separated by a mountain, which changes shape every turn.

    Artillery Duel (Wikipedia)
  4. 1990

    Tank Wars, by Kenny Morse, introduces buying weapons and several computer-controlled tanks.

    Artillery game (Wikipedia)
  5. 1991
    Gorillas

    Gorillas ships as a QBasic sample program with MS-DOS 5.0, its BASIC code open to anyone who wants to read or change it.

    Gorillas (Wikipedia)
  6. 1991
    Scorched Earth

    Scorched Earth, Wendell Hicken's shareware game, adds destructible terrain and ever more powerful weapons, and is considered the genre's modern archetype.

    Scorched Earth (Wikipedia)
  7. 1995
    Worms

    Worms swaps tanks for teams of worms, with time-limited turns and explosions that blow holes in randomly generated maps; its success leads to a long series.

    Worms (1995) (Wikipedia)
  8. 2009

    Angry Birds takes the idea to the touch screen, where you launch with a slingshot and must anticipate the trajectory to knock down structures.

    Artillery game (Wikipedia)

Reference games

1982

Artillery Duel

Perkins Engineering (John Perkins). Bally Astrocade, Atari 2600, ColecoVision, Commodore 64, VIC-20

Two cannons face each other across a mountain, and each player chooses the angle and force of the shot. It took the genre from BASIC programs to consoles, with a different mountain on every turn.

1991

Gorillas

Microsoft. MS-DOS

Two gorillas on top of skyscrapers throw explosive bananas at each other by typing the angle and speed of each throw, with wind that changes every round. It shipped with QBasic in MS-DOS 5.0, so many people both played it and read its code.

1995

Worms

Team17 (Andy Davidson). Amiga, MS-DOS, PlayStation, Sega Saturn, Mega Drive, Super NES, Game Boy, Mac OS, Atari Jaguar

Teams of worms take turns attacking each other with bazookas, grenades and other weapons on randomly generated maps; every explosion leaves a round hole in the terrain, and each turn has a time limit. It started a long series.

2025

Worms 13k

Craig Smith. navegador

A tribute to Worms made in under 13 kilobytes for js13kGames 2025. Two teams of worms take turns attacking each other with bazookas, a minigun and cluster bombs on destructible terrain. It runs in the browser, on one keyboard or online.

Exercise

In the demo, change the gravity and the maximum wind strength and note how the difficulty changes. Then use a few shots to find which angle reaches furthest on flat ground and compare it with the physics of projectile motion.

Sources