DiceRoller service
This commit is contained in:
parent
bd21b53b06
commit
c01ab549fd
|
@ -11,11 +11,22 @@ class Stat < ApplicationRecord
|
||||||
uniqueness: true
|
uniqueness: true
|
||||||
validates :value, presence: true,
|
validates :value, presence: true,
|
||||||
numericality: true
|
numericality: true
|
||||||
|
validate :validate_roll_command
|
||||||
|
|
||||||
before_validation :set_slug
|
before_validation :set_slug
|
||||||
|
|
||||||
|
def roll
|
||||||
|
DiceRoller.new(roll_command, stat: self).roll
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def validate_roll_command
|
||||||
|
return if roll_command.blank?
|
||||||
|
|
||||||
|
DiceRoller.new(roll_command).valid?
|
||||||
|
end
|
||||||
|
|
||||||
def set_slug
|
def set_slug
|
||||||
return if slug.present? || name.blank?
|
return if slug.present? || name.blank?
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DiceRoller
|
||||||
|
def initialize(roll_command, stat: nil)
|
||||||
|
@roll_command = roll_command
|
||||||
|
@stat = stat&.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def roll
|
||||||
|
result = 0
|
||||||
|
operator = nil
|
||||||
|
roll_command_parts.each do |part|
|
||||||
|
case part
|
||||||
|
when /\A\d*d\d+\z/
|
||||||
|
operator.nil? ? (result += roll_dice(part)) : (result = result.send(operator, roll_dice(part)))
|
||||||
|
when /\A\d+\z/
|
||||||
|
operator.nil? ? (result += part.to_i) : (result = result.send(operator, part.to_i))
|
||||||
|
when "self"
|
||||||
|
operator.nil? ? (result += @stat) : (result = result.send(operator, @stat))
|
||||||
|
when /\A[+\-*\/]\z/
|
||||||
|
operator = part
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
return if @roll_command.blank?
|
||||||
|
|
||||||
|
# No repeated math operators
|
||||||
|
return false if @roll_command.match?(/[+\-*\/]{2,}/)
|
||||||
|
|
||||||
|
# No leading or trailing math operators
|
||||||
|
return false if @roll_command.match?(/\A[+\-*\/]/) || @roll_command.match?(/[+\-*\/]\z/)
|
||||||
|
|
||||||
|
@roll_command.match?(
|
||||||
|
/
|
||||||
|
\A(
|
||||||
|
(\d*d\d*) |
|
||||||
|
([+\-*\/]) |
|
||||||
|
(\d+) |
|
||||||
|
(self)
|
||||||
|
)*\z/xi,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def roll_command_parts
|
||||||
|
@roll_command.scan(/([+\-*\/])|(\d*d\d+)|(\d+)|(self)/xi)
|
||||||
|
.flatten
|
||||||
|
.compact_blank
|
||||||
|
end
|
||||||
|
|
||||||
|
def roll_dice(command)
|
||||||
|
parts = command.downcase.split("d").compact_blank
|
||||||
|
die_type = parts.last
|
||||||
|
dice_number = parts.length > 1 ? parts.first.to_i : 1
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
dice_number.times do
|
||||||
|
result += rand(1..die_type.to_i)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,4 +22,10 @@ class StatTest < ActiveSupport::TestCase
|
||||||
assert_not_equal existing_stat.slug, stat.slug
|
assert_not_equal existing_stat.slug, stat.slug
|
||||||
assert_equal "#{existing_stat.slug}-2", stat.slug
|
assert_equal "#{existing_stat.slug}-2", stat.slug
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rolls with roll_command" do
|
||||||
|
stat = stats(:strength)
|
||||||
|
stat.roll_command = "1d6"
|
||||||
|
100.times { assert (1..6).cover?(stat.roll) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DiceRollerTest < ActiveSupport::TestCase
|
||||||
|
test "correctly validates strings" do
|
||||||
|
valid_strings = [
|
||||||
|
"2d6",
|
||||||
|
"12",
|
||||||
|
"self",
|
||||||
|
"d12",
|
||||||
|
"d20+self",
|
||||||
|
"8+D8-self",
|
||||||
|
]
|
||||||
|
|
||||||
|
invalid_strings = [
|
||||||
|
"+",
|
||||||
|
"sel+10",
|
||||||
|
"d8++13",
|
||||||
|
]
|
||||||
|
|
||||||
|
valid_strings.each do |roll_command|
|
||||||
|
assert DiceRoller.new(roll_command).valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
invalid_strings.each do |roll_command|
|
||||||
|
assert_not DiceRoller.new(roll_command).valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rolls appropriate results" do
|
||||||
|
100.times do
|
||||||
|
assert (1..6).include? DiceRoller.new("d6").roll
|
||||||
|
assert (2..12).include? DiceRoller.new("2d6").roll
|
||||||
|
assert (11..30).include? DiceRoller.new("d20+self", stat: stats(:strength)).roll
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
1
todo.md
1
todo.md
|
@ -1,5 +1,4 @@
|
||||||
- fix inconsistencies in nested create: do we need to submit the id or use in url?
|
- fix inconsistencies in nested create: do we need to submit the id or use in url?
|
||||||
- add stat stimulus controller to update stats
|
|
||||||
- default avatars
|
- default avatars
|
||||||
- add uuid/slug to characters and any other url-visible ids
|
- add uuid/slug to characters and any other url-visible ids
|
||||||
- shared/private notes
|
- shared/private notes
|
||||||
|
|
Loading…
Reference in New Issue