checkpoint

This commit is contained in:
2025-11-07 09:03:25 +00:00
parent 562b33dff9
commit 58c83f6140
5 changed files with 274 additions and 11 deletions

180
main.py
View File

@@ -1,14 +1,84 @@
from collections import defaultdict, deque
from enum import Enum
from math import ceil
from typing import Dict, List, Tuple, Optional
from typing import Dict, List, Tuple, Optional, Any
import networkx as nx
from flask import Flask, render_template, request
from pydantic import BaseModel, Field
from plus import Items, Machines, Recipes, Recipe
from rich import print
app = Flask(__name__)
# Helpers to map item names to safe query parameter keys
def _slugify(name: str) -> str:
s = ''.join(ch.lower() if ch.isalnum() else '_' for ch in name)
# collapse consecutive underscores and trim
out = []
prev_us = False
for ch in s:
if ch == '_':
if not prev_us:
out.append('_')
prev_us = True
else:
out.append(ch)
prev_us = False
return ''.join(out).strip('_')
def compute_chain(targets: Dict[Items, float]) -> Tuple[Dict[str, float], List[dict], Dict[str, float], Dict[str, float]]:
class Production(BaseModel):
recipe: Recipe
quantity: float
def get_recipe(item: Items, recipe_map: Dict[Items, list[Recipes]]) -> Optional[Recipe]:
return recipe_map.get(item, (None,))[0]
def compute_chain2(targets: Dict[Items, float]) -> Any:
if not targets:
return {}
recipe_map: Dict[Items, list[Recipes]] = defaultdict(list)
for r in Recipes:
for o in r.value.outputs:
recipe_map[o].append(r.value)
# print(recipe_map)
demand = defaultdict(float)
for target in targets:
demand[target] = targets[target]
production = defaultdict(float)
# add demands to production
# find a recipe for that demand
# add inputs to demand
queue = deque(targets)
g = nx.DiGraph()
production_queue = []
raw_resources = defaultdict(float)
production_chain = defaultdict(float)
while queue:
item = queue.popleft()
recipe = get_recipe(item, recipe_map)
if recipe is None:
raw_resources[item] += demand[item] - raw_resources[item]
continue
levels = []
for out, quantity in recipe.outputs.items():
if out in demand:
target_quantity = demand[out] - production[out]
if target_quantity > 0:
levels.append(target_quantity / quantity)
else:
levels.append(0)
production_level = max(levels) if max(levels) > 0 else 0
production_chain[recipe.name] = production_level
for out, quantity in recipe.outputs.items():
production[out] += production_level * quantity
for inp, quantity in recipe.inputs.items():
queue.append(inp)
demand[inp] += production_level * quantity
print(demand, production, raw_resources, production_chain)
return production
def compute_chain(targets: Dict[Items, float], preferred_by_output: Optional[Dict[Items, str]] = None) -> Tuple[Dict[str, float], List[dict], Dict[str, float], Dict[str, float]]:
"""
Given desired output rates (item -> units/min), compute:
- required raw input rates (raw item -> units/min)
@@ -19,6 +89,8 @@ def compute_chain(targets: Dict[Items, float]) -> Tuple[Dict[str, float], List[d
Now supports alternate recipes: when multiple recipes produce the same output item,
a selection heuristic is used unless an explicit preference is configured.
preferred_by_output: optional mapping from output Item -> recipe name to force selection for that item.
"""
# Build a mapping from output item -> list of recipes that produce it
output_to_recipes: Dict[Items, List[Recipe]] = {}
@@ -28,8 +100,7 @@ def compute_chain(targets: Dict[Items, float]) -> Tuple[Dict[str, float], List[d
output_to_recipes.setdefault(out_item, []).append(recipe)
# Optional explicit preferences: map output Item -> recipe name to prefer
# Users can populate/modify this mapping elsewhere if desired.
PREFERRED_RECIPE_BY_OUTPUT: Dict[Items, str] = {}
PREFERRED_RECIPE_BY_OUTPUT: Dict[Items, str] = preferred_by_output or {}
# Heuristic to select a recipe when multiple alternatives exist
def select_recipe_for(item: Items) -> Optional[Recipe]:
@@ -177,6 +248,30 @@ def index():
# Read from query parameters for bookmarkable URLs
item_name = request.args.get("item") or selected_item
rate_str = request.args.get("rate")
selected_recipe = request.args.get("recipe") or ""
# Parse per-item recipe overrides from query params recipe_for_<slug(item)>
# Build slug -> Items map
slug_to_item: Dict[str, Items] = { _slugify(i.value.name): i for i in Items }
overrides: Dict[Items, str] = {}
for key, value in request.args.items():
if not key.startswith("recipe_for_"):
continue
if value is None or value == "":
continue
slug = key[len("recipe_for_"):]
item_enum = slug_to_item.get(slug)
if not item_enum:
continue
# Validate that the value is a valid recipe option for this item
candidates = []
for r in Recipes:
rec = r.value
if item_enum in rec.outputs:
candidates.append(rec.name)
if value in candidates:
overrides[item_enum] = value
rate = None
if rate_str is not None and rate_str != "":
try:
@@ -191,13 +286,39 @@ def index():
if rate is not None:
selected_rate = rate
# Determine candidate recipes for the selected output item
recipe_options: List[str] = []
item_obj_for_options = name_to_item.get(selected_item)
if item_obj_for_options is not None:
for r in Recipes:
recipe = r.value
if item_obj_for_options in recipe.outputs:
recipe_options.append(recipe.name)
recipe_options.sort()
# Validate selected_recipe against available options
if selected_recipe not in recipe_options:
selected_recipe = ""
else:
selected_recipe = ""
# Build preferred map merging top-level selection and overrides
preferred: Optional[Dict[Items, str]] = None
if selected_recipe or overrides:
preferred = {}
preferred.update(overrides)
if selected_recipe and item_obj_for_options is not None:
preferred[item_obj_for_options] = selected_recipe
# Compute and also prepare per-item override options based on resulting chain
overrides_ui: List[dict] = []
if not error and item_name and rate is not None:
item_obj = name_to_item.get(item_name)
if item_obj is None:
error = "Unknown item selected."
else:
targets = {item_obj: rate}
raw, steps, outputs, unused = compute_chain(targets)
raw, steps, outputs, unused = compute_chain(targets, preferred_by_output=preferred)
result = {
"targets": {item_name: rate},
"raw": raw,
@@ -206,6 +327,46 @@ def index():
"unused": unused,
}
# Collect unique output items from steps
unique_items = []
seen = set()
for s in steps:
item_nm = s.get("item")
if item_nm and item_nm not in seen:
seen.add(item_nm)
unique_items.append(item_nm)
# For each item, compute candidate recipes and current selection
for item_nm in unique_items:
item_enum2 = name_to_item.get(item_nm)
if not item_enum2:
continue
candidates = []
for r in Recipes:
rec = r.value
if item_enum2 in rec.outputs:
candidates.append({
"name": rec.name,
"building": rec.building.value.name,
})
if len(candidates) <= 1:
continue # only show when alternates exist
candidates.sort(key=lambda x: (x["name"]))
sel = None
if preferred and item_enum2 in preferred:
sel = preferred[item_enum2]
slug = _slugify(item_nm)
overrides_ui.append({
"item_name": item_nm,
"slug": slug,
"options": candidates,
"selected": sel or "",
})
# Build reset query (clear overrides)
reset_query = f"?item={selected_item}&rate={selected_rate}"
if selected_recipe:
reset_query += f"&recipe={selected_recipe}"
return render_template(
"index.html",
items=item_names,
@@ -213,6 +374,10 @@ def index():
error=error,
selected_item=selected_item,
selected_rate=selected_rate,
recipe_options=recipe_options,
selected_recipe=selected_recipe,
overrides_ui=overrides_ui,
reset_query=reset_query,
)
@@ -222,4 +387,5 @@ def create_app():
if __name__ == "__main__":
# For local dev: python main.py
app.run(host="0.0.0.0", port=5000, debug=True)
# app.run(host="0.0.0.0", port=5000, debug=True)
compute_chain2({Items.ModularFrame: 45.0})