Module:Dice: Difference between revisions

From Caves of Qud Wiki
Jump to navigation Jump to search
m (remove now-unnecessary header comment)
(update to v1.0.0)
Line 1: Line 1:
local Dice = {}
local Dice = {}
Dice.__index = Dice
local dice_metatable = {__index = Dice}
local private = setmetatable({}, {__mode = 'k'})


--# Private
local DICE_STRING_PARSE_ERROR_FORMAT = [[


local function newDice(diceTable)
I couldn't make sense of this as a dice string: %s
     return setmetatable({diceTable = diceTable}, Dice)
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
end


--# Interface
-- 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


function Dice.fromDiceString(diceString)
local function map_sum_dice(dice, mapper)
     local diceTable = {}
     local sum = 0


     for term in diceString:gsub('%s', ''):gmatch'[+-]?[^+-]+' do
    for _, subdice in ipairs(private[dice].dice_list) do
         local quantity, size = term:match'^([+-]?%d+)d(%d+)$'
        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 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
         if quantity ~= nil and size ~= nil then
             diceTable[#diceTable + 1] = {
             table.insert(dice_list, {
                 quantity = tonumber(quantity, 10),
                 quantity = tonumber(quantity, 10),
                 size = tonumber(size, 10),
                 size = tonumber(size, 10),
             }
             })
         else
         else
             local quantity = term:match'^([+-]?%d+)$'
             local quantity = term:match(CONSTANT_PATTERN)


             if quantity ~= nil then
             if quantity ~= nil then
                 diceTable[#diceTable + 1] = {
                 table.insert(dice_list, {
                     quantity = tonumber(quantity, 10),
                     quantity = tonumber(quantity, 10),
                     size = 1,
                     size = 1,
                 }
                 })
             else
             else
                 error([[
                 error(DICE_STRING_PARSE_ERROR_FORMAT:format(dice_string, term))
 
I couldn't make sense of this as a dice string: ]] .. diceString .. [[
 
In particular, this part doesn't look like a term I understand: ]] .. term)
             end
             end
         end
         end
     end
     end


     return newDice(diceTable)
     return new_dice(dice_list)
end
end


Dice.fromString = Dice.fromDiceString
Dice.from_string = Dice.from_dice_string
 
--### Dice.from_range_string


function Dice.fromRangeString(rangeString)
function Dice.from_range_string(range_string)
     local minimum, maximum = rangeString:gsub('%s', ''):match'^(%d+)-(%d+)$'
     local minimum, maximum = range_string:gsub('%s', ''):match(RANGE_PATTERN)


     if minimum ~= nil and maximum ~= nil then
     if minimum ~= nil and maximum ~= nil then
         return newDice{
         return new_dice{
             {
             {
                 quantity = 1,
                 quantity = 1,
Line 59: Line 214:
         }
         }
     else
     else
         error("I couldn't make sense of this as a range string: "
         error(RANGE_STRING_PARSE_ERROR_FORMAT:format(range_string, term))
            .. rangeString)
    end
end
 
function Dice:minimum()
    local sum = 0
 
    for _, subdice in ipairs(self.diceTable) do
        if subdice.quantity >= 0 then
            sum = sum + subdice.quantity * 1
        else
            sum = sum + subdice.quantity * subdice.size
        end
    end
 
    return sum
end
 
function Dice:mean()
    local sum = 0
 
    for _, subdice in ipairs(self.diceTable) do
        sum = sum + subdice.quantity * (1 + subdice.size) / 2
    end
 
    return sum
end
 
function Dice:maximum()
    local sum = 0
 
    for _, subdice in ipairs(self.diceTable) do
        if subdice.quantity >= 0 then
            sum = sum + subdice.quantity * subdice.size
        else
            sum = sum + subdice.quantity * 1
        end
     end
     end
    return sum
end
end


--# Export
--##


return Dice
return Dice

Revision as of 05:05, 19 July 2020

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 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