Ex­po­nen­tial Idle Guides

Day 5: Bal­ance in All Things

Guide writ­ten by prop. Con­tri­bu­tions from the Amaz­ing Com­munity.

Feel free to use the gloss­ary as needed.

Hola. It is dawn of the fifth day.

Today we are go­ing to bring the the­ory down to earth, be­cause ad­mit­tedly, big num­bers are very bland, and in this guide, you have to listen to me, be­cause this world needs sav­ing from mean­ing­less dopam­ine-in­du­cing di­ver­gence.

In this les­son, we will learn about a way to look at the the­ory’s bal­ance, so that we can fix its di­ver­gence. Then, we will con­tinue by im­ple­ment­ing a new term that util­ises the the­ory’s stor­age, called the in­ternal state.

A new frame­work #

When it comes to bal­an­cing, there is a rudi­ment­ary yet supris­ing way of bal­an­cing de­vised by me, which has yet to be talked about within these circles. That is, view­ing each term as a con­tri­bu­tion to the total in­come ‘power’. Let me in­tro­duce the concept:

Your tau is rep­res­en­ted by 100%, a goal to be beaten. Within a pub­lic­a­tion, your terms and pub­lic­a­tion mul­ti­plier com­bine to­gether to try to get past this goal, within fi­nite time. The stronger they are, the less time it takes to reach your pre­vi­ous high score. So dis­reg­ard­ing the ini­tial cost of up­grades and that fi­nite time, we can rep­res­ent the the­ory’s total power as the sum of each ter­m’s power, ex­pressed log­ar­ith­mic­ally (re­mem­ber this). The total power is usu­ally a num­ber close to 1, or 100%, where the the­ory does not de­cay. The lower it is from 100%, the faster it de­cays, e.g. 90% de­cays faster than 95%; while it al­ways di­verges with a total power greater than 100%.

When ap­ply­ing this frame­work:

Let’s ex­am­ine a ter­m’s growth, start­ing with the simplest up­grade: c2=2x. I will give the for­mula for its power as 1/cost pro­gress, and this will make sense as I give ex­amples. Ima­gine that c2 had a cost pro­gress of 1, which means each level is twice as ex­pens­ive as the pre­vi­ous. However, when you buy a level, your in­come doubles, which means that the next level will cost the same amount of time as the pre­vi­ous, and the next of the next, ad in­fin­itum. In this case, c2’s power is cal­cu­lated to be 1/1=1, ex­actly 100%. In­deed, if your the­ory con­sists of only this c2, it shall never de­cay.

Let’s change the cost pro­gress to 2. Each level is now four times as ex­pens­ive, so the next level will take 4/2=2 times the amount of time to reach as the pre­vi­ous level. In this case, c2’s power is cal­cu­lated to be 1/2=0.5, or 50%.

Got the hang of it?

Next, let’s ex­am­ine the up­grade c1, a step­wise up­grade with base 2 and length 5. For this up­grade, 5 levels are needed for it to nearly double in value, which means that asymp­tot­ic­ally, its power has a for­mula of 1/(length×cost pro­gress). For ex­ample, a c1 up­grade with a cost pro­gress of 1, and base 2 length 5 as be­fore, has a power of 1/(5×1)=0.2, or 20%.

For vari­ations of c2 with a dif­fer­ent base than 2, such as ex or 3x, the power for­mula be­comes log2base/cost pro­gress. This is the case with our up­grade f, which is pro­por­tion­ate to ϕx (ϕ be­ing the golden ra­tio), and so its power is rep­res­en­ted as log2ϕ/cost pro­gress.

For the pub­lic­a­tion mul­ti­plier, the ad­ded power is simply tau’s ex­po­nent in the for­mula, scaled ac­cord­ing to the cur­rency-to-tau con­ver­sion rate.

With our powers com­bined #

Let’s ap­ply this frame­work to our the­ory.

Sum­mat­ing all the terms yields a total power of 1.062, or 106.2%. No won­der it’s skyrock­et­ing so hard. We haven’t even taken into ac­count the mile­stone that we had im­ple­men­ted yes­ter­day (in this case, the ex­po­nent 1.15 would be mul­ti­plied to c1 to yield 23%).

