/* 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; }