feat: add Challenges.seedFor and Challenges.forDay
This commit is contained in:
1 parent
ae5d1a414d
commit
0e8655e871
1 file changed
+69
@@ -0,0 +1,69 @@
|
|||||||
|
-- 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
|
||||||
Reference in new issue
Block a user