We can see that f is quite the most power­ful up­grade in our the­ory. While we want to re­duce its power over­all, we don’t want it to be­come so bad that the early game slows down to a crawl. Let’s in­tro­duce a new tool to our bal­an­cing toolkit: com­pos­ite costs. These al­low the the­ory to split the up­grade into two lay­ers, each with its own cost model:

import { CompositeCost, ExponentialCost, FreeCost, LinearCost } from '../api/Costs';

let init = () =>
{
    ...
    {
        f = theory.createUpgrade(2, currency, new CompositeCost(30,
        new ExponentialCost(100, 1.618034),
        new ExponentialCost(1e16, 1.618034 * 1.5)));
        ...
    }
}

The ar­gu­ments for an Com­pos­ite­Cost con­structor are as fol­lows:

These mod­els can be of any type, from ex­po­nen­tial to lin­ear, to free, or even to an­other com­pos­ite cost. We will also be mak­ing the first model faster by re­du­cing the ini­tial cost from 200 to 100. Now, in game, you won’t see this in ac­tion yet, since your levels may be well over a mil­lion, but you can re­set the the­ory in its set­tings in or­der to play again.

To do this, head to the the­ory se­lec­tion menu by press­ing the switch sym­bol at the top left corner, then head­ing to My The­ory. Click on the cog to ac­cess the set­tings, then simply hit Re­set. You will go back to 0 tau and all other pro­gress has been re­set. And as you be­grudgingly go through the trenches of… Oh? You need hacks? Take this line, try past­ing it onto SDK’s com­mand line (in the bot­tom left corner).

currency.value *= 10;

As you reach f level 30, you no­tice that f’s cost jumps down as the com­pos­ite cost switches to the second model. This won’t af­fect any­thing, but your play­ers might be an­noyed by that. Simply set the second cost’s ini­tial cost to something higher, or just leave it be, be­cause who does­n’t want lower costs to go faster?

Cal­cu­lat­ing re­bal­anced powers #

Let’s take a look at each ter­m’s power again:

Sum­mat­ing all terms yields 0.9493, or 94.93%. Con­grat­u­la­tions! We have suc­cess­fully cre­ated a mar­gin for new up­grades to be in­cluded in the fu­ture.

Time to ac­cu­mu­late new friends #

So far, our up­grades have been quite straight for­ward: click, and the in­come grows in­stantly. However, in many the­or­ies, both base game and cus­tom ones, there are a num­ber of up­grades that grows the value of an­other term, which is then mul­ti­plied to the in­come. These are called cu­mu­lat­ive up­grades, and are com­monly called q be­cause of the name (do not listen to those who name their in­stant up­grades q!)

Since the early game is quite covered, let’s po­s­i­tion the q up­grade a bit fur­ther into pro­gres­sion. One of the ways to do this, is to re­quire a mile­stone to un­lock the up­grade. Let’s make a mile­stone that un­locks q:

let qMs;

let init = () =>
{
    ...
    {
        qMs = theory.createMilestoneUpgrade(1, 1);
        qMs.description = Localization.getUpgradeAddTermDesc('q');
        qMs.info = Localization.getUpgradeAddTermInfo('q');
        qMs.boughtOrRefunded = (_) =>
        {
            theory.invalidatePrimaryEquation();
            updateAvailability();
        }
    }

    updateAvailability();
}

var updateAvailability = () => {};

Sim­ilar to the c1 ex­po­nent mile­stone, we as­sign static strings, and up­date the primary equa­tion when we buy or re­fund it. However, in or­der to make the q up­grade also ap­pear and dis­ap­pear (al­though at this point we haven’t pro­grammed it), let’s make an up­dateAvail­ab­il­ity func­tion and call it here, as well as at the end of init. Let’s leave it empty for now.

Next, let’s add an up­grade for q be­fore the term it­self, so that we have something to put into up­dateAvail­ab­il­ity:

let qdot;

let init = () =>
{
    ...
    {
        qdot = theory.createUpgrade(4, currency, new ExponentialCost(1e45, 2.5));
        let getDesc = (level) => `\\dot{q} = ${getqdot(level).toString(0)}`;
        qdot.getDescription = (amount) => Utils.getMath(getDesc(qdot.level));
        qdot.getInfo = (amount) => Utils.getMathTo(getDesc(qdot.level),
        getDesc(qdot.level + amount));
    }
}

var updateAvailability = () =>
{
    qdot.isAvailable = qMs.level > 0;
};

