179 lines
7.4 KiB
Lua
179 lines
7.4 KiB
Lua
-- 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
|