Tracks the hovered large cell (col, row) separately from marker hit-testing, since it needs to update on any cursor motion, not just when crossing a marker's hit radius, redrawn only when the actual cell changes, not on every pixel of motion within the same one. Lines drawn inside the existing clip region so they never bleed past the visible viewport at any zoom/pan state, dimmer than the main grid lines (0.6x alpha, tuned down further after an initial too-loud pass) so they read as a subtle aid, not competing visual noise. Verified with a rendered screenshot: the subdivision lines appear only within the hovered cell, correctly clipped, and stay faint against the main grid. |
||
|---|---|---|
| assets/icons | ||
| docs | ||
| src/fenigma | ||
| tests | ||
| .gitignore | ||
| icon_alt.png | ||
| icon.png | ||
| install.sh | ||
| pytest.ini | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
| run.sh | ||
| showcase.png | ||
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.
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.