let getqdot = (level) => Utils.getStepwisePowerSum(level, 2, 10, 0);

Al­though I have been call­ing the up­grade q, its proper name is q˙ (‘q dot’), which in­dic­ates the change of q ac­cord­ing to time. This is called the dot nota­tion, or New­ton’s nota­tion, after Isaac New­ton. The game uses this nota­tion be­cause it is much more com­pact com­pared to Leib­n­iz’s nota­tion (dq/dt).

Now, let’s ac­tu­ally im­ple­ment the q term, and dis­play its value on screen:

let q = BigNumber.ONE;

var tick = (elapsedTime, multiplier) =>
{
    ...
    // Don't forget to double check your implementation!
    let dq = qMs.level ? dt * getqdot(qdot.level) : BigNumber.ZERO;
    q += dq;

    currency.value += dt * bonus * getc1(c1.level).pow(getc1Exp(c1ExpMs.level)) * getc2(c2.level) * (BigNumber.ONE + getf(f.level)) * (qMs.level ? q : BigNumber.ONE);

    theory.invalidateTertiaryEquation();
}

var getPrimaryEquation = () => `\\dot{\\rho} = c_1${c1ExpMs.level ? `^{${getc1Exp(c1ExpMs.level)}}` : ''}c_2(1+f)${qMs.level ? 'q' : ''}`;

var getTertiaryEquation = () => qMs.level ? `q = ${q.toString()}` : '';

var postPublish = () =>
{
    q = BigNumber.ONE;
}

This snip­pet of code has a lot to un­pack. The simple idea is that, for every tick, an amount of dq is ad­ded onto our cu­mu­lat­ive vari­able q, which is cal­cu­lated by our q˙ up­grade times the amount of time passed. Our in­come is then mul­ti­plied by q amongst other terms. However, what hap­pens when we don’t have the mile­stone, is that q has to stop grow­ing (dq is sub­sti­tuted with zero), and q in the in­come for­mula is sub­sti­tuted with one since we do not have the term un­locked. The primary and ter­tiary equa­tions are also re­flect­ive of these changes, and the ter­tiary equa­tion spe­cific­ally needs to be up­dated every tick to track the q value. Fi­nally, q should be re­set every time we hit Pub­lish, in the post­Pub­lish func­tion.

A the­ory’s in­ternal state #

Now, hit Save again on your the­ory file (without chan­ging any­thing) and wait for the SDK to up­load it. No­tice that your q has been re­set back to one! (I am such a mean teacher). The reason why this hap­pens, is that un­like up­grade levels, cur­ren­cies, or mile­stones, terms like q are just reg­u­lar pro­gram­ming vari­ables, which aren’t saved any­where out­side of the scope that it’s run­ning in. In or­der to save these vari­ables, the game provides us a way to store (seri­al­ise) them, in the form of an in­ternal state. The in­ternal state is stored as a string, and is loaded once after everything in the the­ory file has been loaded (which means it is run even after init).

The the­ory provides two func­tions for hand­ling the in­ternal state:

In most ex­amples of the­ory im­ple­ment­a­tions you’ve seen, either in the samples folder, or even cus­tom the­or­ies, the in­ternal state is of­ten laid out in a straight line. While this can save space, it will def­in­itely be a pain in your tooshie if you de­cide to change the state format by in­sert­ing or re­arran­ging ele­ments. Be­cause of this, I will in­tro­duce to you the JSON method, which al­lows the in­ternal state to be saved in a key-and-value format:

var getInternalState = () => JSON.stringify
({
    q: q.toBase64String()
});

var setInternalState = (stateStr) =>
{
    if(!stateStr)
        return;

    let state = JSON.parse(stateStr);
    q = BigNumber.fromBase64String(state.q) ?? q;
}

This uses JavaS­cript’s JSON namespace, with string­ify to seri­al­ise, and parse to deseri­al­ise. You may no­tice that I’m also us­ing other mys­ter­i­ous meth­ods here: to­Base64String and from­Base64String. In JavaS­cript, un­like plain ob­jects, in­stances of a class (such as BigNum­ber, or even your own classes) can’t in­nately be seri­al­ised into JSON[^1]. In­stead, BigNum­ber (of which the q vari­able is an in­stance) uses to­Base64String to seri­al­ise into a string, and from­Base64String to deseri­al­ise from it. So, con­tained in this state ob­ject is a key q, which stores the base 64 rep­res­ent­a­tion of the vari­able q, which can be ac­cessed with state.q.

