Files
CookieMonster/src/Disp/HelperFunctions/CalculateSingleTileChances.js
2025-11-10 21:33:21 +01:00

241 lines
8.4 KiB
JavaScript

/* eslint-disable guard-for-in */
/* eslint-disable no-continue */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-param-reassign */
/**
* Calculates the possible plants for a tile after the next garden tick.
*
* @param {Object} minigame A reference to the garden minigame
* @param {number} plantId The ID of the tile's current plant (-1 if empty)
* @param {number} age The age of the tile's current plant (ignored if empty)
* @param {Object[][]} neighResults The calculated results of each neighboring tile
* @param {number[]} cardinals The indices of orthogonally adjacent tiles in `neighResults`
* @param {number[]} tileBoosts The boosts to aging and weed spawning this tile is recieving from nearby tiles in the format `[ageMult, weedMult]`
* @param {number} dragonBoost The current calculated value of the boost from the "Supreme Intellect" dragon aura
* @returns {{plantId: number, isMature: boolean, p: number}[]} The possible resulting tile states and their probabilities
*/
export default function CalculateSingleTileChances(
minigame,
plantId,
age,
neighResults,
cardinals,
tileBoosts,
dragonBoost,
) {
const { getMuts, plants, plantsById, plantContam, soilsById, soil } = minigame;
const soilWeedMult = soilsById[soil].weedMult;
const epsilon = 1e-12;
const futureStates = [];
const AddOutcome = function (outcome) {
if (outcome.p < epsilon) return;
const existing = futureStates.find(
(o) =>
o.plantId === outcome.plantId &&
(outcome.plantId === -1 || o.isMature === outcome.isMature),
);
if (existing) existing.p += outcome.p;
else futureStates.push(outcome);
};
let runningP = 1;
if (plantId !== -1) {
// tile has plant
const currentPlant = plantsById[plantId];
const AgeThresholdP = function (multiplier, threshold) {
// probability that plant's age increases by at least threshold
const minAge = currentPlant.ageTick * multiplier;
const maxAge = minAge + currentPlant.ageTickR * multiplier;
if (Math.abs(maxAge - minAge) < epsilon) {
if (minAge >= threshold) return 1;
if (minAge < threshold - 1) return 0;
return minAge - (threshold - 1);
}
if (threshold <= minAge) return 1;
if (threshold - 1 >= maxAge) return 0;
const lower = Math.max(minAge, threshold - 1);
const upper = Math.min(maxAge, threshold);
const a = 0.5 * (upper - (threshold - 1)) ** 2 - 0.5 * (lower - (threshold - 1)) ** 2;
const b = Math.max(0, maxAge - Math.max(minAge, threshold));
return (a + b) / (maxAge - minAge);
};
// death
if (!currentPlant.immortal) {
const deathP = AgeThresholdP(tileBoosts[0] * dragonBoost, 100 - age);
AddOutcome({ plantId: -1, isMature: false, p: deathP * runningP });
runningP -= deathP * runningP;
}
// contamination
if (!currentPlant.noContam) {
const contamChances = [];
for (const key in plantContam) {
let inclusionP = plantContam[key];
if (plants[key].weed) inclusionP *= soilWeedMult;
contamChances.push([key, inclusionP]);
}
let totalContamP = 0;
for (const [key1, inclusionP1] of contamChances) {
if (plants[key1].id === plantId || inclusionP1 === 0) continue;
let hasNeighborP = 0;
for (const i of cardinals) {
let isNeighborP = 0;
for (const o of neighResults[i]) {
if (o.plantId === key1 && o.isMature) isNeighborP += o.p;
}
hasNeighborP += isNeighborP * (1 - hasNeighborP);
}
if (hasNeighborP < epsilon) continue;
let q = [1];
for (const [key2, inclusionP2] of contamChances) {
if (key1 !== key2) {
const newQ = Array(q.length + 1).fill(0);
for (let k = 0; k < q.length; k++) {
newQ[k] += q[k] * (1 - inclusionP2);
newQ[k + 1] += q[k] * inclusionP2;
}
q = newQ;
}
}
const E = q.reduce((acc, probXk, k) => acc + probXk / (1 + k), 0);
const contamP = inclusionP1 * E * hasNeighborP * runningP;
AddOutcome({ plantId: plants[key1].id, isMature: false, p: contamP });
totalContamP += contamP;
}
runningP -= totalContamP;
}
// survival (remaining probability)
const maturityP = AgeThresholdP(tileBoosts[0] * dragonBoost, currentPlant.mature - age);
AddOutcome({ plantId, isMature: true, p: maturityP * runningP });
AddOutcome({ plantId, isMature: false, p: (1 - maturityP) * runningP });
} else {
// tile is empty
let noNeighborsChance = 1;
neighResults.forEach((neighbor) => {
let emptyChance = 0;
const emptyOutcome = neighbor.find((outcome) => outcome.plantId === -1);
if (emptyOutcome) emptyChance = emptyOutcome.p;
noNeighborsChance *= emptyChance;
});
// weeds
const weedChance = 0.002 * soilWeedMult * tileBoosts[1];
AddOutcome({ plantId: 13, isMature: false, p: weedChance * noNeighborsChance });
AddOutcome({ plantId: -1, isMature: false, p: (1 - weedChance) * noNeighborsChance });
runningP -= noNeighborsChance;
// mutation
if (noNeighborsChance < 1) {
let totalMutationP = 0;
const CombineNeighbors = function (i = 0, current = [], comboP = 1, results = []) {
if (i >= neighResults.length) {
results.push({ tiles: current.slice(), comboP });
return results;
}
for (const state of neighResults[i])
CombineNeighbors(i + 1, [...current, state], comboP * state.p, results);
return results;
};
const loopsBase = (soilsById[soil].key === 'woodchips' ? 3 : 1) * dragonBoost;
for (const combo of CombineNeighbors()) {
// possible combinations of neighboring tiles
const neighs = {};
const neighsM = {};
for (const tile of combo.tiles) {
if (tile.plantId === -1) continue;
const { key } = plantsById[tile.plantId];
neighs[key] = (neighs[key] || 0) + 1;
if (tile.isMature) neighsM[key] = (neighsM[key] || 0) + 1;
}
const muts = getMuts(neighs, neighsM);
let perLoopMutationP = 0; // probability that any mutation occurs on a single loop with this combo
const mutationChances = [];
for (const [key, mutValue] of muts) {
let inclusionP = mutValue;
if (plants[key].weed) inclusionP *= soilWeedMult;
if (plants[key].weed || plants[key].fungus) inclusionP *= tileBoosts[1];
if (inclusionP > 0) mutationChances.push([key, inclusionP]);
}
const perLoopMutations = [];
for (const [key1, inclusionP1] of mutationChances) {
let q = [1];
for (const [key2, inclusionP2] of mutationChances) {
if (key1 !== key2) {
const newQ = Array(q.length + 1).fill(0);
for (let k = 0; k < q.length; k++) {
newQ[k] += q[k] * (1 - inclusionP2);
newQ[k + 1] += q[k] * inclusionP2;
}
q = newQ;
}
}
const E = q.reduce((acc, probXk, k) => acc + probXk / (1 + k), 0);
const mutationP = inclusionP1 * E;
perLoopMutationP += mutationP;
perLoopMutations.push([key1, mutationP * combo.comboP * runningP]);
}
if (perLoopMutationP === 0) continue;
let loopsBoost = 1;
if (loopsBase > 1) {
if (Number.isInteger(loopsBase)) {
loopsBoost = (1 - (1 - perLoopMutationP) ** loopsBase) / perLoopMutationP;
} else {
const loopsFloor = Math.floor(loopsBase);
const loopsFrac = loopsBase % 1;
const boostN = (1 - (1 - perLoopMutationP) ** loopsFloor) / perLoopMutationP;
const boostNp1 = (1 - (1 - perLoopMutationP) ** (loopsFloor + 1)) / perLoopMutationP;
loopsBoost = (1 - loopsFrac) * boostN + loopsFrac * boostNp1;
}
}
for (const [key, mutationP] of perLoopMutations) {
AddOutcome({ plantId: plants[key].id, maturityP: 0, p: mutationP * loopsBoost });
totalMutationP += mutationP * loopsBoost;
}
}
runningP -= totalMutationP;
}
// nothing (remaining probability)
AddOutcome({ plantId: -1, isMature: false, p: runningP });
}
const total = futureStates.reduce((sum, o) => sum + o.p, 0);
if (Math.abs(total - 1) > 1e-9) {
futureStates.forEach((o) => {
o.p /= total;
});
}
return futureStates;
}