para_dice 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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