Skip to main content
💻 Null Notes

The three laws of incremental games

The following are the three laws of incremental games:

  1. It should have at least one way to generate a resource
  2. It should have at least one thing to spend that resource
  3. It should have at least some way to automate the generation of that resource

I consider these to be the essential features that a game must implement in order to be considered an incremental game.

Demo #

Here's an example of a very small game that implements the laws.

Play the game:

Click at least three times.Good! Now try to get your number up to 100. Great job! Can you make it to 1,000?

Total clicks ever: ; Auto Clickers: ( clicks/second)

Source code for the game
<style>

.fadedout {
    opacity: 0;
}

.fadein {
    opacity: 1;
}

.remove {
    display:none;
}

.fadedout, .fadein {
    -webkit-transition: all 1s ease-in-out;
    -moz-transition: all 1s ease-in-out;
    -o-transition: all 1s ease-in-out;
    -ms-transition: all 1s ease-in-out;
    transition: all 1s ease-in-out;
}

/* CSS */
.some-button {
  align-items: center;
  appearance: none;
  background-color: #FCFCFD;
  border-radius: 4px;
  border-width: 0;
  box-shadow: rgba(45, 35, 66, 0.4) 0 2px 4px,rgba(45, 35, 66, 0.3) 0 7px 13px -3px,#D6D6E7 0 -3px 0 inset;
  box-sizing: border-box;
  color: #36395A;
  cursor: pointer;
  display: inline-flex;
  height: 48px;
  font-family: Consolas, monospace;
  justify-content: center;
  line-height: 1;
  list-style: none;
  overflow: hidden;
  padding-left: 16px;
  padding-right: 16px;
  position: relative;
  text-align: left;
  text-decoration: none;
  transition: box-shadow .15s,transform .15s;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  white-space: nowrap;
  will-change: box-shadow,transform;
  font-size: 18px;
}

.some-button:focus {
  box-shadow: #D6D6E7 0 0 0 1.5px inset, rgba(45, 35, 66, 0.4) 0 2px 4px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #D6D6E7 0 -3px 0 inset;
}

.some-button:hover {
  box-shadow: rgba(45, 35, 66, 0.4) 0 4px 8px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #D6D6E7 0 -3px 0 inset;
  transform: translateY(-2px);
}

.some-button:active {
  box-shadow: #D6D6E7 0 3px 7px inset;
  transform: translateY(2px);
}

.gamecontainer {
    display:flex;
    flex-direction: column;
    align-content: stretch;
}

</style>

<div class="gamecontainer">
<p><button id="somebutton" class="some-button">Add&nbsp;<span id="increment-value"></span>&nbsp;to this &rarr;&nbsp;<span id="somecounter">0</span></button> <button id="someupgrade-button" class="some-button fadedout">Spend&nbsp;<span id="upgrade-cost"></span>&nbsp;for +1/click</button> <button id="buyAutoClickerButtonElement" class="some-button fadedout">Spend&nbsp;<span id="buyAutoClickerButtonUpgradeCostElement"></span>&nbsp;for +1 click/second</button></p>

<p id="helpTextContainer"><span id="helptext1">Click at least three times.</span><span id="helptext2" class="fadedout">Good! Now try to get your number up to 100.</span><span id="helptext3" class="fadedout"> Great job! Can you make it to 1,000?</span><div id="autoClickersStatsContainer" class="fadedout">Total clicks ever: <span id="totalClicksEver"></span>; Auto Clickers: <span id="numAutoClickers"></span> (<span id="autoClickerCPS"></span> clicks/second)</div></p>
</div>

<script type="text/javascript">
var totalClicksEver = 0;
var e1value = 0;
var incrementValue = 1;
var upgradeCost = 1;
var buyAutoClickerButtonUpgradeCost = 100;

const setInterval2 = (fn,time) => {
    // A place to store the timeout Id (later)
    let timeout = null;

    // calculate the time of the next execution
    let nextAt = Date.now() + time;

    const wrapper = () => {
        // calculate the time of the next execution:
        nextAt += time;

        // set a timeout for the next execution time:
        timeout = setTimeout(wrapper, nextAt - Date.now());

        // execute the function:
        return fn();
    };

    // Set the first timeout, kicking off the recursion:
    timeout = setTimeout(wrapper, nextAt - Date.now());

        // A way to stop all future executions:
    const cancel = () => clearTimeout(timeout);

    // Return an object with a way to halt executions:
    return { cancel };
};

