para_dice 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.pullreview.yml +77 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +9 -0
  7. data/Guardfile +27 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +51 -0
  10. data/Rakefile +10 -0
  11. data/VERSION +1 -0
  12. data/bin/dice_tests.rb +27 -0
  13. data/data/face_types.yml +27 -0
  14. data/data/gotham_dice.yml +55 -0
  15. data/data/specs/not_really_yaml.yml +22 -0
  16. data/data/specs/simple_array.yml +7 -0
  17. data/data/specs/simple_hash.yml +4 -0
  18. data/data/star_wars_dice.yml +87 -0
  19. data/lib/gotham_dice.rb +44 -0
  20. data/lib/para_dice/bag.rb +44 -0
  21. data/lib/para_dice/cup.rb +19 -0
  22. data/lib/para_dice/die.rb +64 -0
  23. data/lib/para_dice/faces/arrayed.rb +34 -0
  24. data/lib/para_dice/faces/ranged.rb +38 -0
  25. data/lib/para_dice/numbered_die.rb +13 -0
  26. data/lib/para_dice/poly_bag.rb +31 -0
  27. data/lib/para_dice/results/and_keep.rb +34 -0
  28. data/lib/para_dice/results/array.rb +13 -0
  29. data/lib/para_dice/results/cancel_opposing.rb +46 -0
  30. data/lib/para_dice/results/hash.rb +14 -0
  31. data/lib/para_dice/results/number.rb +39 -0
  32. data/lib/para_dice/results/split_faces.rb +23 -0
  33. data/lib/para_dice/string_die.rb +15 -0
  34. data/lib/para_dice/utility.rb +54 -0
  35. data/lib/para_dice.rb +9 -0
  36. data/lib/star_wars_dice.rb +41 -0
  37. data/para_dice.gemspec +34 -0
  38. data/spec/gotham_dice_spec.rb +19 -0
  39. data/spec/para_dice/bag_spec.rb +53 -0
  40. data/spec/para_dice/cup_spec.rb +73 -0
  41. data/spec/para_dice/die_spec.rb +21 -0
  42. data/spec/para_dice/faces/ranged_spec.rb +42 -0
  43. data/spec/para_dice/faces/string_arrayed_spec.rb +41 -0
  44. data/spec/para_dice/numbered_die_spec.rb +19 -0
  45. data/spec/para_dice/poly_bag_spec.rb +9 -0
  46. data/spec/para_dice/results/and_keep_spec.rb +30 -0
  47. data/spec/para_dice/results/array_spec.rb +8 -0
  48. data/spec/para_dice/results/cancel_opposing_spec.rb +18 -0
  49. data/spec/para_dice/results/hash_spec.rb +6 -0
  50. data/spec/para_dice/results/number_spec.rb +44 -0
  51. data/spec/para_dice/results/parse_faces_spec.rb +9 -0
  52. data/spec/para_dice/string_die_spec.rb +14 -0
  53. data/spec/para_dice/utility_spec.rb +82 -0
  54. data/spec/para_dice_spec.rb +10 -0
  55. data/spec/spec_helper.rb +82 -0
  56. data/spec/star_wars_dice_spec.rb +19 -0
  57. metadata +274 -0
