This repository allows a chatbot agent to play NetHack, a classic text-based dungeon exploration game, and provides a web app for humans to watch the action with popcorn buckets in hand. What becomes quickly obvious in this experiment is that while chatbots do well with text, of course, they really struggle with map reading and spatial awareness.
Also, because NetHack can go on for thousands of turns, it's not practical to feed the entire game history to the agent as context. Instead, the agent manages its own working memory in the form of notes that it can create and delete. This allows the agent to remember the past and plan ahead, at least to some extent. The limitations of this approach quickly become obvious as well.
This app was developed in tandem with Claude Code. I wrote the fun parts, integrating the agent with NetHack, while Claude handled the plumbing needed to expose a NetHack API, similar to the NetHack Learning Environment. Claude also wrote the web app's UI, and did a bang-up job IMHO.
Claude's description of the project follows below.
An F# API that lets callers play NetHack 5.0 as a pure-looking function
GameState -> Action -> GameStateplus a console app and an LLM agent that play through it. Under the hood, real NetHack runs headless behind its "shim" window port, driven from .NET via P/Invoke.
| Path | What it is |
|---|---|
Core/ |
NetHack 5.0 source, as a git submodule → brianberns/NetHack branch libnh-shim. Adds a NetHackNative DLL build (the shim window port exposed as a callable library: nhmain, nhglue_set_handler, glyph/menu helpers) on top of upstream NetHack. |
NetHack.Api/ |
The API library: domain types (GameState, Observation, Action, Prompt, …), JSON, an in-process Stub engine, and the real Native engine (P/Invoke into NetHackNative.dll). |
native/ |
C glue compiled into the DLL — nhglue.c (variadic→fixed callback trampoline), nhglue_ext.c (glyph decode + menu building), and nethack_exports.def. |
NetHack.Cli/ |
Interactive and scripted console over the API. |
NetHack.Agent/ |
An LLM (Microsoft.Extensions.AI + OpenAI) plays via the API. |
NetHack.Tests/ |
xUnit tests (against the Stub engine). |
- Windows x64.
- .NET 10 SDK (
dotnet --version≥ 10). - Visual Studio 2022+ (or Build Tools for Visual Studio) with the
Desktop development with C++ workload — MSVC x64 toolset + a Windows SDK.
Required to build the native
NetHackNative.dll. - Internet access once, to fetch NetHack's third-party prerequisites (Lua and PDCursesMod), which upstream does not vendor.
git clone --recurse-submodules https://github.com/brianberns/NetHack-FSharp.git
cd NetHack-FSharpIf you already cloned without submodules:
git submodule update --initFrom the Core submodule (these land in Core/lib/ and are not tracked by git):
cd Core
sys\windows\fetch.cmd lua
sys\windows\fetch.cmd pdcursesmod
cd ..If
fetch.cmd pdcursesmodreportstar: This does not look like a tar archive: the zip downloaded fine, butfetch.cmdshells out totar, and if a GNUtar(e.g. from Git for Windows) is first onPATHit can't unpack a zip. Run the command from a plaincmd.exe(so Windows' bundled bsdtar wins), or extractCore\lib\pdcursesmod.zipyourself so thatcurses.hlands directly inCore\lib\pdcursesmod\. For example, in PowerShell:Expand-Archive Core\lib\pdcursesmod.zip -DestinationPath $env:TEMP\pdc -Force Remove-Item -Recurse -Force Core\lib\pdcursesmod Move-Item (Get-ChildItem $env:TEMP\pdc)[0].FullName Core\lib\pdcursesmod
NetHackNative.vcxproj depends on static libs and generated headers produced by
the stock NetHack solution, so build the solution first, then the DLL.
Find MSBuild (or open a Developer Command Prompt for VS, where msbuild is on
PATH):
for /f "usebackq delims=" %M in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -find "MSBuild\**\Bin\MSBuild.exe"`) do set MSBUILD=%MThen:
"%MSBUILD%" Core\sys\windows\vs\NetHack.sln -m -p:Configuration=Release -p:Platform=x64
"%MSBUILD%" Core\sys\windows\vs\NetHack\NetHackNative.vcxproj -m -p:Configuration=Release -p:Platform=x64This produces Core\binary\Release\x64\NetHackNative.dll along with the data
files the engine needs at runtime (nhdat500, *.template, record, …).
Alternatively: open
Core\sys\windows\vs\NetHack.slnin Visual Studio, build the solution (Release | x64), then build the NetHack/NetHackNative project.
dotnet build NetHack-FSharp.slnxThe Native engine locates the DLL by walking up from the running assembly to
Core/binary/Release/x64. Override with the NETHACK_NATIVE_DIR environment
variable or NetHack.Api.Native.dataDirOverride if it lives elsewhere.
dotnet run --project NetHack.Cli # real NetHack (default)
dotnet run --project NetHack.Cli -- --stub # in-process fake engine, no DLL needed
dotnet run --project NetHack.Cli -- native-demo # scripted, non-interactiveInteractive keys: hjkl/yubn move, q quaff, s search, i inventory,
J dump the current state as JSON, Q quit.
dotnet test NetHack.TestsCredentials are read from user secrets (kept out of the repo):
dotnet user-secrets set "OpenAI:ApiKey" "sk-..." --project NetHack.Agent
# optional:
# OpenAI:Model (default gpt-4o-mini)
# OpenAI:BaseUrl (OpenAI-compatible endpoints, e.g. GitHub Models)
# OpenAI:StepDelayMs (pace requests; raise on rate-limited tiers)
# OpenAI:TimeoutSeconds (per-call hard timeout)
dotnet run --project NetHack.Agent -- 40 # 40 = step budgetThe agent sends the GameState as JSON each turn and gets back a structured
action; it keeps a short self-authored "notes" scratchpad as its only memory,
so each request stays small.
- NetHack keeps all game state in C globals, so there is one live game per process; the API enforces this and a web service would scale across processes.
- After rebuilding
NetHackNative.dll, just rerun — the F# apps stage a fresh copy of the DLL and data files next to their executable on startup. - Build outputs (
Core/binary,Core/tools,bin,obj, …) are gitignored; the DLL is produced by the build above, not stored in the repo.