In this snip­pet, we also en­counter a new syn­tax: the double ques­tion mark ??. This is called the nullish co­ales­cence, and it is used where we don’t want null to be as­signed to our vari­able, and in­stead, the right hand side would be what that as­sign­ment de­faults to, if the left side hap­pens to be null.

Now, save the the­ory, and let it run for a minute. Thanks to the in­ternal state, whenever you go off­line or switch to an­other the­ory, q’s value should be pre­served.

Af­ter­math #

Be­fore we end our ses­sion today, let’s take a look at what we had dis­cussed earlier: term powers. q˙ is a step­wise up­grade of base 2 length 10, with a cost pro­gress of 2.5. Un­der this frame­work, it should have a power of 1/(10×2.5)=0.04, or 4%, which brings the total power of the the­ory from 94.93% to 98.93%. While how much q’s cu­mu­lat­ive nature en­hances its power (or whether it even does so at all) is still a mys­tery, we can be sure that we did make pro­gress today, so I shall see you to­mor­row.

Mean­while, the source code after today’s work can be found here:

import { BigNumber } from '../api/BigNumber';
import { CompositeCost, ExponentialCost, FreeCost, LinearCost } from '../api/Costs';
import { Localization } from '../api/Localization';
import { theory } from '../api/Theory';
import { Utils } from '../api/Utils';

var id = 'my_theory';
var name = 'My Theory';
var description = 'The one and only.';
var authors = 'Stuart Clickus';

let currency;
let clicker;
let c1, c2;
let f;
let qdot;
let c1ExpMs, qMs;

let q = BigNumber.ONE;

let init = () =>
{
    currency = theory.createCurrency();

    {
        clicker = theory.createUpgrade(0, currency, new FreeCost);
        clicker.description = Utils.getMath('\\rho \\leftarrow \\rho + 1');
        clicker.info = 'Increases currency by 1';
        clicker.bought = (amount) => currency.value += 1;
    }

    {
        c1 = theory.createUpgrade(1, currency, new ExponentialCost(10, 1));
        let getDesc = (level) => `c_1 = ${getc1(level).toString(0)}`;
        let getInfo = (level) =>
        {
            if(c1ExpMs.level)
                return `c_1^{${getc1Exp(c1ExpMs.level)}}=
                ${getc1(level).pow(getc1Exp(c1ExpMs.level)).toString()}`;
            return getDesc(level);
        }

        c1.getDescription = (amount) => Utils.getMath(getDesc(c1.level));
        c1.getInfo = (amount) => Utils.getMathTo(getInfo(c1.level),
        getInfo(c1.level + amount));
    }

    {
        c2 = theory.createUpgrade(3, currency, new ExponentialCost(500, 3));
        let getDesc = (level) => `c_2 = ${getc2(level).toString(0)}`;
        c2.getDescription = (amount) => Utils.getMath(`c_2 = 2^{${c2.level}}`);
        c2.getInfo = (amount) => Utils.getMathTo(getDesc(c2.level),
        getDesc(c2.level + amount));
    }

    {
        f = theory.createUpgrade(2, currency, new CompositeCost(30,
        new ExponentialCost(100, 1.618034),
        new ExponentialCost(1e16, 1.618034 * 1.5)));
        let getDesc = (level) => `f = ${getf(level).toString(0)}`;
        f.getDescription = (amount) => Utils.getMath(getDesc(f.level));
        f.getInfo = (amount) => Utils.getMathTo(getDesc(f.level),
        getDesc(f.level + amount));
    }

    {
        qdot = theory.createUpgrade(4, currency, new ExponentialCost(1e45, 2.5));
        let getDesc = (level) => `\\dot{q} = ${getqdot(level).toString(0)}`;
        qdot.getDescription = (amount) => Utils.getMath(getDesc(qdot.level));
        qdot.getInfo = (amount) => Utils.getMathTo(getDesc(qdot.level),
        getDesc(qdot.level + amount));
    }

    theory.createPublicationUpgrade(0, currency, BigNumber.from('1e7'));
    theory.createBuyAllUpgrade(1, currency, BigNumber.from('1e12'));
    theory.createAutoBuyerUpgrade(2, currency, BigNumber.from('1e17'));
    
    theory.setMilestoneCost(new LinearCost(15, 15));

    {
        c1ExpMs = theory.createMilestoneUpgrade(0, 5);
        c1ExpMs.description = Localization.getUpgradeIncCustomExpDesc('c_1', '0.03');
        c1ExpMs.info = Localization.getUpgradeIncCustomExpInfo('c_1', '0.03');
        c1ExpMs.boughtOrRefunded = (_) => theory.invalidatePrimaryEquation();
    }

    {
        qMs = theory.createMilestoneUpgrade(1, 1);
        qMs.description = Localization.getUpgradeAddTermDesc('q');
        qMs.info = Localization.getUpgradeAddTermInfo('q');
        qMs.boughtOrRefunded = (_) =>
        {
            theory.invalidatePrimaryEquation();
            updateAvailability();
        }
    }

    updateAvailability();
}