@@ -0,0 +1,38 @@
1
+ require 'virtus'
2
+ module ParaDice
3
+ module Faces
4
+ # a module to be included into a ParaDice::Die or similar object.
5
+ # Uses a Range object to provide a faces, and provides special count and
6
+ # random_face options
7
+ module Ranged
8
+ include Virtus.module
9
+
10
+ # @!attribute range
11
+ # return [Range]
12
+ attribute :range, Range, default: :quit_if_no_range
13
+
14
+ # @return [Fixnum] size of range
15
+ def face_count
16
+ range.size
17
+ end
18
+
19
+ # @note this can be an unexpectedly expensive action for large ranges,
20
+ # please use carefully
21
+ # @return [Array<Fixnum>] result of range.to_a
22
+ def faces
23
+ @faces ||= range.to_a
24
+ end
25
+
26
+ # @param [nil,Random] random_face_rng defaults to self.rng
27
+ # @return [Fixnum] random element of range
28
+ def random_face(random_face_rng = rng)
29
+ random_face_rng.rand(range.size) + range.first
30
+ end
31
+
32
+ private
33
+ def quit_if_no_range
34
+ raise ArgumentError.new('no :range entry found in initialization hash')
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ require 'para_dice/die'
2
+ require 'para_dice/faces/ranged'
3
+ module ParaDice
4
+ # a standard die like d6 d10, d20, d100 or exotic like d17, d32, d11-10, d100-999
5
+ class NumberedDie
6
+ include Die
7
+ include Faces::Ranged
8
+
9
+ def self.get(max)
10
+ self.new(name: "d#{max}", range: 1..max)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ require 'virtus' # TODO check on usage later
2
+
3
+ require 'para_dice/bag'
4
+ require 'para_dice/numbered_die'
5
+ require 'para_dice/results/number'
6
+ module ParaDice
7
+ # A ParaDice bag that has all of the standard dice: d6, d10, d10, and that you
8
+ # can add more like d33
9
+ class PolyBag
10
+ # contains all of the dice to start the bag with
11
+ DEFAULT_DICE = [2, 3, 4, 6, 8, 10, 12, 20, 30, 100]
12
+ # the result stack that Bag will pass by default to cups.
13
+ DEFAULT_READERS = [Results::Number]
14
+
15
+ # generate a new poly bag with optional rng
16
+ # @param [nil, Random, #rand] random number generator to pass to all cups
17
+ # (which will in turn pass it to each)
18
+ # @return [PolyBag]
19
+ def self.get(rng = nil)
20
+ a = ParaDice::Bag.new(name: 'Poly Dice',
21
+ rng: rng,
22
+ default_readers: DEFAULT_READERS)
23
+
24
+ DEFAULT_DICE.each do |num|
25
+ die = NumberedDie.get(num)
26
+ a.add_die(die)
27
+ end
28
+ return a
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ module ParaDice
2
+ module Results
3
+ # Results class to implement a roll 5 and keep 3 style of play
4
+ class AndKeep
5
+ # @!attribute [rw] :default_keep
6
+ # @return [Fixnum] the default number of dice to keep
7
+ # @!attribute [rw] :default_low_results
8
+ # @return [Boolean] keep dice off of the low end of the order. false by default
9
+ # @!attribute [rw] :default_sort_by
10
+ # @return [Proc] the sort by block defaults to passing the object through to sort
11
+ DEFAULT_SORT_BY = ->(obj) { obj }
12
+ attr_accessor :default_keep, :default_low_results, :default_sort_by
13
+ # @param [Regex,String] default_keep
14
+ def initialize(default_keep = nil)
15
+ @default_keep = default_keep
16
+ end
17
+
18
+ # @param [Array<#to_s>] faces
19
+ # @param [Regex,String] keep default: default_keep
20
+ # @param [Boolean] low_results if true take the bottom of the order
21
+ # @yield [obj] provides obj to call methods on to determine value for use
22
+ # in Array.order_by. Numbers sort easy, names are trickier
23
+ # @return [Array<String>]
24
+ def resolve(faces, keep = default_keep, low_results = false, &blk)
25
+ results_method = low_results ? :first : :last
26
+ if block_given?
27
+ faces.sort_by(&blk).to_a.send(results_method, keep)
28
+ else
29
+ faces.sort_by(&default_sort_by).to_a.send(results_method, keep)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ module ParaDice
2
+ module Results
3
+ # Results class to output the list of faces. It's a Noop that sets the
4
+ # Api for Results files
5
+ module Array
6
+ # @param [Array] faces
7
+ # @return array of faces passed in
8
+ def self.resolve(faces)
9
+ faces
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ require 'para_dice/utility'
2
+
3
+ module ParaDice
4
+ module Results
5
+ # Results class to have some faces cancel other faces out of the results,
6
+ # can also be used to drop faces entirely by defining them as their own opposite
7
+ class CancelOpposing
8
+ # @!attribute rw default_opposing
9
+ # default to DEFAULT_OPPOSING = []
10
+ # @return [Array<Array>] list of pairs of faces that cancel each other out
11
+ # use [['blank','blank']] as opposing to eliminate blank results
12
+ # use [['good','bad'], ['hero','villain']] to have good and bad, heroes
13
+ # villains to cancel each other out
14
+ attr_accessor :default_opposing
15
+
16
+ # set empty here so it will work if no values passed to it.
17
+ DEFAULT_OPPOSING = []
18
+ # @param [Array<Array>] opposing defaults to DEFAULT_OPPOSING=[]
19
+ def initialize(default_opposing = DEFAULT_OPPOSING)
20
+ @default_opposing = default_opposing
21
+ end
22
+
23
+ # @param [Array<#to_s>] faces
24
+ # @param [Array<Array>] opposing defaults to default_opposing
25
+ # @return [Array<String>]
26
+ def resolve(faces, opposing = default_opposing)
27
+ summary = Utility.to_summary(faces)
28
+ opposing.each do |one_face, another_face|
29
+ one_face = one_face.to_s
30
+ another_face = another_face.to_s
31
+ next unless summary.key?(one_face) && summary.key?(another_face)
32
+ if summary[one_face] == summary[another_face]
33
+ summary.delete(one_face)
34
+ summary.delete(another_face)
35
+ else
36
+ low, high = [one_face, another_face].sort_by { |e| summary[e] }
37
+ summary[high] -= summary[low]
38
+ summary.delete(low)
39
+ end
40
+ end
41
+
42
+ Utility.from_summary(summary)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ require 'para_dice/utility'
2
+ module ParaDice
3
+ module Results
4
+ # Results class to return a hash summary of a dice result
5
+ module Hash
6
+ # @param [Array] faces
7
+ # @return [Hash{Object=>Fixnum}]summary hash
8
+ # @see Utility#to_summary
9
+ def self.resolve(faces)
10
+ Utility.to_summary(faces)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ module ParaDice
2
+ module Results
3
+ # Number is meant to run a single operation across all faces of the results in
4
+ # order to do things like add up all of the results. This class gets inserted
5
+ # as a result responder as opposed to the Module uses. The default is to add all
6
+ # of the elements together and return the result
7
+
8
+ # @note this class could also be used for more complex operations using the
9
+ # reduce with operator and initial values. Blocks seem like another likely
10
+ # refinement. But I couldn't come with a reason to use them yet that didn't
11
+ # seem more confusing than coding a new result resolver
12
+ class Number
13
+ # If not overridden the initial value is 0
14
+ DEFAULT_INITIAL = 0
15
+ # If not overridden the operator is :+
16
+ DEFAULT_OPERATOR = :+
17
+ # @!attribute [rw] default_initial_value
18
+ # @return [Numeric,Obj]
19
+ # @!attribute [rw] default_operator
20
+ # @return [Symbol]
21
+ attr_accessor :default_initial_value, :default_operator
22
+
23
+ # @param [Numeric, Obj] default_operator
24
+ # @param [Symbol] default_operator
25
+ def initialize(initial_val = DEFAULT_INITIAL, default_operator = DEFAULT_OPERATOR)
26
+ @default_initial_value = initial_val
27
+ @default_operator = default_operator
28
+ end
29
+
30
+ # @param [Array<Numeric,Object>] faces
31
+ # @param [Numeric,Object] initial value, defaults to DEFAULT_INITIAL = 0
32
+ # @param [Symbol] operator, defaults to DEFAULT_OPERATOR = :+
33
+ # @return [Numeric, Object] result of inject with faces, initial_value, and operator
34
+ def resolve(faces, initial_value = default_initial_value, operator = default_operator)
35
+ faces.inject(initial_value, operator)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ module ParaDice
2
+ module Results
3
+ # Results class to take faces and split them given a string or regexp
4
+ class SplitFaces
5
+ # @!attribute default_split_token
6
+ # @return [Regexp, String] used by split to separate token into pieces
7
+ attr_accessor :default_split_token
8
+ # when performing the split, use this regexp by default
9
+ DEFAULT_SPLIT_TOKEN = /-|_/
10
+ # @param [Regex,String] split_token defaults to DEFAULT_SPLIT_TOKEN=/-|_/
11
+ def initialize(default_split_token = DEFAULT_SPLIT_TOKEN)
12
+ @default_split_token = default_split_token
13
+ end
14
+
15
+ # @param [Array<#to_s>] faces
16
+ # @param [Regex,String] split_token defaults to default_split_token
17
+ # @return [Array<String>]
18
+ def resolve(faces, split_token = default_split_token)
19
+ faces.map { |f| f.to_s.split(split_token) }.flatten(1)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ require 'para_dice/die'
2
+ require 'para_dice/faces/arrayed'
3
+
4
+ module ParaDice
5
+ # A Simple die using an array of string faces
6
+ # @see ParaDice::Die and ParaDice::Faces::StringArrayed
7
+ class StringDie
8
+ include ParaDice::Die
9
+ include ParaDice::Faces::Arrayed
10
+
11
+ # @!attribute [rw] description
12
+ # @return [String] defaults to a string like 'faces: foo, bar, baz'
13
+ attribute :description, String, default: ->(s, a) { 'faces: ' + s.faces.join(', ') }
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ require 'facets/string/modulize'
2
+ require 'hashie'
3
+
4
+ module ParaDice
5
+ module Utility
6
+ class IndifferentHash < Hash
7
+ include Hashie::Extensions::IndifferentAccess
8
+ end
9
+
10
+ # TODO ADD EXAMPLE
11
+ # @param [Class,Module,#to_s] name
12
+ # @param [Class, Module, #to_s] namespace
13
+ # @return [false] if no name is found at top level or namespace
14
+ # @return [Class,Module] the class or module represented by name
15
+ # and optionally in namespace
16
+ def self.module_from(name, namespace = nil)
17
+ return name if name.is_a? Module
18
+ return false if name.empty?
19
+ self.module_from_top(name) || self.module_from_namespace(name, namespace)
20
+ end
21
+
22
+ # to_summary creates a hash and populates it with uniq elements of the
23
+ # array, and the count of how many there are
24
+ # @param [Array] arr a list to summarize
25
+ # @return [Hash]
26
+ def self.to_summary(arr)
27
+ arr.reduce(Hash.new(0)) { |h, e| h[e] += 1; h }
28
+ end
29
+
30
+ # from_summary creates an array and populates with a number of each key
31
+ # equal to the value for that key
32
+ # TODO ADD EXAMPLE
33
+ # @param [Hash<Object,Fixnum>] summary a hash to inflate a summary into an array
34
+ # @return [Array<Object>]
35
+ def self.from_summary(summary)
36
+ summary.each_pair.map { |e, c| Array.new(c) { e } }.flatten
37
+ end
38
+
39
+ private
40
+ def self.module_from_top(raw_name)
41
+ name = raw_name.to_s.modulize
42
+ return false if name.empty?
43
+ return false unless Kernel.const_defined?(name)
44
+ Kernel.const_get(name)
45
+ end
46
+
47
+ def self.module_from_namespace(raw_name, namespace)
48
+ return false unless namespace
49
+ name = raw_name.to_s.modulize
50
+ return false unless namespace.const_defined?(name)
51
+ namespace.const_get(name)
52
+ end
53
+ end
54
+ end
data/lib/para_dice.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'para_dice/die'
2
+ # A Container Class for the key parts of the library: Bag, Die, Cup, Readers
3
+ module ParaDice
4
+ # @param [Fixnum] max
5
+ # @ return [Die] die with sides 1..max
6
+ def self.numbered_die(max)
7
+ NumberedDie.get(max)
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ require 'para_dice/string_die'
2
+ require 'para_dice/results/cancel_opposing'
3
+ require 'para_dice/results/split_faces'
4
+ require 'para_dice/results/hash'
5
+ require 'para_dice/bag'
6
+ require 'yaml'
7
+
8
+ # A class for representing the game dice from Star Wars: Edge of the Empire,
9
+ # Star Wars: Age of Rebellion, Star Wars: Force and Destiny. Mostly here as
10
+ # an example of how to create a custom dice bag.
11
+ # @note no claim is made for the name StarWarsDice
12
+ class StarWarsDice < ParaDice::Bag
13
+ # yaml file containing dice definitions
14
+ DATA_FILE_NAME = 'data/star_wars_dice.yml'
15
+ # opposite facing to be used by the reader CancelOpposing
16
+ OPPOSING = [
17
+ ['blank', 'blank'],
18
+ ['success', 'failure'],
19
+ ['advantage', 'threat']
20
+ ]
21
+ # The normal reader order for StarWarsDice. First split the face names using
22
+ # '_', then cancel opposing faces, finally return as a hash
23
+ DEFAULT_READERS = [
24
+ ParaDice::Results::SplitFaces.new('_'),
25
+ ParaDice::Results::CancelOpposing.new(OPPOSING),
26
+ ParaDice::Results::Hash
27
+ ]
28
+
29
+ # @!attribute [rw] dice
30
+ # @return [Hash<String,StringDie>]
31
+ attribute :dice, Hash[String => ParaDice::StringDie]
32
+ # @!attribute [rw] default_readers
33
+ # @return [Array<#resolve>]
34
+ attribute :default_readers, Array, default: ->(*a) { DEFAULT_READERS }
35
+
36
+ # load and return a bag of StarWarsDice
37
+ def self.get
38
+ hash = YAML.load_file(DATA_FILE_NAME)
39
+ self.new(name: 'Star Wars Dice', dice: hash)
40
+ end
41
+ end
data/para_dice.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "para_dice"
7
+ spec.version = File.read('VERSION')
8
+ spec.authors = ["Scott M Parrish"]
9
+ spec.email = ["anithri@gmail.com"]
10
+ spec.summary = %q{Game Dev Toolbox.}
11
+ spec.description = %q{A Toolbox to help me build a board game.}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency 'virtus', '~> 1.0.3'
21
+ spec.add_runtime_dependency 'facets', '~> 2.9.3'
22
+ spec.add_runtime_dependency 'hashie', '~> 3.3.1'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.7.6'
25
+ spec.add_development_dependency 'yard', '~> 0.8.7.6'
26
+ spec.add_development_dependency 'rake', '~> 10.3.2'
27
+ spec.add_development_dependency 'rspec', '~> 3.1.0'
28
+ spec.add_development_dependency 'guard-rspec', '~> 4.3.1'
29
+ spec.add_development_dependency 'guard-bundler', '~> 2.0.0'
30
+ spec.add_development_dependency 'version', '~> 1.0.0'
31
+ spec.add_development_dependency 'fuubar', '~> 2.0.0'
32
+
33
+ end
34
+
@@ -0,0 +1,19 @@
1
+ require 'rspec'
2
+ require 'gotham_dice'
3
+
4
+ describe GothamDice do
5
+ subject { described_class.get }
6
+ it { is_expected.to be_a described_class }
7
+ it { expect(subject.dice.keys.count).to eq 6 }
8
+ it { expect(subject.dice.values.all? { |d| d.is_a? ParaDice::StringDie }).to be true }
9
+ it { expect(subject.default_readers.size).to eq(3) }
10
+ it { expect(subject.default_readers[0]).to be_a ParaDice::Results::SplitFaces }
11
+ it { expect(subject.default_readers[1]).to be_a ParaDice::Results::CancelOpposing }
12
+ it { expect(subject.default_readers[2]).to eq ParaDice::Results::Hash }
13
+
14
+ describe '.get_cup' do
15
+ let(:dice) { ['combat', 'combat', 'detect', 'crime', 'move', 'move', 'crime', 'combat'] }
16
+ subject { described_class.get.get_cup(*dice) }
17
+ it { expect(subject.roll!.size).to eq 8 }
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ require 'rspec'
2
+
3
+ require 'para_dice/bag'
4
+ require 'para_dice/numbered_die'
5
+ require 'para_dice/results/number'
6
+ describe ParaDice::Bag do
7
+ let(:d4) { ParaDice::NumberedDie.get(4) }
8
+ let(:d8) { ParaDice::NumberedDie.get(8) }
9
+ let(:d12) { ParaDice::NumberedDie.get(12) }
10
+ let(:bag) do
11
+ bag = described_class.new
12
+ bag.add_die d4
13
+ bag.add_die d12
14
+ bag.add_die d8
15
+ bag.default_readers << ParaDice::Results::Number.new
16
+ bag
17
+ end
18
+
19
+ context 'with all defaults' do
20
+ subject { described_class.new }
21
+ it do
22
+ is_expected.to have_attributes(name: 'Dice Bag',
23
+ dice: {},
24
+ default_readers: [])
25
+ end
26
+ end
27
+
28
+ describe '.add_die(die)' do
29
+ let(:new_die) { ParaDice::NumberedDie.get(20) }
30
+ subject { described_class.new }
31
+ specify { expect(subject.dice).to be_empty }
32
+ it 'adds die to hash' do
33
+ subject.add_die(new_die)
34
+ expect(subject.dice['d20']).to eq new_die
35
+ end
36
+ end
37
+
38
+ describe 'get_dice(*dice_names)' do
39
+ let(:names) { ['d8', 'd8', 'd12'] }
40
+ subject { bag.get_dice(*names) }
41
+ it { expect(subject[0]).to eq d8 }
42
+ it { expect(subject[1]).to eq d8 }
43
+ it { expect(subject[2]).to eq d12 }
44
+ end
45
+
46
+ describe 'get_cup(*dice_names)' do
47
+ let(:names) { ['d8', 'd8', 'd12'] }
48
+ subject { bag.get_cup(*names) }
49
+ it { is_expected.to be_a ParaDice::Cup }
50
+ it { expect(subject.roll).to be_a Fixnum }
51
+ it { expect(subject.roll).to be_between 3, 28 }
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ require 'rspec'
2
+
3
+ require 'para_dice/cup'
4
+ require 'para_dice/results/cancel_opposing'
5
+ require 'para_dice/results/hash'
6
+
7
+ describe ParaDice::Cup do
8
+
9
+ describe 'initialize' do
10
+ context 'with no arguments' do
11
+ subject { described_class.new }
12
+
13
+ it { is_expected.to have_attributes(dice: [], readers: [], rng: Random) }
14
+ end
15
+
16
+ context 'with rng seed' do
17
+ subject { described_class.new(rng: Random.new(111)) }
18
+ it { is_expected.to have_attributes(dice: [], readers: [], rng: Random) }
19
+
20
+ it { expect(subject.rng.seed).to eq 111 }
21
+ end
22
+ end
23
+
24
+ describe '.roll(roll_rng)' do
25
+ let(:fbb) { ParaDice::StringDie.new(name: 'FBB', faces: [:foo, :bar, :baz]) }
26
+
27
+ it 'call roll on all dice...' do
28
+ cup = described_class.new(dice: [fbb, fbb, fbb])
29
+ rng = double('rng')
30
+ allow(rng).to receive(:rand).and_return(2, 0, 2)
31
+ expect(rng).to receive(:rand).exactly(3).times
32
+ cup.roll(rng)
33
+ end
34
+
35
+ it '...return array' do
36
+ cup = described_class.new(dice: [fbb, fbb, fbb])
37
+ rng = double('rng')
38
+ allow(rng).to receive(:rand).and_return(2, 0, 2)
39
+ expect(cup.roll(rng)).to eq ['baz', 'foo', 'baz']
40
+ end
41
+ end
42
+
43
+ describe '.roll' do
44
+ let(:fbb) { ParaDice::StringDie.new(name: 'FBB', faces: [:foo, :bar, :baz]) }
45
+ let(:rng) do
46
+ rng = double('rng')
47
+ allow(rng).to receive(:rand).and_return(2, 0, 2, 1, 2)
48
+ rng
49
+ end
50
+ let(:dice_list) { [fbb, fbb, fbb, fbb, fbb] }
51
+ let(:cup) { described_class.new(dice: dice_list) }
52
+ context 'with no readers' do
53
+ subject { cup.roll(rng) }
54
+ it { is_expected.to eq ['baz', 'foo', 'baz', 'bar', 'baz'] }
55
+ end
56
+ context 'with a hash_reader' do
57
+ subject { cup.roll(rng) }
58
+ let(:readers) { [ParaDice::Results::Hash] }
59
+ let(:cup) { described_class.new(dice: dice_list, readers: readers)
60
+ it { is_expected.to eq('baz' => 3, 'foo' => 1, 'bar' => 1) } }
61
+ end
62
+ context 'with a cancel_opposing reader' do
63
+ subject { cup.roll(rng) }
64
+ let(:cancel_out) { [['foo', 'baz']] }
65
+ let(:readers) do
66
+ [ParaDice::Results::CancelOpposing.new(cancel_out),
67
+ ParaDice::Results::Hash]
68
+ end
69
+ let(:cup) { described_class.new(dice: dice_list, readers: readers) }
70
+ it { is_expected.to eq('baz' => 2, 'bar' => 1) }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ require 'rspec'
2
+ require 'para_dice/die'
3
+ describe ParaDice::Die do
4
+ before(:all) do
5
+ class AbstractDie
6
+ include ParaDice::Die
7
+ end
8
+ end
9
+
10
+ let(:name) { 'fake_die' }
11
+ subject { AbstractDie.new(name: name) }
12
+ it { is_expected.to be_a ParaDice::Die }
13
+ it { is_expected.to have_attributes(name: 'fake_die', description: '') }
14
+ it { expect { subject.random_face }.to raise_error NotImplementedError, /random_face:/ }
15
+ it { expect { subject.faces }.to raise_error NotImplementedError, /faces:/ }
16
+ it { expect { subject.face_count }.to raise_error NotImplementedError, /face_count:/ }
17
+ it 'will raise an error if no id is given to initializer' do
18
+ expect { AbstractDie.new({}) }.to raise_error ArgumentError, /:name/
19
+ end
20
+
21
+ end
@@ -0,0 +1,42 @@
1
+ require 'rspec'
2
+ require 'para_dice/die'
3
+ require 'para_dice/faces/ranged'
4
+
5
+ describe ParaDice::Faces::Ranged do
6
+
7
+ before(:all) do
8
+ class RangedFaceDie
9
+ include ParaDice::Die
10
+ include ParaDice::Faces::Ranged
11
+ end
12
+ end
13
+
14
+ describe 'initialize' do
15
+ let(:name) { 'd10-19' }
16
+ let(:range) { 10..19 }
17
+ subject { RangedFaceDie.new(name: name, range: range) }
18
+ it { is_expected.to be_a ParaDice::Die }
19
+ it do
20
+ is_expected.to have_attributes(faces: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
21
+ name: name,
22
+ range: range,
23
+ face_count: 10)
24
+ end
25
+
26
+ it 'will call rand with size on rng' do
27
+ rng = double('random')
28
+ expect(rng).to receive(:rand).with(10).and_return(2)
29
+ subject.random_face(rng)
30
+ end
31
+
32
+ it 'will return the right value for a give rnd.rand result' do
33
+ rng = double('random')
34
+ allow(rng).to receive(:rand).with(10).and_return 4
35
+ expect(subject.random_face(rng)).to eq 14
36
+ end
37
+
38
+ it 'will raise an error if no range is given to initializer' do
39
+ expect { RangedFaceDie.new(name: 'oops').range }.to raise_error ArgumentError, /:range/
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ require 'rspec'
2
+
3
+ require 'para_dice/faces/arrayed'
4
+
5
+ describe ParaDice::Faces::Arrayed do
6
+
7
+ before(:all) do
8
+ class StringFaceDie
9
+ include ParaDice::Die
10
+ include ParaDice::Faces::Arrayed
11
+ end
12
+ end
13
+
14
+ describe 'initialize' do
15
+ let(:name) { 'FBB' }
16
+ let(:faces) { [:foo, :bar, :baz] }
17
+ subject { StringFaceDie.new(name: name, faces: faces) }
18
+ it { is_expected.to be_a ParaDice::Die }
19
+ it do
20
+ is_expected.to have_attributes(faces: ['foo', 'bar', 'baz'],
21
+ name: name,
22
+ face_count: 3)
23
+ end
24
+
25
+ it 'will call rand with size on rng' do
26
+ rng = double('random')
27
+ expect(rng).to receive(:rand).with(3).and_return(2)
28
+ subject.random_face(rng)
29
+ end
30
+
31
+ it 'will return the right value for a give rnd.rand result' do
32
+ rng = double('random')
33
+ allow(rng).to receive(:rand).with(3).and_return 1
34
+ expect(subject.random_face(rng)).to eq 'bar'
35
+ end
36
+
37
+ it 'will raise an error if no range is given to initializer' do
38
+ expect { StringFaceDie.new(name: 'oops') }.to raise_error ArgumentError, /:faces/
39
+ end
40
+ end
41
+ end