rollable/src/rollable/die.cr

163 lines
3.4 KiB
Crystal

require "./is_rollable"
# Not a front class. It is used to represent a type of dice with faces
#
# A `Die` is a range of Integer values.
# It is rollable.
#
# Example:
# ```
# d = Die.new(1..6)
# d.min # => 1
# d.max # => 6
# d.average # => 3.5
# d.test # => a random value included in 1..6
# ```
# TODO: make it a Struct ?
class Rollable::Die < Rollable::IsRollable
MAX = 1000
EXPLODING_ITERATIONS = 4
@faces : Range(Int32, Int32)
getter exploding : Bool
getter faces
def initialize(@faces, @exploding = false)
raise ParsingError.new "Cannot die with more than #{MAX} faces (#{@faces})" if @faces.size > MAX
if @faces.end < @faces.begin
@faces = @faces.end..@faces.begin
end
end
def clone
Die.new(@faces, @exploding)
end
def initialize(nb_faces : Int32, @exploding = false)
raise ParsingError.new "Cannot die with more than #{MAX} faces (#{nb_faces})" if nb_faces > MAX
@faces = 1..nb_faces
if @faces.end < @faces.begin
@faces = @faces.end..@faces.begin
end
end
# Number of faces of the `Die`
def size
@faces.size
end
def fixed?
size == 1
end
def negative?
min < 0 && max < 0
end
def like?(other : Die)
@faces == other.faces || @faces == other.reverse.faces
end
# Reverse the values
#
# Example:
# ```
# Die.new(1..6).reverse # => Die.new -6..-1
# ```
def reverse : Die
Die.new -@faces.end..-@faces.begin, @exploding
end
def reverse!
@faces = -@faces.end..-@faces.begin
self
end
def max : Int32
if @exploding
@faces.end * EXPLODING_ITERATIONS
else
@faces.end
end
end
def min : Int32
@faces.begin
end
private def explode(&block)
EXPLODING_ITERATIONS.times do |_|
value = @faces.to_a.sample
yield value
break if value != @faces.end
end
end
# Return a random value in the range of the dice
def test : Int32
if @exploding
sum = 0
explode { |value| sum += value }
sum
else
@faces.to_a.sample
end
end
# Mathematical expectation.
#
# A d6 will have a expected value of 3.5
def average : Float64
proba = @faces.size.to_f64
non_exploding_average = @faces.reduce { |r, l| r + l }.to_f64 / proba
if @exploding
EXPLODING_ITERATIONS.times.reduce(0.0) {|base, i| base + non_exploding_average / proba ** i }.round(3)
else
non_exploding_average
end
end
# Return a string.
# - It may be a fixed value ```(n..n) => "#{n}"```
# - It may be a dice ```(1..n) => "D#{n}"```
# - Else, ```(a..b) => "D(#{a},#{b})"```
def to_s : String
string = if self.size == 1
min.to_s
elsif self.min == 1
"D#{@faces.end}"
else
"D(#{@faces.begin},#{@faces.end})"
end
string = "!#{string}" if @exploding
return string
end
def ==(right : Die)
@faces == right.faces && @exploding == right.exploding
end
{% for op in [">", "<", ">=", "<="] %}
def {{ op.id }}(right : Die)
return false if @exploding != right.exploding
average != right.average ?
average {{ op.id }} right.average :
max != right.max ?
max {{ op.id }} right.max :
min {{ op.id }} right.min
end
{% end %}
def <=>(right : Die) : Int32
if average != right.average
average - right.average > 0 ? 1 : -1
elsif max != right.max
max - right.max <=> 0
else
min - right.min <=> 0
end
end
end
#