Ex­po­nen­tial Idle Guides

Day 6: Di­men­sion Ex­pan­sion

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

Feel free to use the gloss­ary as needed.

Hi class. Now that we’ve dealt with our pre­vi­ous prob­lems, today we’re go­ing to pick up the pace. First, I will in­tro­duce you to the most com­mon player strategies, and through a new mile­stone, im­ple­ment it in our the­ory. Then, we will im­ple­ment a qual­ity of life fea­ture, in the form of a timer.

In­tro­duc­tion to player strategies #

Every game ever con­ceived is, and will be, sub­jec­ted to the evol­u­tion of strategies within its circle of play­ers. Idle games are no ex­cep­tion, as des­pite its ap­par­ent name, a lot of them are de­signed to re­ward player activ­ity, of­ten with a faster rate of pro­gress than simply id­ling about. And in Ex­po­nen­tial Idle, where everything is broken down into fun­da­mental arith­met­ics, this be­comes much easier to see.

Con­sid­er­ing the up­grades we have within our the­ory:

As you can see, each up­grade scales in a dif­fer­ent way. However, our auto­buy does not care, and in­stead treat every up­grade the same, al­ways buy­ing the cheapest one pos­sible, even though they aren’t worth the same. This gave rise to a strategy that is now com­mon within the com­munity: the doub­ling chase, de­noted by a lower case d in strategy no­men­clature. This strategy tells the player to save for the strongest up­grade (usu­ally a doub­ling, hence the name), while only manu­ally buy­ing worse up­grades at a frac­tion of that price, and tog­gling auto­buy off for them. In our the­ory, c2 is the strongest per-level up­grade, there­fore every other up­grade should be delayed ac­cord­ing to the doub­ling chase strategy.

For step­wise up­grades such as c1 and q˙, there is also an­other strategy to buy them de­pend­ing on where they are in the step­wise cycle. These are called mod x in strategy no­men­clature, with x cor­res­pond­ing to the cycle length. More in­form­a­tion about strategies in­volving step­wise up­grades can be found here.

Mile­stone swap­ping #

Pre­vi­ously, in Day 4, we were in­tro­duced to mile­stones, a way to lock power boosts be­hind pro­gres­sion. No­tice that un­like sim­ilar un­lock sys­tems in other games such as skill trees, mile­stones can be freely re­fun­ded. While this is primar­ily in­ten­ded to al­low ex­per­i­ment­a­tion, play­ers have found that in spe­cific situ­ations, switch­ing between dif­fer­ent mile­stones can fa­cil­it­ate much faster pro­gress than just stick­ing to one mile­stone. This is called mile­stone swap­ping.

In the­or­ies, the most com­monly im­ple­men­ted form of mile­stone swap­ping is where there is a mile­stone that boosts in­stant in­come, and an­other that boosts a cu­mu­lat­ive term. In our the­ory, the cu­mu­lat­ive term is q, and the in­stant terms are c1, c2, and f. As we already have a mile­stone that boosts c1, let’s im­ple­ment an­other that boosts q˙. To pre­vent the the­ory’s power from be­ing af­fected by this mile­stone, let’s make it a con­stant boost by 2x per level, in­stead of rais­ing an ex­po­nent:

let qdotMs;

let init = () =>
{
    ...
    {
        qdotMs = theory.createMilestoneUpgrade(2, 3);
        qdotMs.description = Localization.getUpgradeMultCustomDesc('\\dot{q}', '2');
        qdotMs.info = Localization.getUpgradeMultCustomInfo('\\dot{q}', '2');
    }
}

let getqdot = (level) => Utils.getStepwisePowerSum(level, 2, 10, 0) * Math.pow(2, qdotMs.level);

Now, if you re­load, you will see that in the mile­stone menu, the mile­stone will be shown as ×q˙ by 2. However, there is still a prob­lem. You may no­tice that you can take this mile­stone, even if its pre­requis­ite (Un­lock q) is not taken. Let’s fix it.

