Module:Dice
Jump to navigation
Jump to search
This module is maintained and documented on GitHub.
local Dice = {}
local dice_metatable = {__index = Dice}
local private = setmetatable({}, {__mode = 'k'})
local DICE_STRING_PARSE_ERROR_FORMAT = [[
I couldn't make sense of this as a dice string: %s
In particular, this part doesn't look like a term I understand: %s]]
local RANGE_STRING_PARSE_ERROR_FORMAT = [[
I couldn't make sense of this as a range string: %s]]
local DICE_TERM_PATTERN = '[+-]?[^+-]+'
local XDY_PATTERN = '^([+-]?%d+)d(%d+)$'
local CONSTANT_PATTERN = '^([+-]?%d+)$'
local RANGE_PATTERN = '^(%d+)-(%d+)$'
local function new_dice(dice_list)
local result = {}
private[result] = {dice_list = dice_list}
return setmetatable(result, dice_metatable)
end
-- Return a dice-string-compatible version of the given function.
local function dice_method(method)
return function (dice)
if type(dice) == 'string' then
return method(Dice.from_dice_string(dice))
else
return method(dice)
end
end
end
local function map_sum_dice(dice, mapper)
local sum = 0
for _, subdice in ipairs(private[dice].dice_list) do
sum = sum + mapper(subdice.quantity, subdice.size)
end
return sum
end
--# Exports
--## Computing the statistics of a roll
--### Dice.average
function Dice:average()
return map_sum_dice(self, function (quantity, size)
return quantity * ((1 + size) / 2)
end)
end
Dice.average = dice_method(Dice.average)
Dice.ev = Dice.average
Dice.expected_value = Dice.average
Dice.mean = Dice.average
--### Dice.maximum
function Dice:maximum()
return map_sum_dice(self, function (quantity, size)
return math.max(quantity * 1, quantity * size)
end)
end
Dice.maximum = dice_method(Dice.maximum)
Dice.max = Dice.maximum
--### Dice.minimum
function Dice:minimum()
return map_sum_dice(self, function (quantity, size)
return math.min(quantity * 1, quantity * size)
end)
end
Dice.minimum = dice_method(Dice.minimum)
Dice.min = Dice.minimum
--### Dice.range
function Dice:range()
return math.abs(self:maximum() - self:minimum() + 1)
end
Dice.range = dice_method(Dice.range)
--### Dice.variance
function Dice:variance()
return map_sum_dice(self, function (quantity, size)
local single_die_average = (1 + size) / 2
local sum = 0
for n = 1, size do
sum = sum + (n - single_die_average) ^ 2 / size
end
return math.abs(quantity) * sum
end)
end
Dice.variance = dice_method(Dice.variance)
--### Dice.roll
function Dice:roll()
return map_sum_dice(self, function (quantity, size)
local sum = 0
for n = 1, math.abs(quantity) do
sum = sum + math.random(1, size)
end
if quantity >= 0 then
return sum
else
return -sum
end
end)
end
Dice.roll = dice_method(Dice.roll)
Dice.sample = Dice.roll
--### Dice.compare
function Dice.compare(dice_a, dice_b)
local average_a, average_b = Dice.average(dice_a), Dice.average(dice_b)
if average_a > average_b then
return 1, 'greater average'
elseif average_b > average_a then
return -1, 'greater average'
else
local range_a, range_b = Dice.range(dice_a), Dice.range(dice_b)
if range_a < range_b then
return 1, 'smaller range'
elseif range_b < range_a then
return -1, 'smaller range'
else
local variance_a, variance_b =
Dice.variance(dice_a), Dice.variance(dice_b)
if variance_a < variance_b then
return 1, 'less variance'
elseif variance_b < variance_a then
return -1, 'less variance'
else
return 0, 'no difference'
end
return 0, 'inconclusive'
end
end
end
--## Parsing a roll from a string
--### Dice.from_dice_string
function Dice.from_dice_string(dice_string)
local dice_list = {}
for term in dice_string:gsub('%s', ''):gmatch(DICE_TERM_PATTERN) do
local quantity, size = term:match(XDY_PATTERN)
if quantity ~= nil and size ~= nil then
table.insert(dice_list, {
quantity = tonumber(quantity, 10),
size = tonumber(size, 10),
})
else
local quantity = term:match(CONSTANT_PATTERN)
if quantity ~= nil then
table.insert(dice_list, {
quantity = tonumber(quantity, 10),
size = 1,
})
else
error(DICE_STRING_PARSE_ERROR_FORMAT:format(dice_string, term))
end
end
end
return new_dice(dice_list)
end
Dice.from_string = Dice.from_dice_string
--### Dice.from_range_string
function Dice.from_range_string(range_string)
local minimum, maximum = range_string:gsub('%s', ''):match(RANGE_PATTERN)
if minimum ~= nil and maximum ~= nil then
return new_dice{
{
quantity = 1,
size = maximum - minimum + 1,
},
{
quantity = minimum - 1,
size = 1,
},
}
else
error(RANGE_STRING_PARSE_ERROR_FORMAT:format(range_string, term))
end
end
--##
return Dice