madderlib 0.1.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 (40) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +22 -0
  3. data/README.rdoc +173 -0
  4. data/Rakefile +134 -0
  5. data/lib/madderlib.rb +27 -0
  6. data/lib/madderlib/builder.rb +659 -0
  7. data/lib/madderlib/conditional/allowed.rb +144 -0
  8. data/lib/madderlib/conditional/helper.rb +135 -0
  9. data/lib/madderlib/conditional/likely.rb +162 -0
  10. data/lib/madderlib/conditional/recur.rb +103 -0
  11. data/lib/madderlib/conditional/registry.rb +56 -0
  12. data/lib/madderlib/conditional/repeat.rb +130 -0
  13. data/lib/madderlib/context.rb +140 -0
  14. data/lib/madderlib/core.rb +217 -0
  15. data/lib/madderlib/extensions.rb +40 -0
  16. data/lib/madderlib/instruction.rb +171 -0
  17. data/lib/madderlib/phrase.rb +284 -0
  18. data/lib/madderlib/sequencer.rb +337 -0
  19. data/madderlib.gemspec +72 -0
  20. data/spec/benchmark_spec.rb +69 -0
  21. data/spec/builder_spec.rb +321 -0
  22. data/spec/builder_to_other_spec.rb +47 -0
  23. data/spec/builder_to_sequencer_spec.rb +388 -0
  24. data/spec/conditional_allowed_spec.rb +130 -0
  25. data/spec/conditional_helper_spec.rb +131 -0
  26. data/spec/conditional_likely_spec.rb +138 -0
  27. data/spec/conditional_recur_spec.rb +102 -0
  28. data/spec/conditional_registry_spec.rb +94 -0
  29. data/spec/conditional_repeat_spec.rb +88 -0
  30. data/spec/doc_spec.rb +550 -0
  31. data/spec/error_spec.rb +33 -0
  32. data/spec/examples_spec.rb +151 -0
  33. data/spec/extensions_spec.rb +47 -0
  34. data/spec/grammar_spec.rb +101 -0
  35. data/spec/instruction_spec.rb +133 -0
  36. data/spec/kernel_spec.rb +58 -0
  37. data/spec/phrase_spec.rb +7 -0
  38. data/spec/sequencer_spec.rb +317 -0
  39. data/spec/spec_helper.rb +54 -0
  40. metadata +98 -0