const generators =
    {
        autoClickers: {
            name_singular: 'Auto Clicker',
            name_plural: 'Auto Clickers',
            numPurchased: 0,
            upgradeCost: 100,
            unlocked: true,
            purchased: false, // Really just lets us know the first one has been purchased
            unlock: function() {
                unlocked = true;
            },
            canAfford: function() { return this.upgradeCost <= e1value },
            buyOne: function() {
                if(!this.purchased) { this.purchased = true; }
                e1value -= this.upgradeCost;
                this.numPurchased += 1;
                this.upgradeCost += 115;
            },
            update: function() { // Called once every second IF purchased
                handleBulkClicks(this.numPurchased);
            }

        }
    }


const triggers = [
    { // Show some help text to the user at the beginning
        fired: false,
        condition: function() { return e1value >= 100 },
        event: function() {
            fadeOut('helptext2');
            fadeOut('helptext2');
            fadeIn('helptext3');
        }
    },
    {   // Show the auto clickers count label
        fired: false,
        condition: function() { return generators.autoClickers.numPurchased > 0 },
        event: function() {
            fadeIn('autoClickersStatsContainer');
            fadeOut('helpTextContainer');
        }
    },
    {
        fired: false,
        condition: function() { return e1value >= 3 },
        event: function() {
            fadeIn('someupgrade-button');
            fadeOut('helptext1');
            fadeIn('helptext2');
        }
    },
    {
        fired: false,
        condition: function() { return upgradeCost >= 5 },
        event: function() {
            generators.autoClickers.unlocked = true;
            fadeIn('buyAutoClickerButtonElement');
        }
    }
]

var e1valueElement = document.getElementById('somecounter');
var upgradeCostElement = document.getElementById('upgrade-cost');
var incrementValueElement = document.getElementById('increment-value')
var buyAutoClickerButtonUpgradeCostElement = document.getElementById('buyAutoClickerButtonUpgradeCostElement')

function fadeIn(someElementId) {
    document.getElementById(someElementId).classList.add('fadein');
}

function fadeOut(someElementId) {
    document.getElementById(someElementId).classList.add('fadedout');
    document.getElementById(someElementId).classList.add('remove');
    document.getElementById(someElementId).classList.remove('fadein');
}

function handleClick() {
    totalClicksEver += incrementValue;
    e1value += incrementValue;
    e1valueElement.textContent = parseInt(e1value);
    updateAll();
}

function handleBulkClicks(clicks) {
    totalClicksEver += (incrementValue * clicks);
    e1value += (incrementValue * clicks);
    e1valueElement.textContent = parseInt(e1value);
    updateAll();
}

function handleBuy() {
    e1value -= upgradeCost;

    upgradeCost += Math.floor(incrementValue / 2);
    incrementValue += 1;

    updateAll();
}

function buyAutoClicker() {
    generators.autoClickers.buyOne();
    updateAll();
}

function updateAll() {
    updatePrices();
    checkUnlocks();
    updateButtonClickability();
}

function updateGenerators() {
    if(generators.autoClickers.purchased) { generators.autoClickers.update()}
}

function updatePrices() { // Should be "updatePricesAndStats"
    e1valueElement.textContent = parseInt(e1value);
    upgradeCostElement.textContent = parseInt(upgradeCost);
    incrementValueElement.textContent = parseInt(incrementValue);
    buyAutoClickerButtonUpgradeCostElement.textContent = generators.autoClickers.upgradeCost;
    document.getElementById('totalClicksEver').textContent = parseInt(totalClicksEver);
    document.getElementById('numAutoClickers').textContent = parseInt(generators.autoClickers.numPurchased);
    document.getElementById('autoClickerCPS').textContent = parseInt(generators.autoClickers.numPurchased*incrementValue);
}

function a() {
    for(let a =0; a<100; a++) {
        handleClick();
    }
}

function checkUnlocks() {
    triggers.forEach( (t) => {
        if(t.condition()) {
            t.fired = true;
            t.event();
        }
    });
}

function updateButtonClickability() {
    document.getElementById('someupgrade-button').disabled = (e1value <= upgradeCost) ? true : false;
    document.getElementById('buyAutoClickerButtonElement').disabled = (e1value <= generators.autoClickers.upgradeCost) ? true : false;

}

document.getElementById('somebutton').addEventListener('click', handleClick, false);
document.getElementById('someupgrade-button').addEventListener('click', handleBuy, false);
document.getElementById('buyAutoClickerButtonElement').addEventListener('click', buyAutoClicker, false);
updatePrices();
setInterval2(updateGenerators,1000);
</script>
<noscript>
You'll need to enable JavaScript and use a browser that supports it to play this demo.
</noscript>