Skip to content

Commit 4389543

Browse files
committed
Merge executed improvement plans
2 parents 5e67ef0 + 9070bde commit 4389543

7 files changed

Lines changed: 746 additions & 196 deletions

File tree

.github/workflows/checks.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Checks
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
checks:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Rust
16+
uses: dtolnay/rust-toolchain@stable
17+
18+
- name: Setup Bun
19+
uses: oven-sh/setup-bun@v2
20+
21+
- name: Check Rust formatting
22+
run: cargo fmt --check
23+
24+
- name: Test Rust
25+
run: cargo test
26+
27+
- name: Install frontend dependencies
28+
run: bun install --frozen-lockfile
29+
30+
- name: Check frontend
31+
run: bun run check
32+
33+
- name: Audit frontend dependencies
34+
run: bun audit

index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ <h2>OPTIMIZATION PARAMS</h2>
9999

100100
<div class="control-row">
101101
<div class="control-item">
102-
<label for="param-sigma">Walking Radius: <span id="param-sigma-value">200m</span></label>
102+
<label for="param-sigma"
103+
>Walking Radius: <span id="param-sigma-value">200m</span></label
104+
>
103105
<input id="param-sigma" type="range" min="50" max="1000" step="50" value="200" />
104106
</div>
105107
</div>

src/app.js

Lines changed: 11 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// FICSIT Starting Position Optimizer
22
// Web Dashboard Logic
33

4+
import { hasOptimizationObjective, nonZeroWeights, parseNodes, RESOURCES } from "./mapContracts.js";
5+
46
const LAND_MASK_SECTORS = 128;
57
const LAND_MASK_BUFFER_CM = 22000;
68
const MAP_PIXEL_TO_CM = 1 / 0.0013653321;
@@ -19,41 +21,6 @@ const PRESET_SIGMAS = {
1921
collectibles: 1000,
2022
};
2123

22-
// Resource Registry and Stylings
23-
const RESOURCES = [
24-
// Core Resources
25-
{ id: "iron", name: "Iron Ore", color: "#4682B4", category: "core" },
26-
{ id: "copper", name: "Copper Ore", color: "#B87333", category: "core" },
27-
{ id: "limestone", name: "Limestone", color: "#E5E5C0", category: "core" },
28-
{ id: "coal", name: "Coal", color: "#555555", category: "core" },
29-
{ id: "water", name: "Water (Static)", color: "#00BFFF", category: "core" },
30-
{ id: "oil", name: "Crude Oil", color: "#2E2E2E", category: "core" },
31-
{ id: "sulfur", name: "Sulfur", color: "#DAA520", category: "core" },
32-
{ id: "quartz", name: "Raw Quartz", color: "#FFD1DC", category: "core" },
33-
{ id: "caterium", name: "Caterium Ore", color: "#FFD700", category: "core" },
34-
{ id: "bauxite", name: "Bauxite", color: "#CD5C5C", category: "core" },
35-
{ id: "uranium", name: "Uranium", color: "#7FFF00", category: "core" },
36-
{ id: "sam", name: "SAM Ore", color: "#9370DB", category: "core" },
37-
{ id: "nitrogenwell", name: "Nitrogen Gas Well", color: "#48D1CC", category: "core" },
38-
{ id: "waterwell", name: "Water Well", color: "#1E90FF", category: "core" },
39-
{ id: "geyser", name: "Geyser", color: "#A9A9A9", category: "core" },
40-
41-
// Collectibles / Research
42-
{ id: "blueslug", name: "Blue Slug", color: "#00FFFF", category: "collectible" },
43-
{ id: "yellowslug", name: "Yellow Slug", color: "#FFFF00", category: "collectible" },
44-
{ id: "purpleslug", name: "Purple Slug", color: "#FF00FF", category: "collectible" },
45-
{ id: "mercer", name: "Mercer Sphere", color: "#FF7F50", category: "collectible" },
46-
{ id: "somersloop", name: "Somersloop", color: "#D1C4E9", category: "collectible" },
47-
{ id: "harddrive", name: "Hard Drive", color: "#CD7F32", category: "collectible" },
48-
{ id: "paleberry", name: "Paleberry", color: "#DB7093", category: "collectible" },
49-
{ id: "berylnut", name: "Berylnut", color: "#D2B48C", category: "collectible" },
50-
{ id: "baconagaric", name: "Bacon Agaric", color: "#BC8F8F", category: "collectible" },
51-
52-
// Threats
53-
{ id: "sporeflower", name: "Spore Flower", color: "#8B008B", category: "threat" },
54-
{ id: "gaspillar", name: "Gas Pillar", color: "#2E8B57", category: "threat" },
55-
];
56-
5724
// App State
5825
const state = {
5926
rawNodes: [],
@@ -158,156 +125,6 @@ function computeBuildableLandPolygon(nodes) {
158125
});
159126
}
160127

