feat: extend warren with splitCommaList and playLoop

This commit is contained in:
dualfroz committed 2026-09-08 16:30:10 +02:00
1 parent 2dbaa30813
commit 15e1cbd8a5
1 file changed
+141 -1
+141 -1
View File
@@ -1,12 +1,44 @@
-- Headless entry point. Examples: -- Headless entry point. Examples:
-- lua warren.lua --seed 42 --auto 50 -- lua warren.lua --seed 42 --auto 50
-- lua warren.lua --seed 7 --auto 30 --save run.sav -- lua warren.lua --seed 7 --auto 30 --save run.sav
-- lua warren.lua --seed 3 --class ranger --auto 50
-- lua warren.lua --load run.sav -- lua warren.lua --load run.sav
-- lua warren.lua --meta legacy.sav --seed 3 --auto 200
-- lua warren.lua --meta legacy.sav --buy vigor
-- lua warren.lua --meta legacy.sav --report
-- lua warren.lua --meta legacy.sav --seed 3 --auto 200 --mutators famine,nightblind
-- lua warren.lua --seed 3 --auto 200 --morgue run.morgue
-- lua warren.lua --seed 3 --auto 200 --verify
-- lua warren.lua --challenge 42 --auto 200
-- lua warren.lua --meta legacy.sav --export-csv runs.csv
-- lua warren.lua --meta legacy.sav --export-compendium bestiary.csv
-- lua warren.lua --seed 3 --class fighter --play
-- lua warren.lua --load run.sav --play
local scriptDir = arg[0]:match("^(.*)[/\\]") or "." local scriptDir = arg[0]:match("^(.*)[/\\]") or "."
package.path = scriptDir .. "/src/?.lua;" .. scriptDir .. "/../src/?.lua;" .. package.path package.path = scriptDir .. "/src/?.lua;" .. scriptDir .. "/../src/?.lua;" .. package.path
local Game = require("game") local Game = require("game")
local Meta = require("meta")
local Compendium = require("compendium")
local Graves = require("graves")
local Ascension = require("ascension")
local Morgue = require("morgue")
local Leaderboard = require("leaderboard")
local Replay = require("replay")
local Challenges = require("challenges")
local Analytics = require("analytics")
local Titles = require("titles")
local Export = require("export")
local Streaks = require("streaks")
local function splitCommaList(text)
local list = {}
for part in text:gmatch("[^,]+") do
list[#list + 1] = part
end
return list
end
local function parseArgs(argv) local function parseArgs(argv)
local opts = { seed = os.time() & 0xFFFF, auto = 0 } local opts = { seed = os.time() & 0xFFFF, auto = 0 }
@@ -17,13 +49,121 @@ local function parseArgs(argv)
elseif flag == "--auto" then opts.auto = math.tointeger(tonumber(argv[i + 1])); i = i + 2 elseif flag == "--auto" then opts.auto = math.tointeger(tonumber(argv[i + 1])); i = i + 2
elseif flag == "--save" then opts.save = argv[i + 1]; i = i + 2 elseif flag == "--save" then opts.save = argv[i + 1]; i = i + 2
elseif flag == "--load" then opts.load = argv[i + 1]; i = i + 2 elseif flag == "--load" then opts.load = argv[i + 1]; i = i + 2
elseif flag == "--class" then opts.class = argv[i + 1]; i = i + 2
elseif flag == "--meta" then opts.metaPath = argv[i + 1]; i = i + 2
elseif flag == "--buy" then opts.buy = argv[i + 1]; i = i + 2
elseif flag == "--report" then opts.report = true; i = i + 1
elseif flag == "--morgue" then opts.morguePath = argv[i + 1]; i = i + 2
elseif flag == "--verify" then opts.verify = true; i = i + 1
elseif flag == "--mutators" then opts.mutators = splitCommaList(argv[i + 1]); i = i + 2
elseif flag == "--challenge" then opts.challengeDay = math.tointeger(tonumber(argv[i + 1])); i = i + 2
elseif flag == "--export-csv" then opts.exportPath = argv[i + 1]; i = i + 2
elseif flag == "--export-compendium" then opts.exportCompendiumPath = argv[i + 1]; i = i + 2
elseif flag == "--play" then opts.play = true; i = i + 1
else i = i + 1 end else i = i + 1 end
end end
return opts return opts
end end
-- Interactive terminal loop: renders the current frame, reads one line of
-- input, and hands it straight to Game:manualStep - the same command
-- interpreter (see commands.lua) a scripted test drives without a real
-- terminal attached. "quit"/"exit"/"q" leave the run exactly where it
-- stands rather than ending it, so a save taken right after --play still
-- captures whatever the last accepted command left behind. Runs until the
-- player dies, wins, quits, or stdin closes (a redirected script of
-- commands hitting EOF ends the loop the same way an interactive quit
-- would).
local function playLoop(game)
io.write(game:render(), "\n")
while game.player:isAlive() and not game.won do
io.write("> ")
io.flush()
local line = io.read("l")
if not line then break end
local command = line:match("^%s*(.-)%s*$")
if command == "quit" or command == "exit" or command == "q" then
break
elseif command ~= "" then
local _, message = game:manualStep(command)
if message then io.write(message, "\n") end
io.write(game:render(), "\n")
end
end
if game.won then
io.write("you escape with the Amulet of Warren - you win!\n")
elseif not game.player:isAlive() then
io.write("you have died.\n")
end
end
local function reportMeta(meta)
local toNext = Titles.toNext(meta.renown)
local titleLine = toNext and string.format("%s (%d renown to next title)", Titles.forRenown(meta.renown), toNext)
or string.format("%s (top of the ladder)", Titles.forRenown(meta.renown))
io.write(string.format("renown %d | runs %d | wins %d | best depth %d | win rate %.0f%% | title %s\n",
meta.renown, meta.totalRuns, meta.wins, meta.bestDepth, Analytics.winRate(meta) * 100, titleLine))
local streakLength, streakOutcome = Streaks.current(meta.runHistory)
io.write(string.format("streak: %d %s in a row | best win streak %d\n",
streakLength, streakOutcome == nil and "runs" or (streakOutcome and "wins" or "losses"),
Streaks.bestWinStreak(meta.runHistory)))
for _, def in ipairs(Meta.PERKS) do
io.write(string.format(" perk %-12s %s (%d renown)\n",
def.key, Meta.isOwned(meta, def.key) and "[owned]" or "[locked]", def.cost))
end
io.write("challenges:\n")
for _, preset in ipairs(Challenges.CATALOG) do
io.write(" " .. Challenges.describe(preset) .. "\n")
end
for _, ach in ipairs(Meta.ACHIEVEMENTS) do
io.write(string.format(" achievement %-14s %s\n",
ach.key, meta.achievements[ach.key] and "[unlocked]" or "[locked]"))
end
for _, def in ipairs(Ascension.MUTATORS) do
io.write(string.format(" mutator %-12s %s (+%d%% renown) %s\n",
def.key, def.desc, def.renownBonus, Ascension.isUnlocked(meta, def.key) and "[unlocked]" or "[locked]"))
end
io.write("compendium:\n")
for _, entry in ipairs(Compendium.entries(meta.compendium or Compendium.new())) do
if entry.seen then
io.write(string.format(" %-14s slain %-3d %s\n", entry.name, entry.kills, entry.flavor or ""))
else
io.write(string.format(" %-14s [undiscovered]\n", entry.key))
end
end
local board = meta.leaderboard or Leaderboard.new()
io.write("leaderboard:\n")
for rank, entry in ipairs(Leaderboard.top(board)) do
io.write(string.format(" #%-2d %-6d %-10s depth %-2d turn %-5d seed %s %s\n",
rank, entry.score, entry.className, entry.depth, entry.turn, tostring(entry.seed),
entry.won and "(won)" or ""))
end
if #board > 0 then
io.write(string.format(" average score %.1f | favorite class %s\n",
Analytics.averageScore(board), Analytics.favoriteClass(board) or "none"))
end
end
local opts = parseArgs(arg) local opts = parseArgs(arg)
local game = opts.load and Game.load(opts.load) or Game.new(opts.seed)
local challengePreset
if opts.challengeDay then
challengePreset, opts.seed = Challenges.forDay(opts.challengeDay)
opts.class = challengePreset.class
opts.mutators = challengePreset.mutators
io.write(string.format("challenge: %s, seed %d\n", Challenges.describe(challengePreset), opts.seed))
end
local meta = opts.metaPath and Meta.load(opts.metaPath) or nil
if meta and opts.buy then
local success, message = Meta.purchase(meta, opts.buy)
io.write(message, "\n")
if not success then os.exit(1) end
end
local game = opts.load and Game.load(opts.load)
or Game.new(opts.seed, { class = opts.class, meta = meta, graves = meta and meta.graves, mutators = opts.mutators })
for _ = 1, opts.auto do for _ = 1, opts.auto do
if not game.player:isAlive() then break end if not game.player:isAlive() then break end