Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
2 changes: 2 additions & 0 deletions data/json/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local voltmeter = require("./voltmeter")
local slimepit = require("./slimepit")
local artifact_analyzer = require("./artifact_analyzer")
local lua_traits = require("./lua_traits")
local action_menu_macros = require("lib.action_menu_macros")

local mod = game.mod_runtime[game.current_mod]
local storage = game.mod_storage[game.current_mod]
Expand All @@ -11,3 +12,4 @@ mod.slimepit = slimepit
mod.artifact_analyzer = artifact_analyzer
mod.lua_traits = lua_traits
lua_traits.register(mod)
action_menu_macros.register_defaults()
150 changes: 150 additions & 0 deletions data/lua/lib/action_menu_macros.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
local ui = require("lib.ui")

local action_menu_macros = {}

local function popup_recent_messages()
local entries = gapi.get_messages(12)
if #entries == 0 then
ui.popup(locale.gettext("No recent messages."))
return
end

local lines = { locale.gettext("Recent Messages"), "" }
for _, entry in ipairs(entries) do
table.insert(lines, string.format("[%s] %s", entry.time, entry.text))
end

ui.popup(table.concat(lines, "\n"))
end

local function popup_recent_lua_log()
local entries = gapi.get_lua_log(20)
if #entries == 0 then
ui.popup(locale.gettext("No recent Lua log entries."))
return
end

local lines = { locale.gettext("Recent Lua Log"), "" }
for _, entry in ipairs(entries) do
local source_prefix = entry.from_user and "> " or ""
table.insert(lines, string.format("[%s] %s%s", entry.level, source_prefix, entry.text))
end

ui.popup(table.concat(lines, "\n"))
end

local function announce_current_turn()
local turn_value = gapi.current_turn():to_turn()
gapi.add_msg(string.format(locale.gettext("Current turn: %d"), turn_value))
end

local function report_agent_context()
local avatar = gapi.get_avatar()
local map = gapi.get_map()
local pos = avatar:get_pos_ms()
local turn_value = gapi.current_turn():to_turn()
local details = string.format(
"[AI] turn=%d local_ms=(%d,%d,%d) outside=%s sheltered=%s",
turn_value,
pos.x,
pos.y,
pos.z,
tostring(map:is_outside(pos)),
tostring(map:is_sheltered(pos))
)
gapi.add_msg(details)
end

local function report_look_target()
local target = gapi.look_around()
if not target then
gapi.add_msg(locale.gettext("Look canceled."))
return
end

local target_abs = gapi.get_map():get_abs_ms(target)
gapi.add_msg(
string.format(
"[AI] look local_ms=(%d,%d,%d) abs_ms=(%d,%d,%d)",
target.x,
target.y,
target.z,
target_abs.x,
target_abs.y,
target_abs.z
)
)
end

local function report_adjacent_choice()
local target = gapi.choose_adjacent(locale.gettext("Choose adjacent tile for AI context"), true)
if not target then
gapi.add_msg(locale.gettext("Adjacent selection canceled."))
return
end

local target_abs = gapi.get_map():get_abs_ms(target)
gapi.add_msg(
string.format(
"[AI] adjacent local_ms=(%d,%d,%d) abs_ms=(%d,%d,%d)",
target.x,
target.y,
target.z,
target_abs.x,
target_abs.y,
target_abs.z
)
)
end

action_menu_macros.register_defaults = function()
gapi.register_action_menu_entry({
id = "bn_macro_recent_messages",
name = locale.gettext("Recent Messages"),
description = locale.gettext("Show the latest in-game messages in a popup."),
category = "info",
fn = popup_recent_messages,
})

gapi.register_action_menu_entry({
id = "bn_macro_recent_lua_log",
name = locale.gettext("Recent Lua Log"),
description = locale.gettext("Show the latest Lua console log entries in a popup."),
category = "info",
fn = popup_recent_lua_log,
})

gapi.register_action_menu_entry({
id = "bn_macro_current_turn",
name = locale.gettext("Current Turn"),
description = locale.gettext("Print the current absolute turn in the message log."),
category = "info",
fn = announce_current_turn,
})

gapi.register_action_menu_entry({
id = "bn_macro_agent_context",
name = locale.gettext("AI Context Packet"),
description = locale.gettext("Print turn, local coordinates, and shelter/outside state."),
category = "info",
fn = report_agent_context,
})

gapi.register_action_menu_entry({
id = "bn_macro_look_target",
name = locale.gettext("AI Look Target"),
description = locale.gettext("Pick a tile via look-around and print local/absolute coordinates."),
category = "info",
fn = report_look_target,
})

gapi.register_action_menu_entry({
id = "bn_macro_adjacent_target",
name = locale.gettext("AI Adjacent Target"),
description = locale.gettext("Pick an adjacent tile and print local/absolute coordinates."),
category = "info",
fn = report_adjacent_choice,
})
end

return action_menu_macros
62 changes: 62 additions & 0 deletions docs/en/dev/guides/bench.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Curses CLI Benchmark Protocol

This guide defines a repeatable benchmark loop for Cataclysm-BN `curses-cli` automation and documents known churn points plus mitigation.

## Goal

Run a deterministic scenario that validates real gameplay control quality:

1. start as evacuee,
2. collect baseline tools,
3. travel toward nearest town,
4. fight naturally encountered enemies,
5. archive cast and compact artifacts.

The benchmark is invalid when debug spawning/wish/debug-menu actions are used.

## Standard run

```bash
deno task pr:verify:curses-cli start --state-file /tmp/curses-bench.json --render-webp false
deno task pr:verify:curses-cli inputs-jsonl --state-file /tmp/curses-bench.json
# execute scripted scenario steps
deno task pr:verify:curses-cli capture --state-file /tmp/curses-bench.json --id bench-final --caption "Benchmark final state" --lines 120
deno task pr:verify:curses-cli stop --state-file /tmp/curses-bench.json --status passed
```

## Artifact contract

- Cast: `/tmp/curses-cli/casts/*.cast`
- Session manifest and captures: `/tmp/curses-cli/runs/live-*/`
- Runtime exports:
- `available_keys.json`
- `available_macros.json`
- `ai_state.json`

## Churn control checklist

- Validate current mode before each critical key sequence.
- Guard against nested UI states (`look`, map, debug, targeting, lua console).
- Keep safe-mode policy explicit for the profile.
- Use macro IDs (`macro:<id>`) for robust intent calls where possible.
- Persist stop reason category on failure (`menu_drift`, `mode_trap`, `safe_mode_interrupt`, `repro_drift`).

## Planned compact dump

A compact dump should be preferred over raw repeated full snapshots for AI loops. Target payload:

- ASCII pane excerpt (trimmed)
- available inputs (prompt-derived)
- available action keys JSON snapshot
- available macros JSON snapshot
- recent logs and ai_state summary
- run metadata (turn, coords, mode, stop reason)

This reduces token load while preserving actionable context.

## Low-priority reproducibility improvements

- Add benchmark profile presets with fixed seed and explicit scenario/profession.
- Add a pre-generated benchmark world/save fixture to avoid early-world variance.
- Add deterministic character template to avoid random traits affecting control difficulty.
- Add fast-start option to skip non-critical intro screens.
15 changes: 15 additions & 0 deletions opencode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"curses_mcp": {
"type": "local",
"command": [
"deno",
"task",
"pr:verify:curses-mcp:server"
],
"enabled": true,
"timeout": 15000
}
}
}
Loading
Loading