summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.txt1
-rw-r--r--beanpole.lua4
-rw-r--r--init.lua385
-rw-r--r--init.lua_orig192
-rw-r--r--statistics.lua150
5 files changed, 696 insertions, 36 deletions
diff --git a/README.txt b/README.txt
index aa7b320..d10ad53 100644
--- a/README.txt
+++ b/README.txt
@@ -13,6 +13,7 @@ This mod works by adding your new plant to the {growing=1} group and numbering t
Changelog:
+1.20 - NEW growing routine added that allows crops to grow while player is away doing other things (thanks prestidigitator)
1.14 - Added Green Beans from Crops mod (thanks sofar), little bushels in the wild but need to be grown using beanpoles crafted with 4 sticks (2 either side)
1.13 - Fixed seed double-placement glitch. Mapgen now uses 0.4.12+ for plant generation
1.12 - Player cannot place seeds in protected area, also growing speeds changed to match defaults
diff --git a/beanpole.lua b/beanpole.lua
index 0387fa0..895b6fa 100644
--- a/beanpole.lua
+++ b/beanpole.lua
@@ -65,7 +65,7 @@ minetest.register_craft({
}
})
--- Define Corn growth stages
+-- Define Green Bean growth stages
minetest.register_node("farming:beanpole_1", {
drawtype = "plantlike",
@@ -140,7 +140,7 @@ minetest.register_node("farming:beanpole_4", {
sounds = default.node_sound_leaves_defaults(),
})
--- Last stage of Corn growth doesnnot have growing=1 so abm never has to check these
+-- Last stage of Green Bean growth does not have growing=1 so abm never has to check these
minetest.register_node("farming:beanpole_5", {
drawtype = "plantlike",
diff --git a/init.lua b/init.lua
index aee9976..90714f0 100644
--- a/init.lua
+++ b/init.lua
@@ -1,6 +1,7 @@
--[[
- Minetest Farming Redo Mod 1.14 (11th May 2015)
+ Minetest Farming Redo Mod 1.20 (20th May 2015)
by TenPlus1
+ NEW growing routine by prestidigitator
]]
farming = {}
@@ -8,6 +9,39 @@ farming.mod = "redo"
farming.path = minetest.get_modpath("farming")
farming.hoe_on_use = default.hoe_on_use
+farming.DEBUG = false
+-- farming.DEBUG = {} -- Uncomment to turn on profiling code/functions
+
+local DEBUG_abm_runs = 0
+local DEBUG_abm_time = 0
+local DEBUG_timer_runs = 0
+local DEBUG_timer_time = 0
+if farming.DEBUG then
+ function farming.DEBUG.reset_times()
+ DEBUG_abm_runs = 0
+ DEBUG_abm_time = 0
+ DEBUG_timer_runs = 0
+ DEBUG_timer_time = 0
+ end
+
+ function farming.DEBUG.report_times()
+ local abm_n = DEBUG_abm_runs
+ local abm_dt = DEBUG_abm_time
+ local abm_avg = (abm_n > 0 and abm_dt / abm_n) or 0
+ local timer_n = DEBUG_timer_runs
+ local timer_dt = DEBUG_timer_time
+ local timer_avg = (timer_n > 0 and timer_dt / timer_n) or 0
+ local dt = abm_dt + timer_dt
+ print("ABM ran for "..abm_dt.."µs over "..abm_n.." runs: "..
+ abm_avg.."µs/run")
+ print("Timer ran for "..timer_dt.."µs over "..timer_n.." runs: "..
+ timer_avg.."µs/run")
+ print("Total farming time: "..dt.."µs")
+ end
+else
+end
+
+local statistics = dofile(farming.path.."/statistics.lua")
dofile(farming.path.."/soil.lua")
dofile(farming.path.."/hoes.lua")
dofile(farming.path.."/grass.lua")
@@ -31,6 +65,319 @@ dofile(farming.path.."/donut.lua")
dofile(farming.path.."/mapgen.lua")
dofile(farming.path.."/compatibility.lua") -- Farming Plus compatibility
+-- Utility Functions
+
+local time_speed = tonumber(minetest.setting_get("time_speed")) or 72
+local SECS_PER_CYCLE = (time_speed > 0 and 24 * 60 * 60/time_speed) or nil
+
+local function clamp(x, min, max)
+ return (x < min and min) or (x > max and max) or x
+end
+
+local function in_range(x, min, max)
+ return min <= x and x <= max
+end
+
+--- Tests the amount of day or night time between two times.
+ --
+ -- @param t_game
+ -- The current time, as reported by mintest.get_gametime().
+ -- @param t_day
+ -- The current time, as reported by mintest.get_timeofday().
+ -- @param dt
+ -- The amount of elapsed time.
+ -- @param count_day
+ -- If true, count elapsed day time. Otherwise, count elapsed night time.
+ -- @return
+ -- The amount of day or night time that has elapsed.
+ --
+local function day_or_night_time(t_game, t_day, dt, count_day)
+ local t1_day = t_day - dt / SECS_PER_CYCLE
+
+ local t1_c, t2_c -- t1_c < t2_c and t2_c always in [0, 1)
+ if count_day then
+ if t_day < 0.25 then
+ t1_c = t1_day + 0.75 -- Relative to sunup, yesterday
+ t2_c = t_day + 0.75
+ else
+ t1_c = t1_day - 0.25 -- Relative to sunup, today
+ t2_c = t_day - 0.25
+ end
+ else
+ if t_day < 0.75 then
+ t1_c = t1_day + 0.25 -- Relative to sundown, yesterday
+ t2_c = t_day + 0.25
+ else
+ t1_c = t1_day - 0.75 -- Relative to sundown, today
+ t2_c = t_day - 0.75
+ end
+ end
+
+ local dt_c = clamp(t2_c, 0, 0.5) - clamp(t1_c, 0, 0.5) -- this cycle
+ if t1_c < -0.5 then
+ local nc = math.floor(-t1_c)
+ t1_c = t1_c + nc
+ dt_c = dt_c + 0.5 * nc + clamp(-t1_c - 0.5, 0, 0.5)
+ end
+
+ return dt_c * SECS_PER_CYCLE
+end
+
+--- Tests the amount of elapsed day time.
+ --
+ -- @param dt
+ -- The amount of elapsed time.
+ -- @return
+ -- The amount of day time that has elapsed.
+ --
+local function day_time(dt)
+ return day_or_night_time(minetest.get_gametime(), minetest.get_timeofday(), dt, true)
+end
+
+--- Tests the amount of elapsed night time.
+ --
+ -- @param dt
+ -- The amount of elapsed time.
+ -- @return
+ -- The amount of night time that has elapsed.
+ --
+local function night_time(time_game, time_day, dt, count_day)
+ return day_or_night_time(minetest.get_gametime(), minetest.get_timeofday(), dt, false)
+end
+
+
+-- Growth Logic
+
+local STAGE_LENGTH_AVG = 160.0
+local STAGE_LENGTH_DEV = STAGE_LENGTH_AVG / 6
+local MIN_LIGHT = 13
+local MAX_LIGHT = 1000
+
+--- Determines plant name and stage from node.
+ --
+ -- Separates node name on the last underscore (_).
+ --
+ -- @param node
+ -- Node or position table, or node name.
+ -- @return
+ -- List (plant_name, stage), or nothing (nil) if node isn't loaded
+ --
+local function plant_name_stage(node)
+ local name
+ if type(node) == 'table' then
+ if node.name then
+ name = node.name
+ elseif node.x and node.y and node.z then
+ node = minetest.get_node_or_nil(node)
+ name = node and node.name
+ end
+ else
+ name = tostring(node)
+ end
+ if not name or name == "ignore" then return nil end
+
+ local sep_pos = name:find("_[^_]+$")
+ if sep_pos and sep_pos > 1 then
+ local stage = tonumber(name:sub(sep_pos + 1))
+ if stage and stage >= 0 then
+ return name:sub(1, sep_pos - 1), stage
+ end
+ end
+
+ return name, 0
+end
+
+--- Map from node name to
+ -- { plant_name = ..., name = ..., stage = n, stages_left = { node_name, ... } }
+local plant_stages = {}
+farming.plant_stages = plant_stages
+
+--- Registers the stages of growth of a (possible plant) node.
+ --
+ -- @param node
+ -- Node or position table, or node name.
+ -- @return
+ -- The (possibly zero) number of stages of growth the plant will go through
+ -- before being fully grown, or nil if not a plant.
+ --
+local register_plant_node
+-- Recursive helper
+local function reg_plant_stages(plant_name, stage, force_last)
+ local node_name = plant_name and plant_name .. "_" .. stage
+ local node_def = node_name and minetest.registered_nodes[node_name]
+ if not node_def then return nil end
+
+ local stages = plant_stages[node_name]
+ if stages then return stages end
+
+ if minetest.get_item_group(node_name, "growing") > 0 then
+ local ns = reg_plant_stages(plant_name, stage + 1, true)
+
+ local stages_left = (ns and { ns.name, unpack(ns.stages_left) }) or {}
+ stages = { plant_name = plant_name, name = node_name, stage = stage, stages_left = stages_left }
+
+ if #stages_left > 0 then
+ local old_constr = node_def.on_construct
+ local old_destr = node_def.on_destruct
+ minetest.override_item(node_name,
+ {
+ on_construct = function(pos)
+ if old_constr then old_constr(pos) end
+ farming.handle_growth(pos)
+ end,
+
+ on_destruct = function(pos)
+ minetest.get_node_timer(pos):stop()
+ if old_destr then old_destr(pos) end
+ end,
+
+ on_timer = function(pos, elapsed)
+ return farming.plant_growth_timer(pos, elapsed, node_name)
+ end,
+ })
+ end
+ elseif force_last then
+ stages = { plant_name = plant_name, name = node_name, stage = stage, stages_left = {} }
+ else
+ return nil
+ end
+
+ plant_stages[node_name] = stages
+ return stages
+end
+
+register_plant_node = function(node)
+ local plant_name, stage = plant_name_stage(node)
+ if plant_name then
+ local stages = reg_plant_stages(plant_name, stage, false)
+ return stages and #stages.stages_left
+ else
+ return nil
+ end
+end
+
+local function set_growing(pos, stages_left)
+ if not stages_left then return end
+
+ local timer = minetest.get_node_timer(pos)
+ if stages_left > 0 then
+ if not timer:is_started() then
+ local stage_length = statistics.normal(STAGE_LENGTH_AVG, STAGE_LENGTH_DEV)
+ stage_length = clamp(stage_length, 0.5 * STAGE_LENGTH_AVG, 3.0 * STAGE_LENGTH_AVG)
+ timer:set(stage_length, -0.5 * math.random() * STAGE_LENGTH_AVG)
+ end
+ elseif timer:is_started() then
+ timer:stop()
+ end
+end
+
+--- Detects a plant type node at the given position, starting or stopping the plant growth timer as appopriate
+ --
+ -- @param pos
+ -- The node's position.
+ -- @param node
+ -- The cached node table if available, or nil.
+ --
+function farming.handle_growth(pos, node)
+ if not pos then return end
+ local stages_left = register_plant_node(node or pos)
+ if stages_left then set_growing(pos, stages_left) end
+end
+
+minetest.after(0,
+ function()
+ for _, node_def in pairs(minetest.registered_nodes) do
+ register_plant_node(node_def)
+ end
+ end)
+
+local abm_func = farming.handle_growth
+if farming.DEBUG then
+ local normal_abm_func = abm_func
+ abm_func = function(...)
+ local t0 = minetest.get_us_time()
+ local r = { normal_abm_func(...) }
+ local t1 = minetest.get_us_time()
+ DEBUG_abm_runs = DEBUG_abm_runs + 1
+ DEBUG_abm_time = DEBUG_abm_time + (t1 - t0)
+ return unpack(r)
+ end
+end
+
+-- Just in case a growing type or added node is missed (also catches existing
+-- nodes added to map before timers were incorporated).
+minetest.register_abm({
+ nodenames = { "group:growing" },
+ interval = 300,
+ chance = 1,
+ action = abm_func})
+
+--- Plant timer function.
+ --
+ -- Grows plants under the right conditions.
+ --
+function farming.plant_growth_timer(pos, elapsed, node_name)
+ local stages = plant_stages[node_name]
+ if not stages then return false end
+
+ local max_growth = #stages.stages_left
+ if max_growth <= 0 then return false end
+
+ if stages.plant_name == "farming:cocoa" then
+ if not minetest.find_node_near(pos, 1, { "default:jungletree", "moretrees:jungletree_leaves_green" }) then
+ return true
+ end
+ else
+ local under = minetest.get_node_or_nil({ x = pos.x, y = pos.y - 1, z = pos.z })
+ if not under or under.name ~= "farming:soil_wet" then return true end
+ end
+
+ local growth
+
+ local light_pos = { x = pos.x, y = pos.y + 1, z = pos.z }
+ local lambda = elapsed / STAGE_LENGTH_AVG
+ if lambda < 0.1 then return true end
+ if max_growth == 1 or lambda < 2.0 then
+ local light = minetest.get_node_light(light_pos)
+ if not in_range(light, MIN_LIGHT, MAX_LIGHT) then return true end
+ growth = 1
+ else
+ local night_light = minetest.get_node_light(light_pos, 0)
+ local day_light = minetest.get_node_light(light_pos, 0.5)
+ local night_growth = in_range(night_light, MIN_LIGHT, MAX_LIGHT)
+ local day_growth = in_range(day_light, MIN_LIGHT, MAX_LIGHT)
+
+ if not night_growth then
+ if not day_growth then return true end
+ lambda = day_time(elapsed) / STAGE_LENGTH_AVG
+ elseif not day_growth then
+ lambda = night_time(elapsed) / STAGE_LENGTH_AVG
+ end
+
+ growth = statistics.poisson(lambda, max_growth)
+ if growth < 1 then return true end
+ end
+
+ minetest.swap_node(pos, { name = stages.stages_left[growth] })
+
+ return growth ~= max_growth
+end
+
+if farming.DEBUG then
+ local timer_func = farming.plant_growth_timer;
+ farming.plant_growth_timer = function(pos, elapsed, node_name)
+ local t0 = minetest.get_us_time()
+
+ local r = { timer_func(pos, elapsed, node_name) }
+
+ local t1 = minetest.get_us_time()
+ DEBUG_timer_runs = DEBUG_timer_runs + 1
+ DEBUG_timer_time = DEBUG_timer_time + (t1 - t0)
+ return unpack(r)
+ end
+end
+
+
-- Place Seeds on Soil
function farming.place_seed(itemstack, placer, pointed_thing, plantname)
@@ -72,38 +419,6 @@ function farming.place_seed(itemstack, placer, pointed_thing, plantname)
end
end
--- Single ABM Handles Growing of All Plants
-
-minetest.register_abm({
- nodenames = {"group:growing"},
- neighbors = {"farming:soil_wet", "default:jungletree"},
- interval = 80,
- chance = 2,
-
- action = function(pos, node)
-
- -- split plant name (e.g. farming:wheat_1)
- local plant = node.name:split("_")[1].."_"
- local numb = node.name:split("_")[2]
-
- -- fully grown ?
- if not minetest.registered_nodes[plant..(numb + 1)] then return end
-
- -- cocoa pod on jungle tree ?
- if plant ~= "farming:cocoa_" then
-
- -- growing on wet soil ?
- if minetest.get_node({x=pos.x,y=pos.y-1,z=pos.z}).name ~= "farming:soil_wet" then return end
- end
-
- -- enough light ?
- if minetest.get_node_light(pos) < 13 then return end
-
- -- grow
- minetest.set_node(pos, {name=plant..(numb + 1)})
-
- end
-})
-- Function to register plants (for compatibility)
@@ -163,7 +478,8 @@ farming.register_plant = function(name, def)
g = {snappy = 3, flammable = 2, plant = 1, not_in_creative_inventory = 1, attached_node = 1}
end
- minetest.register_node(mname .. ":" .. pname .. "_" .. i, {
+ local node_name = mname .. ":" .. pname .. "_" .. i
+ minetest.register_node(node_name, {
drawtype = "plantlike",
waving = 1,
tiles = {mname .. "_" .. pname .. "_" .. i .. ".png"},
@@ -176,6 +492,7 @@ farming.register_plant = function(name, def)
groups = g,
sounds = default.node_sound_leaves_defaults(),
})
+ register_plant_node(node_name)
end
-- Return info
diff --git a/init.lua_orig b/init.lua_orig
new file mode 100644
index 0000000..aee9976
--- /dev/null
+++ b/init.lua_orig
@@ -0,0 +1,192 @@
+--[[
+ Minetest Farming Redo Mod 1.14 (11th May 2015)
+ by TenPlus1
+]]
+
+farming = {}
+farming.mod = "redo"
+farming.path = minetest.get_modpath("farming")
+farming.hoe_on_use = default.hoe_on_use
+
+dofile(farming.path.."/soil.lua")
+dofile(farming.path.."/hoes.lua")
+dofile(farming.path.."/grass.lua")
+dofile(farming.path.."/wheat.lua")
+dofile(farming.path.."/cotton.lua")
+dofile(farming.path.."/carrot.lua")
+dofile(farming.path.."/potato.lua")
+dofile(farming.path.."/tomato.lua")
+dofile(farming.path.."/cucumber.lua")
+dofile(farming.path.."/corn.lua")
+dofile(farming.path.."/coffee.lua")
+dofile(farming.path.."/melon.lua")
+dofile(farming.path.."/sugar.lua")
+dofile(farming.path.."/pumpkin.lua")
+dofile(farming.path.."/cocoa.lua")
+dofile(farming.path.."/raspberry.lua")
+dofile(farming.path.."/blueberry.lua")
+dofile(farming.path.."/rhubarb.lua")
+dofile(farming.path.."/beanpole.lua")
+dofile(farming.path.."/donut.lua")
+dofile(farming.path.."/mapgen.lua")
+dofile(farming.path.."/compatibility.lua") -- Farming Plus compatibility
+
+-- Place Seeds on Soil
+
+function farming.place_seed(itemstack, placer, pointed_thing, plantname)
+ local pt = pointed_thing
+
+ -- check if pointing at a node
+ if not pt and pt.type ~= "node" then
+ return
+ end
+
+ local under = minetest.get_node(pt.under)
+ local above = minetest.get_node(pt.above)
+
+ -- check if pointing at the top of the node
+ if pt.above.y ~= pt.under.y+1 then
+ return
+ end
+
+ -- return if any of the nodes is not registered
+ if not minetest.registered_nodes[under.name]
+ or not minetest.registered_nodes[above.name] then
+ return
+ end
+
+ -- can I replace above node, and am I pointing at soil
+ if not minetest.registered_nodes[above.name].buildable_to
+ or minetest.get_item_group(under.name, "soil") < 2
+ or minetest.get_item_group(above.name, "plant") ~= 0 then -- ADDED this line for multiple seed placement bug
+ return
+ end
+
+ -- add the node and remove 1 item from the itemstack
+ if not minetest.is_protected(pt.above, placer:get_player_name()) then
+ minetest.add_node(pt.above, {name=plantname})
+ if not minetest.setting_getbool("creative_mode") then
+ itemstack:take_item()
+ end
+ return itemstack
+ end
+end
+
+-- Single ABM Handles Growing of All Plants
+
+minetest.register_abm({
+ nodenames = {"group:growing"},
+ neighbors = {"farming:soil_wet", "default:jungletree"},
+ interval = 80,
+ chance = 2,
+
+ action = function(pos, node)
+
+ -- split plant name (e.g. farming:wheat_1)
+ local plant = node.name:split("_")[1].."_"
+ local numb = node.name:split("_")[2]
+
+ -- fully grown ?
+ if not minetest.registered_nodes[plant..(numb + 1)] then return end
+
+ -- cocoa pod on jungle tree ?
+ if plant ~= "farming:cocoa_" then
+
+ -- growing on wet soil ?
+ if minetest.get_node({x=pos.x,y=pos.y-1,z=pos.z}).name ~= "farming:soil_wet" then return end
+ end
+
+ -- enough light ?
+ if minetest.get_node_light(pos) < 13 then return end
+
+ -- grow
+ minetest.set_node(pos, {name=plant..(numb + 1)})
+
+ end
+})
+
+-- Function to register plants (for compatibility)
+
+farming.register_plant = function(name, def)
+ local mname = name:split(":")[1]
+ local pname = name:split(":")[2]
+
+ -- Check def table
+ if not def.description then
+ def.description = "Seed"
+ end
+ if not def.inventory_image then
+ def.inventory_image = "unknown_item.png"
+ end
+ if not def.steps then
+ return nil
+ end
+
+ -- Register seed
+ minetest.register_node(":" .. mname .. ":seed_" .. pname, {
+ description = def.description,
+ tiles = {def.inventory_image},
+ inventory_image = def.inventory_image,
+ wield_image = def.inventory_image,
+ drawtype = "signlike",
+ groups = {seed = 1, snappy = 3, attached_node = 1},
+ paramtype = "light",
+ paramtype2 = "wallmounted",
+ walkable = false,
+ sunlight_propagates = true,
+ selection_box = {type = "fixed", fixed = {-0.5, -0.5, -0.5, 0.5, -5/16, 0.5},},
+ on_place = function(itemstack, placer, pointed_thing)
+ return farming.place_seed(itemstack, placer, pointed_thing, mname .. ":"..pname.."_1")
+ end
+ })
+
+ -- Register harvest
+ minetest.register_craftitem(":" .. mname .. ":" .. pname, {
+ description = pname:gsub("^%l", string.upper),
+ inventory_image = mname .. "_" .. pname .. ".png",
+ })
+
+ -- Register growing steps
+ for i=1,def.steps do
+ local drop = {
+ items = {
+ {items = {mname .. ":" .. pname}, rarity = 9 - i},
+ {items = {mname .. ":" .. pname}, rarity= 18 - i * 2},
+ {items = {mname .. ":seed_" .. pname}, rarity = 9 - i},
+ {items = {mname .. ":seed_" .. pname}, rarity = 18 - i * 2},
+ }
+ }
+
+ local g = {snappy = 3, flammable = 2, plant = 1, not_in_creative_inventory = 1, attached_node = 1, growing = 1}
+ -- Last step doesn't need growing=1 so Abm never has to check these
+ if i == def.steps then
+ g = {snappy = 3, flammable = 2, plant = 1, not_in_creative_inventory = 1, attached_node = 1}
+ end
+
+ minetest.register_node(mname .. ":" .. pname .. "_" .. i, {
+ drawtype = "plantlike",
+ waving = 1,
+ tiles = {mname .. "_" .. pname .. "_" .. i .. ".png"},
+ paramtype = "light",
+ walkable = false,
+ buildable_to = true,
+ is_ground_content = true,
+ drop = drop,
+ selection_box = {type = "fixed", fixed = {-0.5, -0.5, -0.5, 0.5, -5/16, 0.5},},
+ groups = g,
+ sounds = default.node_sound_leaves_defaults(),
+ })
+ end
+
+ -- Return info
+ local r = {seed = mname .. ":seed_" .. pname, harvest = mname .. ":" .. pname}
+ return r
+end
+
+--[[ Cotton (example, is already registered in cotton.lua)
+farming.register_plant("farming:cotton", {
+ description = "Cotton seed",
+ inventory_image = "farming_cotton_seed.png",
+ steps = 8,
+})
+--]]
diff --git a/statistics.lua b/statistics.lua
new file mode 100644
index 0000000..aaa4f51
--- /dev/null
+++ b/statistics.lua
@@ -0,0 +1,150 @@
+local statistics = {}
+
+local ROOT_2 = math.sqrt(2.0)
+
+-- Approximations for erf(x) and erfInv(x) from
+-- https://en.wikipedia.org/wiki/Error_function
+local erf
+local erf_inv
+
+local A = 8 * (math.pi - 3.0)/(3.0 * math.pi * (4.0 - math.pi))
+local B = 4.0 / math.pi
+local C = 2.0/(math.pi * A)
+local D = 1.0 / A
+
+erf = function(x)
+ if x == 0 then return 0; end
+ local xSq = x * x
+ local aXSq = A * xSq
+ local v = math.sqrt(1.0 - math.exp(-xSq * (B + aXSq)/(1.0 + aXSq)))
+ return (x > 0 and v) or -v
+end
+
+erf_inv = function(x)
+ if x == 0 then return 0; end
+ if x <= -1 or x >= 1 then return nil; end
+ local y = math.log(1 - x * x)
+ local u = C + 0.5 * y
+ local v = math.sqrt(math.sqrt(u * u - D * y) - u)
+ return (x > 0 and v) or -v
+end
+
+local function std_normal(u)
+ return ROOT_2 * erf_inv(2.0 * u - 1.0)
+end
+
+local poisson
+local cdf_table = {}
+
+local function generate_cdf(lambda_index, lambda)
+ local max = math.ceil(4 * lambda)
+ local pdf = math.exp(-lambda)
+ local cdf = pdf
+ local t = { [0] = pdf }
+
+ for i = 1, max - 1 do
+ pdf = pdf * lambda / i
+ cdf = cdf + pdf
+ t[i] = cdf
+ end
+
+ return t
+end
+
+for li = 1, 100 do
+ cdf_table[li] = generate_cdf(li, 0.25 * li)
+end
+
+poisson = function(lambda, max)
+ if max < 2 then
+ return (math.random() < math.exp(-lambda) and 0) or 1
+ elseif lambda >= 2 * max then
+ return max
+ end
+
+ local u = math.random()
+ local lambda_index = math.floor(4 * lambda + 0.5)
+ local cdfs = cdf_table[lambda_index]
+
+ if cdfs then
+ lambda = 0.25 * lambda_index
+
+ if u < cdfs[0] then return 0; end
+ if max > #cdfs then max = #cdfs + 1 else max = math.floor(max); end
+ if u >= cdfs[max - 1] then return max; end
+
+ if max > 4 then -- Binary search
+ local s = 0
+ while s + 1 < max do
+ local m = math.floor(0.5 * (s + max))
+ if u < cdfs[m] then max = m; else s = m; end
+ end
+ else
+ for i = 1, max - 1 do
+ if u < cdfs[i] then return i; end
+ end
+ end
+
+ return max
+ else
+ local x = lambda + math.sqrt(lambda) * std_normal(u)
+ return (x < 0.5 and 0) or (x >= max - 0.5 and max) or math.floor(x + 0.5)
+ end
+end
+
+-- Error function.
+statistics.erf = erf
+
+-- Inverse error function.
+statistics.erf_inv = erf_inv
+
+--- Standard normal distribution function (mean 0, standard deviation 1).
+ --
+ -- @return
+ -- Any real number (actually between -3.0 and 3.0).
+ --
+statistics.std_normal = function()
+ local u = math.random()
+ if u < 0.001 then
+ return -3.0
+ elseif u > 0.999 then
+ return 3.0
+ end
+ return std_normal(u)
+end
+
+--- Standard normal distribution function (mean 0, standard deviation 1).
+ --
+ -- @param mu
+ -- The distribution mean.
+ -- @param sigma
+ -- The distribution standard deviation.
+ -- @return
+ -- Any real number (actually between -3*sigma and 3*sigma).
+ --
+statistics.normal = function(mu, sigma)
+ local u = math.random()
+ if u < 0.001 then
+ return mu - 3.0 * sigma
+ elseif u > 0.999 then
+ return mu + 3.0 * sigma
+ end
+ return mu + sigma * std_normal(u)
+end
+
+--- Poisson distribution function.
+ --
+ -- @param lambda
+ -- The distribution mean and variance.
+ -- @param max
+ -- The distribution maximum.
+ -- @return
+ -- An integer between 0 and max (both inclusive).
+ --
+statistics.poisson = function(lambda, max)
+ lambda, max = tonumber(lambda), tonumber(max)
+ if not lambda or not max or lambda <= 0 or max < 1 then return 0; end
+ return poisson(lambda, max)
+end
+
+return statistics