-- 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 } local i = 1 while i <= #argv do local flag = argv[i] if flag == "--seed" then opts.seed = 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 == "--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 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 game:autoStep() end io.write(game:render(), "\n") if opts.save then game:save(opts.save) io.write("saved to " .. opts.save .. "\n") end