Every kind of up­grade you can im­ple­ment in this game has a vis­ib­il­ity at­trib­ute. This is called is­Avail­able, and it con­trols whether or not an up­grade is dis­played on screen. Let’s modify qdotMs so that its vis­ib­il­ity is con­trolled by qMs:

let init = () =>
{
    ...
    {
        ...
        qdotMs.isAvailable = false;
    }
}

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

If we re­load the the­ory now, the q˙ mile­stone will dis­ap­pear from the menu, un­less we have a point in the q un­lock mile­stone. Suc­cess!

Let’s also make it so that the q un­lock mile­stone can’t be re­fun­ded when we have also taken the q˙ mile­stone, by us­ing the can­BeRe­fun­ded at­trib­ute, which con­stantly up­dates when we as­sign to it an ar­row func­tion:

let init = () =>
{
    ...
    {
        ...
        qMs.canBeRefunded = () => qdotMs.level == 0;
    }
}

Now that we’ve suc­cess­fully im­ple­men­ted the new mile­stone, let’s take a look at how we can util­ise the mile­stone swap strategy to make our pro­gress faster. Let’s as­sume we are at a high score of 1e45 tau. We would have three mile­stone points, and we can also start to pur­chase q˙. Our first point should be spent on un­lock­ing q, which leaves us two points. Now, sup­pose we were to spend the two points on c1 ex­po­nent (our mile­stone con­fig­ur­a­tion would be 2/​1/​0). We would have a lot of in­come for ρ, but our q growth would be much less than if we were to spend those points on q˙. So now we have a q de­fi­cit. We trans­fer those two points to q˙ (our con­fig­ur­a­tion would be 0/​1/​2), which would max­im­ise q’s growth. But then, after a while, our ρ would be at a de­fi­cit, so we switch back to 2/​1/​0. This is the es­sence of mile­stone swap­ping, boost­ing our pro­gress fur­ther than if we were to stay all the time on only one con­fig­ur­a­tion[^1].

Timer time #

Now that our the­ory is com­plete in terms of con­tent, let’s di­vert our at­ten­tion to­wards qual­ity of life and pol­ish­ing for the rest of the week. Today, we will be im­ple­ment­ing a timer to show how much time we’ve spent in a pub­lic­a­tion. It won’t be in the form of a UI ele­ment (like in Riemann Zeta Func­tion), as cus­tom UI is out of the scope for this week. In­stead, we will be util­ising the qua­tern­ary area provided by the API. This area can be seen in The­ory 2 (Dif­fer­en­tial Cal­cu­lus), where cu­mu­lat­ive terms q1 to q4 and r1 to r4 are dis­played in a column.

First, let’s cre­ate a time vari­able. Call it t. t shall in­crease whenever tick() is called, and re­set whenever we pub­lish:

let t = 0;

var tick = (elapsedTime, multiplier) =>
{
    t += elapsedTime;
    ...
}

var postPublish = () =>
{
    t = 0;
    ...
}

Since t is also a nor­mal vari­able like q, to pre­vent los­ing it, we should also save it in the in­ternal state:

var getInternalState = () => JSON.stringify
({
    t,
    ...
});

var setInternalState = (stateStr) =>
{
    ...
    t = state.t ?? t;
}

Now, while our vari­able is func­tional, we have yet to dis­play it on screen. Let’s define a qua­tern­ary entry, then up­date it along with tick(). A qua­tern­ary entry is defined with two fields: its name and its value, dis­play­ing as the left and right side of the equal sign re­spect­ively.

let quaternary =
[
    new QuaternaryEntry('t', null)
];

var tick = (elapsedTime, multiplier) =>
{
    ...    
    theory.invalidateQuaternaryValues();
}

var getQuaternaryEntries = () =>
{
    quaternary[0].value = t.toFixed(1);
    return quaternary;
}

Now, we fi­nally see our pub­lic­a­tion time dis­played on the right side of the screen. The toFixed() method al­lows us to round the num­ber to one decimal place, so we don’t have to watch float­ing point pre­ci­sion go wild. Try pub­lish­ing!

Wait a minute #

