163 lines
3.4 KiB
Crystal
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
|
|
#
|