A Streamlit app for exploring Elden Ring datasets, ranking candidates, and optimizing armor/talisman choices with multi-stat scoring.
- Loads CSV datasets from
data/(includingdata/items/). - Provides a UI for sorting/ranking and side-by-side comparison workflows.
- Supports optimization modes for armor and talismans (single and set-based flows).
- Includes script-based startup, verification, and recovery workflows for fast development loops.
- Depends on
plotlyfor chart and interaction paths used by the UI.
docs/README.md— onboarding index for docs and session deep dives.docs/DEVELOPMENT.md— architecture guide, module roles, and testing conventions.docs/optimizer/README.md— optimizer documentation hub.docs/developer/icon_and_stat_layout_customization.md— current UI layout/icon/detailed-scope customization points.
CONTRIBUTING.md— setup, verification, and pull request guidelines.SECURITY.md— how to report a security vulnerability.LICENSE— all rights reserved; see license for permitted use.
elden_ring_data_ui/
├── app.py # Main Streamlit UI
├── app_support/ # Detailed-scope text/focus/placeholders helpers
├── data_loader.py # CSV loading, column profiles, cache-backed read helpers
├── optimizer/ # Optimization package (legacy + dialect API + strategies)
├── histogram_views.py # Histogram rendering + interaction config
├── histogram_layout.py # Shared histogram sizing/layout helpers
├── ui_components.py # Parsing/UI utility helpers
├── scripts/ # PowerShell automation: setup, verify, run, recover
├── tools/ # Verification utilities used by scripts
├── data/ # Datasets and load-profile config
│ ├── active_datasets.json # Default active datasets in app
│ └── column_loading_instructions.json
└── docs/session/ # Session notes, deep dives, and audits
.\setup.ps1That script creates or refreshes a uv-managed .venv and installs the pinned packages from requirements.txt.
Manual equivalent:
uv venv --python 3.11 .venv
uv pip install --reinstall --python .\.venv\Scripts\python.exe -r requirements.txt
.\.venv\Scripts\Activate.ps1
python -m streamlit run app.pyThe uv-managed .venv created by setup.ps1 is the supported setup path.
The scripts/ folder is the best path for repeatable runs.
| Command | Use it when | Lifecycle owner | Browser behavior | Logical restart path |
|---|---|---|---|---|
./scripts/run_streamlit_local.ps1 |
You want a direct foreground Streamlit session while developing | Current terminal only | Streamlit prints the URL; browser opening is manual | Ctrl+C, then rerun ./scripts/run_streamlit_local.ps1 |
./scripts/start-app.ps1 |
You want the managed detached app flow | tools.runtime_controller |
Does not open an external browser by default; pass -OpenBrowser to opt in to a fresh window after start/restart |
./scripts/recover-app.ps1 |
./scripts/recover-app.ps1 |
The managed app needs a clean controller-backed restart | tools.runtime_controller |
Does not open an external browser by default; pass -OpenBrowser to opt in to a fresh window after recovery |
This is the preferred managed restart command |
./scripts/stop_streamlit_port.ps1 -Port 8501 |
You want to stop the current local app session | tools.runtime_controller by default; emergency hard kill only with -ForceAnyListener |
None | Follow with start-app.ps1 or run_streamlit_local.ps1 |
./scripts/run-all.ps1 -RunApp |
You want reset + environment + verification + managed start in one command | run-all.ps1 orchestrates, controller owns the final app lifecycle |
No external browser by default; pass -OpenBrowser to opt in |
Rerun run-all.ps1 -RunApp or use recover-app.ps1 after the initial bootstrap |
-
If you started the app with
run_streamlit_local.ps1, restart it by stopping that terminal withCtrl+Cand runningrun_streamlit_local.ps1again. -
If you rerun
start-app.ps1while the same app still owns port8501, the controller now closes that current session and starts a fresh one instead of reusing the old window. -
If you started the app with
start-app.ps1orrun-all.ps1 -RunApp, restart it withrecover-app.ps1when you want the explicit managed restart command. -
By default, the app stays headless and local. Managed wrappers suppress external browser launch, and
.streamlit/config.tomlkeeps Streamlit headless unless you explicitly opt in to browser opening. -
If you only need to stop the app, use
stop_streamlit_port.ps1 -Port 8501. -
If you need the slow but highest-confidence cycle, use
run-all.ps1 -RunApp. -
Foreground local-only run (recommended while developing):
./scripts/run_streamlit_local.ps1
-
Stop app by closing terminal (
Ctrl+C) or explicitly freeing port 8501:./scripts/stop_streamlit_port.ps1 -Port 8501
-
Full bootstrap + run app:
./scripts/run-all.ps1 -RunApp
-
Fast relaunch while iterating UI:
./scripts/run-all.ps1 -SkipReset -QuickVerify -RunApp -OpenBrowser
-
Ultra-fast relaunch (skip reset/env/verify):
./scripts/run-all.ps1 -UltraQuick -RunApp
-
Verify only:
./scripts/verify-workspace.ps1
-
Recovery if app is stuck/nonresponsive:
./scripts/recover-app.ps1
- Streamlit defaults are configured in
.streamlit/config.tomlto bind tolocalhoston port8501. .streamlit/config.tomlkeeps rawpython -m streamlit run app.pyheadless by default, so the app stays inside the editor workflow unless you opt into browser opening.- CORS and XSRF protection are enabled by default.
./scripts/run_streamlit_local.ps1runs in the current terminal so stopping is explicit (Ctrl+C)../scripts/start-app.ps1intentionally launches a detached/background process for convenience, so it can continue after terminal/editor close.
- LAN exposure is opt-in. If you change server address to
0.0.0.0or pass--server.address 0.0.0.0, devices on your network may be able to access the app. - Only use LAN mode on trusted networks and restore localhost defaults when finished.
- Default active datasets are controlled by
data/active_datasets.json. - The default selector now prioritizes the full supported top-level set:
armorstalismansashesOfWarbossescreaturesincantationslocationsnpcsshieldsshields_upgradesskillssorceriesspiritAshesweaponsweapons_upgrades- Untouched
data/items/*catalog datasets stay out of the main selector until they get a curated UI pass.
- If a registry entry is present but still deferred, the selector label appends
Not implemented yetinstead of silently hiding it. - Upgrade tables use a browse-only progression view with grouped item details rather than the ranked item-card layout.
- Use the left-sidebar
Layout:dropdown to switch fromSingle datasettoSide by side. - Side-by-side mode renders two embedded panes, each running the existing dataset interface independently.
- Use
Left pane dataset:andRight pane dataset:to choose starting datasets for each pane. - Use
Pane height:to increase the embedded pane height when you want more of each interface visible without scrolling. - The embedded panes still use the full dataset UI, so each pane can switch datasets, views, ranking controls, and detail inspectors independently.
- Single-stat selection uses direct sort behavior.
- Multi-stat selection (2+ valid stats) uses optimizer ranking.
- Upgrade-table datasets are browse-only and do not expose ranking controls.
- Current optimization methods:
Maximin(maximin_normalized, default)Weighted Sum(weighted_sum_normalized)
- Optimization engines in UI (Optimization view):
Legacy Ranking(legacy)Advanced Optimizer(advanced)
- Armor full-scope behavior:
Advanced Optimizer+Stat Rankingperforms true full-set ranking using prune-first combination search.Advanced Optimizer+Encounter Survivalperforms full-set encounter ranking.Advanced Optimizer+Customscope supports slot-lock constraints (include_names) while optimizing the remaining slots.Legacy Rankingfull-scope preview remains a per-slot composed view (not full-set combinatorial optimization).
- Weighted Sum now only uses stats whose weight is greater than zero.
- If exactly one weighted stat remains active, ranking falls back to that stat's single-stat sort behavior.
- If all weights are zero, optimization is blocked with a validation error.
- Optimization metadata columns include:
__opt_score__opt_tiebreak__opt_method__opt_rank
- Dataset:
armors(recommended) - View mode:
Optimization view(do not useDetailed viewfor this flow) - In the right control column set:
Optimization engine=Advanced OptimizerObjective=Encounter Survival(or keepStat Ranking)Choose Scope=Customto lock specific armor slots and optimize around themEncounter profile= one ofdata/profiles/*.yamlStatus Penalty Weightas desired
-
UI-facing stat labels are now centralized in
data/stat_ui_map.json. -
The app uses permanently capitalized display names such as:
Holy Damage NegationFire Damage NegationStrike Damage NegationPoison ResistanceScarlet Rot ResistanceBleed ResistanceFrost ResistanceSleep ResistanceMadness ResistanceDeath Blight Resistance
-
Stat icon metadata/provenance is maintained in
data/icons/icons.json. -
App stat labels prefer local icon assets from
data/icons/icons.json(local_path) and fall back to emoji labels if files are missing. -
Verify local icon asset availability with:
python scripts/verify_icon_assets.py -
Aggregated resistance names (
Immunity,Robustness,Focus,Vitality) are kept internally for compatibility but hidden from user-facing card/table output.
./scripts/start-app.ps1opens app URLs in a dedicated Edge window (--new-window) rather than opening a new tab.- If an app window for the same localhost port already exists, the script focuses and refreshes it.
weightis minimized.- Selected non-weight stats are maximized.
- For normalization-based methods,
weightis inverted after normalization so higher normalized score is always better.
DataLoaderreads fromdata/anddata/items/.- Column-loading profiles are driven by
data/column_loading_instructions.json. - Streamlit caching is used for file reads and loader resources.
- Cached dataset and column-instruction reads are keyed by each file's current size and modification time, so normal file edits should invalidate stale reads without a manual cache reset.
Use one of these reliable refresh paths:
-
Managed restart for controller-owned sessions (recommended when you used
start-app.ps1orrun-all.ps1 -RunApp):./scripts/recover-app.ps1
-
Full reset + verify + launch:
./scripts/run-all.ps1 -RunApp
-
Manual foreground restart for direct local runs: stop Streamlit with
Ctrl+C, then rerun:./scripts/run_streamlit_local.ps1
Regression note:
- File-based data loading and column-instruction caching are keyed by file signature, so normal edits invalidate stale reads.
- This behavior is covered by tests in
tests/.
-
Lightweight import/data sanity:
python -m tools.final_check -
Optimizer sanity checks:
python -m tools.optimizer_check -
Dialect + encounter smoke checks:
python -m tools.optimizer_smoke -
Consolidated verify pipeline:
./scripts/verify-workspace.ps1
-
Full regression suite:
python -m unittest discover tests -
Focused UI smoke suite:
python -m unittest tests.test_ui_smoke -
Fast verify mode:
./scripts/verify-workspace.ps1 -Quick
- Current GitHub Actions CI runs:
- Linux:
ruff check .andpython -m tools.workspace_verify - Windows:
python -m tools.workspace_verify
- Linux:
tools.workspace_verifyrunstools.final_check,tools.optimizer_check,tools.optimizer_smoke, and split unit-test steps.- On Windows, the unittest portion is reported as
tests_coreandtests_runtime_controller. - The unit-test suite now includes Streamlit UI smoke coverage for the default detailed view and the main optimization flow.
./scripts/verify-workspace.ps1 -Quickkeeps the wrapper fast by skipping optimizer and test execution.- Use
python -m tools.workspace_verifyor the wrapper before release-critical changes when you need full verification.
-
Issue: the uv-managed
.venvis missing or stale- Run
./setup.ps1
- Run
-
Issue: app port conflict / stale Streamlit process
- Run
./scripts/reset-dev-session.ps1 - Or run
./scripts/stop_streamlit_port.ps1 -Port 8501 - Then relaunch with
./scripts/start-app.ps1
- Run
-
Issue: datasets not loading as expected
- Confirm CSV exists in
data/ordata/items/ - Check dataset key/path mapping in
data_loader.py - Run
python -m tools.final_checkfor a fast probe
- Confirm CSV exists in
-
Issue: optimization output looks wrong
- Verify selected stat names match real columns
- Ensure at least 2 valid stats for optimization mode
- Run
python -m tools.optimizer_check
-
optimizer/legacy.py- Method registry and dispatch
- Objective direction handling (
weightminimization) - Scoring implementations:
_score_maximin_normalized_score_weighted_sum_normalized
-
optimizer/api.py- Dialect-first entrypoint
optimize(df, request)
- Dialect-first entrypoint
-
optimizer/dialect.py- Request loading, validation, canonicalization
-
optimizer/strategies/encounter_survival.py- Encounter-aware survival objective
Mand compositeJ
- Encounter-aware survival objective
-
optimizer/strategies/full_set_prune.py- Full-set armor pruning + enumeration
-
optimizer/strategies/full_set_stat_rank.py- Full-set stat-rank pruning + enumeration
-
app.py- UI controls for optimization method selection
- Session-level caching for optimizer ranking results
- View-specific rendering of ranked outputs
A focused, incremental path to “refine to perfection”:
- Add per-method diagnostics in UI (show normalized stat contributions).
- Add robust tie-break controls (secondary/tertiary sort strategy selection).
- Add optional hard constraints (for example, strict max weight before scoring).
- Add Pareto frontier mode for trade-off exploration (non-dominated set view).
- Validate against curated build scenarios and add repeatable benchmark cases.
See docs/session/ for historical notes and implementation deep dives. Start with:
docs/session/2026-02-14_repo_deep_dive.mddocs/session/2026-02-15_startup_and_verify_deep_dive.mddocs/session/2026-02-15_commit_summary.md
- Validation and data-integrity helpers live in
tools/. setup.ps1remains available as a legacy entry point.