253 lines
12 KiB
JavaScript
253 lines
12 KiB
JavaScript
const MULTIPLIERS = {
|
|
"Rainbow": 50,
|
|
"Golden": 20,
|
|
"Frozen": 10,
|
|
"Bloodlit": 4,
|
|
"Pollinated": 3,
|
|
"Wet": 2,
|
|
"Chilled": 2
|
|
};
|
|
|
|
const BASELINE_GOLDEN_CHANCE = 0.01; // 1.0%
|
|
|
|
class Decision {
|
|
|
|
/**
|
|
* @param {Object} crop - { species, baseValue, scale, mutations: [] }
|
|
* @param {Object} worldEvents - {
|
|
* Time_Until_Next_Event_Hrs,
|
|
* Time_Assuming_Growth_Speed_Boost,
|
|
* Time_Remaining_Hrs,
|
|
* P_Next_Rain_Thunderstorm,
|
|
* P_Next_Frost,
|
|
* Time_To_Next_Blood_Moon_Hrs,
|
|
* Event_Active
|
|
* }
|
|
*/
|
|
static shouldHarvest(crop, worldEvents) {
|
|
// Step 3.1: Deterministic Value (V_HarvestNow)
|
|
const vHarvestNow = this.calculateDeterministicValue(crop);
|
|
|
|
// Step 3.2: Growth Progression & Time Compression
|
|
// Delta T effective
|
|
// If Event_Active is true, speed is 1.5x, else 1.0x
|
|
const speedMultiplier = worldEvents.Event_Active ? 1.5 : 1.0;
|
|
const deltaTEff = worldEvents.Time_Until_Next_Event_Hrs * speedMultiplier;
|
|
|
|
const tRemNext = worldEvents.Time_Remaining_Hrs - deltaTEff;
|
|
|
|
// Critical Check: If crop matures during wait, harvest NOW (continuing value is capped/complicated)
|
|
// Ignoring complicated capped value logic for now as per instructions "E is capped" -> imply we might just harvest?
|
|
// Actually instructions say: "If T_rem <= 0 ... calculation focuses only on expected mutation gains over Delta t"
|
|
// But usually if it matures, we might as well harvest or we risk rotting?
|
|
// For this V1 implementation, if it matures, let's assume we proceed with E calculation but T_rem is 0.
|
|
// However, the prompt says "If T_rem > 0, the harvest is delayed". Implicitly if <=0, maybe we consider it 'mature' state.
|
|
// Let's stick to the prompt's implied logic for now: calculate E regardless, but note the state.
|
|
|
|
// Step 3.3: Stochastic Transition Value (E)
|
|
const E = this.calculateStochasticTransition(crop, vHarvestNow, worldEvents);
|
|
|
|
// Step 3.4: Strategic Interval Check (Blood Moon)
|
|
let maxE = E;
|
|
let strategy = "Standard Wait";
|
|
|
|
// Check if we can wait for Blood Moon
|
|
// Use simplified logic: if generic wait leaves us with time, AND we have enough time for BM
|
|
// Note: T_rem(t + Delta_t) > 0 check from prompt
|
|
if (tRemNext > 0 && worldEvents.Time_To_Next_Blood_Moon_Hrs <= worldEvents.Time_Remaining_Hrs) {
|
|
const eBM = this.calculateBloodMoonValue(vHarvestNow);
|
|
if (eBM > E) {
|
|
maxE = eBM;
|
|
strategy = "Blood Moon Wait";
|
|
}
|
|
}
|
|
|
|
// Step 3.5: Optimal Decision Synthesis
|
|
const decision = {
|
|
action: vHarvestNow >= maxE ? "HARVEST_NOW" : "WAIT",
|
|
vHarvestNow: vHarvestNow,
|
|
expectedValue: maxE,
|
|
strategy: strategy,
|
|
details: {
|
|
E_Standard: E,
|
|
tRemNext: tRemNext
|
|
}
|
|
};
|
|
|
|
return decision;
|
|
}
|
|
|
|
static calculateDeterministicValue(crop) {
|
|
let multiplier = 1.0;
|
|
if (crop.mutations) {
|
|
crop.mutations.forEach(m => {
|
|
const val = MULTIPLIERS[m] || 1.0; // Default to 1 if unknown or wait? Assume multiplicative
|
|
multiplier *= val;
|
|
});
|
|
}
|
|
// Base Value * Mass * Multipliers
|
|
// Mass is 'scale'
|
|
return (crop.baseValue || 1) * (crop.scale || 1) * multiplier;
|
|
}
|
|
|
|
static calculateStochasticTransition(crop, vNow, worldEvents) {
|
|
const mutations = new Set(crop.mutations || []);
|
|
|
|
// A. Rain/Thunderstorm
|
|
// Base Gain: 50% chance of Wet (2x)
|
|
// Fusion: If Chilled present, Wet can fuse to Frozen (10x). Chilled (2x) is lost.
|
|
// Fusion Logic: "Rain ... has a chance to fuse it" - let's assume 100% chance IF Rain happens for this calculation?
|
|
// Prompt says: "If Chilled is present, Rain... has a chance". Let's assume the event IS Rain.
|
|
// Wait, structure is: E = P_Rain * V_Rain + ...
|
|
// Inside V_Rain: What is the gain?
|
|
// Prompt: "1. Calculate expected fusion gain factor".
|
|
// Assumption: If 'Chilled' exists, 2x becomes 10x? Or is it 50% chance?
|
|
// Prompt says "Rain... has a chance". Usually fusion is guaranteed if conditions met in these games?
|
|
// Let's assume for V1: If Rain hits, and Chilled exists => Frozen (10x).
|
|
// If Chilled NOT exists => Wet (2x) with 50% chance.
|
|
|
|
let multiplierGainA = 1.0;
|
|
if (mutations.has("Chilled")) {
|
|
// Fusion: Chilled (2x) -> Frozen (10x). Gain factor = 10 / 2 = 5x?
|
|
// Wait, vNow already includes Chilled(2x).
|
|
// New Multiplier Product would be: (Old_Prod / 2) * 10 = Old_Prod * 5.
|
|
// So Gain Factor is 5.
|
|
// Probability of fusion? "has a chance". Let's assume 100% GIVEN Rain for now, or 50%?
|
|
// "Base Gain: 50% chance of 2x Wet".
|
|
// Let's assume Fusion is also 50% chance if Chilled is there? Or maybe the event IS the fusion?
|
|
// Let's use 50% for fusion chance given Rain.
|
|
multiplierGainA = 1.0 + 0.5 * (5.0 - 1.0); // Exp Value of factor?
|
|
// Actually, let's look at value:
|
|
// avg_mult = 0.5 * 5 (Fusion) + 0.5 * 1 (Nothing) = 3.0 relative to current?
|
|
// No, if nothing happens, it stays 1.0.
|
|
// So E[Factor] = 0.5 * 5 + 0.5 * 1 = 3.0?
|
|
} else {
|
|
// Wet (2x). Gain from 1.0 to 2.0.
|
|
// If we already have Wet? Usually mutations are unique?
|
|
// Prompt says "Identify all current, UNIQUE mutations".
|
|
// If we have Wet, can we get double Wet? Assume No.
|
|
if (!mutations.has("Wet") && !mutations.has("Frozen")) { // Assuming Frozen prevents Wet?
|
|
multiplierGainA = 1.0 + 0.5 * (2.0 - 1.0); // 0.5 * 2 + 0.5 * 1 = 1.5
|
|
}
|
|
}
|
|
const V_A = vNow * multiplierGainA;
|
|
|
|
// B. Frost
|
|
// Base Gain: 50% chance of Chilled (2x).
|
|
// Fusion: If Wet present -> Frozen (10x). Replaces Wet (2x).
|
|
let multiplierGainB = 1.0;
|
|
if (mutations.has("Wet")) {
|
|
// Fusion: Wet(2x) -> Frozen(10x). Factor = 5.
|
|
// Chance: 50%?
|
|
multiplierGainB = 1.0 + 0.5 * (5.0 - 1.0); // 3.0
|
|
} else {
|
|
if (!mutations.has("Chilled") && !mutations.has("Frozen")) {
|
|
multiplierGainB = 1.0 + 0.5 * (2.0 - 1.0); // 1.5
|
|
}
|
|
}
|
|
const V_B = vNow * multiplierGainB;
|
|
|
|
// C. Other Events (Baseline)
|
|
// 1.0% chance of Golden (20x).
|
|
// Gain factor = 20.
|
|
// V_C = vNow * (1 + P_Golden * 20) ???
|
|
// Wait, "V_HarvestNow * (1 + P_Golden * 20)" -> This implies P_Golden is small,
|
|
// e.g. 0.01. So val = vNow * (1 + 0.2) = 1.2 * vNow?
|
|
// "Calculate the expected baseline gain V_C"
|
|
// Prompt formula: V_C = V_HarvestNow * (1 + P_Golden * 20)
|
|
// Is it additive? Gain = 20x value? Or is specific mutation 20x?
|
|
// If I have 1x multiplier. Get Golden -> 20x. Gain is +19x?
|
|
// Factor = 20.
|
|
// Expected Factor = (1 - P) * 1 + P * 20 = 1 - P + 20P = 1 + 19P.
|
|
// Prompt says "1 + P_Golden * 20". This is approx 1 + 20P. Close enough.
|
|
// Let's use Prompt Formula EXACTLY.
|
|
const V_C = vNow * (1 + BASELINE_GOLDEN_CHANCE * 20); // 1 + 0.01*20 = 1.2
|
|
|
|
// Total E
|
|
let P_A = worldEvents.P_Next_Rain_Thunderstorm;
|
|
let P_B = worldEvents.P_Next_Frost;
|
|
|
|
// CRITICAL CHECK: If crop matures before the event, we can't get the event benefit
|
|
// worldEvents.Time_Until_Next_Event_Hrs is the wait time.
|
|
// worldEvents.Time_Remaining_Hrs is how much growth is left.
|
|
// If Wait > Remaining, we mature before event.
|
|
// Note: Speed multiplier is already applied to Calculate tRemNext, but that's what remains AFTER wait.
|
|
// If tRemNext < 0, it means we matured DURING the wait.
|
|
// If we mature during the wait, do we get the event?
|
|
// The event starts AT the end of the wait. So if we mature BEFORE the end of wait, we miss it.
|
|
const timeToEvent = worldEvents.Time_Until_Next_Event_Hrs; // Real hours
|
|
// Need to check if we mature before this real time passed?
|
|
// Crop matures in Time_Remaining_Hrs (assuming standard speed? or current speed?)
|
|
// If current speed is 1x.
|
|
// If speed is 1.5x (Event Active), Time_Remaining_Hrs is still "hours at 1x"?
|
|
// Usually Time_Remaining is "real time remaining"?
|
|
// Let's assume Time_Remaining_Hrs is "Time until maturity in real hours at current speed" provided by caller?
|
|
// Or is it "Mass needed"?
|
|
// Prompt Check 3.2: "Time Remaining in the next state: T_rem(t+dt) = Time_Remaining_Hrs - Delta_T_eff"
|
|
// Delta_T_eff = Time_Until_Next_Event * Speed.
|
|
// This implies Time_Remaining_Hrs is in "Growth Units" (Hours at 1x speed).
|
|
|
|
// If T_rem(t+dt) <= 0, we mature *during* or *before* the event.
|
|
// Since the event triggers *at* t+dt, we are already mature.
|
|
// Thus, we likely miss the specific "Event Mutation" which usually requires growing *during* the event (or the event affecting the plant).
|
|
// If the event is "Rain starts now", and we are already mature, does Rain affect us?
|
|
// Usually mutations happen *while growing*.
|
|
// So if tRemNext <= 0, we set P_A and P_B to 0.
|
|
|
|
// Also, from user prompt: "If T_rem(t+dt) <= 0 ... calculation focuses only on expected mutation gains over Delta t"
|
|
// This confirms we exclude the specific Event outcome that happens *at* the transition.
|
|
|
|
const deltaTEff = worldEvents.Time_Until_Next_Event_Hrs * (worldEvents.Event_Active ? 1.5 : 1.0);
|
|
const nextStateRem = worldEvents.Time_Remaining_Hrs - deltaTEff;
|
|
|
|
if (nextStateRem <= 0) {
|
|
P_A = 0;
|
|
P_B = 0;
|
|
// P_C absorbs the probability? Or do we just lose it?
|
|
// "The expected value of waiting, E, is the sum... for the three possible random event outcomes"
|
|
// If Rain is impossible, does P_Rain become "Nothing"?
|
|
// Yes, prob of Rain event occuring is real, but effect is null.
|
|
// So V_A becomes V_Now? Or just exclude from sum?
|
|
// Since probabilities must sum to 1?
|
|
// Actually, P_Next_Rain is prob of *Event*. The Event happens regardless of our crop.
|
|
// But our *Gain* is 0.
|
|
// So V_A = V_Now. V_B = V_Now.
|
|
// Effectively, we just take the Baseline gain.
|
|
}
|
|
|
|
const P_C = 1.0 - P_A - P_B;
|
|
|
|
// If we can't benefit from events, V_A and V_B should fallback to V_Now?
|
|
// Or V_C?
|
|
// If Event happens (Rain), but we are mature -> No effect -> Value = V_Now.
|
|
const final_V_A = (nextStateRem <= 0) ? vNow : V_A;
|
|
const final_V_B = (nextStateRem <= 0) ? vNow : V_B;
|
|
// Baseline gain applies over Delta t?
|
|
// "1.0% random chance per growth cycle (Delta t)"
|
|
// If we only grow for part of Delta t?
|
|
// Let's assume Baseline applies fully for now, or maybe only if we are growing?
|
|
// If we stop growing halfway, chance is lower.
|
|
// But let's stick to simple: V_C applies.
|
|
|
|
return (P_A * final_V_A) + (P_B * final_V_B) + (P_C * V_C);
|
|
}
|
|
|
|
static calculateBloodMoonValue(vNow) {
|
|
// 33% chance of 4x Bloodlit.
|
|
// Note: Bloodlit is 4x.
|
|
// Formula from prompt: vNow * (1 + 0.33 * 4x)
|
|
// "4x" probably means the number 4.
|
|
return vNow * (1 + 0.33 * 4);
|
|
}
|
|
}
|
|
|
|
// Browser / Node compatibility
|
|
if (typeof window !== 'undefined') {
|
|
window.MagicBot = window.MagicBot || {};
|
|
window.MagicBot.Decision = Decision;
|
|
}
|
|
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = Decision;
|
|
}
|