Skip to content

Commit ee671db

Browse files
committed
Align yield explanations
1 parent 4389543 commit ee671db

1 file changed

Lines changed: 152 additions & 62 deletions

File tree

src/optimizer.rs

Lines changed: 152 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,64 @@ fn distance_to_nearest_water(
138138
min_dist_m
139139
}
140140

141+
fn decay_weight(
142+
distance_m: f64,
143+
distance_sq_m: f64,
144+
sigma: f64,
145+
decay_func: crate::models::DistanceDecay,
146+
) -> f64 {
147+
match decay_func {
148+
crate::models::DistanceDecay::Gaussian => {
149+
let two_sigma_sq = 2.0 * sigma * sigma;
150+
(-distance_sq_m / two_sigma_sq).exp()
151+
}
152+
crate::models::DistanceDecay::Exponential => (-distance_m / sigma).exp(),
153+
crate::models::DistanceDecay::PowerLaw => 1.0 / (distance_m / sigma + 1.0),
154+
crate::models::DistanceDecay::Linear => (1.0 - distance_m / sigma).max(0.0),
155+
crate::models::DistanceDecay::LogisticStep => {
156+
if distance_m <= sigma {
157+
1.0
158+
} else {
159+
0.05
160+
}
161+
}
162+
}
163+
}
164+
165+
fn obstructed_node_contributes(obstructed: bool, game_phase: crate::models::GamePhase) -> bool {
166+
!obstructed
167+
|| (game_phase != crate::models::GamePhase::Phase1
168+
&& game_phase != crate::models::GamePhase::Phase2)
169+
}
170+
171+
fn node_yield_contribution(
172+
node: &OptNode,
173+
decay: f64,
174+
game_phase: crate::models::GamePhase,
175+
) -> f64 {
176+
if obstructed_node_contributes(node.obstructed, game_phase) {
177+
node.multiplier * decay
178+
} else {
179+
0.0
180+
}
181+
}
182+
183+
fn virtual_water_yield(
184+
x: f64,
185+
y: f64,
186+
opt_nodes: &[OptNode],
187+
waterwell_idx: Option<usize>,
188+
config: &OptimizerConfig,
189+
) -> f64 {
190+
let water_dist = distance_to_nearest_water(x, y, opt_nodes, waterwell_idx);
191+
decay_weight(
192+
water_dist,
193+
water_dist * water_dist,
194+
config.sigma,
195+
config.decay_func,
196+
)
197+
}
198+
141199
/// Estimates the ground altitude Z at a coordinate (x, y) using KNN IDW (Inverse Distance Weighting)
142200
fn estimate_altitude(
143201
x: f64,
@@ -432,32 +490,8 @@ fn calculate_utility(
432490

433491
if d_sq <= radius_m_sq {
434492
let d = d_sq.sqrt();
435-
let decay = match config.decay_func {
436-
crate::models::DistanceDecay::Gaussian => {
437-
let two_sigma_sq = 2.0 * config.sigma * config.sigma;
438-
(-d_sq / two_sigma_sq).exp()
439-
}
440-
crate::models::DistanceDecay::Exponential => (-d / config.sigma).exp(),
441-
crate::models::DistanceDecay::PowerLaw => 1.0 / (d / config.sigma + 1.0),
442-
crate::models::DistanceDecay::Linear => (1.0 - d / config.sigma).max(0.0),
443-
crate::models::DistanceDecay::LogisticStep => {
444-
if d <= config.sigma {
445-
1.0
446-
} else {
447-
0.05
448-
}
449-
}
450-
};
451-
let mut contribution = node.multiplier * decay;
452-
453-
// Obstructed nodes are locked behind Nobelisk explosives (Phase 1 & 2)
454-
if node.obstructed
455-
&& (config.game_phase == crate::models::GamePhase::Phase1
456-
|| config.game_phase == crate::models::GamePhase::Phase2)
457-
{
458-
contribution = 0.0;
459-
}
460-
493+
let decay = decay_weight(d, d_sq, config.sigma, config.decay_func);
494+
let contribution = node_yield_contribution(node, decay, config.game_phase);
461495
yields[node.res_idx] += contribution;
462496

463497
// Keep track of nearby heights using Welford's algorithm
@@ -475,24 +509,7 @@ fn calculate_utility(
475509

476510
// Add virtual water yield based on proximity to mapped lakes/ponds or waterwells.
477511
if let Some(&water_idx) = res_to_idx.get("water") {
478-
let water_dist = distance_to_nearest_water(x, y, opt_nodes, waterwell_idx);
479-
let water_decay = match config.decay_func {
480-
crate::models::DistanceDecay::Gaussian => {
481-
let two_sigma_sq = 2.0 * config.sigma * config.sigma;
482-
(-(water_dist * water_dist) / two_sigma_sq).exp()
483-
}
484-
crate::models::DistanceDecay::Exponential => (-water_dist / config.sigma).exp(),
485-
crate::models::DistanceDecay::PowerLaw => 1.0 / (water_dist / config.sigma + 1.0),
486-
crate::models::DistanceDecay::Linear => (1.0 - water_dist / config.sigma).max(0.0),
487-
crate::models::DistanceDecay::LogisticStep => {
488-
if water_dist <= config.sigma {
489-
1.0
490-
} else {
491-
0.05
492-
}
493-
}
494-
};
495-
yields[water_idx] = water_decay;
512+
yields[water_idx] = virtual_water_yield(x, y, opt_nodes, waterwell_idx, config);
496513
}
497514

498515
// 3. Terrain Flatness Penalty (Population std dev of local node heights)
@@ -771,23 +788,11 @@ fn run_hill_climbing(
771788

772789
// Accumulate decay-weighted yield for this resource type
773790
let d_m = d_sq_3d.sqrt() / 100.0;
774-
let decay = match config.decay_func {
775-
crate::models::DistanceDecay::Gaussian => {
776-
let two_sigma_sq = 2.0 * config.sigma * config.sigma;
777-
(-(d_m * d_m) / two_sigma_sq).exp()
778-
}
779-
crate::models::DistanceDecay::Exponential => (-d_m / config.sigma).exp(),
780-
crate::models::DistanceDecay::PowerLaw => 1.0 / (d_m / config.sigma + 1.0),
781-
crate::models::DistanceDecay::Linear => (1.0 - d_m / config.sigma).max(0.0),
782-
crate::models::DistanceDecay::LogisticStep => {
783-
if d_m <= config.sigma {
784-
1.0
785-
} else {
786-
0.05
787-
}
788-
}
789-
};
790-
*resource_yields.entry(name.clone()).or_insert(0.0) += node.multiplier * decay;
791+
let decay = decay_weight(d_m, d_m * d_m, config.sigma, config.decay_func);
792+
let contribution = node_yield_contribution(node, decay, config.game_phase);
793+
if contribution > 0.0 {
794+
*resource_yields.entry(name.clone()).or_insert(0.0) += contribution;
795+
}
791796
}
792797
}
793798

@@ -798,6 +803,13 @@ fn run_hill_climbing(
798803
}
799804
}
800805

806+
if res_to_idx.contains_key("water") {
807+
let water_yield = virtual_water_yield(curr_x, curr_y, opt_nodes, waterwell_idx, config);
808+
if water_yield > 0.0 {
809+
resource_yields.insert("water".to_string(), water_yield);
810+
}
811+
}
812+
801813
let terrain_ruggedness = if tri_count > 0 {
802814
tri_sum / tri_count as f64
803815
} else {
@@ -1209,7 +1221,30 @@ pub fn optimize(nodes: &[ResourceNode], config: &OptimizerConfig) -> Vec<Optimiz
12091221
#[cfg(test)]
12101222
mod tests {
12111223
use super::*;
1212-
use crate::models::{GamePhase, OptimizerConfig};
1224+
use crate::models::{GamePhase, OptimizerConfig, Purity, ResourceNode};
1225+
use std::collections::HashMap;
1226+
1227+
fn node(resource_type: &str, x: f64, y: f64, obstructed: bool) -> ResourceNode {
1228+
ResourceNode {
1229+
resource_type: resource_type.to_string(),
1230+
purity: Purity::Normal,
1231+
x,
1232+
y,
1233+
z: 0.0,
1234+
obstructed,
1235+
}
1236+
}
1237+
1238+
fn config_for(resources: &[(&str, f64)]) -> OptimizerConfig {
1239+
let mut config = OptimizerConfig::default();
1240+
config.weights = HashMap::new();
1241+
for (name, weight) in resources {
1242+
config.weights.insert((*name).to_string(), *weight);
1243+
}
1244+
config.strategy = crate::models::SearchStrategy::Fast;
1245+
config.ignore_spawns = true;
1246+
config
1247+
}
12131248

12141249
#[test]
12151250
fn test_ignore_spawns() {
@@ -1257,6 +1292,61 @@ mod tests {
12571292
assert!(ignored_score > constrained_score);
12581293
}
12591294

1295+
#[test]
1296+
fn resource_yields_exclude_early_phase_obstructed_nodes() {
1297+
let nodes = vec![node("iron", 0.0, 0.0, true)];
1298+
let mut config = config_for(&[("iron", 1.0)]);
1299+
config.game_phase = GamePhase::Phase1;
1300+
config.sigma = 500.0;
1301+
1302+
let ctx = prepare_context(&nodes, &config);
1303+
let result = run_hill_climbing(
1304+
0.0,
1305+
0.0,
1306+
&ctx.opt_nodes,
1307+
&ctx.spatial_grid,
1308+
&config,
1309+
ctx.num_resources,
1310+
&ctx.weights_arr,
1311+
&ctx.epsilons_arr,
1312+
&ctx.res_to_idx,
1313+
ctx.waterwell_idx,
1314+
&ctx.land_mask,
1315+
);
1316+
1317+
assert_eq!(result.obstructed_nodes.get("Normal iron"), Some(&1));
1318+
assert_eq!(result.local_nodes.get("Normal iron"), None);
1319+
assert_eq!(
1320+
result.resource_yields.get("iron").copied().unwrap_or(0.0),
1321+
0.0
1322+
);
1323+
}
1324+
1325+
#[test]
1326+
fn resource_yields_include_virtual_static_water() {
1327+
let nodes = vec![node("iron", 140000.0, 230000.0, false)];
1328+
let mut config = config_for(&[("iron", 0.1), ("water", 1.0)]);
1329+
config.game_phase = GamePhase::Phase2;
1330+
config.sigma = 500.0;
1331+
1332+
let ctx = prepare_context(&nodes, &config);
1333+
let result = run_hill_climbing(
1334+
140000.0,
1335+
230000.0,
1336+
&ctx.opt_nodes,
1337+
&ctx.spatial_grid,
1338+
&config,
1339+
ctx.num_resources,
1340+
&ctx.weights_arr,
1341+
&ctx.epsilons_arr,
1342+
&ctx.res_to_idx,
1343+
ctx.waterwell_idx,
1344+
&ctx.land_mask,
1345+
);
1346+
1347+
assert!(result.resource_yields.get("water").copied().unwrap_or(0.0) > 0.0);
1348+
}
1349+
12601350
#[test]
12611351
fn test_default_nodes_optimize() {
12621352
let nodes = crate::data_loader::load_default_nodes();

0 commit comments

Comments
 (0)