feat: add pairKey, Alchemy.herb and Analytics.winRate
This commit is contained in:
1 parent
aa657fdc88
commit
6821564c67
2 files changed
+160
No files matched your search
+102
@@ -0,0 +1,102 @@
|
||||
-- Herb-based potion brewing, layered on top of items.lua's existing potion
|
||||
-- catalog rather than inventing a second one: a cauldron turns two foraged
|
||||
-- herbs into the same potions rollItem already scatters as loot, so a
|
||||
-- brewed potion drinks, throws (throwing.lua) and identifies exactly like a
|
||||
-- found one. Recipes are fixed data, the same way crafting.lua's elemental
|
||||
-- riders are fixed - the uncertainty here is not "what does this pair do"
|
||||
-- but whether two ill-matched herbs curdle instead of brewing cleanly. See
|
||||
-- Game:brewAt for how a curdle is resolved against the brewer.
|
||||
|
||||
local Items = require("items")
|
||||
|
||||
local Alchemy = {}
|
||||
|
||||
Alchemy.SUNPETAL = "sunpetal"
|
||||
Alchemy.BLOODROOT = "bloodroot"
|
||||
Alchemy.EMBERLEAF = "emberleaf"
|
||||
Alchemy.FROSTMOSS = "frostmoss"
|
||||
Alchemy.NIGHTCAP = "nightcap"
|
||||
Alchemy.MOONBELL = "moonbell"
|
||||
|
||||
-- Fixed order so a given rng draw always names the same herb. New herbs
|
||||
-- append at the end (see builder.lua's own comment on Alchemy.order) so an
|
||||
-- existing seed's earlier picks never change which herb they name.
|
||||
Alchemy.order = {
|
||||
Alchemy.BLOODROOT, Alchemy.EMBERLEAF, Alchemy.FROSTMOSS, Alchemy.NIGHTCAP, Alchemy.SUNPETAL,
|
||||
Alchemy.MOONBELL,
|
||||
}
|
||||
|
||||
Alchemy.names = {
|
||||
[Alchemy.SUNPETAL] = "sunpetal",
|
||||
[Alchemy.BLOODROOT] = "bloodroot",
|
||||
[Alchemy.EMBERLEAF] = "emberleaf",
|
||||
[Alchemy.FROSTMOSS] = "frostmoss",
|
||||
[Alchemy.NIGHTCAP] = "nightcap",
|
||||
[Alchemy.MOONBELL] = "moonbell",
|
||||
}
|
||||
|
||||
local CURDLE = { curdle = true }
|
||||
|
||||
-- Canonical order-independent key for a herb pair: the two herb keys sorted
|
||||
-- and joined, so brewing (A, B) and (B, A) always land on the same recipe.
|
||||
local function pairKey(a, b)
|
||||
if a > b then a, b = b, a end
|
||||
return a .. "+" .. b
|
||||
end
|
||||
Alchemy.pairKey = pairKey
|
||||
|
||||
-- Same-herb pairs concentrate that herb's own aspect; cross pairs blend two
|
||||
-- aspects into a shared potion, except the pairings whose aspects outright
|
||||
-- oppose each other (light versus shade, fire versus frost, day versus
|
||||
-- dream), which curdle instead of brewing.
|
||||
Alchemy.recipes = {
|
||||
[pairKey(Alchemy.SUNPETAL, Alchemy.SUNPETAL)] = { effect = "heal", power = 18 },
|
||||
[pairKey(Alchemy.SUNPETAL, Alchemy.BLOODROOT)] = { effect = "heal", power = 10 },
|
||||
[pairKey(Alchemy.SUNPETAL, Alchemy.EMBERLEAF)] = { effect = "haste" },
|
||||
[pairKey(Alchemy.SUNPETAL, Alchemy.FROSTMOSS)] = { effect = "fortify" },
|
||||
[pairKey(Alchemy.SUNPETAL, Alchemy.NIGHTCAP)] = CURDLE,
|
||||
[pairKey(Alchemy.BLOODROOT, Alchemy.BLOODROOT)] = { effect = "strength" },
|
||||
[pairKey(Alchemy.BLOODROOT, Alchemy.EMBERLEAF)] = { effect = "strength" },
|
||||
[pairKey(Alchemy.BLOODROOT, Alchemy.FROSTMOSS)] = { effect = "fortify" },
|
||||
[pairKey(Alchemy.BLOODROOT, Alchemy.NIGHTCAP)] = { effect = "poison", power = 4 },
|
||||
[pairKey(Alchemy.EMBERLEAF, Alchemy.EMBERLEAF)] = { effect = "haste" },
|
||||
[pairKey(Alchemy.EMBERLEAF, Alchemy.FROSTMOSS)] = CURDLE,
|
||||
[pairKey(Alchemy.EMBERLEAF, Alchemy.NIGHTCAP)] = { effect = "poison", power = 5 },
|
||||
[pairKey(Alchemy.FROSTMOSS, Alchemy.FROSTMOSS)] = { effect = "fortify" },
|
||||
[pairKey(Alchemy.FROSTMOSS, Alchemy.NIGHTCAP)] = { effect = "poison", power = 5 },
|
||||
[pairKey(Alchemy.NIGHTCAP, Alchemy.NIGHTCAP)] = { effect = "poison", power = 7 },
|
||||
-- Moonbell's own aspect is dream-fog, so concentrating it addles the
|
||||
-- brewer's own mind rather than anyone else's - the same "own aspect
|
||||
-- turned inward" shape as every other same-herb pair above. Sunlight is
|
||||
-- the one aspect it outright opposes (wakeful day versus dreaming night,
|
||||
-- the same shape as sunpetal/nightcap's light-versus-shade curdle above),
|
||||
-- so that pairing curdles instead of brewing.
|
||||
[pairKey(Alchemy.MOONBELL, Alchemy.MOONBELL)] = { effect = "confuse" },
|
||||
[pairKey(Alchemy.SUNPETAL, Alchemy.MOONBELL)] = CURDLE,
|
||||
[pairKey(Alchemy.BLOODROOT, Alchemy.MOONBELL)] = { effect = "clarity" },
|
||||
[pairKey(Alchemy.EMBERLEAF, Alchemy.MOONBELL)] = { effect = "blind" },
|
||||
[pairKey(Alchemy.FROSTMOSS, Alchemy.MOONBELL)] = { effect = "levitate" },
|
||||
[pairKey(Alchemy.NIGHTCAP, Alchemy.MOONBELL)] = { effect = "poison", power = 6 },
|
||||
}
|
||||
|
||||
function Alchemy.herb(key)
|
||||
return { kind = Items.HERB, herb = key, name = Alchemy.names[key] .. " herb", glyph = ";" }
|
||||
end
|
||||
|
||||
-- The fixed outcome for brewing herbA with herbB together, order
|
||||
-- independent. Returns nil only when either key names no known herb.
|
||||
function Alchemy.lookup(herbA, herbB)
|
||||
if not Alchemy.names[herbA] or not Alchemy.names[herbB] then return nil end
|
||||
return Alchemy.recipes[pairKey(herbA, herbB)]
|
||||
end
|
||||
|
||||
-- Resolves a completed brew into a fresh potion item, or (nil, "curdle") for
|
||||
-- an opposed pairing - the caller decides what a curdle does to the brewer.
|
||||
function Alchemy.brew(herbA, herbB)
|
||||
local recipe = Alchemy.lookup(herbA, herbB)
|
||||
if not recipe then return nil, "unknown herb" end
|
||||
if recipe.curdle then return nil, "curdle" end
|
||||
return Items.potionOf(recipe.effect, recipe.power)
|
||||
end
|
||||
|
||||
return Alchemy
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Aggregate, read-only statistics over meta.lua's own cross-run history:
|
||||
-- how often a body of runs actually won, which class shows up most among
|
||||
-- them, and where a fresh score would land against that history - three
|
||||
-- questions renown and achievements never answer on their own, since both
|
||||
-- only ever look at the one run in front of them, never the shape of every
|
||||
-- run that came before it.
|
||||
|
||||
local Analytics = {}
|
||||
|
||||
-- Fraction of recorded runs that ended in victory. 0 for a state that has
|
||||
-- not recorded any runs yet rather than dividing by zero.
|
||||
function Analytics.winRate(meta)
|
||||
if meta.totalRuns == 0 then return 0 end
|
||||
return meta.wins / meta.totalRuns
|
||||
end
|
||||
|
||||
-- Mean score across every leaderboard entry (see leaderboard.lua), 0 for an
|
||||
-- empty board.
|
||||
function Analytics.averageScore(board)
|
||||
if #board == 0 then return 0 end
|
||||
local total = 0
|
||||
for _, entry in ipairs(board) do total = total + entry.score end
|
||||
return total / #board
|
||||
end
|
||||
|
||||
-- The className appearing most often on `board`, ties broken toward
|
||||
-- whichever class was seen first on the board - so the answer only ever
|
||||
-- depends on the board's own entry order, never on however pairs() happens
|
||||
-- to walk a count table. nil for an empty board.
|
||||
function Analytics.favoriteClass(board)
|
||||
local counts, order = {}, {}
|
||||
for _, entry in ipairs(board) do
|
||||
if not counts[entry.className] then order[#order + 1] = entry.className end
|
||||
counts[entry.className] = (counts[entry.className] or 0) + 1
|
||||
end
|
||||
local best, bestCount = nil, 0
|
||||
for _, className in ipairs(order) do
|
||||
if counts[className] > bestCount then
|
||||
best, bestCount = className, counts[className]
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- How many of `board`'s entries `score` would actually outrank, as a
|
||||
-- fraction from 0 (worse than or tied with everything) to 1 (better than
|
||||
-- everything). An empty board always answers 1 - there is nothing left to
|
||||
-- beat, so nothing holds the score back.
|
||||
function Analytics.percentile(board, score)
|
||||
if #board == 0 then return 1 end
|
||||
local beaten = 0
|
||||
for _, entry in ipairs(board) do
|
||||
if score > entry.score then beaten = beaten + 1 end
|
||||
end
|
||||
return beaten / #board
|
||||
end
|
||||
|
||||
return Analytics
|
||||
Reference in new issue
Block a user