NetHack Expert Machine Operator — automated NetHack player
Project description
NHEMO — NetHack Expert Machine Operator
An automated NetHack 3.6.x player that combines a rule-based behavior tree with optional LLM consultation to play the game from screen-reading to strategic decision-making.
MVP goal: a bot that consistently reaches Minetown as a dwarven Valkyrie.
What is NHEMO?
NetHack is a notoriously complex roguelike — decades of players have failed to ascend, and the game is famously hostile to automation. NHEMO approaches the problem in layers:
- Screen reader — connects to a live NetHack process via a PTY and parses the terminal output using a virtual terminal emulator (pyte).
- Parser — translates the raw screen into structured game state: map tiles, player vitals, messages, menus, dungeon memory.
- Decision engine — a priority-ordered behavior tree (survival > tactical > strategic > explore) chooses an action each turn.
- LLM layer (future) — consults an LLM for novel situations the rule base doesn't cover.
The bot never sees raw NetHack internals. It reads only what a human player would see on a terminal — making it work with any standard NetHack installation.
Current Status
NHEMO is under active development. Completed slices:
| Slice | Description | Status |
|---|---|---|
| V1 | Spawn NetHack via PTY, read screen, send keystrokes | done |
| V2 | Parse map, find player @, move without walking into walls |
done |
| V3 | Parse status bars and messages; detect hunger and low HP; eat and heal | done |
| V4 | A* pathfinding, room-first exploration, doors, boulders, stairs, multi-level descent | done |
| V5 | Combat: detect monsters, melee, flee, pick up items | planned |
| V6 | SQLite persistence: dungeon memory, stash locations, session resume | planned |
| V7 | Knowledge base: auto-generated from NetHack source + hand-authored overlays | planned |
| V8 | LLM integration: abstract provider, context assembly, Claude + OpenAI backends | planned |
| V9 | Full behavior tree + goal system + main game loop | planned |
| V10 | MVP integration: consistent Minetown arrival as dwarven Valkyrie | planned |
See docs/TASKS.md for the granular task list with checkbox progress.
Requirements
- Python 3.12+
- uv (package manager)
- NetHack 3.6.x installed — the real binary, not a wrapper script.
On many distros:
/usr/lib/nethack/nethack
Note: NHEMO uses the setgid NetHack binary directly. Do not point it at shell wrappers like
/usr/bin/nethack— those overrideNETHACKOPTIONSand break the custom configuration.
For development / contributing (additional):
- just (command runner) —
cargo install justor via your package manager (apt install just,brew install just, etc.)
Installation
git clone https://gitlab.com/your-username/nhemo.git
cd nhemo
uv sync
Development setup
# Install dev dependencies (pytest, ruff, ty)
uv sync --extra dev
# Install the pre-commit lint hook (run once after cloning)
just install-hooks
# Verify your setup — lists all available dev tasks
just
The pre-commit hook runs just lint (ruff + ty) automatically before every
commit. If it fails, run just lint-fix to auto-fix and retry.
Quick Start
# 30-turn smoke test (default)
uv run python -m nhemo
# Watch the bot play live
uv run python -m nhemo --turns 500 --watch
# Slower watch mode, easier to follow
uv run python -m nhemo --turns 500 --watch --delay 0.5
# Verbose per-turn logging to stderr (safe to combine with --watch)
uv run python -m nhemo --turns 200 --watch --log-level DEBUG
# Capture log to file
uv run python -m nhemo --turns 500 --log-level DEBUG 2>nhemo_debug.log
Configuration
Copy and edit config.yaml to change the NetHack binary path, character
selection, or LLM settings:
nethack:
binary_path: "/usr/lib/nethack/nethack" # adjust if needed
player:
role: "Valkyrie"
race: "Dwarf"
alignment: "Neutral"
llm:
mode: "offline" # offline | complex_only | full
A recommended .nethackrc (parsing-friendly options, vi-key movement) is
included and loaded automatically.
Observability
Logging
uv run python -m nhemo --turns 200 --log-level DEBUG
| Level | What you see |
|---|---|
WARNING (default) |
Parser failures (player/map not found), bot stuck and giving up on a position |
INFO |
Level transitions, doors opened/kicked, stairs descended, food eaten, monsters killed |
DEBUG |
Every turn: position, visited tile count, stuck state, chosen action and phase |
Screen recording
Record a session and replay it against a modified parser to catch regressions without running a live game:
# Record
uv run python -m nhemo --turns 200 --record /tmp/game.nhrec
# Replay through the current parser
uv run python tools/screen_recorder.py replay /tmp/game.nhrec
# Inspect a specific turn's raw screen
uv run python tools/screen_recorder.py show /tmp/game.nhrec --turn 42
# Diff two recordings
uv run python tools/screen_recorder.py diff /tmp/before.nhrec /tmp/after.nhrec
Decision event log
uv run python -m nhemo --turns 200 --log-events /tmp/events.jsonl
Each turn emits one JSON line capturing what the bot decided and why:
{"turn":42,"screen_mode":"NORMAL","action":"MOVE_E","phase":"pathfind","pos":[12,40],"hp":15,"max_hp":19,"dlvl":"Dlvl:3"}
All flags can be combined freely — they write to independent streams:
uv run python -m nhemo --turns 500 --watch --log-events /tmp/events.jsonl --log-level DEBUG 2>nhemo_debug.log
Watch mode display
Running with --watch renders the NetHack screen live, followed by NHEMO's own
status line and a persistent message log:
This door is locked. ← row 0: game message
← rows 1-21: dungeon map
--------
|....#.|
|......|
|......|
|......|
----.--- +
## @
# ##
...
-------.-------- #----.-
|..............| #|....|
|......$........#####|"....### ----------
|..............| #.....|#############..$$.....|
|..............|`####------ ##### |........|
---------------- ######|.<......|
----------
← row 21: blank separator
NHEMO the Stripling St:16 Dx:14 Co:17 In:8 Wi:12 Ch:8 Lawful ← status bar 1
Dlvl:1 $:0 HP:18(18) Pw:1(1) AC:6 Xp:1/0 T:118 ← status bar 2
NHEMO turn 321 action: --- ← NHEMO status line
── message log ────────────────────────────────────── ← last 8 messages
t 314: This door is locked.
t 315: This door is locked.
t 316: This door is locked.
t 317: This door is locked.
t 318: This door is locked.
t 319: This door is locked.
t 320: This door is locked.
> t 321: This door is locked. ← most recent (bold >)
NetHack screen (rows 0–23)
| Area | What it shows |
|---|---|
| Row 0 | Game message — combat results, door status, hunger warnings, etc. |
| Rows 1–21 | Dungeon map — @ is the player, + closed door, # corridor, . floor, $ gold, </> stairs, f monster, etc. |
| Row 22 | Status bar 1 — name, title, attribute scores, alignment |
| Row 23 | Status bar 2 — depth (Dlvl:), gold ($), HP, power (Pw), armour class (AC), experience (Xp), game turn (T:) |
NHEMO status line
NHEMO turn N— NHEMO's loop iteration counter (see below).action:— the last action chosen: anActionenum name (MOVE_E,WAIT,MOVE_DOWN, …) or---when aCommandSequenceran (open/kick door, eat item) rather than a single keystroke.
Message log
The last 8 game messages with their NHEMO turn number. The most recent line is
prefixed with >. Messages are deduplicated — if the same text appears on
consecutive turns it is only appended once.
NHEMO turn vs. game turn (T:)
The NHEMO turn counter and NetHack's T: counter are not the same.
T: advances once per in-game timed action. NHEMO's turn counter advances once
per main-loop iteration, including iterations that send a free (zero-time) key
or no key at all:
| Situation | NHEMO turn +1? | T: +1? |
|---|---|---|
| Normal movement or combat | yes | yes |
Waiting in place (WAIT) |
yes | yes |
| Opening/kicking a door | yes | yes |
Dismissing --More-- (space) |
yes | no |
| Dismissing a menu or overlay | yes | no |
Replying to character-creation prompts (y) |
yes | no |
LOOK after a kill (: command) |
yes | no |
So NHEMO turns always run ahead of T:, and the gap grows whenever there is
heavy --More-- output (long combat logs, intro text) or many kills (each
triggers a free LOOK to detect dropped items). This is expected and correct.
Running Tests
uv run pytest # unit tests only
uv run pytest -m integration # requires a live NetHack binary
uv run pytest --cov=src/nhemo # with coverage
Architecture (for collaborators)
Component overview
┌─────────────────────────────────────────────────────────┐
│ __main__.py │
│ (game loop / CLI) │
└────────────────────────────┬────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ Interface │ │ Parser │ │ Decision │
│ (PTY/pyte)│ │ (screen → │ │ (behavior │
│ │ │ game state) │ │ tree) │
└────────────┘ └──────────────┘ └──────────────┘
│ │
┌────────┴────────┐ │
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌────────┐
│ State │ │ Knowledge │ │ LLM │
│(game/map/│ │ Base (YAML) │ │ layer │
│ dungeon) │ └──────────────┘ └────────┘
└──────────┘
│
┌──────▼──────┐
│ Persistence │
│ (SQLite) │
└─────────────┘
Key modules
| Package | Description |
|---|---|
src/nhemo/interface/ |
Abstract interface protocol + PTY backend (pexpect + pyte) |
src/nhemo/parser/ |
Screen → structured state: map tiles, status bars, messages, menus |
src/nhemo/state/ |
Game state model: player, dungeon memory, level tiles, inventory |
src/nhemo/decision/ |
Behavior tree framework, node types, YAML loader (V9) |
src/nhemo/knowledge/ |
YAML knowledge base loader and query interface (V7) |
src/nhemo/llm/ |
Abstract LLM provider + concrete backends (V8) |
src/nhemo/persistence/ |
SQLite schema and data access layer (V6) |
src/nhemo/recording.py |
Screen recording and event log infrastructure |
Three-level action system
NHEMO's decision engine produces actions at three granularities:
- Level 1 — GameAction: a single keystroke (move north, wait, search)
- Level 2 — CommandSequence: multi-keystroke interaction with screen checks between steps (eat item, open door). Aborts on unexpected screen state.
- Level 3 — TacticalPlan: multi-command plan toward a mini-goal (e.g., clear a room). Interruptible by survival priorities — plans suspend and resume.
Data-driven behavior tree
The tree structure is defined in data/default_bt.yaml and loaded at startup
via a node registry (decorator-registered conditions and actions). You can
experiment with strategies without touching Python code.
Priority order: Survival → Tactical → Strategic → Explore
Design documents
docs/PLAN.md— architecture decisions, slice breakdown, post-MVP roadmapdocs/SPEC.md— detailed design: class diagrams, APIs, schemas, behavior treedocs/TASKS.md— granular implementation tasks with completion status
Roadmap
Beyond the V10 MVP (consistent Minetown arrival):
- Phase 2 (V11–V17): Mines End, Sokoban, shop interaction, altar use, item identification, quest completion
- Phase 3 (V18–V21): Gehennom, Vlad's Tower, Sanctum
- Phase 4 (V22–V24): Ascension run through the elemental planes
- Phase 5 (V25–V32): Telnet/SSH backend, wiki knowledge ingestion, ttyrec learning, multi-role support, BT A/B testing, LLM-guided tree evolution
Contributing
Contributions are welcome. The project uses a structured implementation workflow
— each unit of work is a single task in docs/TASKS.md tied to a section of
docs/SPEC.md. Reading PLAN → SPEC → TASKS in that order gives you the full
picture before touching code.
A few conventions:
- Python 3.12, type hints on all public APIs
- One task → one commit, descriptive message
- Every task ships with its own tests (
uv run pytest) - No over-engineering: implement exactly what the task specifies
License
Copyright (C) 2025 Javier Novoa C.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nhemo-0.1.0.tar.gz.
File metadata
- Download URL: nhemo-0.1.0.tar.gz
- Upload date:
- Size: 67.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c9441627f9f8ccebe07f4910344a368964f652089c2df033cdcc183ebf8fde3
|
|
| MD5 |
1b38edae3fb0ff1cd626eb4965120907
|
|
| BLAKE2b-256 |
fb09dd4633475d5a08bd3d90e045052707f2227fda4b55ddd92409b0b193ce80
|
File details
Details for the file nhemo-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nhemo-0.1.0-py3-none-any.whl
- Upload date:
- Size: 69.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f32b4be542871b29811fd6330ffe2bf6ab19419c85522ad863a40db3f9bc1754
|
|
| MD5 |
edd29792d96f1a5ef72bf4474bb64366
|
|
| BLAKE2b-256 |
ec046d483c4427539bf3a4bf7641a5d3c9599aa7bbd5ae6277cc94f1d1adcd40
|