Skip to content

Instantly share code, notes, and snippets.

@JoshuaGrams
Last active June 2, 2018 09:53
Show Gist options
  • Save JoshuaGrams/7e1f37199af0fce50bbb95f6ea183d32 to your computer and use it in GitHub Desktop.
Save JoshuaGrams/7e1f37199af0fce50bbb95f6ea183d32 to your computer and use it in GitHub Desktop.
skill.lua
-- Skill Progression
--
-- Deterministic - each skill has XP which counts up.
--
-- Each level costs x% more than the previous one.
--
-- You learn the same amount whether you pass or fail a skill
-- check.
--
-- Learning is curved based on skill vs. difficulty. The
-- difference is mapped onto a bell-shaped curve.
--
-- Mental fatigue increases every time you use a skill, and
-- decreases over time. At some point fatigue causes you to
-- smoothstep down from full learning to no learning.
----------------------------------------------------------------
-- Curves and other maths.
local min, max = math.min, math.max
local floor = math.floor
local log = math.log -- natural logarithm
local function clamp(x, lo, hi)
return max(lo, min(x, hi))
end
local function mapRange(x, inLo, inHi, outLo, outHi)
return outLo + (outHi - outLo) * (x - inLo) / (inHi - inLo)
end
-- Eighth-degree polynomial roughly approximating a three-sigma
-- Gaussian on [0..1]. The area under the curve is 128/315.
-- Instead of the Gaussian's 68/95/99.7, we have 71/98/100.
local function bell8(t)
local p = 4 * t * (1-t) -- Parabolic arc
local p2 = p * p
return p2 * p2
end
local function stepDown3(t)
local s = 1 - t
return s * s * (1 + 2*t)
end
----------------------------------------------------------------
-- You learn more when a task is near your current skill level.
local function learning(skill, requirement, sigma)
local delta = skill.level - requirement
local t = mapRange(delta, -3 * sigma, 3 * sigma, 0, 1)
return bell8(clamp(t, 0, 1))
end
----------------------------------------------------------------
-- Mental fatigue. Steps from full learning at `fatigueStart`
-- down to no learning (0) at `fatigueFinish`.
local function fatigue(skill)
local lo, hi = skill.fatigueStart, skill.fatigueFinish
local t = mapRange(skill.fatigue, lo, hi, 0, 1)
return stepDown3(clamp(t, 0, 1))
end
----------------------------------------------------------------
-- Skill object.
local Skill = {}
-- Keep properties and meta-properties in the same table.
Skill.__index = Skill
function Skill.new(class, ...)
local obj = setmetatable({}, class)
obj:set(...)
return obj
end
-- Allow creating an object with `Skill(...)`
setmetatable(Skill, {__call = Skill.new})
Skill.random = math.random
-- Fatigue properties are on class/prototype, but could be
-- overridden by individual skils if necessary.
-- Fatigue is capped at the limit.
Skill.fatigueLimit = 50
-- When does learning start decreasing? When does it
-- completely stop?
Skill.fatigueStart, Skill.fatigueFinish = 5, 10
-- Fatigue to add per check and remove per tick.
Skill.fatigueAdd, Skill.fatigueDecay = 1, 1
function Skill.set(self, cost, costFactor)
self.level, self.points = 0, 0
self.cost, self.costFactor = cost, costFactor
self.fatigue = 0
self.lastUpdated = false
end
-- Arguments:
-- * Current `time` (in whatever units you're using).
-- * Experience `points` to be added to this skill.
-- * Task's `required` skill level.
-- * Standard deviation for 3 `sigma` bell curve.
function Skill.check(self, time, points, required, sigma)
-- Reduce fatigue over time.
if self.lastUpdated then
local dt = time - self.lastUpdated
self.fatigue = max(0, self.fatigue - self.fatigueDecay * dt)
end
self.lastUpdated = time
-- Accumulate experience: points modified by task difficulty
-- and mental fatigue.
local feasible = learning(self, required, sigma)
local tired = fatigue(self)
self.fatigue = min(self.fatigueLimit, self.fatigue + self.fatigueAdd)
self.points = self.points + points * feasible * tired
-- Update level based on points.
local x = 1 - self.points * (1 - self.costFactor) / self.cost
self.level = floor(log(x) / log(self.costFactor))
-- Check against requirement.
local chance = self.level > required and 1 or feasible
return Skill.random() < chance
end
return Skill
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment