Last active
June 2, 2018 09:53
-
-
Save JoshuaGrams/7e1f37199af0fce50bbb95f6ea183d32 to your computer and use it in GitHub Desktop.
skill.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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