l_system 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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