
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 screenTwo 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
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 pxThe 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)); } }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); } });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); }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(); }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; });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)} %`; });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");
// #endregionDesign 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
- 1969Origin
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) - 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) - 1982Artillery Duel
Artillery Duel brings to consoles the duel between two cannons separated by a mountain, which changes shape every turn.
Artillery Duel (Wikipedia) - 1990
Tank Wars, by Kenny Morse, introduces buying weapons and several computer-controlled tanks.
Artillery game (Wikipedia) - 1991Gorillas
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) - 1991Scorched 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) - 1995Worms
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) - 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
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
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.
1991
Scorched Earth
Up to ten tanks take turns firing over terrain that is destroyed by every explosion. Between rounds you buy ever more powerful weapons, and the wind bends the shots. It was distributed as shareware and is considered the genre's modern archetype.
1995
Worms
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
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.