@@ -0,0 +1,144 @@
1
+ module MadderLib
2
+ module Conditional
3
+ #= Allowed
4
+ #
5
+ #Introduces support for conditional usage of an Instruction
6
+ module Allowed
7
+
8
+ #= Allowed::Phrase
9
+ #
10
+ #Introduces support for conditional usage of an Instruction
11
+ #
12
+ #If a Phrase has no Instructions that can be used, it will return an empty result and be omitted
13
+ #
14
+ #See: Allowed::Instruction
15
+ module Phrase
16
+ #Adds conditional logic to the current Instruction
17
+ #
18
+ #See: Instruction#assuming
19
+ def assuming(*args, &block)
20
+ self.instruction.assuming *args, &block
21
+ end
22
+ alias :presuming :assuming
23
+ alias :if :assuming
24
+
25
+ #Adds conditional logic to the current Instruction
26
+ #
27
+ #See: Instruction#forbidding
28
+ def forbidding(*args, &block)
29
+ self.instruction.forbidding *args, &block
30
+ end
31
+ alias :unless :forbidding
32
+ end
33
+
34
+
35
+
36
+ #= Allowed::Instruction
37
+ #
38
+ #Introduces support for conditional usage of an Instruction
39
+ #
40
+ #The conditional logic is evaluated for each separate execution of the Builder.
41
+ #If the conditional returns false, then the Instruction is excluded from use.
42
+ #If it returns true, then the Instruction is still a viable candidate (though other constraints may be applied)
43
+ #
44
+ #See: Allowed::Phrase
45
+ module Instruction
46
+ def self.included(target) #:nodoc:
47
+ # register a test to test all allowances for the instruction
48
+ # return false at the first one that fails
49
+ target.add_test do |instruction, context|
50
+ failure = instruction.conditional_allowances.find do |block|
51
+ # first failure stops us
52
+ (! Context.invoke(block, context))
53
+ end
54
+
55
+ failure.nil?
56
+ end
57
+ end
58
+
59
+
60
+
61
+ #The instruction will only be used if these conditions are true
62
+ #
63
+ #The id of a Phrase can be provided.
64
+ #If so, then the condition tested is: the reference must have already been added to the Builder's result.
65
+ #This can be identified through Context#spoken
66
+ #
67
+ #Alternately, a custom block can be provided.
68
+ #The block can either take no arguments, or a Context.
69
+ #The Instruction may only be used when that block returns a true (eg. non-false) value
70
+ #
71
+ #Examples:
72
+ # switch = false
73
+ # builder = madderlib do
74
+ # an(:on).says('on').assuming { switch }
75
+ # an(:off).says('off').if { ! switch }
76
+ # say('bright').if :on
77
+ # say('dark').if :off
78
+ # end
79
+ #
80
+ # builder.sentence.should eql('off dark')
81
+ # switch = true
82
+ # builder.sentence.should eql('on bright')
83
+ def assuming(id=nil, &block)
84
+ if block
85
+ raise Error, 'block AND id provided, requires one or the other' if id
86
+ Context.validate(block)
87
+ else
88
+ # true if the id expressed has been spoken
89
+ block = lambda {|context| context.spoken_ids.include?(id) }
90
+ end
91
+
92
+ # set it aside for a lazy day
93
+ conditional_allowances << block
94
+ self
95
+ end
96
+ alias :presuming :assuming
97
+ alias :if :assuming
98
+
99
+ #The instruction will only be used if these conditions are false.
100
+ #This is the logical opposite of assuming.
101
+ #
102
+ #The id of a Phrase can be provided.
103
+ #If so, then the condition tested is: the reference must not have been added to the Builder's result.
104
+ #This can be identified through Context#spoken
105
+ #
106
+ #Alternately, a custom block can be provided.
107
+ #The block can either take no arguments, or a Context.
108
+ #The Instruction may only be used when that block returns a false value
109
+ #
110
+ #Examples:
111
+ # switch = false
112
+ # builder = madderlib do
113
+ # an(:on).says('on').forbidding { ! switch }
114
+ # an(:off).says('off').unless { switch }
115
+ # say('bright').unless :off
116
+ # say('dark').unless :on
117
+ # end
118
+ #
119
+ # builder.sentence.should eql('off dark')
120
+ # switch = true
121
+ # builder.sentence.should eql('on bright')
122
+ def forbidding(id=nil, &block)
123
+ if block
124
+ raise Error, 'block AND id provided, requires one or the other' if id
125
+ Context.validate(block)
126
+
127
+ self.assuming {|context| ! Context.invoke(block, context) }
128
+ else
129
+ # true unless the id expressed has been spoken
130
+ self.assuming {|context| ! context.spoken_ids.include?(id) }
131
+ end
132
+ end
133
+ alias :unless :forbidding
134
+
135
+
136
+
137
+ def conditional_allowances #:nodoc:
138
+ @conditional_allowances ||= []
139
+ end
140
+ end
141
+
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,135 @@
1
+ module MadderLib
2
+ module Conditional
3
+ module Helper #:nodoc: all
4
+
5
+ class TestBlock
6
+ FALSE = lambda { false }
7
+ ONE = lambda { 1 }
8
+
9
+ attr_reader :criteria
10
+
11
+ def initialize(*args, &block)
12
+ if block
13
+ # if we get a block, use it!
14
+ raise Error, 'block AND args provided, requires one or the other' if (args && (! args.empty?))
15
+ raise Error, 'block arity should be 0; 1 (count) or; 2 (count, Context)' if (block.arity > 2)
16
+
17
+ # it will remain unchanging
18
+ @criteria = block
19
+ else
20
+ # leave the originals alone
21
+ args = args.clone
22
+
23
+ begin
24
+ # how does it start?
25
+ arg = args.shift
26
+
27
+ if Range === arg
28
+ # we received a Range
29
+ @criteria = arg
30
+ elsif arg.respond_to?(:integer?) && arg.integer?
31
+ upper = args.first
32
+ if upper && upper.respond_to?(:integer?) && upper.integer?
33
+ # we can make a Range from that
34
+ @criteria = Range.new(arg, args.shift)
35
+ else
36
+ # just a count
37
+ @criteria = arg
38
+ end
39
+ else
40
+ raise Error, "invalid test block argument : #{arg.inspect}"
41
+ end
42
+ rescue Error => e
43
+ raise e
44
+ rescue Exception => e
45
+ # wrap
46
+ raise e
47
+ raise Error.new("invalid test block argument : #{arg.inspect}", e)
48
+ end
49
+
50
+ # beyond that, is there a unit?
51
+ # we deal with all of that during build
52
+ @units = args.shift
53
+ end
54
+ end
55
+
56
+
57
+
58
+ def block
59
+ if Proc === @criteria
60
+ # the block will do its own testing
61
+ @criteria
62
+ elsif Range === @criteria
63
+ # we'll stop somewhere in that range
64
+ # note that it's inclusive
65
+ # if the caller says '2 .. 4', 4 should be a possibility
66
+ limit = unitize(@criteria.rand_inclusive)
67
+ lambda {|count| count < limit }
68
+ elsif @criteria.integer?
69
+ limit = unitize(@criteria)
70
+ lambda {|count| count < limit }
71
+ else
72
+ # never will succeed
73
+ FALSE
74
+ end
75
+ end
76
+
77
+ def invoke(*args, &given_block)
78
+ # take in a block, or use the internally held one
79
+ # allows for external buffering
80
+ b = block_given? ? given_block : self.block
81
+ p = args
82
+
83
+ if b.arity < 1
84
+ # how interesting! arity of -1
85
+ b.call
86
+ else
87
+ # only as many args as supported
88
+ # do not worry about shortfall; that'll be a failure
89
+ p.pop while b.arity < p.size
90
+ b.call *p
91
+ end
92
+ end
93
+
94
+
95
+
96
+ def to_i(context)
97
+ value = nil
98
+
99
+ if Proc === @criteria
100
+ value = MadderLib::Context.invoke(@criteria, context)
101
+ elsif Range === @criteria
102
+ value = @criteria.max
103
+ elsif @criteria.integer?
104
+ value = @criteria
105
+ end
106
+
107
+ # has to be an integer, by definition
108
+ if value && value.respond_to?(:integer?) && value.integer?
109
+ unitize(value)
110
+ else
111
+ nil
112
+ end
113
+ end
114
+
115
+
116
+
117
+ # - - - - -
118
+ protected
119
+
120
+ def unitize(limit)
121
+ case @units
122
+ when :minutes, :minute
123
+ # no diff, for now
124
+ limit
125
+ when :times, :time
126
+ limit
127
+ else
128
+ limit
129
+ end
130
+ end
131
+ end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,162 @@
1
+ module MadderLib
2
+ module Conditional
3
+ #= Likely
4
+ #
5
+ #Introduces support for proportional selection from multiple Instructions in a given Phrase
6
+ module Likely
7
+ #The default weight for an Instruction, which is 1
8
+ DEFAULT_WEIGHT = 1
9
+
10
+ #= Likely::Phrase
11
+ #
12
+ #Introduces support for proportional selection from multiple Instructions in a given Phrase
13
+ #
14
+ #This is a very fancy way of saying 'weighted choices'.
15
+ #Using Phrase#alternately, multiple Instructions can be added to the same Phrase.
16
+ #Each one will either use the DEFAULT_WEIGHT, if not otherwise specified, or:
17
+ #
18
+ #* a numeric value
19
+ #* a Proc / lambda / block / closure which returns a numeric value
20
+ #
21
+ #The weights for all Instructions are totalled prior to each execution of the Builder.
22
+ #A random weight is chosen, and that defines the Instruction to be used
23
+ #
24
+ #See: Likely::Instruction
25
+ module Phrase
26
+ def self.included(target) #:nodoc:
27
+ # before each run, we need to prepare ourself
28
+ target.add_prepare do |phrase, context|
29
+ # no point in likelihood when there's only one choice
30
+ unless phrase.instructions.size < 2
31
+ weights = []
32
+ phrase.instructions.each do |instruction|
33
+ # put on a default weight if no other option
34
+ while (tester = instruction.conditional_likely_tester).nil?
35
+ instruction.likely(DEFAULT_WEIGHT)
36
+ end
37
+
38
+ weight = tester.to_i(context)
39
+ raise Error, 'invalid weight for instruction : #{instruction.words}' unless weight
40
+ weights << weight
41
+ end
42
+
43
+ # easy distributions
44
+ total = 0
45
+ Range.new(0, weights.size - 1).each do |index|
46
+ weight, instruction = weights[index], phrase.instructions[index]
47
+
48
+ state = context.state(instruction)
49
+ state[:likely_lower] = total
50
+ state[:likely_upper] = (total += weight)
51
+ end
52
+
53
+ # choose a random value
54
+ state = context.state(phrase)
55
+ state[:likely_total] = total
56
+ state[:likely_count] = rand(total) # Range.new(0, total).rand_inclusive
57
+ end
58
+ end
59
+ end
60
+
61
+
62
+
63
+ #Adds proportional logic to the current Instruction
64
+ #
65
+ #See: Instruction#likely
66
+ def likely(*args, &block)
67
+ self.instruction.likely *args, &block
68
+ end
69
+ alias :weight :likely
70
+ alias :weighted :likely
71
+ alias :weighing :likely
72
+ alias :odds :likely
73
+ end
74
+
75
+
76
+
77
+ #= Likely::Instruction
78
+ #
79
+ #Introduces support for proportional selection from multiple Instructions in a given Phrase
80
+ #
81
+ #See: Likely::Phrase
82
+ module Instruction
83
+ def self.included(target) #:nodoc:
84
+ # register a test to test all allowances for the instruction
85
+ # return false at the first one that fails
86
+ target.add_test do |instruction, context|
87
+ phrase = instruction.phrase
88
+ test = true
89
+
90
+ state = context.state(phrase)
91
+ total = state[:likely_total]
92
+ count = state[:likely_count]
93
+
94
+ # will only have a count if there's likelihood calc required
95
+ if count
96
+ state = context.state(instruction)
97
+ lower = state[:likely_lower]
98
+ upper = state[:likely_upper]
99
+
100
+ test = (count >= lower) && (count < upper)
101
+
102
+ if test && phrase.respond_to?(:recurs?) && phrase.recurs?
103
+ # set it up for the next recurrance
104
+ # we don't want the same thing over and over again
105
+ state[:likely_count] = rand(total)
106
+ end
107
+ end
108
+
109
+ test
110
+ end
111
+ end
112
+
113
+
114
+
115
+ #Specifies the likelihood of this Instruction being used, compared to its siblings in their Phrase
116
+ #
117
+ #If provided, the arguments should contain:
118
+ #* a numeric value, which becomes the weight
119
+ #* a Range, or two numerics (which define a Range), from which the weight is chosen randomly
120
+ #* a Proc / lambda / block / closure, which returns a numeric value. \
121
+ #The block can either take no arguments, or a Context.
122
+ #
123
+ #See: Instruction#alternately
124
+ #
125
+ #Examples:
126
+ # builder = madderlib do
127
+ # say('parsley').likely(4)
128
+ # alternately(3).say('sage')
129
+ # alternately.say('rosemary').weighted(2).or.say('thyme')
130
+ # end
131
+ #
132
+ # usage = {}
133
+ # 60.times do
134
+ # key = builder.sentence
135
+ # usage[key] = (usage[key] || 0) + 1
136
+ # end
137
+ #
138
+ # # if proportions were accurately reproducible:
139
+ # # usage['parsley'].should eql(20)
140
+ # # usage['sage'].should eql(15)
141
+ # # usage['rosemary'].should eql(10)
142
+ # # usage['thyme'].should eql(5)
143
+ def likely(*args, &block)
144
+ # build a tester, set it aside
145
+ @likely_tester = Helper::TestBlock.new *args, &block
146
+ self
147
+ end
148
+ alias :weight :likely
149
+ alias :weighted :likely
150
+ alias :weighing :likely
151
+ alias :odds :likely
152
+
153
+
154
+
155
+ def conditional_likely_tester #:nodoc:
156
+ @likely_tester
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,103 @@
1
+ module MadderLib
2
+ module Conditional
3
+ #= Recur
4
+ #
5
+ #Introduces support for recurrant usage of a Phrase
6
+ module Recur
7
+
8
+ #= Recur::Phrase
9
+ #
10
+ #Introduces support for recurrant usage of a Phrase.
11
+ #This is particularly useful for an AnywherePhrase, whose position is randomly chosen.
12
+ #If it has a recurrence, it may appear multiple times.
13
+ #All of this is independent of whatever Instructions are contained within the Phrase
14
+ module Phrase
15
+ def self.included(target) #:nodoc:
16
+ # before each run, we need to prepare ourself
17
+ target.add_prepare do |phrase, context|
18
+ unless phrase.conditional_recur_tester
19
+ # we'll only run once if not told otherwise
20
+ phrase.recur 1
21
+ end
22
+
23
+ # we must retain state (the number of times called)
24
+ # and a consistent block for testing
25
+ state = context.state(phrase)
26
+ state[:recur_block] = phrase.conditional_recur_tester.block
27
+ state[:recur_count] = 0
28
+ end
29
+
30
+ # register a test for recurrance
31
+ target.add_test do |phrase, context|
32
+ # where we at now
33
+ state = context.state(phrase)
34
+ block = state[:recur_block]
35
+ count = state[:recur_count]
36
+ state[:recur_count] = count + 1
37
+
38
+ # the block call returns the result of our test
39
+ # we buffered the block, but want to invoke it conveniently
40
+ phrase.conditional_recur_tester.invoke(count, context, &block)
41
+ end
42
+ end
43
+
44
+
45
+
46
+ #Specifies the recurrance of this Phrase
47
+ #
48
+ #If provided, the arguments should contain:
49
+ #* a numeric value, which becomes the count
50
+ #* a Range, or two numerics (which define a Range), from which the count is chosen randomly
51
+ #* a Proc / lambda / block / closure, which returns false when the recurrance should stop. \
52
+ #The block can either take (a) no arguments, (b) the recurrance count, or; (c) the count <i>and</i> a Context.
53
+ #
54
+ #A recurrance count of 0 will exclude the Phrase from the Builder result
55
+ #
56
+ #A recurrance always ends when any Instruction returns an empty set of words.
57
+ #Processing will skip to the next Phrase, even if more recurrances are available
58
+ #
59
+ #Examples:
60
+ # builder = madderlib do
61
+ # say(:start)
62
+ # say(:end)
63
+ # anytime.recurring(2).say(:any)
64
+ # anytime.recurring {|count| count < 2 }.say(:also)
65
+ # end
66
+ #
67
+ # words = builder.words
68
+ # words.find_all {|word| word == 'any' }.should have(2).items
69
+ # words.find_all {|word| word == 'also' }.should have(2).items
70
+ def recur(*args, &block)
71
+ # build a tester, set it aside
72
+ @recur_tester = Helper::TestBlock.new *args, &block
73
+ self
74
+ end
75
+ alias :recurs :recur
76
+ alias :recurring :recur
77
+
78
+ #Returns true if the Phrase can recur.
79
+ #By default, the recurrance count is 1, in which case this method returns false
80
+ def recurs?(context=MadderLib::Context::EMPTY)
81
+ !! (conditional_recur_tester && (conditional_recur_tester.to_i(context) > 1))
82
+ end
83
+
84
+
85
+
86
+ def conditional_recur_tester #:nodoc:
87
+ @recur_tester
88
+ end
89
+ end
90
+
91
+
92
+
93
+ module Instruction #:nodoc:
94
+ # not impacted
95
+ # the Phrase repeats, not the Instruction
96
+ # we could proxy the recur back to the active phrase
97
+ # but that would be confusing
98
+ # you should only set up recurrence once; it's singleton to the phrase
99
+ end
100
+
101
+ end
102
+ end
103
+ end