Skip to content

Commit a3fd895

Browse files
committed
Unify preset configs
1 parent 4389543 commit a3fd895

4 files changed

Lines changed: 243 additions & 107 deletions

File tree

src/app.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@ let PRESETS = {};
1212
let PRESETS_RAW = [];
1313
let buildableLandPolygon = [];
1414

15-
const PRESET_SIGMAS = {
16-
phase1: 200,
17-
phase2: 300,
18-
phase3: 400,
19-
phase4: 600,
20-
phase5: 800,
21-
collectibles: 1000,
22-
};
23-
2415
// App State
2516
const state = {
2617
rawNodes: [],
@@ -636,12 +627,13 @@ function applyPhasePreset(phaseId) {
636627
if (rawPreset) {
637628
state.config.ignoreSpawns = rawPreset.ignore_spawns;
638629
els.paramIgnoreSpawns.value = rawPreset.ignore_spawns ? "true" : "false";
630+
setWalkingRadius(rawPreset.sigma);
639631
} else {
640632
// Fallbacks just in case
641633
state.config.ignoreSpawns = false;
642634
els.paramIgnoreSpawns.value = "false";
635+
setWalkingRadius(200);
643636
}
644-
setWalkingRadius(PRESET_SIGMAS[phaseId] ?? rawPreset?.sigma ?? 200);
645637

646638
// Redraw weight sliders UI
647639
renderWeightSliders();

src/main.rs

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,23 @@ mod models;
33
mod optimizer;
44
mod server;
55

6-
use models::{GamePhase, OptimizerConfig, PurityOverride};
6+
use models::{OptimizerConfig, PurityOverride};
77
use std::collections::HashMap;
88
use std::env;
99

1010
fn apply_preset_weights(preset_idx: usize, weights: &mut HashMap<String, f64>) {
11-
weights.clear();
12-
match preset_idx {
13-
0 => models::GamePhase::Phase1.apply_weights(weights),
14-
1 => models::GamePhase::Phase2.apply_weights(weights),
15-
2 => models::GamePhase::Phase3.apply_weights(weights),
16-
3 => models::GamePhase::Phase4.apply_weights(weights),
17-
4 => models::GamePhase::Phase5.apply_weights(weights),
18-
5 => apply_collectibles_weights(weights),
19-
_ => {}
11+
if let Some(preset) = models::all_presets().get(preset_idx) {
12+
*weights = preset.build_weights();
2013
}
2114
}
2215

23-
fn apply_collectibles_weights(weights: &mut HashMap<String, f64>) {
24-
weights.clear();
25-
weights.insert("blueslug".to_string(), 0.3);
26-
weights.insert("yellowslug".to_string(), 0.8);
27-
weights.insert("purpleslug".to_string(), 1.2);
28-
weights.insert("mercer".to_string(), 1.0);
29-
weights.insert("somersloop".to_string(), 1.0);
30-
weights.insert("harddrive".to_string(), 1.5);
31-
weights.insert("paleberry".to_string(), 0.0);
32-
weights.insert("berylnut".to_string(), 0.0);
33-
weights.insert("baconagaric".to_string(), 0.0);
16+
fn apply_preset_argument(input: &str, config: &mut OptimizerConfig) -> bool {
17+
if let Some(preset) = models::preset_by_id_or_phase(input) {
18+
models::apply_preset_to_config(preset, config);
19+
true
20+
} else {
21+
false
22+
}
3423
}
3524

3625
fn print_help() {
@@ -80,7 +69,9 @@ fn main() {
8069

8170
let mut custom_file_path: Option<String> = None;
8271
let mut config = OptimizerConfig::default();
83-
GamePhase::Phase1.apply_weights(&mut config.weights);
72+
if let Some(preset) = models::preset_by_id_or_phase("phase1") {
73+
models::apply_preset_to_config(preset, &mut config);
74+
}
8475

8576
let mut output_json = false;
8677
let mut run_simulation = false;
@@ -197,10 +188,7 @@ fn main() {
197188
"--tier" | "--phase" => {
198189
if i + 1 < args.len() {
199190
let phase_str = &args[i + 1];
200-
if let Some(phase) = GamePhase::from_str(phase_str) {
201-
phase.apply_weights(&mut config.weights);
202-
config.game_phase = phase;
203-
} else {
191+
if !apply_preset_argument(phase_str, &mut config) {
204192
eprintln!("Error: Invalid phase/tier value '{}'.", phase_str);
205193
return;
206194
}
@@ -211,8 +199,7 @@ fn main() {
211199
}
212200
}
213201
"--collectibles" => {
214-
apply_collectibles_weights(&mut config.weights);
215-
config.game_phase = GamePhase::Phase5;
202+
apply_preset_argument("collectibles", &mut config);
216203
i += 1;
217204
}
218205
"--ignore-spawns" | "--ignore-starting-areas" => {
@@ -312,7 +299,7 @@ fn run_full_simulation_matrix(nodes: &[models::ResourceNode]) {
312299
}
313300

314301
let mut sim_configs = Vec::new();
315-
let presets = 0..6;
302+
let presets = 0..models::all_presets().len();
316303
let purities = [
317304
PurityOverride::Default,
318305
PurityOverride::Normal,
@@ -330,18 +317,18 @@ fn run_full_simulation_matrix(nodes: &[models::ResourceNode]) {
330317
models::DistanceDecay::Linear,
331318
models::DistanceDecay::LogisticStep,
332319
];
333-
let sigma = 700.0;
334320

335321
for preset_idx in presets {
336322
for &purity in &purities {
337323
for &utility in &utilities {
338324
for &decay in &decays {
325+
let preset = models::all_presets()[preset_idx];
339326
sim_configs.push(SimConfig {
340327
preset_idx,
341328
purity,
342329
utility,
343330
decay,
344-
sigma,
331+
sigma: preset.sigma,
345332
});
346333
}
347334
}
@@ -355,21 +342,15 @@ fn run_full_simulation_matrix(nodes: &[models::ResourceNode]) {
355342
let results: Vec<(SimConfig, optimizer::OptimizationResult)> = sim_configs
356343
.into_iter()
357344
.map(|config| {
345+
let preset = models::all_presets()[config.preset_idx];
358346
let mut opt_config = OptimizerConfig {
359347
sigma: config.sigma,
360348
weights: HashMap::new(),
361349
purity_override: config.purity,
362350
strategy: models::SearchStrategy::Hybrid,
363351
utility_func: config.utility,
364352
decay_func: config.decay,
365-
game_phase: match config.preset_idx {
366-
0 => models::GamePhase::Phase1,
367-
1 => models::GamePhase::Phase2,
368-
2 => models::GamePhase::Phase3,
369-
3 => models::GamePhase::Phase4,
370-
4 => models::GamePhase::Phase5,
371-
_ => models::GamePhase::Phase1,
372-
},
353+
game_phase: preset.phase,
373354
ignore_spawns: true,
374355
};
375356
apply_preset_weights(config.preset_idx, &mut opt_config.weights);
@@ -425,3 +406,32 @@ fn run_full_simulation_matrix(nodes: &[models::ResourceNode]) {
425406
csv_path
426407
);
427408
}
409+
410+
#[cfg(test)]
411+
mod tests {
412+
use super::*;
413+
414+
#[test]
415+
fn cli_phase_preset_application_sets_radius_and_spawn_behavior() {
416+
let mut phase1_config = OptimizerConfig::default();
417+
assert!(apply_preset_argument("phase1", &mut phase1_config));
418+
assert_eq!(phase1_config.sigma, 200.0);
419+
assert!(!phase1_config.ignore_spawns);
420+
421+
let mut phase4_config = OptimizerConfig::default();
422+
assert!(apply_preset_argument("phase4", &mut phase4_config));
423+
assert_eq!(phase4_config.sigma, 600.0);
424+
assert!(phase4_config.ignore_spawns);
425+
}
426+
427+
#[test]
428+
fn cli_collectibles_preset_application_uses_shared_weights() {
429+
let mut config = OptimizerConfig::default();
430+
assert!(apply_preset_argument("collectibles", &mut config));
431+
432+
assert_eq!(config.sigma, 1000.0);
433+
assert!(config.ignore_spawns);
434+
assert_eq!(config.weights.get("blueslug").copied(), Some(0.3));
435+
assert_eq!(config.weights.get("harddrive").copied(), Some(1.5));
436+
}
437+
}

src/models.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,131 @@ impl GamePhase {
214214
}
215215
}
216216

217+
#[derive(Debug, Clone, Copy)]
218+
pub struct PresetDescriptor {
219+
pub id: &'static str,
220+
pub name: &'static str,
221+
pub phase: GamePhase,
222+
pub sigma: f64,
223+
pub ignore_spawns: bool,
224+
weight_builder: fn(&mut HashMap<String, f64>),
225+
}
226+
227+
impl PresetDescriptor {
228+
pub fn build_weights(self) -> HashMap<String, f64> {
229+
let mut weights = HashMap::new();
230+
(self.weight_builder)(&mut weights);
231+
weights
232+
}
233+
}
234+
235+
fn apply_phase1_weights(weights: &mut HashMap<String, f64>) {
236+
GamePhase::Phase1.apply_weights(weights);
237+
}
238+
239+
fn apply_phase2_weights(weights: &mut HashMap<String, f64>) {
240+
GamePhase::Phase2.apply_weights(weights);
241+
}
242+
243+
fn apply_phase3_weights(weights: &mut HashMap<String, f64>) {
244+
GamePhase::Phase3.apply_weights(weights);
245+
}
246+
247+
fn apply_phase4_weights(weights: &mut HashMap<String, f64>) {
248+
GamePhase::Phase4.apply_weights(weights);
249+
}
250+
251+
fn apply_phase5_weights(weights: &mut HashMap<String, f64>) {
252+
GamePhase::Phase5.apply_weights(weights);
253+
}
254+
255+
pub fn apply_collectibles_weights(weights: &mut HashMap<String, f64>) {
256+
weights.clear();
257+
weights.insert("blueslug".to_string(), 0.3);
258+
weights.insert("yellowslug".to_string(), 0.8);
259+
weights.insert("purpleslug".to_string(), 1.2);
260+
weights.insert("mercer".to_string(), 1.0);
261+
weights.insert("somersloop".to_string(), 1.0);
262+
weights.insert("harddrive".to_string(), 1.5);
263+
weights.insert("paleberry".to_string(), 0.0);
264+
weights.insert("berylnut".to_string(), 0.0);
265+
weights.insert("baconagaric".to_string(), 0.0);
266+
}
267+
268+
pub static PRESET_DESCRIPTORS: &[PresetDescriptor] = &[
269+
PresetDescriptor {
270+
id: "phase1",
271+
name: "Phase 1 — Early Game (Tiers 1-2)",
272+
phase: GamePhase::Phase1,
273+
sigma: 200.0,
274+
ignore_spawns: false,
275+
weight_builder: apply_phase1_weights,
276+
},
277+
PresetDescriptor {
278+
id: "phase2",
279+
name: "Phase 2 — Steel & Coal (Tiers 3-4)",
280+
phase: GamePhase::Phase2,
281+
sigma: 300.0,
282+
ignore_spawns: false,
283+
weight_builder: apply_phase2_weights,
284+
},
285+
PresetDescriptor {
286+
id: "phase3",
287+
name: "Phase 3 — Oil & Quartz (Tiers 5-6)",
288+
phase: GamePhase::Phase3,
289+
sigma: 400.0,
290+
ignore_spawns: false,
291+
weight_builder: apply_phase3_weights,
292+
},
293+
PresetDescriptor {
294+
id: "phase4",
295+
name: "Phase 4 — Aluminum & Nuclear (Tiers 7-8)",
296+
phase: GamePhase::Phase4,
297+
sigma: 600.0,
298+
ignore_spawns: true,
299+
weight_builder: apply_phase4_weights,
300+
},
301+
PresetDescriptor {
302+
id: "phase5",
303+
name: "Phase 5 — Quantum (Tier 9)",
304+
phase: GamePhase::Phase5,
305+
sigma: 800.0,
306+
ignore_spawns: true,
307+
weight_builder: apply_phase5_weights,
308+
},
309+
PresetDescriptor {
310+
id: "collectibles",
311+
name: "Collectibles — Slugs, Artifacts & Hard Drives",
312+
phase: GamePhase::Phase5,
313+
sigma: 1000.0,
314+
ignore_spawns: true,
315+
weight_builder: apply_collectibles_weights,
316+
},
317+
];
318+
319+
pub fn all_presets() -> &'static [PresetDescriptor] {
320+
PRESET_DESCRIPTORS
321+
}
322+
323+
pub fn preset_by_id_or_phase(input: &str) -> Option<&'static PresetDescriptor> {
324+
let input = input.to_lowercase();
325+
all_presets()
326+
.iter()
327+
.find(|preset| preset.id == input.as_str())
328+
.or_else(|| GamePhase::from_str(&input).and_then(preset_by_phase))
329+
}
330+
331+
pub fn preset_by_phase(phase: GamePhase) -> Option<&'static PresetDescriptor> {
332+
all_presets().iter().find(|preset| preset.phase == phase)
333+
}
334+
335+
pub fn apply_preset_to_config(preset: &PresetDescriptor, config: &mut OptimizerConfig) {
336+
config.weights = preset.build_weights();
337+
config.game_phase = preset.phase;
338+
config.sigma = preset.sigma;
339+
config.ignore_spawns = preset.ignore_spawns;
340+
}
341+
217342
#[derive(Debug, Clone, Serialize, Deserialize)]
218343
pub struct ResourceNode {
219344
#[serde(rename = "type")]
@@ -377,3 +502,43 @@ impl Default for OptimizerConfig {
377502
}
378503
}
379504
}
505+
506+
#[cfg(test)]
507+
mod tests {
508+
use super::*;
509+
510+
#[test]
511+
fn applying_phase_presets_sets_full_config() {
512+
let mut phase1_config = OptimizerConfig::default();
513+
let phase1 = preset_by_id_or_phase("phase1").expect("phase1 preset missing");
514+
apply_preset_to_config(phase1, &mut phase1_config);
515+
assert_eq!(phase1_config.game_phase, GamePhase::Phase1);
516+
assert_eq!(phase1_config.sigma, 200.0);
517+
assert!(!phase1_config.ignore_spawns);
518+
assert!(!phase1_config.weights.is_empty());
519+
520+
let mut phase4_config = OptimizerConfig::default();
521+
let phase4 = preset_by_id_or_phase("phase4").expect("phase4 preset missing");
522+
apply_preset_to_config(phase4, &mut phase4_config);
523+
assert_eq!(phase4_config.game_phase, GamePhase::Phase4);
524+
assert_eq!(phase4_config.sigma, 600.0);
525+
assert!(phase4_config.ignore_spawns);
526+
assert!(!phase4_config.weights.is_empty());
527+
}
528+
529+
#[test]
530+
fn collectibles_preset_has_expected_weights_and_radius() {
531+
let preset = preset_by_id_or_phase("collectibles").expect("collectibles preset missing");
532+
let weights = preset.build_weights();
533+
534+
assert_eq!(preset.phase, GamePhase::Phase5);
535+
assert_eq!(preset.sigma, 1000.0);
536+
assert!(preset.ignore_spawns);
537+
assert_eq!(weights.get("blueslug").copied(), Some(0.3));
538+
assert_eq!(weights.get("yellowslug").copied(), Some(0.8));
539+
assert_eq!(weights.get("purpleslug").copied(), Some(1.2));
540+
assert_eq!(weights.get("mercer").copied(), Some(1.0));
541+
assert_eq!(weights.get("somersloop").copied(), Some(1.0));
542+
assert_eq!(weights.get("harddrive").copied(), Some(1.5));
543+
}
544+
}

0 commit comments

Comments
 (0)