diff --git a/warren.lua b/warren.lua index c7f7ef8..72c4cd1 100644 --- a/warren.lua +++ b/warren.lua @@ -1,12 +1,44 @@ -- Headless entry point. Examples: -- lua warren.lua --seed 42 --auto 50 -- 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 --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 "." package.path = scriptDir .. "/src/?.lua;" .. scriptDir .. "/../src/?.lua;" .. package.path 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 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 == "--save" then opts.save = 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 end return opts 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 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 if not game.player:isAlive() then break end