
Incremental game
A game where you repeat a simple action, such as clicking, to gather a resource that you then invest in upgrades that produce it on their own, ever faster. Progress continues even when you are not playing, and the challenge is deciding what to invest in and when to reset in exchange for permanent bonuses.
Also known as: idle game, clicker game, idler
Defining mechanics
- A minimal manual action, such as a click or a tap, that produces the resource. It is all that matters at the start and almost nothing later.
- Generators that produce on their own and whose price grows exponentially with each copy (for example, base × 1.15^n), while their output only grows linearly.
- Upgrades that multiply production and unlocks that open new layers (other resources, buildings or areas).
- Progress that continues without the player, in the background or worked out on return from the time elapsed.
- The prestige reset. You wipe your progress in exchange for a permanent currency or bonus that speeds up the next run.
Gameplay loop
Produce, spend on whatever boosts production most, wait (or click) until the next purchase and, when progress stalls, do a prestige reset to climb again faster.
Our demo
Controls. Mouse or finger only. Click or tap the circle to produce and tap a button to buy. Space or Enter only start the game.
Open full screenYou click a circle to make energy and spend it on four generators that produce on their own, on multiplying upgrades and, when it pays off, on a prestige reset. Just enough to feel the exponential growth that defines the genre.
Made with Kaplay, in about 158 lines of JavaScript.
How it's made
Setup
The game is created and the constants that most change the pace are defined, which are the goal, the price growth (15%) and the prestige bonus. Prestige is stored outside the scene so it survives the reset.
kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [22, 24, 38], // Everything is played with the mouse or a finger; the keyboard only starts. buttons: { start: { keyboard: ["space", "enter"] } }, }); const GOAL = 1e6; // collect 1 M energy to win const COST_GROWTH = 1.15; // each copy of a generator costs 15% more than the last const PRESTIGE_BONUS = 0.5; // +50% production per prestige point, forever let prestige = 0; // lives outside the scene, so it survives a reset let resets = 0; let startTime = 0;Big numbers
Huge numbers are the genre's scenery, and they must be readable: 1234567 is shown as 1.23 M. The number is divided by 1000 as many times as possible and the letter (K, M, B, T) is added. Note that B is the short-scale billion, 10^9.
// Big numbers are the genre's scenery: show 1.23 K / 4.56 M instead of 1234567. // K = thousand, M = million, B = billion (short scale, 10^9), T = 10^12. const UNITS = ["", "K", "M", "B", "T"]; function fmt(n) { if (n < 10) return n.toFixed(1).replace(/\.0$/, ""); if (n < 1000) return String(Math.floor(n)); const i = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1); return +(n / 1000 ** i).toFixed(2) + " " + UNITS[i]; // the + drops trailing zeros }Generators and exponential cost
Each generator costs base × 1.15^n, where n is how many you own. The collector costs 10, then 12, then 14 (rounded up)… and the 20th already costs 143. Production, however, grows linearly (each copy adds the same amount), so every purchase pays off a bit less than the last. That is why at some point it is better to jump to the next generator, which is expensive but yields far more. The tension between buying «more of the same» and «something new» is the heart of the design.
// Each generator produces on its own. Its price follows the classic formula // cost = base × 1.15^owned // so price grows exponentially while production grows linearly: every new // copy is a little worse value, and sooner or later the next tier is better. const generators = [ { name: t({ es: "Recolector", en: "Collector" }), base: 10, rate: 1 }, { name: t({ es: "Dron", en: "Drone" }), base: 100, rate: 10 }, { name: t({ es: "Fábrica", en: "Factory" }), base: 1000, rate: 80 }, { name: t({ es: "Reactor", en: "Reactor" }), base: 10000, rate: 600 }, ].map((g) => ({ ...g, owned: 0, mult: 1 })); const cost = (g) => Math.ceil(g.base * COST_GROWTH ** g.owned); const prestigeMult = () => 1 + PRESTIGE_BONUS * prestige; const perSecond = () => generators.reduce((sum, g) => sum + g.owned * g.rate * g.mult, 0) * globalMult * prestigeMult();The production loop
Every frame adds production per second multiplied by dt(), the time elapsed. That way the game produces the same at 30 or 144 frames per second, and the same calculation can work out what was produced while the player was away.
// The core loop: every frame adds (production per second × seconds elapsed). // dt() makes it independent of the frame rate: 30 or 144 fps give the same. function earn(amount) { energy += amount; runTotal += amount; if (energy >= GOAL && !won) { won = true; go("end", resets, time() - startTime); } } onUpdate(() => earn(perSecond() * dt()));The click
The click is the only active action. At first it is everything, but soon generators make thousands per second and clicking becomes almost decorative. The circle's small bounce and the rising number give each tap immediate feedback, which is what makes it feel good.
// The click is the only "active" action: essential at the start, almost // irrelevant later, when generators produce thousands per second. const orb = add([circle(64), pos(130, 200), anchor("center"), color(90, 200, 255), outline(6, rgb(40, 110, 170)), area(), scale(1)]); orb.onClick(() => { const gain = clickPower * prestigeMult(); earn(gain); orb.scale = vec2(0.92); add([text("+" + fmt(gain), { size: 18, font: FONT }), pos(mousePos()), anchor("center"), color(255, 240, 160), opacity(1), move(UP, 60), lifespan(0.6, { fade: 0.4 })]); }); orb.onUpdate(() => (orb.scale = orb.scale.lerp(vec2(1), dt() * 10))); orb.add([text(t({ es: "¡Clic!", en: "Click!" }), { size: 20, font: FONT }), anchor("center"), color(20, 40, 70)]);Multiplying upgrades
Upgrades multiply instead of adding. A ×2 on something you own 40 copies of is worth more than several new purchases. Keeping the next upgrade always in view gives a near goal, and that sums up why the genre is so compelling, as the next purchase is always just within reach.
// Upgrades multiply instead of adding: a ×2 on something you own a lot of // is worth more than several new generators. They unlock in order. const upgrades = [ { cost: 50, label: t({ es: "Clic ×2", en: "Click ×2" }), apply: () => (clickPower *= 2) }, { cost: 250, label: t({ es: "Recolectores ×3", en: "Collectors ×3" }), apply: () => (generators[0].mult *= 3) }, { cost: 1500, label: t({ es: "Drones ×2", en: "Drones ×2" }), apply: () => (generators[1].mult *= 2) }, { cost: 6000, label: t({ es: "Clic ×5", en: "Click ×5" }), apply: () => (clickPower *= 5) }, { cost: 20000, label: t({ es: "Fábricas ×2", en: "Factories ×2" }), apply: () => (generators[2].mult *= 2) }, { cost: 80000, label: t({ es: "Reactores ×2", en: "Reactors ×2" }), apply: () => (generators[3].mult *= 2) }, { cost: 300000, label: t({ es: "Todo ×2", en: "Everything ×2" }), apply: () => (globalMult *= 2) }, ]; let nextUpgrade = 0;Prestige reset
When you reset you lose everything except prestige points, which add +50% production forever. Points grow with the square root of what you produced: 10 K gives 1 and 40 K gives 2. Doubling the run does not double the points, so the decision is when to reset. Too early and you gain little, too late and you waste time on a run that has already stalled. Climbing back far faster than the first time is the genre's reward.
// Resetting throws away this run in exchange for permanent points. // The gain grows with the square root of what you produced: doubling the // run does not double the points, so it pays to reset neither too early // nor too late. That decision is the genre's long-term strategy. const prestigeGain = () => Math.floor(Math.sqrt(runTotal / 10000)); function doPrestige() { if (prestigeGain() < 1) return; prestige += prestigeGain(); resets++; go("game"); // the scene starts from zero, but `prestige` remains }The panel
Every button is redrawn every frame from the state. What you can afford lights up and the rest is dimmed. In an incremental game the interface is almost the whole game, because it shows the price, the output and how far the next goal is.
// A panel of buttons rebuilt each frame from the state: what you can afford // is lit up, the rest is dimmed. const hud = add([text("", { size: 22, font: FONT }), pos(130, 40), anchor("center")]); const sub = add([text("", { size: 14, font: FONT, align: "center" }), pos(130, 76), anchor("center"), color(170, 190, 230)]); function button(x, y, w, h, onPress) { const b = add([rect(w, h, { radius: 6 }), pos(x, y), color(44, 50, 78), outline(2, rgb(90, 100, 150)), area()]); const label = b.add([text("", { size: 13, font: FONT, width: w - 16 }), pos(8, h / 2), anchor("left")]); b.onClick(onPress); return { b, label }; } const genButtons = generators.map((g, i) => button(280, 14 + i * 56, 344, 48, () => { if (energy < cost(g)) return; energy -= cost(g); g.owned++; }), ); const upButton = button(280, 244, 344, 40, () => { const u = upgrades[nextUpgrade]; if (!u || energy < u.cost) return; energy -= u.cost; u.apply(); nextUpgrade++; }); const prestigeButton = button(280, 294, 344, 52, doPrestige); const paint = (btn, enabled) => { btn.b.color = enabled ? rgb(60, 80, 130) : rgb(36, 40, 60); btn.label.color = enabled ? rgb(255, 255, 255) : rgb(130, 135, 160); }; onUpdate(() => { hud.text = fmt(energy) + " " + t({ es: "energía", en: "energy" }); sub.text = `${fmt(perSecond())} ${t({ es: "por segundo", en: "per second" })}\n${t({ es: "Meta", en: "Goal" })}: ${fmt(GOAL)} · ${t({ es: "Prestigio", en: "Prestige" })}: ${prestige} (×${prestigeMult().toFixed(1)})`; generators.forEach((g, i) => { const each = g.rate * g.mult * globalMult * prestigeMult(); genButtons[i].label.text = `${g.name} ×${g.owned} · +${fmt(each)}/s ${t({ es: "c/u", en: "each" })}\n${t({ es: "Precio", en: "Price" })}: ${fmt(cost(g))}`; paint(genButtons[i], energy >= cost(g)); }); const u = upgrades[nextUpgrade]; upButton.label.text = u ? `${t({ es: "Mejora", en: "Upgrade" })}: ${u.label} · ${t({ es: "Precio", en: "Price" })}: ${fmt(u.cost)}` : t({ es: "No quedan mejoras", en: "No upgrades left" }); paint(upButton, u && energy >= u.cost); const gain = prestigeGain(); prestigeButton.label.text = gain >= 1 ? t({ es: "Reiniciar con prestigio", en: "Prestige reset" }) + ` (+${gain}): ${t({ es: "producción", en: "production" })} ×${(1 + PRESTIGE_BONUS * (prestige + gain)).toFixed(1)}` : t({ es: `Reinicio con prestigio: produce ${fmt(10000 - runTotal)} más para desbloquearlo`, en: `Prestige reset: produce ${fmt(10000 - runTotal)} more to unlock it` }); paint(prestigeButton, gain >= 1); });Scenes
There are three scenes, title, game and end. The prestige reset is simply entering game again with go(), because the scene starts from scratch and only the prestige variable, which lives outside it, remembers what was earned.
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: 16, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255), ]); onButtonPress("start", next); onClick(next); } const newGame = () => { prestige = 0; resets = 0; startTime = time(); go("game"); }; scene("title", () => screen( t({ es: "Juego incremental", en: "Incremental game" }), t({ es: "Haz clic en el círculo para producir energía y gástala en generadores que producen solos.\nLlega a 1 M. Cuando puedas, reinicia con prestigio para ir más rápido.\nHaz clic o pulsa espacio para empezar.", en: "Click the circle to make energy and spend it on generators that produce on their own.\nReach 1 M. When you can, do a prestige reset to go faster.\nClick or press space to start.", }), newGame, ), ); scene("end", (resetCount, seconds) => screen( t({ es: "¡1 M de energía!", en: "1 M energy!" }), t({ es: `Lo lograste en ${Math.round(seconds)} s con ${resetCount} reinicios con prestigio.\n¿Lo harías más rápido reiniciando antes o después? Haz clic para repetir.`, en: `You did it in ${Math.round(seconds)} s with ${resetCount} prestige resets.\nWould resetting earlier or later be faster? Click to play again.`, }), newGame, ), ); go("title");
What we simplified
- The game is not saved and there is no progress while the tab is closed; in a real game this is almost mandatory.
- Upgrades appear one at a time in a fixed order, with no upgrade tree or achievements.
- There is a goal (1 M) so the game ends; many games in the genre have no ending.
- The numbers fit in JavaScript numbers; past 1.8 × 10^308 a big-number library would be needed.
To modify it
- Save the game with localStorage and, on return, add what would have been produced in the time elapsed (offline progress).
- Add a fifth generator and choose its base price and output so that buying it is neither obvious nor pointless.
- Replace the prestige square root with a cube root and test whether resetting still pays off.
Full code
// Incremental game demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";
// #region setup
kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [22, 24, 38],
// Everything is played with the mouse or a finger; the keyboard only starts.
buttons: { start: { keyboard: ["space", "enter"] } },
});
const GOAL = 1e6; // collect 1 M energy to win
const COST_GROWTH = 1.15; // each copy of a generator costs 15% more than the last
const PRESTIGE_BONUS = 0.5; // +50% production per prestige point, forever
let prestige = 0; // lives outside the scene, so it survives a reset
let resets = 0;
let startTime = 0;
// #endregion
// #region format
// Big numbers are the genre's scenery: show 1.23 K / 4.56 M instead of 1234567.
// K = thousand, M = million, B = billion (short scale, 10^9), T = 10^12.
const UNITS = ["", "K", "M", "B", "T"];
function fmt(n) {
if (n < 10) return n.toFixed(1).replace(/\.0$/, "");
if (n < 1000) return String(Math.floor(n));
const i = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
return +(n / 1000 ** i).toFixed(2) + " " + UNITS[i]; // the + drops trailing zeros
}
// #endregion
scene("game", () => {
let energy = 0;
let runTotal = 0; // everything produced in this run (prestige is based on it)
let clickPower = 1;
let globalMult = 1;
let won = false;
// #region generators
// Each generator produces on its own. Its price follows the classic formula
// cost = base × 1.15^owned
// so price grows exponentially while production grows linearly: every new
// copy is a little worse value, and sooner or later the next tier is better.
const generators = [
{ name: t({ es: "Recolector", en: "Collector" }), base: 10, rate: 1 },
{ name: t({ es: "Dron", en: "Drone" }), base: 100, rate: 10 },
{ name: t({ es: "Fábrica", en: "Factory" }), base: 1000, rate: 80 },
{ name: t({ es: "Reactor", en: "Reactor" }), base: 10000, rate: 600 },
].map((g) => ({ ...g, owned: 0, mult: 1 }));
const cost = (g) => Math.ceil(g.base * COST_GROWTH ** g.owned);
const prestigeMult = () => 1 + PRESTIGE_BONUS * prestige;
const perSecond = () =>
generators.reduce((sum, g) => sum + g.owned * g.rate * g.mult, 0) * globalMult * prestigeMult();
// #endregion
// #region production
// The core loop: every frame adds (production per second × seconds elapsed).
// dt() makes it independent of the frame rate: 30 or 144 fps give the same.
function earn(amount) {
energy += amount;
runTotal += amount;
if (energy >= GOAL && !won) {
won = true;
go("end", resets, time() - startTime);
}
}
onUpdate(() => earn(perSecond() * dt()));
// #endregion
// #region clicking
// The click is the only "active" action: essential at the start, almost
// irrelevant later, when generators produce thousands per second.
const orb = add([circle(64), pos(130, 200), anchor("center"), color(90, 200, 255), outline(6, rgb(40, 110, 170)), area(), scale(1)]);
orb.onClick(() => {
const gain = clickPower * prestigeMult();
earn(gain);
orb.scale = vec2(0.92);
add([text("+" + fmt(gain), { size: 18, font: FONT }), pos(mousePos()), anchor("center"), color(255, 240, 160), opacity(1), move(UP, 60), lifespan(0.6, { fade: 0.4 })]);
});
orb.onUpdate(() => (orb.scale = orb.scale.lerp(vec2(1), dt() * 10)));
orb.add([text(t({ es: "¡Clic!", en: "Click!" }), { size: 20, font: FONT }), anchor("center"), color(20, 40, 70)]);
// #endregion
// #region upgrades
// Upgrades multiply instead of adding: a ×2 on something you own a lot of
// is worth more than several new generators. They unlock in order.
const upgrades = [
{ cost: 50, label: t({ es: "Clic ×2", en: "Click ×2" }), apply: () => (clickPower *= 2) },
{ cost: 250, label: t({ es: "Recolectores ×3", en: "Collectors ×3" }), apply: () => (generators[0].mult *= 3) },
{ cost: 1500, label: t({ es: "Drones ×2", en: "Drones ×2" }), apply: () => (generators[1].mult *= 2) },
{ cost: 6000, label: t({ es: "Clic ×5", en: "Click ×5" }), apply: () => (clickPower *= 5) },
{ cost: 20000, label: t({ es: "Fábricas ×2", en: "Factories ×2" }), apply: () => (generators[2].mult *= 2) },
{ cost: 80000, label: t({ es: "Reactores ×2", en: "Reactors ×2" }), apply: () => (generators[3].mult *= 2) },
{ cost: 300000, label: t({ es: "Todo ×2", en: "Everything ×2" }), apply: () => (globalMult *= 2) },
];
let nextUpgrade = 0;
// #endregion
// #region prestige
// Resetting throws away this run in exchange for permanent points.
// The gain grows with the square root of what you produced: doubling the
// run does not double the points, so it pays to reset neither too early
// nor too late. That decision is the genre's long-term strategy.
const prestigeGain = () => Math.floor(Math.sqrt(runTotal / 10000));
function doPrestige() {
if (prestigeGain() < 1) return;
prestige += prestigeGain();
resets++;
go("game"); // the scene starts from zero, but `prestige` remains
}
// #endregion
// #region interface
// A panel of buttons rebuilt each frame from the state: what you can afford
// is lit up, the rest is dimmed.
const hud = add([text("", { size: 22, font: FONT }), pos(130, 40), anchor("center")]);
const sub = add([text("", { size: 14, font: FONT, align: "center" }), pos(130, 76), anchor("center"), color(170, 190, 230)]);
function button(x, y, w, h, onPress) {
const b = add([rect(w, h, { radius: 6 }), pos(x, y), color(44, 50, 78), outline(2, rgb(90, 100, 150)), area()]);
const label = b.add([text("", { size: 13, font: FONT, width: w - 16 }), pos(8, h / 2), anchor("left")]);
b.onClick(onPress);
return { b, label };
}
const genButtons = generators.map((g, i) =>
button(280, 14 + i * 56, 344, 48, () => {
if (energy < cost(g)) return;
energy -= cost(g);
g.owned++;
}),
);
const upButton = button(280, 244, 344, 40, () => {
const u = upgrades[nextUpgrade];
if (!u || energy < u.cost) return;
energy -= u.cost;
u.apply();
nextUpgrade++;
});
const prestigeButton = button(280, 294, 344, 52, doPrestige);
const paint = (btn, enabled) => {
btn.b.color = enabled ? rgb(60, 80, 130) : rgb(36, 40, 60);
btn.label.color = enabled ? rgb(255, 255, 255) : rgb(130, 135, 160);
};
onUpdate(() => {
hud.text = fmt(energy) + " " + t({ es: "energía", en: "energy" });
sub.text = `${fmt(perSecond())} ${t({ es: "por segundo", en: "per second" })}\n${t({ es: "Meta", en: "Goal" })}: ${fmt(GOAL)} · ${t({ es: "Prestigio", en: "Prestige" })}: ${prestige} (×${prestigeMult().toFixed(1)})`;
generators.forEach((g, i) => {
const each = g.rate * g.mult * globalMult * prestigeMult();
genButtons[i].label.text = `${g.name} ×${g.owned} · +${fmt(each)}/s ${t({ es: "c/u", en: "each" })}\n${t({ es: "Precio", en: "Price" })}: ${fmt(cost(g))}`;
paint(genButtons[i], energy >= cost(g));
});
const u = upgrades[nextUpgrade];
upButton.label.text = u
? `${t({ es: "Mejora", en: "Upgrade" })}: ${u.label} · ${t({ es: "Precio", en: "Price" })}: ${fmt(u.cost)}`
: t({ es: "No quedan mejoras", en: "No upgrades left" });
paint(upButton, u && energy >= u.cost);
const gain = prestigeGain();
prestigeButton.label.text = gain >= 1
? t({ es: "Reiniciar con prestigio", en: "Prestige reset" }) + ` (+${gain}): ${t({ es: "producción", en: "production" })} ×${(1 + PRESTIGE_BONUS * (prestige + gain)).toFixed(1)}`
: t({ es: `Reinicio con prestigio: produce ${fmt(10000 - runTotal)} más para desbloquearlo`, en: `Prestige reset: produce ${fmt(10000 - runTotal)} more to unlock it` });
paint(prestigeButton, gain >= 1);
});
// #endregion
});
// #region scenes
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: 16, font: FONT, align: "center", width: 560 }),
pos(center().add(0, 30)),
anchor("center"),
color(200, 210, 255),
]);
onButtonPress("start", next);
onClick(next);
}
const newGame = () => {
prestige = 0;
resets = 0;
startTime = time();
go("game");
};
scene("title", () =>
screen(
t({ es: "Juego incremental", en: "Incremental game" }),
t({
es: "Haz clic en el círculo para producir energía y gástala en generadores que producen solos.\nLlega a 1 M. Cuando puedas, reinicia con prestigio para ir más rápido.\nHaz clic o pulsa espacio para empezar.",
en: "Click the circle to make energy and spend it on generators that produce on their own.\nReach 1 M. When you can, do a prestige reset to go faster.\nClick or press space to start.",
}),
newGame,
),
);
scene("end", (resetCount, seconds) =>
screen(
t({ es: "¡1 M de energía!", en: "1 M energy!" }),
t({
es: `Lo lograste en ${Math.round(seconds)} s con ${resetCount} reinicios con prestigio.\n¿Lo harías más rápido reiniciando antes o después? Haz clic para repetir.`,
en: `You did it in ${Math.round(seconds)} s with ${resetCount} prestige resets.\nWould resetting earlier or later be faster? Click to play again.`,
}),
newGame,
),
);
go("title");
// #endregionDesign challenges
- Balancing the curves. If prices grow much faster than production the game stalls, and if they grow slower it breaks. It is usually modelled in a spreadsheet before coding.
- Keeping interest when the player's action is minimal, by revealing new systems bit by bit so there is always a goal in sight.
- Handling huge numbers. They must be shown in a readable way (K, M, scientific notation) and, beyond about 1.8 × 10^308, the limit of JavaScript numbers, big-number libraries are needed.
- Working out what was produced while the game was closed from the saved time, without changing the device clock being an easy cheat.
- Designing ethically. The constant reward loop can become compulsive and has been used for aggressive monetisation; Cow Clicker was born as a satire of that.
History
- 2002OriginProgress Quest
Progress Quest (2002), by Eric Fredricksen, a self-playing parody of online role-playing games, is credited with starting idle games.
Incremental game (Wikipedia) - 2010Cow Clicker
Researcher Ian Bogost releases Cow Clicker on Facebook, a satire of social games in which you can only click your cow every six hours unless you pay or invite friends.
Cow Clicker (Wikipedia) - 2013Candy Box 2
Candy Box!, by aniwey, starts as a candy counter and opens up into a small role-playing game in ASCII art. Its sequel, Candy Box 2, comes out in October of the same year.
Candy Box! (Wikipedia) - 2013A Dark Room
A Dark Room, by Doublespeak Games, starts with a fire to stoke and gradually reveals a village, a map and a story with an ending. It shows the genre can tell stories, and its author releases the code as free software.
A Dark Room (Wikipedia) - 2013Cookie Clicker
Cookie Clicker, by Julien "Orteil" Thiennot, is played in the browser and reaches 200,000 daily players a month after release; it is credited with a key role in the rise of the genre. Its "ascension" restarts the game in exchange for permanent bonuses.
Cookie Clicker (Wikipedia) - 2014AdVenture Capitalist
AdVenture Capitalist is among the first to add earnings while the game is closed and to monetise the genre with in-game purchases.
Incremental game (Wikipedia) - 2014Clicker Heroes
Clicker Heroes pioneers prestige mechanics in mobile games, and in 2015 is part of the wave of idle games arriving on Steam.
Incremental game (Wikipedia) - 2017Universal Paperclips
Universal Paperclips, by Frank Lantz, turns the "paperclip maximizer" thought experiment into a free incremental browser game with several stages and an ending.
Universal Paperclips (Wikipedia)
Reference games
2002
Progress Quest
You create a character and the game plays itself, slaying monsters and levelling up without your input. It parodies online role-playing games and is credited with starting idle games.
2010
Cow Clicker
You own a cow and can click it once every six hours, unless you pay or invite your friends. Researcher Ian Bogost made it as a satire of Facebook social games and their tricks to retain and charge players.
2013
A Dark Room
You wake up in a cold, dark room and can only stoke the fire. Bit by bit new actions open up (gathering wood, building a village, exploring the world) and the resource counter turns into a text adventure with an ending. Its code is free software (MPL 2.0).
2013
Candy Box 2
It starts as a candy counter and gradually opens up into a small role-playing game drawn in ASCII art. You buy gear, complete quests against monsters and earn items that unlock new areas. Its code is free software (GPLv3).
2013
Cookie Clicker
You click a giant cookie and spend what you make on buildings that bake on their own, each pricier than the last. Its "ascension" system, which restarts the game in exchange for permanent bonuses, became the template for prestige in the genre.
2014
AdVenture Capitalist
You start with a lemonade stand and buy ever larger businesses. Hiring managers makes them run on their own and keep earning even when the game is closed; resetting gives you "angel investors" that multiply your income.
2014
Clicker Heroes
You click monsters to defeat them and use the gold to hire heroes who attack on their own. "Ascending" resets your progress in exchange for hero souls, which buy permanent upgrades; it was among the first to bring prestige to mobile games.
2017
Universal Paperclips
You are an artificial intelligence whose only goal is making paperclips. You start by clicking to make one and end up turning all the matter in the universe into paperclips. It is based on the "paperclip maximizer" thought experiment and, unlike many incremental games, it has an ending.
Exercise
In the demo, change COST_GROWTH from 1.15 to 1.07 and then to 1.30, and note how long it takes to reach 1 M and when you stop buying collectors. Then work out in a spreadsheet the price of the first 20 collectors (10 × 1.15^n) and compare it with what they produce. When is it better to move on to the drone?