A companion app that mostly automates the game 'Iron Nest' for you.
Go to file
Dominik Roth 57e73c58ea Fix real zoom+square-cells bugs, add drag-to-pan, simplify popover buttons
Every fix here was verified directly (numeric checks against the view
math, rendered screenshots, GTK smoke tests), not just reasoned about,
after the previous zoom/square pass shipped with real problems still
in it.

Map fixes:
- Off-viewport entities bled through onto the canvas when zoomed in:
  _km_to_px() has no inherent bound, an entity actually elsewhere on
  the map still got projected and drawn if the result happened to land
  within the canvas's pixel bounds. Clipped all marker/overlay/arrow/
  scout-flight drawing to the grid's own visible rectangle.
- Gridlines themselves bled into the label margin above/left of the
  grid: the line-drawing loop deliberately over-generated a couple of
  lines past the true viewport edge (meant to cover a trailing partial
  cell, which doesn't need its own line, just the whole-integer lines
  already bounding it). Switched to ceil/floor bounds that only ever
  produce in-range lines, verified directly against the view math for
  a fractional zoom/pan (no clip needed for this half, the bug was
  generating the wrong lines in the first place, not failing to hide
  them).
- Square-cell letterbox padding stayed pixel-identical at every zoom
  level (verified: same padding at 1x/2x/5x), which meant the fixed
  bands ate a bigger and bigger share of an already-zoomed-in view.
  Once actually zoomed in with square_cells on, the viewport's own
  aspect now follows the canvas shape instead of staying locked to the
  full map's 20:10 ratio, so cells come out square with zero
  letterboxing rather than fixed padding. The whole-map view (zoom=1)
  is unchanged, that's the one case that has a real reason to keep the
  fixed 20:10 shape.
- Added drag-to-pan, active once actually zoomed in (at 1x the whole
  map's already on screen, nothing to pan to). Guards against a drag's
  release also firing as a click-select via a small pixel threshold.

Popover cleanup (Nest/Spotters/Reference Points/Targets):
- Removed each popover's own 'Load all from screenshot' bulk button
  and every row's individual 'set this one from screenshot' button:
  the universal clipboard button in the header already re-parses a
  fresh screenshot/paste and merges it into everything it recognizes
  by name, these were redundant category- and item-specific ways to
  trigger the exact same merge. Deleted the now-dead
  _set_*_from_screenshot_info() methods along with them.
- The trailing 'Add spotter'/'Add RP'/'Add target'/'Add scout flight'
  rows are now a single full-width button (new _add_row() helper)
  instead of the full two-button _row() layout, there's nothing to
  screenshot into for something that doesn't exist yet.

Verified: full pytest suite, direct numeric check that no gridline
lands outside the grid rectangle at a fractional zoom/pan, a rendered
screenshot at 4x zoom confirming off-viewport entities no longer
appear and gridlines terminate cleanly at the edges, and a full-app
smoke test exercising drag-pan end to end including the post-drag
click-suppression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:04:55 +02:00
assets/icons Shell icon picker: fix real sizing bugs, crop icon padding, inline grid for Add Strike 2026-08-09 19:28:41 +02:00
docs docs: map-vision pipeline status + BFS grid-growing plan 2026-08-09 14:49:40 +02:00
src/fenigma Fix real zoom+square-cells bugs, add drag-to-pan, simplify popover buttons 2026-08-09 20:04:55 +02:00
tests Fix explain_unresolved() not excluding toleranced bearings like solve_location() does 2026-08-09 18:28:48 +02:00
.gitignore Add game-extracted UI icons: target markers, RP letters, shell set, nest, scout plane 2026-08-09 18:54:27 +02:00
icon_alt.png Rewrite README short, rename project to FeNigma, add icon/showcase 2026-08-08 20:15:14 +02:00
icon.png Rewrite README short, rename project to FeNigma, add icon/showcase 2026-08-08 20:15:14 +02:00
install.sh Add install.sh, run.sh uses the venv when present 2026-08-08 20:18:15 +02:00
pytest.ini Add a real pytest suite: 21 tests covering every OCR format + solver geometry 2026-08-09 18:20:27 +02:00
README.md Add a real pytest suite: 21 tests covering every OCR format + solver geometry 2026-08-09 18:20:27 +02:00
requirements-dev.txt Add a real pytest suite: 21 tests covering every OCR format + solver geometry 2026-08-09 18:20:27 +02:00
requirements.txt Sweep em-dashes out of code, docstrings, and requirements.txt 2026-08-08 20:27:53 +02:00
run.sh Rename package/app to FeNigma consistently 2026-08-08 20:20:43 +02:00
showcase.png Rewrite README short, rename project to FeNigma, add icon/showcase 2026-08-08 20:15:14 +02:00


FeNigma

Fe (iron) + enigma, we solve the geometric puzzles.

A companion app that mostly automates IRON NEST: Heavy Turret Simulator for you. Select the game's typewriter orders (selecting text in-game copies it to your clipboard automatically) and it solves the geo puzzle and the trajectory math, handing you ready-to-fire commands: elevation, azimuth, number of powder charges. Screenshotting works the same way when a selection isn't practical. Select or screenshot the field log the same way and it picks up kills and newly-spotted units automatically. You can also plan strikes and scout flights of your own. Pure screen-reading, no game files touched, no input injected.

Showcase

What it does

  • Reads orders, solves the geometry. Copy (or screenshot) the in-game typewriter text and it parses absolute grid refs and relative bearing/distance descriptions, then resolves everything into map coordinates, chained clues ("Bearing 293 from Alpha") included. The map shows its work: the actual bearing lines/circles behind each resolved position. When a description is genuinely ambiguous (two intersections), both candidates are shown instead of guessing.
  • Calculates the shot. Every resolved target gets a live firing card: elevation, azimuth, and minimum powder charge, computed from the Nest.
  • Tracks the battle. A second copy/screenshot of the field log marks units destroyed and folds in newly-spotted contacts, merging with what's already known instead of duplicating it.
  • Plans strikes. Drop a strike anywhere on the map and pick a shell to preview its blast radius before committing.
  • Plans scout flights. Click the map to plot a scout flight's sweep path: it anchors to the large grid square you're pointing at and reads the heading off exactly where in that square you click, previewed live before you commit.
  • Watches the clipboard for you. Toggle auto-watch and every new screenshot or copied intel text gets read and merged automatically, no manual fetch between orders.

Install

./install.sh

Sets up a venv for the Python deps (Pillow, numpy, pytesseract) and checks for the system packages that pip can't install: GTK4/libadwaita bindings and tesseract. If either is missing it prints the package names for your distro and stops, install those and re-run.

Run

./run.sh

Uses the venv from install.sh if one exists, otherwise falls back to system python3. GTK apps with this app ID are single-instance, if a run gets killed uncleanly it can leave a zombie registered on D-Bus and silently no-op the next launch. If ./run.sh seems to do nothing, pgrep -af fenigma and kill any stragglers first.

Tests

pip install -r requirements-dev.txt
pytest

Regression coverage for every intel-text format the OCR pipeline understands and the solver's geometry, in tests/. Run this before trusting a change to ocr.py/solver.py, several of the formats have collided with each other in non-obvious ways before.

Stack

GTK4 + libadwaita (PyGObject) for the UI, Tesseract (via pytesseract) for OCR, Pillow/numpy for preprocessing. Details on the coordinate system, OCR formats, and solver internals live in code comments (solver.py, ocr.py, models.py) rather than here.