New shapes: - 'Listening Post#1 at K6 7:8 ...': a named anchor given inline rather than as its own block, parse_named_at_coord() picks up any '<Name>#<id> at <Coord>' anywhere in the text, RP-shaped like everything else that resolves to a name+coord. - 'Distance 6.28km South-East from X': a listening post gives distance readings with an approximate 8-point compass direction instead of a precise degree bearing, different word order too (the direction sits between the distance and 'from', no separate Bearing keyword). New TargetType.COASTAL_BATTERY for what these turned out to report on. - 'Coastal Battery#2' / 'Listening Post#1': multi-word type names with a real digit id already attached (unlike the Enemy case, nothing to invent), squash_multiword_ids() collapses the embedded space so _NAMED_HEADER_RE and every from-<ref> clue pattern see the single token they expect. Also: squash_bold_spans(), prompted by the observation that a rich-text paste already tells us exactly where a multi-word name starts and ends via its own <b>...</b> wrapping, no need to guess from capitalization the way squash_enemy_names()/squash_multiword_ids() do. Runs before strip_html() while the tags are still there, using each span's own content as an authoritative boundary. Those two whole-document regexes stay as the fallback for plain OCR text, which never has markup to lean on, but squash_multiword_ids() first shipped with a real bug this caught: 'Distance 6.28km South-East from Listening Post#1' matched 'East from ListeningPost' as if it were one multi-word name, backward through the lowercase connector word 'from'. Fixed by requiring Title Case on every word in that whole-document fallback path (the scoped bold-span path never had this problem, it can't reach past a span's own boundary). Verified end to end against the exact example text, including through the real _merge_targets()/_merge_reference_points() app flow, plus the full existing regression sweep (Enemy names, train intel, standard blocks, calibration target). |
||
|---|---|---|
| src/fenigma | ||
| .gitignore | ||
| icon_alt.png | ||
| icon.png | ||
| install.sh | ||
| README.md | ||
| 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.
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.