var updateAvailability = () =>
{
    qdot.isAvailable = qMs.level > 0;
};

let getc1 = (level) => Utils.getStepwisePowerSum(level, 2, 5, 0);
let getc1Exp = (level) => 1 + 0.03 * level;

let getc2 = (level) => BigNumber.TWO.pow(level);

let getqdot = (level) => Utils.getStepwisePowerSum(level, 2, 10, 0);

const fibSqrt5 = BigNumber.FIVE.sqrt();
const fibA = (BigNumber.ONE + fibSqrt5) / BigNumber.TWO;
const fibB = (fibSqrt5 - BigNumber.ONE) / BigNumber.TWO;

let getf = (level) =>
{
    if(level % 2 == 0)
        return (fibA.pow(level) - fibB.pow(level)) / fibSqrt5;
    return (fibA.pow(level) + fibB.pow(level)) / fibSqrt5;
};

var tick = (elapsedTime, multiplier) =>
{
    let dt = BigNumber.from(elapsedTime * multiplier);
    let bonus = theory.publicationMultiplier;

    let dq = qMs.level ? dt * getqdot(qdot.level) : BigNumber.ZERO;
    q += dq;

    currency.value += dt * bonus * getc1(c1.level).pow(getc1Exp(c1ExpMs.level)) * getc2(c2.level) * (BigNumber.ONE + getf(f.level)) * (qMs.level ? q : BigNumber.ONE);

    theory.invalidateTertiaryEquation();
}

var getPrimaryEquation = () => `\\dot{\\rho} = c_1${c1ExpMs.level ? `^{${getc1Exp(c1ExpMs.level)}}` : ''}c_2(1+f)${qMs.level ? 'q' : ''}`;

var getSecondaryEquation = () => `${theory.latexSymbol} = \\max\\rho`;

var getTertiaryEquation = () => qMs.level ? `q = ${q.toString()}` : '';

var postPublish = () =>
{
    q = BigNumber.ONE;
}

var get2DGraphValue = () => currency.value.sign *
(BigNumber.ONE + currency.value.abs()).log10().toNumber();

const pubPower = 0.1;

var getPublicationMultiplier = (tau) => tau.pow(pubPower);

var getPublicationMultiplierFormula = (symbol) => `{${symbol}}^{${pubPower}}`;

var getTau = () => currency.value;

var getCurrencyFromTau = (tau) =>
[
    tau.max(BigNumber.ONE),
    currency.symbol
];

var getInternalState = () => JSON.stringify
({
    q: q.toBase64String()
});

var setInternalState = (stateStr) =>
{
    if(!stateStr)
        return;

    let state = JSON.parse(stateStr);
    q = BigNumber.fromBase64String(state.q) ?? q;
}

init();

[^1] Classes can be seri­al­ised, if the toJSON method is defined to con­vert them into ob­jects first, but BigNum­ber does­n’t have this method. For prim­it­ive vari­ables (strings and num­bers), seri­al­isa­tion looks much easier, as you can omit keys to auto­mat­ic­ally as­sign the vari­ables’ names to them:

let abc = 3;
let str = 'def';

var getInternalState = () => JSON.stringify
({
    abc,
    str
})

var setInternalState = (stateStr) =>
{
    if(!stateStr)
        return;

    let state = JSON.parse(stateStr);
    abc = state.abc ?? abc;
    str = state.str ?? str;
}