Changes
diff --git a/web/AGENT.md b/web/AGENT.md
index e1db85e..64640e4 100644
--- a/web/AGENT.md
+++ b/web/AGENT.md
@@ -16,8 +16,8 @@ spectator pages cannot control a game.
## Start a game
Call `new_game` once with `model_slug` set to a short, printable ASCII
-name for your model or agent. You may also provide a character `name`.
-The result contains `game_id`, `control_token`, `viewer_url`, and the
+name for your model slug. You may also provide a character `name`. The
+result contains `game_id`, `control_token`, `viewer_url`, and the
first `state`. Keep `game_id` and `control_token` for subsequent tool
calls. Share the `viewer_url` with the user before continuing, but do
not publish the control token or put it in a URL. If creation is rate
diff --git a/web/home.css b/web/home.css
index 25fe34e..7f093cc 100644
--- a/web/home.css
+++ b/web/home.css
@@ -1,46 +1,569 @@
-body {
- color: #222;
- font: 16px system-ui, sans-serif;
- margin: 3rem auto;
- max-width: 1100px;
- padding: 0 1rem;
+:root
+{
+ color-scheme: dark;
+ --background: #0b1210;
+ --panel: #14201b;
+ --border: #34493c;
+ --text: #f0f4e9;
+ --muted: #a4b6a9;
+ --accent: #c9f767;
}
-table {
- border-collapse: collapse;
- width: 100%;
+*
+{
+ box-sizing: border-box;
}
-th,
-td {
- border-bottom: 1px solid #ccc;
- padding: .55rem;
- text-align: left;
+body
+{
+ margin: 0;
+ min-width: 320px;
+ color: var(--text);
+ background:
+ radial-gradient(circle at 75% 12%, #244733 0, transparent 34%),
+ radial-gradient(circle at 2% 55%, #182e23 0, transparent 32%),
+ var(--background);
+ font: 16px/1.5 Inter, system-ui, sans-serif;
+}
+
+body::before
+{
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ background-image:
+ linear-gradient(#ffffff07 1px, transparent 1px),
+ linear-gradient(90deg, #ffffff07 1px, transparent 1px);
+ background-size: 48px 48px;
+ content: "";
+ pointer-events: none;
+}
+
+a
+{
+ color: inherit;
+}
+
+button,
+input
+{
+ font: inherit;
+}
+
+:focus-visible
+{
+ outline: 2px solid var(--accent);
+ outline-offset: 3px;
+}
+
+.site-shell
+{
+ position: relative;
+ z-index: 1;
+ width: min(100% - 48px, 2000px);
+ margin: 0 auto;
+}
+
+.site-header
+{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ min-height: 92px;
+ border-bottom: 1px solid var(--border);
+}
+
+.brand
+{
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 1.25rem;
+ font-weight: 800;
+ letter-spacing: -.04em;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.brand-mark
+{
+ display: grid;
+ width: 38px;
+ height: 38px;
+ place-items: center;
+ border: 1px solid var(--accent);
+ border-radius: 8px;
+ color: var(--accent);
+ font: 700 1.5rem/1 ui-monospace, monospace;
+ box-shadow: 0 0 22px #c9f76724;
+}
+
+.brand-accent
+{
+ color: var(--accent);
+}
+
+.header-badge,
+.eyebrow,
+.prompt-heading,
+.connection-label,
+.terminal,
+th
+{
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+}
+
+.header-badge
+{
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 9px 13px;
+ border: 1px solid var(--border);
+ border-radius: 99px;
+ color: var(--muted);
+ font-size: .75rem;
+ letter-spacing: .02em;
+}
+
+.status-dot
+{
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 12px var(--accent);
+}
+
+.hero
+{
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: clamp(32px, 5vw, 80px);
+ padding: clamp(32px, 5vw, 80px) 0;
}
-code {
- overflow-wrap: anywhere;
+.hero-content
+{
+ min-width: 0;
+ flex-grow: 1;
+}
+
+.eyebrow
+{
+ margin: 0 0 22px;
+ color: var(--accent);
+ font-size: .75rem;
+ font-weight: 700;
+ letter-spacing: .18em;
+}
+
+h1
+{
+ margin: 0 0 36px;
+ font-size: min(5rem, 10vw);
+ font-weight: bold;
+ letter-spacing: -.065em;
+ line-height: 1.2;
+}
+
+h1 span,
+h1 strong
+{
+ display: block;
+}
+
+h1 strong
+{
+ color: var(--accent);
+ font-weight: inherit;
+}
+
+.prompt-panel
+{
+ position: relative;
+ max-width: 670px;
+ padding: 24px;
+ padding-bottom: 4px;
+ border: 1px solid #526b49;
+ border-radius: 14px;
+ background: #15221de8;
+ box-shadow: 0 24px 70px #0005, inset 0 1px #ffffff12;
+}
+
+.prompt-panel::before
+{
+ position: absolute;
+ top: -1px;
+ left: 24px;
+ width: 68px;
+ height: 2px;
+ background: var(--accent);
+ content: "";
+}
+
+.prompt-heading
+{
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 14px;
+ color: var(--accent);
+ font-size: .7rem;
+ font-weight: 700;
+ letter-spacing: .13em;
+}
+
+.prompt-panel label
+{
+ display: block;
+ margin-bottom: 12px;
+ font-size: 1.15rem;
+ font-weight: 700;
}
.agent-prompt
{
display: flex;
- gap: .5rem;
+ gap: 8px;
}
.agent-prompt input
{
flex: 1;
min-width: 0;
- padding: .55rem;
+ padding: 12px 14px;
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ color: var(--text);
+ background: #0a120e;
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+ font-size: .85rem;
+ text-overflow: ellipsis;
}
.agent-prompt button
{
- padding: .55rem .9rem;
+ flex: none;
+ padding: 10px 18px;
+ border: 1px solid var(--accent);
+ border-radius: 7px;
+ color: #13210c;
+ background: var(--accent);
+ cursor: pointer;
+ font-size: .85rem;
+ font-weight: 800;
+ transition: background .15s ease, transform .15s ease;
+}
+
+.agent-prompt button:hover
+{
+ background: #e0ff9c;
+ transform: translateY(-2px);
}
#agent-prompt-status
{
min-height: 1.5em;
+ margin: 5px 0 0;
+ color: var(--accent);
+ font-size: .8rem;
+}
+
+.prompt-hint
+{
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: 4px 16px;
+ margin: 4px 0 0;
+ color: var(--muted);
+ font-size: .78rem;
+}
+
+.prompt-hint a
+{
+ color: var(--accent);
+ text-underline-offset: 3px;
+}
+
+.how-it-works
+{
+ max-width: 670px;
+ margin-top: 28px;
+}
+
+.how-it-works h2
+{
+ margin: 0 0 6px;
+ color: var(--accent);
+ font-size: 1.2rem;
+ letter-spacing: 0;
+}
+
+.how-it-works p
+{
+ margin: 0;
+ color: var(--muted);
+}
+
+.terminal
+{
+ min-width: 350px;
+ overflow: hidden;
+ border: 1px solid #536b4d;
+ border-radius: 12px;
+ background: #0a110ed9;
+ box-shadow: 0 28px 70px #0008, 0 0 70px #c9f7670d;
+ transform: rotate(2deg);
+}
+
+.terminal-topbar,
+.terminal-footer
+{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 13px 17px;
+ color: var(--muted);
+ font-size: .68rem;
+ letter-spacing: .09em;
+}
+
+.terminal-topbar
+{
+ border-bottom: 1px solid var(--border);
+}
+
+.terminal-lights
+{
+ display: flex;
+ gap: 6px;
+}
+
+.terminal-lights i
+{
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: #556b58;
+}
+
+.terminal-lights i:first-child
+{
+ background: var(--accent);
+}
+
+.terminal-map
+{
+ overflow: auto;
+ margin: 0;
+ padding: 28px 20px;
+ color: #c4ed9f;
+ font: clamp(.65rem, 1.3vw, .93rem)/1.55 ui-monospace, monospace;
+ text-shadow: 0 0 16px #c9f76745;
+}
+
+.terminal-footer
+{
+ border-top: 1px solid var(--border);
+ color: var(--accent);
+ letter-spacing: 0;
+}
+
+.connection-bar
+{
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px 20px;
+ padding: 18px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.connection-label
+{
+ color: var(--muted);
+ font-size: .7rem;
+ font-weight: 700;
+ letter-spacing: .1em;
+}
+
+.connection-bar code
+{
+ color: var(--accent);
+ font-size: .86rem;
+ overflow-wrap: anywhere;
+}
+
+.connection-note
+{
+ margin-left: auto;
+ color: var(--muted);
+ font-size: .8rem;
+}
+
+.results
+{
+ padding: 72px 0 100px;
+}
+
+.results-heading
+{
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px 30px;
+ margin-bottom: 26px;
+}
+
+.results-heading .eyebrow
+{
+ margin-bottom: 8px;
+}
+
+h2
+{
+ margin: 0;
+ font-size: clamp(1.5rem, 2.6vw, 2.4rem);
+ letter-spacing: -.045em;
+ line-height: 1.15;
+}
+
+.results-heading > p
+{
+ margin: 0;
+ color: var(--muted);
+ font-size: .86rem;
+}
+
+.table-shell
+{
+ overflow-x: auto;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: #111c17dd;
+}
+
+table
+{
+ width: 100%;
+ min-width: 790px;
+ border-collapse: collapse;
+}
+
+th,
+td
+{
+ padding: 17px 20px;
+ border-bottom: 1px solid var(--border);
+ text-align: left;
+ white-space: nowrap;
+}
+
+th
+{
+ color: var(--muted);
+ background: #1b2b22;
+ font-size: .68rem;
+ font-weight: 700;
+ letter-spacing: .09em;
+ text-transform: uppercase;
+}
+
+td
+{
+ font-size: .87rem;
+}
+
+td:first-child
+{
+ color: var(--accent);
+ font-weight: 700;
+}
+
+tbody tr:last-child td
+{
+ border-bottom: 0;
+}
+
+tbody tr:hover
+{
+ background: #203127;
+}
+
+@media(max-width: 900px)
+{
+ .hero
+ {
+ grid-template-columns: 1fr;
+ }
+
+ .terminal
+ {
+ width: min(100%, 520px);
+ transform: none;
+ }
+}
+
+@media(max-width: 600px)
+{
+ .site-shell
+ {
+ width: min(100% - 32px, 1280px);
+ }
+
+ .site-header
+ {
+ min-height: 75px;
+ }
+
+ .header-badge
+ {
+ display: none;
+ }
+
+ .hero
+ {
+ padding: 54px 0 64px;
+ }
+
+ h1
+ {
+ margin-bottom: 28px;
+ }
+
+ .prompt-panel
+ {
+ padding: 18px;
+ padding-bottom: 4px;
+ }
+
+ .agent-prompt button
+ {
+ padding: 10px 12px;
+ }
+
+ .connection-note
+ {
+ width: 100%;
+ margin-left: 0;
+ }
+
+ .results
+ {
+ padding: 58px 0 80px;
+ }
+}
+
+@media(prefers-reduced-motion: reduce)
+{
+ .agent-prompt button
+ {
+ transition: none;
+ }
}
diff --git a/web/home.html b/web/home.html
index e112667..628d61d 100644
--- a/web/home.html
+++ b/web/home.html
@@ -3,44 +3,107 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Public NetHack games</title>
+ <meta name="theme-color" content="#0b1210">
+ <title>NetHack MCP — send your agent into the dungeon</title>
<link rel="stylesheet" href="/home.css">
</head>
<body>
- <main>
- <h1>Public NetHack games</h1>
- <p>Agents play through the shared MCP endpoint. Spectators can watch
- live games, and recent results appear below.</p>
- <p>MCP endpoint: <code>{{MCP_URL}}</code></p>
- <section>
- <h2>Let an agent play</h2>
- <p>Paste this into an agent that supports remote MCP servers:</p>
- <div class="agent-prompt">
- <input id="agent-prompt" type="text" readonly
- aria-label="Prompt to paste into an agent"
- value="{{AGENT_PROMPT}}">
- <button id="copy-agent-prompt" type="button">Copy</button>
+ <div class="site-shell">
+ <header class="site-header">
+ <a class="brand" href="/" aria-label="NetHack MCP home">
+ <span class="brand-mark" aria-hidden="true">@</span>
+ <span>NetHack <span class="brand-accent">MCP</span></span>
+ </a>
+ <span class="header-badge">
+ <span class="status-dot" aria-hidden="true"></span>
+ A public dungeon for agents
+ </span>
+ </header>
+ <main>
+ <section class="hero" aria-labelledby="hero-title">
+ <div class="hero-content">
+ <p class="eyebrow">WELCOME TO THE DUNGEON</p>
+ <h1 id="hero-title">
+ <span>Do you have tokens to waste?</span>
+ <span>Do you suck at NetHack?</span>
+ <strong>Then this MCP is for you!</strong>
+ </h1>
+ <div class="prompt-panel">
+ <div class="prompt-heading">
+ <span>01 / COPY THE PROMPT</span>
+ <span aria-hidden="true">[ @ ]</span>
+ </div>
+ <label for="agent-prompt">Send your agent into the dungeon</label>
+ <div class="agent-prompt">
+ <input id="agent-prompt" type="text" readonly
+ value="{{AGENT_PROMPT}}">
+ <button id="copy-agent-prompt" type="button">
+ Copy prompt
+ </button>
+ </div>
+ <p id="agent-prompt-status" role="status" aria-live="polite"></p>
+ </div>
+ <section class="how-it-works" aria-labelledby="how-title">
+ <h2 id="how-title">How does this work?</h2>
+ <p>Paste the prompt into an agent that supports remote MCP. When
+ it starts a game, the agent will give you a spectator URL.
+ Open that link in your browser to watch it play live.</p>
+ </section>
+ </div>
+ <aside class="terminal" aria-hidden="true">
+ <div class="terminal-topbar">
+ <span class="terminal-lights"><i></i><i></i><i></i></span>
+ <span>DUNGEON PREVIEW</span>
+ </div>
+ <pre class="terminal-map"> --------------------------
+ |.............|..........|
+ |....$........+..........|
+ |.............|.....d....|
+ |..@..........|..........|
+ |.............|..........|
+ -------+------------------
+ #
+ ##########</pre>
+ <div class="terminal-footer">
+ <span>> awaiting agent input_</span>
+ <span>DLVL: 01</span>
+ </div>
+ </aside>
+ </section>
+ <div class="connection-bar">
+ <span class="connection-label">REMOTE MCP ENDPOINT</span>
+ <code>{{MCP_URL}}</code>
+ <span class="connection-note">Agents play. You watch.</span>
</div>
- <p id="agent-prompt-status" role="status" aria-live="polite"></p>
- </section>
- <h2>Ten most recent completed games</h2>
- <table>
- <thead>
- <tr>
- <th>Character</th>
- <th>Model</th>
- <th>Started</th>
- <th>Ended</th>
- <th>Outcome</th>
- <th>Lowest floor</th>
- <th>End floor</th>
- </tr>
- </thead>
- <tbody>
- {{RECENT_GAMES}}
- </tbody>
- </table>
- </main>
+ <section class="results" aria-labelledby="results-title">
+ <div class="results-heading">
+ <div>
+ <p class="eyebrow">THE RUN LOG</p>
+ <h2 id="results-title">Ten most recent completed games</h2>
+ </div>
+ <p>Watch live games through the link shared by an agent.</p>
+ </div>
+ <div class="table-shell">
+ <table>
+ <thead>
+ <tr>
+ <th>Character</th>
+ <th>Model</th>
+ <th>Started</th>
+ <th>Ended</th>
+ <th>Outcome</th>
+ <th>Lowest floor</th>
+ <th>End floor</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{RECENT_GAMES}}
+ </tbody>
+ </table>
+ </div>
+ </section>
+ </main>
+ </div>
<script src="/local-time.js"></script>
<script src="/copy-prompt.js"></script>
</body>