Files
warren/src/challenges.lua
T

70 lines
2.9 KiB
Lua

-- Named challenge presets: a fixed class, a fixed mutator set and a
-- deterministic seed, bound together so "today's challenge" means the same
-- run for everyone who plays it - the daily-seed ritual familiar
-- roguelikes build around, reproduced here without this module ever
-- touching the wall clock itself (see Challenges.seedFor's own
-- caller-supplied day index), so a test can pin any "day" it likes and get
-- the exact same answer back every time.
local Ascension = require("ascension")
local Challenges = {}
Challenges.CATALOG = {
{ key = "iron_gauntlet", name = "Iron Gauntlet", class = "fighter", mutators = { "bloodbound" } },
{ key = "arcane_sprint", name = "Arcane Sprint", class = "mage", mutators = { "famine" } },
{ key = "shadow_creed", name = "Shadow Creed", class = "rogue", mutators = {} },
{ key = "wild_hunt", name = "Wild Hunt", class = "ranger", mutators = { "nightblind" } },
}
Challenges.byKey = {}
for _, def in ipairs(Challenges.CATALOG) do
Challenges.byKey[def.key] = def
end
-- A Knuth multiplicative hash of `dayIndex`, folded into the same 16-bit
-- range warren.lua's own default seed (os.time() & 0xFFFF) already uses, so
-- a challenge seed is never distinguishable from an ordinary one downstream.
function Challenges.seedFor(dayIndex)
return (dayIndex * 2654435761) % 65536
end
-- The preset and seed "day `dayIndex`" always resolves to - a pure function
-- of dayIndex alone, so the same day always hands out the same challenge to
-- everyone who asks it, and a test can walk arbitrary days without waiting
-- on a clock.
function Challenges.forDay(dayIndex)
local preset = Challenges.CATALOG[(dayIndex % #Challenges.CATALOG) + 1]
return preset, Challenges.seedFor(dayIndex)
end
-- True iff `game` was actually played under `preset`'s own terms: started
-- as its class, with every mutator it demands active (see
-- Ascension.isActive) - extra mutators beyond the preset's own are fine,
-- only a missing requirement fails it. Returns (false, reason) naming
-- exactly what fell short, so a caller never has to re-derive why.
function Challenges.satisfiedBy(preset, game)
if game.player.class ~= preset.class then
return false, "started as " .. tostring(game.player.class) .. " instead of " .. preset.class
end
for _, key in ipairs(preset.mutators) do
if not Ascension.isActive(game.ascension, key) then
return false, "missing mutator " .. key
end
end
return true, nil
end
-- One human-readable line describing `preset`: its display name, the class
-- it demands, and every mutator it demands active - exactly the terms
-- Challenges.satisfiedBy itself checks, spelled out for a player deciding
-- whether to attempt it.
function Challenges.describe(preset)
if #preset.mutators == 0 then
return string.format("%s (%s)", preset.name, preset.class)
end
return string.format("%s (%s + %s)", preset.name, preset.class, table.concat(preset.mutators, ", "))
end
return Challenges