Hold on. No­tice that if we go past 60 seconds, the timer al­ways dis­plays the whole num­ber of seconds. This is use­less in­form­a­tion. We would like to di­vide this timer dis­play to at least hours, minutes and seconds. Which means, we will need three qua­tern­ary entries.

let quaternary =
[
    new QuaternaryEntry('h', null),
    new QuaternaryEntry('m', null),
    new QuaternaryEntry('s', null)
];

Now, let’s fig­ure out how to count the num­ber of hours and minutes when we have the total num­ber of seconds. There are 60 seconds in a minute, and 60 minutes in an hour. First, let’s count the minutes from the seconds, and from the minutes, we then count the hours:

var getQuaternaryEntries = () =>
{
    let minutes = Math.floor(t / 60);
    let seconds = t - minutes * 60;
    let hours = Math.floor(minutes / 60);
    minutes -= hours * 60;

    quaternary[0].value = hours;
    quaternary[1].value = minutes;
    quaternary[2].value = seconds.toFixed(1);
    return quaternary;
}

Suc­cess! We have man­aged to dis­play the minutes and hours.

Ex­tra As­sign­ments #

To ex­er­cise what you’ve learned, let’s work on some pol­ish­ing touches:

  1. Try count­ing the num­ber of days.
  2. Move these entries closer to­gether by in­sert­ing an in­vis­ble qua­tern­ary entry on each side.
  1. Dis­play a lead­ing zero if the num­ber of hours/​minutes/​seconds is less than 10. That’s how di­gital watches work, right?

Af­ter­math #

Today, we have learned about player strategies, and how they im­pact the way we design our the­or­ies. We were also in­tro­duced to the last part of the equa­tion UI: the qua­tern­ary entries. I shall see you to­mor­row for the fin­ish­ing touches on this the­ory.

Mean­while, the source code after today’s work can be found here. This code does not con­tain solu­tions to as­sign­ments.

import { BigNumber } from '../api/BigNumber';
import { CompositeCost, ExponentialCost, FreeCost, LinearCost } from '../api/Costs';
import { Localization } from '../api/Localization';
import { QuaternaryEntry, 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, qdotMs;

let q = BigNumber.ONE;
let t = 0;

let quaternary =
[
    new QuaternaryEntry('h', null),
    new QuaternaryEntry('m', null),
    new QuaternaryEntry('s', null)
];

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();
        }
        qMs.canBeRefunded = () => qdotMs.level == 0;
    }

    {
        qdotMs = theory.createMilestoneUpgrade(2, 3);
        qdotMs.description = Localization.getUpgradeMultCustomDesc('\\dot{q}', '2');
        qdotMs.info = Localization.getUpgradeMultCustomInfo('\\dot{q}', '2');
        qdotMs.isAvailable = false;
    }

    updateAvailability();
}

var updateAvailability = () =>
{
    qdot.isAvailable = qMs.level > 0;
    qdotMs.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) * Math.pow(2, qdotMs.level);

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) =>
{
    t += elapsedTime;
    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();
    theory.invalidateQuaternaryValues();
}

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 getQuaternaryEntries = () =>
{
    let minutes = Math.floor(t / 60);
    let seconds = t - minutes * 60;
    let hours = Math.floor(minutes / 60);
    minutes -= hours * 60;

    quaternary[0].value = hours;
    quaternary[1].value = minutes;
    quaternary[2].value = seconds.toFixed(1);
    return quaternary;
}

var postPublish = () =>
{
    t = 0;
    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
({
    t,
    q: q.toBase64String()
});

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

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

init();

[^1] Tech­nic­ally, we have an­other con­fig­ur­a­tion yet to talk about: 1/​1/​1. Whether or not its ef­fect­ive­ness over­shad­ows the mile­stone swap will need fur­ther re­search into strategies. Still, even that con­fig­ur­a­tion would also need to be real­loc­ated to 2/​1/​0 to speed up ρ growth to­wards the end of a pub­lic­a­tion (this is called coast­ing). This strategy, al­though per­formed only once, also clas­si­fies as mile­stone swap­ping.