Skip to main content

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:

  1. Screen reader — connects to a live NetHack process via a PTY and parses the terminal output using a virtual terminal emulator (pyte).
  2. Parser — translates the raw screen into structured game state: map tiles, player vitals, messages, menus, dungeon memory.
  3. Decision engine — a priority-ordered behavior tree (survival > tactical > strategic > explore) chooses an action each turn.
  4. 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 override NETHACKOPTIONS and break the custom configuration.

For development / contributing (additional):

  • just (command runner) — cargo install just or 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: an Action enum name (MOVE_E, WAIT, MOVE_DOWN, …) or --- when a CommandSequence ran (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 roadmap
  • docs/SPEC.md — detailed design: class diagrams, APIs, schemas, behavior tree
  • docs/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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nhemo-0.1.0.tar.gz (67.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nhemo-0.1.0-py3-none-any.whl (69.4 kB view details)

Uploaded Python 3

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

Hashes for nhemo-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3c9441627f9f8ccebe07f4910344a368964f652089c2df033cdcc183ebf8fde3
MD5 1b38edae3fb0ff1cd626eb4965120907
BLAKE2b-256 fb09dd4633475d5a08bd3d90e045052707f2227fda4b55ddd92409b0b193ce80

See more details on using hashes here.

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

Hashes for nhemo-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f32b4be542871b29811fd6330ffe2bf6ab19419c85522ad863a40db3f9bc1754
MD5 edd29792d96f1a5ef72bf4474bb64366
BLAKE2b-256 ec046d483c4427539bf3a4bf7641a5d3c9599aa7bbd5ae6277cc94f1d1adcd40

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page