161-
// Parse Complete Map Data from Raw JSON format
162-
function parseNodes(data) {
163-
const nodes = [];
164-
const waterwellIndices = [];
165-
166-
if (data && data.options) {
167-
data.options.forEach((category) => {
168-
if (category.options) {
169-
category.options.forEach((subcat) => {
170-
if (subcat.options) {
171-
subcat.options.forEach((item) => {
172-
let resType = "";
173-
const id = item.layerId;
174-
if (id.startsWith("limestone")) resType = "limestone";
175-
else if (id.startsWith("iron")) resType = "iron";
176-
else if (id.startsWith("copper")) resType = "copper";
177-
else if (id.startsWith("caterium")) resType = "caterium";
178-
else if (id.startsWith("coal")) resType = "coal";
179-
else if (id.startsWith("oilWell") || id.startsWith("oil")) resType = "oil";
180-
else if (id.startsWith("sulfur")) resType = "sulfur";
181-
else if (id.startsWith("bauxite")) resType = "bauxite";
182-
else if (id.startsWith("quartz")) resType = "quartz";
183-
else if (id.startsWith("uranium")) resType = "uranium";
184-
else if (id.startsWith("sam")) resType = "sam";
185-
else if (id.startsWith("nitrogen")) resType = "nitrogenwell";
186-
else if (id.startsWith("water")) resType = "waterwell";
187-
else if (id.startsWith("geyser")) resType = "geyser";
188-
else if (id === "greenSlugs") resType = "blueslug";
189-
else if (id === "yellowSlugs") resType = "yellowslug";
190-
else if (id === "purpleSlugs") resType = "purpleslug";
191-
else if (id === "mercerSpheres") resType = "mercer";
192-
else if (id === "somersloops") resType = "somersloop";
193-
else if (id === "hardDrives") resType = "harddrive";
194-
else if (id === "paleBerry") resType = "paleberry";
195-
else if (id === "berylNut") resType = "berylnut";
196-
else if (id === "baconAgaric") resType = "baconagaric";
197-
else if (id === "sporeFlowers") resType = "sporeflower";
198-
else if (id === "pillars") resType = "gaspillar";
199-
200-
if (!resType) return;
201-
202-
if (item.markers) {
203-
const addMarker = (marker) => {
204-
let rawType = marker.type || resType;
205-
let mType = resType;
206-
207-
switch (rawType) {
208-
case "Desc_Stone_C":
209-
mType = "limestone";
210-
break;
211-
case "Desc_OreIron_C":
212-
mType = "iron";
213-
break;
214-
case "Desc_OreCopper_C":
215-
mType = "copper";
216-
break;
217-
case "Desc_OreGold_C":
218-
mType = "caterium";
219-
break;
220-
case "Desc_Coal_C":
221-
mType = "coal";
222-
break;
223-
case "Desc_LiquidOil_C":
224-
mType = "oil";
225-
break;
226-
case "Desc_Sulfur_C":
227-
mType = "sulfur";
228-
break;
229-
case "Desc_OreBauxite_C":
230-
mType = "bauxite";
231-
break;
232-
case "Desc_RawQuartz_C":
233-
mType = "quartz";
234-
break;
235-
case "Desc_OreUranium_C":
236-
mType = "uranium";
237-
break;
238-
case "Desc_SAM_C":
239-
mType = "sam";
240-
break;
241-
case "Desc_NitrogenGas_C":
242-
mType = "nitrogenwell";
243-
break;
244-
case "Desc_Water_C":
245-
mType = "waterwell";
246-
break;
247-
default:
248-
if (marker.pathName && marker.pathName.includes("BP_ResourceNodeGeyser")) {
249-
mType = "geyser";
250-
} else {
251-
mType = resType;
252-
}
253-
break;
254-
}
255-
256-
if (mType.startsWith("water")) mType = "waterwell";
257-
if (mType.startsWith("nitrogen")) mType = "nitrogenwell";
258-
259-
let purityStr = marker.purity || item.purity || "normal";
260-
let purityMultiplier = 1.0;
261-
if (purityStr.includes("pure") && !purityStr.includes("impure")) {
262-
purityMultiplier = 2.0;
263-
} else if (purityStr.includes("impure") || purityStr.includes("inpure")) {
264-
purityMultiplier = 0.5;
265-
}
266-
267-
let obstructed = !!marker.obstructed;
268-
if (!obstructed && mType === "caterium") {
269-
const isStartingCaterium =
270-
Math.abs(marker.x - -220000.0) < 50000.0 &&
271-
Math.abs(marker.y - -150000.0) < 50000.0;
272-
if (!isStartingCaterium) obstructed = true;
273-
}
274-
275-
const node = {
276-
resource_type: mType,
277-
x: parseFloat(marker.x),
278-
y: parseFloat(marker.y),
279-
z: parseFloat(marker.z || 0),
280-
purityMultiplier,
281-
purity: purityStr,
282-
obstructed,
283-
};
284-
285-
nodes.push(node);
286-
287-
if (mType === "waterwell") {
288-
waterwellIndices.push(nodes.length - 1);
289-
}
290-
};
291-
292-
if (Array.isArray(item.markers)) {
293-
item.markers.forEach(addMarker);
294-
} else if (typeof item.markers === "object" && item.markers !== null) {
295-
Object.keys(item.markers).forEach((k) => {
296-
addMarker(item.markers[k]);
297-
});
298-
}
299-
}
300-
});
301-
}
302-
});
303-
}
304-
});
305-
}
306-
307-
nodes.waterwellIndices = waterwellIndices;
308-
return nodes;
309-
}
310-
311128
// Distance to nearest water body helper.
312129
// NOTE: We only use static water body rectangles and water wells.
313130
// The old "coast edge" checks (x < -250000 → dist=0) were REMOVED because the map's
@@ -326,6 +143,14 @@ async function runGlobalOptimization() {
326143
els.mapLoading.classList.add("active");
327144

328145
try {
146+
const weights = nonZeroWeights(config.weights);
147+
if (!hasOptimizationObjective(config.weights)) {
148+
els.mapLoading.innerHTML = `<span style="color: #ff3333; font-weight: bold; font-size: 1.1rem; margin-bottom: 12px;">OPTIMIZATION FAILED: Select at least one weighted resource.</span>
149+
<span style="font-size: 0.8rem; color: var(--color-text-muted);">Enable a resource slider or apply a phase preset, then try again.</span>`;
150+
els.mapLoading.classList.add("active");
151+
return;
152+
}
153+
329154
// Build the request body
330155
const reqBody = {
331156
utility_func: config.utilityFunc,
@@ -335,7 +160,7 @@ async function runGlobalOptimization() {
335160
game_phase: config.gamePhase,
336161
sigma: config.sigma,
337162
ignore_spawns: config.ignoreSpawns,
338-
weights: Object.fromEntries(Object.entries(config.weights).filter(([_, v]) => v !== 0)),
163+
weights,
339164
};
340165

341166
const apiRes = await fetch("/api/optimize", {

src/data_loader.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,113 @@ pub fn load_nodes_from_file<P: AsRef<Path>>(
183183
file.read_to_string(&mut s)?;
184184
load_nodes_from_str(&s)
185185
}
186+
187+
#[cfg(test)]
188+
mod tests {
189+
use super::*;
190+
use crate::models::Purity;
191+
192+
#[test]
193+
fn parses_new_map_format_marker() {
194+
let nodes = load_nodes_from_str(
195+
r#"{
196+
"options": [
197+
{
198+
"options": [
199+
{
200+
"options": [
201+
{
202+
"layerId": "ironNodes",
203+
"markers": [
204+
{
205+
"type": "Desc_OreIron_C",
206+
"x": 123.5,
207+
"y": -456.25,
208+
"z": 78.0,
209+
"purity": "RP_Pure",
210+
"obstructed": true
211+
}
212+
]
213+
}
214+
]
215+
}
216+
]
217+
}
218+
]
219+
}"#,
220+
)
221+
.expect("new map format should parse");
222+
223+
assert_eq!(nodes.len(), 1);
224+
assert_eq!(nodes[0].resource_type, "iron");
225+
assert_eq!(nodes[0].purity, Purity::Pure);
226+
assert_eq!(nodes[0].x, 123.5);
227+
assert_eq!(nodes[0].y, -456.25);
228+
assert_eq!(nodes[0].z, 78.0);
229+
assert!(nodes[0].obstructed);
230+
}
231+
232+
#[test]
233+
fn parses_old_resource_node_array_format() {
234+
let nodes = load_nodes_from_str(
235+
r#"[
236+
{
237+
"type": "copper",
238+
"purity": "normal",
239+
"x": 1.0,
240+
"y": 2.0,
241+
"z": 3.0,
242+
"obstructed": false
243+
}
244+
]"#,
245+
)
246+
.expect("old node array format should parse");
247+
248+
assert_eq!(nodes.len(), 1);
249+
assert_eq!(nodes[0].resource_type, "copper");
250+
assert_eq!(nodes[0].purity, Purity::Normal);
251+
assert_eq!(nodes[0].x, 1.0);
252+
assert_eq!(nodes[0].y, 2.0);
253+
assert_eq!(nodes[0].z, 3.0);
254+
assert!(!nodes[0].obstructed);
255+
}
256+
257+
#[test]
258+
fn parses_engine_purity_tokens() {
259+
let fixture = |purity: &str| {
260+
format!(
261+
r#"{{
262+
"options": [
263+
{{
264+
"options": [
265+
{{
266+
"options": [
267+
{{
268+
"layerId": "limestoneNodes",
269+
"markers": [
270+
{{
271+
"type": "Desc_Stone_C",
272+
"x": 0.0,
273+
"y": 0.0,
274+
"purity": "{purity}"
275+
}}
276+
]
277+
}}
278+
]
279+
}}
280+
]
281+
}}
282+
]
283+
}}"#
284+
)
285+
};
286+
287+
let impure = load_nodes_from_str(&fixture("RP_Inpure")).expect("impure token should parse");
288+
let normal = load_nodes_from_str(&fixture("RP_Normal")).expect("normal token should parse");
289+
let pure = load_nodes_from_str(&fixture("RP_Pure")).expect("pure token should parse");
290+
291+
assert_eq!(impure[0].purity, Purity::Impure);
292+
assert_eq!(normal[0].purity, Purity::Normal);
293+
assert_eq!(pure[0].purity, Purity::Pure);
294+
}
295+
}

0 commit comments

Comments
 (0)