l_system 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 586b85b2e6a2981b1d5835eedaf62c451eaa51b4cdd7894db564e100ada8c9e0
4
+ data.tar.gz: efe77e8d01d01759ee5780d9d28bc77c8f0fb355d5f98956ef1304797ded02bb
5
+ SHA512:
6
+ metadata.gz: 44879d5a4397c4ab0899685fbd33d0b7df1d64c73596cc45cbc39801ae5bc1ec9343435b75bc0170c0cd3d6c6d54dc1b191f5622d2ce26695a9b9187d31438cc
7
+ data.tar.gz: b8778fe072f57c36eb92a547b38fc594cb2b1619f7d160395a5700c7c2bfc00cf853df9f9b40fe642d60fcdb37adeff9da3607e74793798181bd6697b75a2e79
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ Y�Sn��㔎}��{'�Q6R
2
+ e����?8t��.u��~?<hr��M�,U�>��F�^_�طQ3@v�PU�C��c�T~��Va�tq��5X�����~�Fe9�p�K��\8��H�?�-v1�YţZP��;:x��mY��_��}���v���Z2�vi$E�5�$sKi�H�O�)���f�"��^��6WSu�p��mr�q!�9�1]�EEV�$���)bT\��C����~EV�c�,�I��8":ȿD�LeX�;� kx�z������A5h�F�_H�O��<6|�'ӟ�̥���P�1�ɳ�L.�
data/.simplecov ADDED
@@ -0,0 +1,9 @@
1
+ # Simplecov config
2
+
3
+ SimpleCov.start do
4
+ add_filter 'spec'
5
+ add_filter 'integration'
6
+ add_group "Needing tests" do |file|
7
+ file.covered_percent < 90
8
+ end
9
+ end
data/History.md ADDED
@@ -0,0 +1,8 @@
1
+ # Release History for l_system
2
+
3
+ ---
4
+
5
+ ## v0.1.0 [2020-02-26] Michael Granger <ged@faeriemud.org>
6
+
7
+ Initial release.
8
+
data/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # LSystem
2
+
3
+ home
4
+ : https://hg.sr.ht/~ged/LSystem
5
+
6
+ code
7
+ : https://hg.sr.ht/~ged/LSystem
8
+
9
+ github
10
+ : https://github.com/ged/l_system
11
+
12
+ docs
13
+ : https://deveiate.org/code/l_system
14
+
15
+
16
+ ## Description
17
+
18
+ A toolkit for creating and using [Lindenmayer Systems][l-system] (L-systems).
19
+ It consists of a class that allows for declaration of the L-system's grammar,
20
+ and another class that allows for the definition of how the symbols output by a
21
+ grammar should be translated into work.
22
+
23
+
24
+ ### Examples
25
+
26
+ Most of these were stolen from [the examples on Wikipedia][l-system], and can be found in the `examples/` directory of the source.
27
+
28
+ #### Algae
29
+
30
+ Lindenmayer's original L-system for modelling the growth of algae.
31
+
32
+ * variables : A B
33
+ * constants : none
34
+ * axiom : A
35
+ * rules : (A → AB), (B → A)
36
+
37
+ Declare the rules:
38
+
39
+ algae = LSystem.declare do
40
+
41
+ variables :A, :B
42
+ axiom 'A'
43
+ rules 'A -> AB',
44
+ 'B -> A'
45
+
46
+ end
47
+
48
+ then iterate it 8 times and print out each generation:
49
+
50
+ iter = algae.each
51
+ 8.times do |i|
52
+ puts "n = %d : %s" % [ i, iter.next ]
53
+ end
54
+
55
+ This outputs:
56
+
57
+ n = 0 : A
58
+ n = 1 : AB
59
+ n = 2 : ABA
60
+ n = 3 : ABAAB
61
+ n = 4 : ABAABABA
62
+ n = 5 : ABAABABAABAAB
63
+ n = 6 : ABAABABAABAABABAABABA
64
+ n = 7 : ABAABABAABAABABAABABAABAABABAABAAB
65
+
66
+
67
+ You can also do more complex tasks with the symbols in each generation. For example, to generate pretty pictures.
68
+
69
+ #### Barnsley fern
70
+
71
+ * variables : X F
72
+ * constants : + − [ ]
73
+ * axiom : X
74
+ * rules : (X → F+[[X]-X]-F[-FX]+X), (F → FF)
75
+ * angle : 25°
76
+
77
+ Declare the rules:
78
+
79
+ fern = LSystem.declare do
80
+
81
+ variables 'X', 'F'
82
+ constants '+', '-', '[', ']'
83
+ axiom 'X'
84
+ rules \
85
+ 'X → F+[[X]-X]-F[-FX]+X',
86
+ 'F → FF'
87
+
88
+ end
89
+
90
+ Then hook them up to a Logo interpreter ([a monkeypatched version][tortoise-patch] of the [Tortoise gem][tortoise-gem]):
91
+
92
+ LSystem.run( fern, 8 ) do
93
+
94
+ # The size of the canvas to draw on
95
+ CANVAS_SIZE = 2000
96
+
97
+ # F: draw forward
98
+ # X : no-op
99
+ # -: turn left 25°
100
+ # +: turn right 25°
101
+ # [: push position and angle
102
+ # ]: pop position and angle
103
+ production_map \
104
+ 'F' => :forward,
105
+ '-' => :turn_left,
106
+ '+' => :turn_right,
107
+ '[' => :save_pos_and_angle,
108
+ ']' => :restore_pos_and_angle
109
+
110
+ ### Set up some instance variables
111
+ def initialize( * )
112
+ super
113
+ @turtle = nil
114
+ @positions = []
115
+ end
116
+
117
+
118
+ on_start do |i, _|
119
+ @turtle = Tortoise::Interpreter.new( CANVAS_SIZE )
120
+ @turtle.setpos( CANVAS_SIZE / 2, 0 )
121
+ @turtle.direction = 90
122
+ @turtle.pd
123
+ end
124
+
125
+
126
+ on_finish do |i, _|
127
+ File.open( "fern_gn#{i}.png", File::WRONLY|File::TRUNC|File::CREAT, 0644, encoding: 'binary' ) do |fh|
128
+ fh.write( @turtle.to_png )
129
+ end
130
+ end
131
+
132
+
133
+ ### Draw a line forward
134
+ def forward
135
+ @turtle.fd( 5 )
136
+ end
137
+
138
+
139
+ ### Turn 25° to the left
140
+ def turn_left
141
+ @turtle.lt( 25 )
142
+ end
143
+
144
+
145
+ ### Turn 25° to the right
146
+ def turn_right
147
+ @turtle.rt( 25 )
148
+ end
149
+
150
+
151
+ ### Save the drawing position and angle
152
+ def save_pos_and_angle
153
+ @positions.push([ @turtle.position, @turtle.direction ])
154
+ end
155
+
156
+
157
+ ### Restore the next saved position and angle
158
+ def restore_pos_and_angle
159
+ to_restore = @positions.pop or raise IndexError, "Position stack underflow"
160
+
161
+ @turtle.setpos( *to_restore.first )
162
+ @turtle.direction = to_restore.last
163
+ end
164
+
165
+ end
166
+
167
+ This generates a sequence of images, which for generation 8 yields:
168
+
169
+ ![Fern Gen 7](examples/fern_gn7.png)
170
+
171
+
172
+ ## Prerequisites
173
+
174
+ * Ruby
175
+
176
+
177
+ ## Installation
178
+
179
+ $ gem install l_system
180
+
181
+
182
+ ## Contributing
183
+
184
+ You can check out the current development source with Mercurial via its
185
+ [project page](https://hg.sr.ht/~ged/LSystem). Or if you prefer Git, via
186
+ [its Github mirror](https://github.com/ged/l_system).
187
+
188
+ After checking out the source, run:
189
+
190
+ $ rake setup
191
+
192
+ This task will install dependencies, and do any other necessary setup for development.
193
+
194
+
195
+ ## Authors
196
+
197
+ - Michael Granger <ged@faeriemud.org>
198
+
199
+
200
+ ## License
201
+
202
+ Copyright (c) 2020, Michael Granger
203
+ All rights reserved.
204
+
205
+ Redistribution and use in source and binary forms, with or without
206
+ modification, are permitted provided that the following conditions are met:
207
+
208
+ * Redistributions of source code must retain the above copyright notice,
209
+ this list of conditions and the following disclaimer.
210
+
211
+ * Redistributions in binary form must reproduce the above copyright notice,
212
+ this list of conditions and the following disclaimer in the documentation
213
+ and/or other materials provided with the distribution.
214
+
215
+ * Neither the name of the author/s, nor the names of the project's
216
+ contributors may be used to endorse or promote products derived from this
217
+ software without specific prior written permission.
218
+
219
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
220
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
221
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
222
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
223
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
224
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
225
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
226
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
227
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
228
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
229
+
230
+
231
+ [l-system]: https://en.wikipedia.org/wiki/L-system
232
+
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby -S rake
2
+
3
+ require 'rake'
4
+ require 'rake/deveiate'
5
+
6
+ EXAMPLE_IMAGES = Rake::FileList[ 'examples/*.png' ]
7
+
8
+
9
+ Rake::DevEiate.setup( 'l_system' ) do |project|
10
+ project.publish_to = 'deveiate:/usr/local/www/public/code'
11
+ end
12
+
13
+
14
+ file EXAMPLE_IMAGES
15
+
16
+ task :docs => EXAMPLE_IMAGES do
17
+ mkdir_p 'docs/examples/'
18
+ cp EXAMPLE_IMAGES.to_a, 'docs/examples/'
19
+ end
20
+
21
+
Binary file
@@ -0,0 +1,147 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'loggability'
5
+
6
+ require 'l_system' unless defined?( LSystem )
7
+
8
+
9
+ # An adapter that connects method calls to an LSystem::RulesEngine's
10
+ # generations.
11
+ class LSystem::ProductionAdapter
12
+ extend Loggability
13
+
14
+
15
+ # Loggability API -- log to the l_system logger
16
+ log_to :l_system
17
+
18
+
19
+ ### Inheritance callback -- add class-instance variables to the +subclass+.
20
+ def self::inherited( subclass )
21
+ super
22
+
23
+ subclass.instance_variable_set( :@production_map, {} )
24
+ subclass.instance_variable_set( :@callbacks, {} )
25
+ end
26
+
27
+
28
+ #
29
+ # DSL Methods
30
+ #
31
+
32
+ ### Declare a Hash of symbols to methods that should be called when one appears
33
+ ### in a generation.
34
+ def self::production_map( productions={} )
35
+ unless productions.empty?
36
+ self.production_map = productions
37
+ end
38
+
39
+ return @production_map
40
+ end
41
+
42
+
43
+ ### Set the Hash of symbols to methods that should be called when one appears in
44
+ ### a generation.
45
+ def self::production_map=( new_map )
46
+ @production_map.replace( new_map )
47
+ end
48
+
49
+
50
+ ### Register a callback that will be called at the start of each new generation.
51
+ ### It will be called with the result of the last generation, or +nil+ if this
52
+ ### is the 0th (axiom) generation.
53
+ def self::on_start( &block )
54
+ self.log.debug "Declaring on_start callback: %p" % [ block ]
55
+ define_method( :on_start, &block )
56
+ end
57
+
58
+
59
+ ### Register a callback that will be called to obtain the result of running a
60
+ ### generation. The result of calling the +block+ will be passed to this
61
+ ### generation's #on_finish callback (if there is one), the next generation's
62
+ ### #on_start callback (if there is one and there's successive geneation), and
63
+ ### returned from #run if this was the last generation.
64
+ def self::result( &block )
65
+ self.log.debug "Declaring result callback: %p" % [ block ]
66
+ define_method( :result, &block )
67
+ end
68
+
69
+
70
+ ### Register a callback that will be called at the end of each new generation.
71
+ ### It will be called with the result of the this generation, which will be
72
+ ### +nil+ if no #result callback is declared.
73
+ def self::on_finish( &block )
74
+ self.log.debug "Declaring on_finish callback: %p" % [ block ]
75
+ define_method( :on_finish, &block )
76
+ end
77
+
78
+
79
+ #
80
+ # Instance methods
81
+ #
82
+
83
+ ### Create a new instance of the ProductionAdapter.
84
+ def initialize
85
+ @production_map = self.class.production_map
86
+ end
87
+
88
+
89
+ ######
90
+ public
91
+ ######
92
+
93
+ ##
94
+ # The map of symbols to production methods
95
+ attr_reader :production_map
96
+
97
+
98
+ ### Run productions for each generation produced by the given +rules_engine+ up to
99
+ ### +iterations+ times.
100
+ def run( rules_engine, iterations )
101
+ self.log.debug "Running %p for up to %d iterations" % [ rules_engine, iterations ]
102
+
103
+ return rules_engine.each.with_index.inject( nil ) do |result, (generation, i)|
104
+ self.log.debug "Running generation %d" % [ i ]
105
+ self.on_start( i, result ) if self.respond_to?( :on_start )
106
+ self.run_generation( generation )
107
+ result = self.result( i ) if self.respond_to?( :result )
108
+ self.log.debug "Result [%d] is: %p" % [ i, result ]
109
+ self.on_finish( i, result ) if self.respond_to?( :on_finish )
110
+
111
+ break result if i >= iterations - 1
112
+ result
113
+ end
114
+ end
115
+
116
+
117
+ ### Run the specified +generation+ by calling productions for each of its
118
+ ### symbols.
119
+ def run_generation( generation )
120
+
121
+ # Make a new one every time to support self-mutating adapters
122
+ dispatch_table = self.make_dispatch_table
123
+
124
+ generation.each_char do |symbol|
125
+ callback = dispatch_table[ symbol ]
126
+
127
+ unless callback
128
+ self.log.warn "No production for symbol %p" % [ symbol ]
129
+ next
130
+ end
131
+
132
+ self.log.debug "%p -> %p" % [ symbol, callback ]
133
+ callback.call
134
+ end
135
+ end
136
+
137
+
138
+ ### Return a Hash of symbols to bound Methods to call for their productions from
139
+ ### the current #production_map.
140
+ def make_dispatch_table
141
+ return self.class.production_map.each_with_object( {} ) do |(symbol, method_name), hash|
142
+ hash[ symbol ] = self.method( method_name )
143
+ end
144
+ end
145
+
146
+ end # class LSystem::ProductionAdapter
147
+
@@ -0,0 +1,185 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'set'
5
+ require 'loggability'
6
+
7
+ require 'l_system' unless defined?( LSystem )
8
+
9
+
10
+ # An engine for iterating over successive applications of the L-System's
11
+ # ruleset to its axiom.
12
+ class LSystem::RulesEngine
13
+ extend Loggability
14
+
15
+
16
+ # Loggability API -- log to the l_system logger
17
+ log_to :l_system
18
+
19
+
20
+ ### Create a new rules engine for an L-System. If the +block+ is present,
21
+ ### it is called with the new instance as +self+.
22
+ def initialize( &block )
23
+ @variables = Set.new
24
+ @constants = Set.new
25
+ @axiom = nil
26
+ @rules = []
27
+
28
+ @rules_as_hash = nil
29
+
30
+ self.instance_eval( &block ) if block
31
+ end
32
+
33
+
34
+ ######
35
+ public
36
+ ######
37
+
38
+ #
39
+ # DSL Methods
40
+ #
41
+
42
+ ### Get/set the system's variables (the replaceable parts of its alphabet).
43
+ def variables( *new_values )
44
+ self.variables = new_values unless new_values.empty?
45
+ return @variables.dup
46
+ end
47
+
48
+
49
+ ### Set the systems variables to +new_values+.
50
+ def variables=( new_values )
51
+ @rules_as_hash = nil
52
+
53
+ new_values = Set.new( new_values, &:to_s )
54
+ unless new_values.disjoint?( self.constants )
55
+ common_char = (new_values & self.constants).to_a.first
56
+ raise ArgumentError, "%p is already included in the constant set" % [ common_char ]
57
+ end
58
+
59
+ @variables = new_values
60
+ end
61
+
62
+
63
+ ### Get/set the system's constants (the static parts of its alphabet).
64
+ def constants( *new_values )
65
+ self.constants = new_values unless new_values.empty?
66
+ return @constants.dup
67
+ end
68
+
69
+
70
+ ### Set the systems constants to +new_values+.
71
+ def constants=( new_values )
72
+ @rules_as_hash = nil
73
+
74
+ new_values = Set.new( new_values, &:to_s )
75
+ unless new_values.disjoint?( self.variables )
76
+ common_char = (new_values & self.variables).to_a.first
77
+ raise ArgumentError, "%p is already included in the variable set" % [ common_char ]
78
+ end
79
+
80
+ @constants = new_values
81
+ end
82
+
83
+
84
+ ### Return the system's variables and constants.
85
+ def alphabet
86
+ return @variables | @constants
87
+ end
88
+
89
+
90
+ ### Get/set the axiom of the system.
91
+ def axiom( new_value=nil )
92
+ self.axiom = new_value if new_value
93
+ return @axiom
94
+ end
95
+
96
+
97
+ ### Set the axiom of the system.
98
+ def axiom=( new_value )
99
+ @axiom = new_value
100
+ end
101
+
102
+
103
+ ### Get/set the system's rules.
104
+ def rules( *new_values )
105
+ self.rules = new_values unless new_values.empty?
106
+ return @rules
107
+ end
108
+
109
+
110
+ ### Set the system's rules.
111
+ def rules=( new_values )
112
+ @rules_as_hash = nil
113
+ @rules = Array( new_values ).map( &:to_s )
114
+ end
115
+
116
+
117
+ #
118
+ # Iteration API
119
+ #
120
+
121
+ ### Apply the system's rules to the given +state+ and return the result.
122
+ def apply_rules( state )
123
+ rules_hash = self.rules_as_hash
124
+ return state.each_char.with_object( String.new(encoding: 'utf-8') ) do |char, new_state|
125
+ new_state << rules_hash[ char ]
126
+ end
127
+ end
128
+
129
+
130
+ ### Yield each successive generation to the given +block+, or return an
131
+ ### Enumerator that will do so if no block is given.
132
+ def each( &block )
133
+ iter = Enumerator.new do |yielder|
134
+ state = new_state = self.axiom.dup
135
+
136
+ begin
137
+ yielder.yield( new_state )
138
+ state = new_state
139
+ new_state = self.apply_rules( state )
140
+ end until state == new_state
141
+ end
142
+
143
+ return iter unless block
144
+ return iter.each( &block )
145
+ end
146
+
147
+
148
+ #########
149
+ protected
150
+ #########
151
+
152
+ ### Return a Hash of tranforms that should be applied to a state during a
153
+ ### generation.
154
+ def rules_as_hash
155
+ unless @rules_as_hash
156
+ @rules_as_hash = self.rules.each_with_object( {} ) do |rule, hash|
157
+ pred, succ = self.parse_rule( rule )
158
+ hash[ pred ] = succ
159
+ end
160
+
161
+ self.alphabet.each do |char|
162
+ @rules_as_hash[ char ] = char unless @rules_as_hash.key?( char )
163
+ end
164
+ end
165
+
166
+ return @rules_as_hash
167
+ end
168
+
169
+
170
+ ### Return a tuple of the predecessor and successor of the given +rule+.
171
+ def parse_rule( rule )
172
+ predecessor, successor = rule.strip.split( /\s*(?:->|→)\s*/, 2 )
173
+ self.log.debug "Parsed rule: %p -> %p" % [ predecessor, successor ]
174
+ successor_set = Set.new( successor.chars )
175
+
176
+ raise "Invalid rule: predecessor %p is not in the variable set %p" %
177
+ [ predecessor, self.variables ] unless self.variables.include?( predecessor )
178
+ raise "Invalid rule: successor %p contains characters not in the alphabet %p" %
179
+ [ successor, self.alphabet ] unless self.alphabet.superset?( successor_set )
180
+
181
+ return predecessor, successor
182
+ end
183
+
184
+ end # class LSystem::RulesEngine
185
+
data/lib/l_system.rb ADDED
@@ -0,0 +1,43 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'loggability'
5
+
6
+
7
+ # A toolkit for creating and using Lindenmayer Systems.
8
+ module LSystem
9
+ extend Loggability
10
+
11
+ # Package version
12
+ VERSION = '0.1.0'
13
+
14
+ # Version control revision
15
+ REVISION = %q$Revision$
16
+
17
+
18
+ # Loggability API -- set up a logger for l_system
19
+ log_as :l_system
20
+
21
+
22
+ autoload :RulesEngine, 'l_system/rules_engine'
23
+ autoload :ProductionAdapter, 'l_system/production_adapter'
24
+
25
+
26
+ ### Declare a new L-System that is configured via the given +block+.
27
+ def self::declare( &block )
28
+ return LSystem::RulesEngine.new( &block )
29
+ end
30
+
31
+
32
+ ### Run a +rules_engine+ at most the given number of +iterations+ using the productions
33
+ ### declared in the +block+, which runs in the context of an anonymous
34
+ ### LSystem::ProductionAdapter class.
35
+ def self::run( rules_engine, iterations=1000, &block )
36
+ raise LocalJumpError, "no block given" unless block
37
+
38
+ adapter = Class.new( LSystem::ProductionAdapter, &block )
39
+ return adapter.new.run( rules_engine, iterations )
40
+ end
41
+
42
+ end # module LSystem
43
+
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'l_system/production_adapter'
6
+
7
+
8
+ RSpec.describe( LSystem::ProductionAdapter ) do
9
+
10
+ let( :algae_rules ) do
11
+ LSystem.declare do
12
+ variables :A, :B
13
+ axiom 'A'
14
+ rules \
15
+ 'A -> AB',
16
+ 'B -> A'
17
+ end
18
+ end
19
+
20
+
21
+ describe "subclasses" do
22
+
23
+ let( :subclass ) do
24
+ Class.new( described_class ) do
25
+
26
+ def initialize
27
+ @calls = []
28
+ end
29
+
30
+ attr_reader :calls
31
+
32
+ def method_missing( sym, *args )
33
+ self.calls << sym
34
+ end
35
+
36
+ def respond_to_missing?( sym, include_all )
37
+ return super if [:on_start, :result, :on_finish].include?( sym )
38
+ return true
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+
45
+ it "can declare a method map for its productions" do
46
+ subclass.production_map \
47
+ 'A' => :do_a_thing,
48
+ 'B' => :do_b_thing
49
+
50
+ instance = subclass.new
51
+ instance.run_generation( 'AABA' )
52
+
53
+ expect( instance.calls ).to eq([
54
+ :do_a_thing,
55
+ :do_a_thing,
56
+ :do_b_thing,
57
+ :do_a_thing,
58
+ ])
59
+ end
60
+
61
+
62
+ it "handles symbols that don't make good method names" do
63
+ subclass.production_map \
64
+ '[' => :push_frame,
65
+ 'A' => :report_depth,
66
+ ']' => :pop_frame
67
+
68
+ instance = subclass.new
69
+ instance.run_generation( 'A[A[[]]A[]]]]' )
70
+
71
+ expect( instance.calls ).to eq([
72
+ :report_depth,
73
+ :push_frame,
74
+ :report_depth,
75
+ :push_frame,
76
+ :push_frame,
77
+ :pop_frame,
78
+ :pop_frame,
79
+ :report_depth,
80
+ :push_frame,
81
+ :pop_frame,
82
+ :pop_frame,
83
+ :pop_frame,
84
+ :pop_frame
85
+ ])
86
+ end
87
+
88
+
89
+ it "can run a rules engine" do
90
+ subclass.production_map \
91
+ 'A' => :report_a,
92
+ 'B' => :report_b
93
+
94
+ instance = subclass.new
95
+ instance.run( algae_rules, 4 )
96
+
97
+ expect( instance.calls ).to eq([
98
+ :report_a,
99
+
100
+ :report_a,
101
+ :report_b,
102
+
103
+ :report_a,
104
+ :report_b,
105
+ :report_a,
106
+
107
+ :report_a,
108
+ :report_b,
109
+ :report_a,
110
+ :report_a,
111
+ :report_b,
112
+ ])
113
+ end
114
+
115
+
116
+ it "can declare lifecycle callbacks" do
117
+ subclass.production_map \
118
+ 'A' => :report_a,
119
+ 'B' => :report_b
120
+ subclass.on_start do |i, previous_result|
121
+ self.calls << [ :on_start, i, previous_result ]
122
+ end
123
+ subclass.result do |i|
124
+ self.calls << [ :result, i ]
125
+ "result_#{i}"
126
+ end
127
+ subclass.on_finish do |i, result|
128
+ self.calls << [ :on_finish, i, result ]
129
+ end
130
+
131
+ instance = subclass.new
132
+ result = instance.run( algae_rules, 4 )
133
+
134
+ expect( instance.calls ).to eq([
135
+ [:on_start, 0, nil],
136
+ :report_a,
137
+ [:result, 0],
138
+ [:on_finish, 0, "result_0"],
139
+
140
+ [:on_start, 1, "result_0"],
141
+ :report_a,
142
+ :report_b,
143
+ [:result, 1],
144
+ [:on_finish, 1, "result_1"],
145
+
146
+ [:on_start, 2, "result_1"],
147
+ :report_a,
148
+ :report_b,
149
+ :report_a,
150
+ [:result, 2],
151
+ [:on_finish, 2, "result_2"],
152
+
153
+ [:on_start, 3, "result_2"],
154
+ :report_a,
155
+ :report_b,
156
+ :report_a,
157
+ :report_a,
158
+ :report_b,
159
+ [:result, 3],
160
+ [:on_finish, 3, "result_3"],
161
+ ])
162
+
163
+ expect( result ).to eq( "result_3" )
164
+ end
165
+
166
+ end
167
+
168
+ end
169
+
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'l_system/rules_engine'
6
+
7
+
8
+ RSpec.describe( LSystem::RulesEngine ) do
9
+
10
+ let( :instance ) { described_class.new }
11
+
12
+
13
+ it "can set the l-system's variables" do
14
+ instance.variables = [ 'A', 'B' ]
15
+
16
+ expect( instance.variables ).to contain_exactly( 'A', 'B' )
17
+ end
18
+
19
+
20
+ it "can declare the l-system's variables" do
21
+ instance.variables 'A', 'B'
22
+
23
+ expect( instance.variables ).to contain_exactly( 'A', 'B' )
24
+ end
25
+
26
+
27
+ it "raises if variables are not disjoint with the constants" do
28
+ instance.variables 'A', 'B'
29
+ expect {
30
+ instance.constants '[', 'A', ']'
31
+ }.to raise_error( ArgumentError, /"A" is already included in the variable set/ )
32
+ end
33
+
34
+
35
+ it "can set the l-system's constants'" do
36
+ instance.constants = [ '[', ']' ]
37
+
38
+ expect( instance.constants ).to contain_exactly( '[', ']' )
39
+ end
40
+
41
+
42
+ it "can declare the l-system's constants'" do
43
+ instance.constants '[', ']'
44
+
45
+ expect( instance.constants ).to contain_exactly( '[', ']' )
46
+ end
47
+
48
+
49
+ it "raises if constants are not disjoint with the variables" do
50
+ instance.constants '[', 'A', ']'
51
+ expect {
52
+ instance.variables 'A', 'B'
53
+ }.to raise_error( ArgumentError, /"A" is already included in the constant set/ )
54
+ end
55
+
56
+
57
+ it "knows what its alphabet is" do
58
+ instance.variables '0', '1'
59
+ instance.constants '[', ']'
60
+
61
+ expect( instance.alphabet ).to contain_exactly( '0', '1', '[', ']' )
62
+ end
63
+
64
+
65
+ it "can apply its rules to a string" do
66
+ instance.variables 'A', 'B'
67
+ instance.rules 'A -> AB', 'B -> A'
68
+
69
+ expect( instance.apply_rules('A') ).to eq( 'AB' )
70
+ expect( instance.apply_rules('AB') ).to eq( 'ABA' )
71
+ expect( instance.apply_rules('ABA') ).to eq( 'ABAAB' )
72
+ expect( instance.apply_rules('ABAAB') ).to eq( 'ABAABABA' )
73
+ expect( instance.apply_rules('ABAABABA') ).to eq( 'ABAABABAABAAB' )
74
+ expect( instance.apply_rules('ABAABABAABAAB') ).to eq( 'ABAABABAABAABABAABABA' )
75
+ expect( instance.apply_rules('ABAABABAABAABABAABABA') ).
76
+ to eq( 'ABAABABAABAABABAABABAABAABABAABAAB' )
77
+ end
78
+
79
+
80
+ it "can create an enumerator that will yield each successive generation" do
81
+ instance.variables 'A', 'B'
82
+ instance.axiom 'A'
83
+ instance.rules 'A -> AB', 'B -> A'
84
+
85
+ result = instance.each
86
+
87
+ expect( result ).to be_an( Enumerator )
88
+ expect( result.next ).to eq( 'A' )
89
+ expect( result.next ).to eq( 'AB' )
90
+ expect( result.next ).to eq( 'ABA' )
91
+ expect( result.next ).to eq( 'ABAAB' )
92
+ expect( result.next ).to eq( 'ABAABABA' )
93
+ expect( result.next ).to eq( 'ABAABABAABAAB' )
94
+ expect( result.next ).to eq( 'ABAABABAABAABABAABABA' )
95
+ expect( result.next ).to eq( 'ABAABABAABAABABAABABAABAABABAABAAB' )
96
+ end
97
+
98
+ end
99
+
@@ -0,0 +1,61 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'spec_helper'
5
+
6
+ require 'rspec'
7
+ require 'l_system'
8
+
9
+ RSpec.describe( LSystem ) do
10
+
11
+ it "allows an L-System to be declared" do
12
+ result = described_class.declare {}
13
+ expect( result ).to be_an( LSystem::RulesEngine )
14
+ end
15
+
16
+
17
+ it "can run an L-System within the context of one or more productions" do
18
+ rules = described_class.declare do
19
+ variables :A, :B
20
+ axiom 'A'
21
+ rules 'A -> AB',
22
+ 'B -> A'
23
+ end
24
+ result = described_class.run( rules, 8 ) do
25
+
26
+ production_map \
27
+ 'A' => :append_A,
28
+ 'B' => :append_B
29
+
30
+ on_start do |_generation_number, _previous_result|
31
+ @output = String.new( encoding: 'utf-8' )
32
+ end
33
+
34
+ result do |_generation_number|
35
+ @output
36
+ end
37
+
38
+ on_finish do |_generation_number, _result|
39
+ @output = nil
40
+ end
41
+
42
+
43
+ def initialize
44
+ @output = nil
45
+ end
46
+
47
+ def append_A
48
+ @output << 'A'
49
+ end
50
+
51
+ def append_B
52
+ @output << 'B'
53
+ end
54
+
55
+ end
56
+
57
+ expect( result ).to eq( 'ABAABABAABAABABAABABAABAABABAABAAB' )
58
+ end
59
+
60
+ end
61
+
@@ -0,0 +1,30 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'simplecov' if ENV['COVERAGE']
5
+
6
+ require 'rspec'
7
+
8
+ require 'loggability/spechelpers'
9
+
10
+
11
+ ### Mock with RSpec
12
+ RSpec.configure do |config|
13
+ config.mock_with( :rspec ) do |mock|
14
+ mock.syntax = :expect
15
+ end
16
+
17
+ config.disable_monkey_patching!
18
+ config.example_status_persistence_file_path = "spec/.status"
19
+ config.filter_run :focus
20
+ config.filter_run_when_matching :focus
21
+ config.order = :random
22
+ config.profile_examples = 5
23
+ config.run_all_when_everything_filtered = true
24
+ config.shared_context_metadata_behavior = :apply_to_host_groups
25
+ # config.warnings = true
26
+
27
+ config.include( Loggability::SpecHelpers )
28
+ end
29
+
30
+
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: l_system
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Granger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIENDCCApygAwIBAgIBATANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdnZWQv
14
+ REM9RmFlcmllTVVEL0RDPW9yZzAeFw0xOTEwMDkwMDM2NTdaFw0yMDEwMDgwMDM2
15
+ NTdaMCIxIDAeBgNVBAMMF2dlZC9EQz1GYWVyaWVNVUQvREM9b3JnMIIBojANBgkq
16
+ hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvyVhkRzvlEs0fe7145BYLfN6njX9ih5H
17
+ L60U0p0euIurpv84op9CNKF9tx+1WKwyQvQP7qFGuZxkSUuWcP/sFhDXL1lWUuIl
18
+ M4uHbGCRmOshDrF4dgnBeOvkHr1fIhPlJm5FO+Vew8tSQmlDsosxLUx+VB7DrVFO
19
+ 5PU2AEbf04GGSrmqADGWXeaslaoRdb1fu/0M5qfPTRn5V39sWD9umuDAF9qqil/x
20
+ Sl6phTvgBrG8GExHbNZpLARd3xrBYLEFsX7RvBn2UPfgsrtvpdXjsHGfpT3IPN+B
21
+ vQ66lts4alKC69TE5cuKasWBm+16A4aEe3XdZBRNmtOu/g81gvwA7fkJHKllJuaI
22
+ dXzdHqq+zbGZVSQ7pRYHYomD0IiDe1DbIouFnPWmagaBnGHwXkDT2bKKP+s2v21m
23
+ ozilJg4aar2okb/RA6VS87o+d7g6LpDDMMQjH4G9OPnJENLdhu8KnPw/ivSVvQw7
24
+ N2I4L/ZOIe2DIVuYH7aLHfjZDQv/mNgpAgMBAAGjdTBzMAkGA1UdEwQCMAAwCwYD
25
+ VR0PBAQDAgSwMB0GA1UdDgQWBBRyjf55EbrHagiRLqt5YAd3yb8k4DAcBgNVHREE
26
+ FTATgRFnZWRARmFlcmllTVVELm9yZzAcBgNVHRIEFTATgRFnZWRARmFlcmllTVVE
27
+ Lm9yZzANBgkqhkiG9w0BAQsFAAOCAYEAFqsr6o0SvQRgjQVmhbQvExRnCMCoW1yb
28
+ FJiN7A5RA2Iy2E61OG1Ul5nGmaDmx/PNB/6JIbIV3B9Uq8aTZx4uOjK7r8vMl1/t
29
+ ZfY7r6HejJfXlcO2m6JDMbpdyEVv916LncBkzZRz6vnnNCx+31f15FKddxujpAFd
30
+ qpn3JRQY+oj7ZkoccL/IUiDpxQWeS3oOoz9qr2kVTp8R50InZimt79FqCl/1m66W
31
+ kdOuf+wM3DDx7Rt4IVNHrhGlyfMr7xjKW1Q3gll+pMN1DT6Ajx/t3JDSEg7BnnEW
32
+ r7AciSO6J4ApUdqyG+coLFlGdtgFTgRHv7ihbQtDI7Z/LV7A4Spn1j2PK3j0Omri
33
+ kSl1hPVigRytfgdVGiLXzvkkrkgj9EknCaj5UHbac7XvVBrljXj9hsnnqTANaKsg
34
+ jBZSA+N+xUTgUWpXjjwsLZjzJkhWATJWq+krNXcqpwXo6HsjmdUxoFMt63RBb+sI
35
+ XrxOxp8o0uOkU7FdLSGsyqJ2LzsR4obN
36
+ -----END CERTIFICATE-----
37
+ date: 2020-02-26 00:00:00.000000000 Z
38
+ dependencies:
39
+ - !ruby/object:Gem::Dependency
40
+ name: loggability
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '0.16'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '0.16'
53
+ - !ruby/object:Gem::Dependency
54
+ name: tortoise
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.9'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.9'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rake-deveiate
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '0.10'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '0.10'
81
+ description: |-
82
+ A toolkit for creating and using {Lindenmayer Systems}[https://en.wikipedia.org/wiki/L-system] (L-systems).
83
+ It consists of a class that allows for declaration of the L-system's grammar,
84
+ and another class that allows for the definition of how the symbols output by a
85
+ grammar should be translated into work.
86
+ email:
87
+ - ged@faeriemud.org
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".simplecov"
93
+ - History.md
94
+ - README.md
95
+ - Rakefile
96
+ - examples/fern_gn7.png
97
+ - lib/l_system.rb
98
+ - lib/l_system/production_adapter.rb
99
+ - lib/l_system/rules_engine.rb
100
+ - spec/l_system/production_adapter_spec.rb
101
+ - spec/l_system/rules_engine_spec.rb
102
+ - spec/l_system_spec.rb
103
+ - spec/spec_helper.rb
104
+ homepage: https://hg.sr.ht/~ged/LSystem
105
+ licenses:
106
+ - BSD-3-Clause
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.1.2
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: A toolkit for creating and using {Lindenmayer Systems}[https://en.wikipedia.org/wiki/L-system]
127
+ (L-systems).
128
+ test_files: []
metadata.gz.sig ADDED
Binary file