sots-engine/src/game/sim/research.cpp

143 lines
5.1 KiB
C++

#include "game/sim/research.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include "game/sim/numeric.h"
namespace sots::sim {
bool RollEdgeAvailable(const EdgeAvailability& edge, Species species, TreeBuildMode mode,
IRandom& rng) {
if (mode == TreeBuildMode::Everything) return true;
const float p = edge.Chance(species);
if (!(p > 0.f)) return false;
if (mode == TreeBuildMode::NoRoll) return true;
if (p >= 1.f) return true;
return rng.NextFloat() <= p;
}
double TechCostMultiplier(int applicableBonusTechsOwned) {
const double m = 1.0 - 0.25 * std::max(0, applicableBonusTechsOwned);
return std::max(0.25, m);
}
int TechCost(int baseCost, double multiplier) {
if (baseCost == kNoResearchCost) return kNoResearchCost;
return std::max(1, Ftol(static_cast<double>(baseCost) * multiplier));
}
namespace {
// The image's copies of two float literals, widened to double: the per-turn decay
// fraction and the "completed early" threshold. Both are (double)0.05f and (double)0.8f,
// not the exact decimals, and both sit on a truncation/compare boundary where the
// difference is observable.
constexpr double kDecayFraction = 0.05000000074505806; // (double)0.05f
constexpr double kEarlyCompletionRatio = 0.800000011920929; // (double)0.8f
// 32-bit signed multiply then truncating division by 100, exactly as the original does it
// (it wraps rather than widening, which only matters for a cost near INT_MAX).
int scale_percent(int cost, unsigned percent) {
const std::int32_t p = static_cast<std::int32_t>(static_cast<std::uint32_t>(cost) * percent);
return static_cast<int>(p / 100);
}
} // namespace
int ResearchSpendFloor(int cost) { return std::max(0, scale_percent(cost, 50u)); }
int ResearchSpendCeiling(int cost) {
return std::max(ResearchSpendFloor(cost), scale_percent(cost, 150u));
}
float ResearchCompletionOdds(int progress, int lo, int hi) {
return static_cast<float>(static_cast<double>(progress - lo) / static_cast<double>(hi));
}
ResearchStepResult ApplyResearchPoints(ResearchNode& node, int points, Species owner, IRandom& rng) {
ResearchStepResult r;
const int cost = node.cost;
const int lo = ResearchSpendFloor(cost);
const int hi = ResearchSpendCeiling(cost);
r.wasCompleteBefore = cost <= node.progress;
// Signed min with no floor at zero: the original clamps neither side.
r.spent = std::min(points, hi - node.progress);
r.overbudget = points - r.spent;
node.progress += r.spent;
const bool nowComplete = cost <= node.progress;
if (node.progress < hi) {
if (r.spent == 0) {
r.odds = 0.f;
r.roll = 1.f;
} else {
r.odds = ResearchCompletionOdds(node.progress, lo, hi);
r.roll = rng.NextFloat();
// Zuul draw a second time and keep the lower (better) roll.
if (owner == Species::Zuul) r.roll = std::min(r.roll, rng.NextFloat());
}
} else {
r.odds = 1.f;
r.roll = 0.f;
}
if (r.odds < r.roll) {
if (!r.wasCompleteBefore && nowComplete) {
r.overbudgetEvent = true;
node.flag = TechFlag::OverBudgetNotified;
}
return r;
}
r.completed = true;
// The ratio is itself narrowed to float32 before the comparison.
const float ratio = static_cast<float>(static_cast<double>(node.progress) / static_cast<double>(cost));
if (static_cast<double>(ratio) < kEarlyCompletionRatio) {
r.completedEarly = true;
node.flag = TechFlag::CompletedEarly;
}
node.state = TechState::Researched;
return r;
}
int DecayResearchProgress(int progress, int cost) {
return std::max(0, progress - Ftol(static_cast<double>(cost) * kDecayFraction));
}
void DecayAllResearch(std::vector<ResearchNode>& nodes) {
for (ResearchNode& n : nodes) {
// The original's guard is `progress != 0`, so a negative progress decays too.
if (n.state == TechState::Available && n.progress != 0) {
n.progress = DecayResearchProgress(n.progress, n.cost);
}
}
}
ResearchTurnResult ProcessResearchTurn(std::vector<ResearchNode>& nodes,
const std::vector<ResearchAllocEntry>& alloc, Species owner,
IRandom& rng) {
ResearchTurnResult out;
out.steps.reserve(alloc.size());
for (const ResearchAllocEntry& e : alloc) {
if (e.nodeIndex < 0 || static_cast<std::size_t>(e.nodeIndex) >= nodes.size()) continue;
ResearchStepResult s = ApplyResearchPoints(nodes[e.nodeIndex], e.points, owner, rng);
out.overbudget += s.overbudget;
out.steps.push_back(s);
}
DecayAllResearch(nodes);
return out;
}
bool RollLabAccident(int oddsPercent, IRandom& rng) {
const int roll = static_cast<int>(rng.NextIntInclusive(100));
return roll < oddsPercent;
}
int LabAccidentLossPercent(double minLoss, double maxLoss, IRandom& rng) {
const double f = Clamp01(static_cast<double>(rng.NextFloat()) * (maxLoss - minLoss) + minLoss);
return static_cast<int>(std::ceil(f * 100.0));
}
} // namespace sots::sim