zyps 0.6.2 → 0.6.3

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.
data/bin/zyps CHANGED
@@ -20,10 +20,12 @@
20
20
 
21
21
  gems_loaded = false
22
22
  begin
23
+ require 'optparse'
23
24
  require 'zyps'
24
25
  require 'zyps/actions'
25
26
  require 'zyps/conditions'
26
27
  require 'zyps/environmental_factors'
28
+ require 'zyps/remote'
27
29
  require 'zyps/views/trails'
28
30
  rescue LoadError
29
31
  if gems_loaded == false
@@ -39,14 +41,36 @@ end
39
41
  include Zyps
40
42
 
41
43
 
44
+ DEFAULT_VIEW_WIDTH = 800
45
+ DEFAULT_VIEW_HEIGHT = 600
46
+ DEFAULT_MAX_SPEED = 200
47
+ DEFAULT_MAX_POPULATION = 100
48
+ DEFAULT_FPS = 60
49
+
50
+
42
51
  class Application
43
52
 
53
+ #Port to open service on.
54
+ attr_accessor :uri
55
+ #Maximum allowed number of objects.
56
+ attr_accessor :max_population
57
+ #View dimensions.
58
+ attr_accessor :view_width, :view_height
59
+
44
60
  #Create app window, game environment, and view.
45
- def initialize(width, height)
61
+ #Set up default values.
62
+ def initialize(
63
+ uri = nil,
64
+ view_width = DEFAULT_VIEW_WIDTH,
65
+ view_height = DEFAULT_VIEW_HEIGHT,
66
+ fps = DEFAULT_FPS,
67
+ max_population = DEFAULT_MAX_POPULATION,
68
+ max_speed = DEFAULT_MAX_SPEED,
69
+ enclosure = true
70
+ )
46
71
 
47
- @width, @height = width, height
48
- @birth_rate = 1
49
- @fps = 30.0
72
+ @uri, @view_width, @view_height, @fps, @max_population, @max_speed, @enclosure =
73
+ uri, view_width, view_height, fps, max_population, max_speed, enclosure
50
74
 
51
75
  #Create a window, and set GTK up to quit when it is closed.
52
76
  window = Gtk::Window.new
@@ -54,15 +78,16 @@ class Application
54
78
  window.signal_connect("delete_event") {false}
55
79
  window.signal_connect("destroy") {Gtk.main_quit}
56
80
 
81
+ #Create environment.
82
+ @environment = Environment.new
83
+
57
84
  #Set up controls.
85
+ #Also initializes @view.
58
86
  window.add(create_controls)
59
87
 
60
88
  #Show all widgets.
61
89
  window.show_all
62
-
63
- #Create environment.
64
- @environment = Environment.new
65
-
90
+
66
91
  #Point view at environment.
67
92
  @environment.add_observer(@view)
68
93
 
@@ -71,20 +96,21 @@ class Application
71
96
 
72
97
  def main
73
98
 
74
-
75
- #Keep all objects within a boundary.
76
- enclosure = Enclosure.new
77
- enclosure.left = 0
78
- enclosure.bottom = 0
79
- enclosure.top = @height
80
- enclosure.right = @width
81
- @environment.environmental_factors << enclosure
99
+ #Keep objects on screen.
100
+ if @enclosure
101
+ enclosure = Enclosure.new()
102
+ enclosure.left = 0
103
+ enclosure.bottom = 0
104
+ enclosure.top = @view_height
105
+ enclosure.right = @view_width
106
+ @environment.environmental_factors << enclosure
107
+ end
82
108
 
83
109
  #Keep all objects under a certain speed.
84
- @environment.environmental_factors << SpeedLimit.new(100)
110
+ @environment.environmental_factors << SpeedLimit.new(@max_speed) if @max_speed
85
111
 
86
112
  #Limit population.
87
- @environment.environmental_factors << PopulationLimit.new(@environment, 25)
113
+ @environment.environmental_factors << PopulationLimit.new(@environment, @max_population) if @max_population
88
114
 
89
115
  #Set up a creature generator.
90
116
  @generator = CreatureGenerator.new(@environment)
@@ -98,7 +124,7 @@ class Application
98
124
  time_per_frame = 1.0 / @fps
99
125
 
100
126
  loop do
101
-
127
+
102
128
  @environment.interact
103
129
 
104
130
  #Determine how much time is left in this frame.
@@ -119,6 +145,16 @@ class Application
119
145
 
120
146
  end
121
147
 
148
+
149
+ #Start a network service.
150
+ if @uri
151
+ server = EnvironmentServer.new(@environment, @uri)
152
+ server.start
153
+ #Disable file system access.
154
+ $SAFE = 2
155
+ end
156
+
157
+
122
158
  #Activate the GUI.
123
159
  Gtk.main
124
160
 
@@ -126,13 +162,13 @@ class Application
126
162
 
127
163
 
128
164
  #Create a view and controls.
129
- def create_controls(width = @width, height = @height, homogeneous = false, spacing = 0, expand = false, fill = false, padding = 0)
165
+ def create_controls(view_width = @view_width, view_height = @view_height, homogeneous = false, spacing = 0, expand = false, fill = false, padding = 0)
130
166
 
131
167
  #Create a container for the view and controls.
132
168
  interface = Gtk::HBox.new(homogeneous, spacing)
133
169
 
134
170
  #Add view to interface.
135
- @view = TrailsView.new(width, height)
171
+ @view = TrailsView.new(view_width, view_height)
136
172
  interface.pack_start(@view.canvas, expand, fill, padding)
137
173
 
138
174
  #When mouse button pressed, record location for use in release event handler.
@@ -156,6 +192,7 @@ class Application
156
192
  :flee => @flee_flag.active?,
157
193
  :push => @push_flag.active?,
158
194
  :pull => @pull_flag.active?,
195
+ :breed => @breed_flag.active?,
159
196
  :eat => @eat_flag.active?
160
197
  )
161
198
  end
@@ -178,18 +215,88 @@ class Application
178
215
  action_controls.pack_start(@push_flag, expand, fill, padding)
179
216
  @pull_flag = Gtk::CheckButton.new("Pull")
180
217
  action_controls.pack_start(@pull_flag, expand, fill, padding)
218
+ @breed_flag = Gtk::CheckButton.new("Breed")
219
+ action_controls.pack_start(@breed_flag, expand, fill, padding)
181
220
  @eat_flag = Gtk::CheckButton.new("Eat")
182
221
  action_controls.pack_start(@eat_flag, expand, fill, padding)
183
222
  #Add the action controls to the panel.
184
223
  control_panel.pack_start(action_controls, expand, fill, padding)
185
224
 
225
+ #Create a group for environment controls.
226
+ environment_controls = Gtk::VBox.new(homogeneous, spacing)
227
+ environment_controls.pack_start(Gtk::Label.new("Environment"), expand, fill, padding)
228
+ @clear_button = Gtk::Button.new("Clear")
229
+ @clear_button.signal_connect("clicked") {
230
+ @environment.objects = []
231
+ }
232
+ environment_controls.pack_start(@clear_button, expand, fill, padding)
233
+ #Add the environment controls to the panel.
234
+ control_panel.pack_start(environment_controls, expand, fill, padding)
235
+
186
236
  #Add the control panel to the interface.
187
237
  interface.pack_start(control_panel, expand, fill, padding)
188
238
 
189
239
  interface
190
240
 
191
241
  end
242
+
243
+
244
+ #Set attributes according to command-line arguments.
245
+ def process_options(arguments)
192
246
 
247
+ #Set up option parser.
248
+ options = OptionParser.new
249
+
250
+ #Define valid options.
251
+ options.on("-h", "--help", TrueClass, "Display program help.") {
252
+ puts options.help
253
+ exit
254
+ }
255
+ options.on(
256
+ "-m",
257
+ "--max-population [number]",
258
+ Integer,
259
+ "The maximum number of allowed game objects. #{DEFAULT_MAX_POPULATION} by default."
260
+ ) {|value| @max_population = value}
261
+ options.on(
262
+ "-s",
263
+ "--max-speed [number]",
264
+ Integer,
265
+ "The fastest an object can go. #{DEFAULT_MAX_SPEED ? DEFAULT_MAX_SPEED : 'No limit'} by default."
266
+ ) {|value| @max_speed = value}
267
+ options.on(
268
+ "-n",
269
+ "--no-enclosure",
270
+ "Disables the barrier that normally keeps objects on the screen."
271
+ ) {|value| @enclosure = false}
272
+ options.on(
273
+ "-u",
274
+ "--uri [uri]",
275
+ String,
276
+ "dRuby URI to run the server on. If not defined, one will be selected and printed to STDOUT."
277
+ ) {|value| @uri = value}
278
+ options.on(
279
+ "-f",
280
+ "--fps [frames]",
281
+ Integer,
282
+ "Number of frames to draw per second. #{DEFAULT_FPS} by default."
283
+ ) {|value| @fps = value}
284
+ options.on(
285
+ "--view-width [pixels]",
286
+ Integer,
287
+ "Window width. #{DEFAULT_VIEW_WIDTH} by default."
288
+ ) {|value| @view_width = value}
289
+ options.on(
290
+ "--view-height [pixels]",
291
+ Integer,
292
+ "Window height. #{DEFAULT_VIEW_HEIGHT} by default."
293
+ ) {|value| @view_height = value}
294
+
295
+ #Parse the options, printing usage if parsing fails.
296
+ options.parse(arguments) rescue puts "#{$!}\nType '#{$0} --help' for valid options."
297
+
298
+ end
299
+
193
300
 
194
301
  end
195
302
 
@@ -224,8 +331,9 @@ class CreatureGenerator
224
331
  @turn_rate = 90
225
332
  @approach_turn_rate = 720
226
333
  @flee_turn_rate = @approach_turn_rate
227
- @pull_strength = 1000
334
+ @pull_strength = 100
228
335
  @push_strength = @pull_strength
336
+ @breed_rate = 10
229
337
 
230
338
  end
231
339
 
@@ -233,7 +341,7 @@ class CreatureGenerator
233
341
  #Create a creature and add it to the environment.
234
342
  def create_creature(options = {})
235
343
 
236
- {
344
+ options = {
237
345
  :x => 0,
238
346
  :y => 0,
239
347
  :speed => 1,
@@ -245,77 +353,96 @@ class CreatureGenerator
245
353
  :flee => false,
246
354
  :push => false,
247
355
  :pull => false,
356
+ :breed => false,
248
357
  :eat => false,
249
- }.merge!(options)
358
+ }.merge(options)
250
359
 
251
- #Set up the creature's vector.
252
- vector = Vector.new(options[:speed], options[:pitch])
360
+ #Create a creature.
361
+ creature = Creature.new(
362
+ '',
363
+ Location.new(options[:x], options[:y]),
364
+ Color.new,
365
+ Vector.new(options[:speed], options[:pitch]),
366
+ 0,
367
+ 5
368
+ )
253
369
 
254
370
  #Set up actions and merge colors according to selected behaviors.
255
- behavior = Behavior.new
256
- behavior.conditions << ProximityCondition.new(@action_proximity)
257
371
  color = Color.new(0.25, 0.25, 0.25)
258
372
  if options[:accelerate]
259
373
  color.red += 0.25
260
374
  color.green += 0.25
261
375
  color.blue += 0.25
262
- behavior.actions << AccelerateAction.new(@acceleration_rate)
376
+ creature.behaviors << create_behavior(:actions => [AccelerateAction.new(@acceleration_rate)])
263
377
  end
264
378
  if options[:turn]
265
379
  color.blue += 1
266
- behavior.actions << TurnAction.new(@turn_rate)
380
+ creature.behaviors << create_behavior(:actions => [TurnAction.new(@turn_rate)])
267
381
  end
268
382
  if options[:approach]
269
383
  color.red += 1
270
- behavior.actions << ApproachAction.new(vector, @approach_turn_rate)
384
+ creature.behaviors << create_behavior(:actions => [ApproachAction.new(@approach_turn_rate, creature.vector)])
271
385
  end
272
386
  if options[:flee]
273
387
  color.red += 0.5; color.green += 0.5 #Yellow.
274
- behavior.actions << FleeAction.new(vector, @flee_turn_rate)
388
+ creature.behaviors << create_behavior(:actions => [FleeAction.new(@flee_turn_rate, creature.vector)])
275
389
  end
276
390
  if options[:push]
277
391
  color.red += 0.5; color.blue += 0.5 #Purple.
278
- behavior.actions << PushAction.new(@push_strength)
392
+ creature.behaviors << create_behavior(:actions => [PushAction.new(@push_strength)])
279
393
  end
280
394
  if options[:pull]
281
395
  color.blue += 0.75; color.green += 0.75 #Aqua.
282
- behavior.actions << PullAction.new(@pull_strength)
396
+ creature.behaviors << create_behavior(:actions => [PullAction.new(@pull_strength)])
397
+ end
398
+ if options[:breed]
399
+ color.green -= 0.1 #Make a bit redder.
400
+ color.blue -= 0.1
401
+ creature.behaviors << create_behavior(
402
+ :actions => [BreedAction.new(@environment, @breed_rate)],
403
+ :conditions => [CollisionCondition.new] #The default ProximityCondition won't do.
404
+ )
283
405
  end
284
-
285
- #Create a creature.
286
- creature = Creature.new(
287
- '',
288
- Location.new(options[:x], options[:y]),
289
- color,
290
- vector,
291
- 0,
292
- 5
293
- )
294
- creature.behaviors << behavior
295
-
296
- #Special handling for eat action, which needs a CollisionCondition.
297
406
  if options[:eat]
298
- creature.color.green += 1
299
- behavior = Behavior.new
300
- behavior.actions << EatAction.new(@environment)
301
- behavior.conditions << CollisionCondition.new
302
- creature.behaviors << behavior
407
+ color.green += 1
408
+ creature.behaviors << create_behavior(
409
+ :actions => [EatAction.new(@environment)],
410
+ :conditions => [CollisionCondition.new] #The default ProximityCondition won't do.
411
+ )
303
412
  end
304
413
 
414
+ creature.color = color
415
+
305
416
  @environment.objects << creature
306
417
 
307
418
  end
419
+
420
+
421
+ def create_behavior(options = {})
422
+
423
+ options = {
424
+ :actions => [],
425
+ :conditions => [ProximityCondition.new(@action_proximity)],
426
+ }.merge(options)
427
+
428
+ behavior = Behavior.new
429
+ behavior.actions = options[:actions]
430
+ behavior.conditions = options[:conditions]
431
+ behavior
432
+
433
+ end
434
+
435
+
308
436
  end
309
437
 
310
438
 
311
439
 
312
440
  begin
313
- #The view width.
314
- WIDTH = 500
315
- #The view height.
316
- HEIGHT = 600
317
- #Run the application.
318
- application = Application.new(WIDTH, HEIGHT)
441
+ #Create a server.
442
+ application = Application.new
443
+ #Parse the command line.
444
+ application.process_options(ARGV)
445
+ #Start the server.
319
446
  application.main
320
447
  rescue => exception
321
448
  #Print error to STDERR and exit with an abnormal status.
data/bin/zyps_demo CHANGED
@@ -353,7 +353,7 @@ class Demo
353
353
  say("We also add a condition that it should only target food.")
354
354
  @environment.objects.each do |creature|
355
355
  approach = Behavior.new
356
- approach.actions << ApproachAction.new(creature.vector)
356
+ approach.actions << ApproachAction.new(360, creature.vector)
357
357
  approach.conditions << TagCondition.new("food")
358
358
  creature.behaviors << approach
359
359
  end
@@ -384,7 +384,7 @@ class Demo
384
384
  say("A FleeAction is just like an ApproachAction, but we head in the OPPOSITE direction.")
385
385
  @environment.objects.each do |creature|
386
386
  flee = Behavior.new
387
- flee.actions << FleeAction.new(creature.vector)
387
+ flee.actions << FleeAction.new(360, creature.vector)
388
388
  flee.conditions << TagCondition.new("predator")
389
389
  creature.behaviors << flee
390
390
  end
data/lib/zyps/actions.rb CHANGED
@@ -21,6 +21,41 @@ require 'zyps'
21
21
  module Zyps
22
22
 
23
23
 
24
+ #Superclass for actions that need to happen at a specific rate.
25
+ class TimedAction < Action
26
+
27
+ #A Clock that tracks time between actions.
28
+ attr_accessor :clock
29
+ #Units per second for action.
30
+ attr_accessor :rate
31
+
32
+ def initialize(rate, *arguments)
33
+ self.rate = rate
34
+ @clock = Clock.new
35
+ end
36
+
37
+ #Make a deep copy.
38
+ def copy
39
+ copy = super
40
+ #Copies should have their own Clock.
41
+ copy.clock = Clock.new
42
+ copy
43
+ end
44
+
45
+ #Units to add to the attribute being changed.
46
+ def delta
47
+ @clock.elapsed_time * rate
48
+ end
49
+
50
+ #Begin tracking time between actions.
51
+ def start(actor, target)
52
+ super
53
+ @clock.reset_elapsed_time
54
+ end
55
+
56
+ end
57
+
58
+
24
59
  #Head toward a target.
25
60
  class FaceAction < Action
26
61
  #Set the actor's heading to point directly at target.
@@ -31,61 +66,44 @@ end
31
66
 
32
67
 
33
68
  #Increase/decrease speed over time.
34
- class AccelerateAction < Action
69
+ class AccelerateAction < TimedAction
35
70
  #Units per second to accelerate.
36
71
  #Can be negative to slow down or go in reverse.
37
72
  attr_accessor :rate
38
- def initialize(rate = 0)
39
- self.rate = rate
40
- @clock = Clock.new
41
- end
42
- #Begin tracking time between actions.
43
- def start(actor, target)
44
- super
45
- @clock.reset_elapsed_time
46
- end
47
73
  #Increase or decrease speed according to elapsed time.
48
74
  def do(actor, target)
49
- actor.vector.speed += @clock.elapsed_time * rate
75
+ actor.vector.speed += delta
50
76
  end
51
77
  end
52
78
 
53
79
 
54
80
  #Turn over time.
55
- class TurnAction < Action
81
+ class TurnAction < TimedAction
56
82
  #Degrees per second to turn.
57
83
  #Positive turns clockwise, negative turns counter-clockwise.
58
84
  attr_accessor :rate
59
- def initialize(rate = 0)
60
- self.rate = rate
61
- @clock = Clock.new
62
- end
63
- #Begin tracking time between actions.
64
- def start(actor, target)
65
- super
66
- @clock.reset_elapsed_time
67
- end
68
85
  #Turn according to elapsed time.
69
86
  def do(actor, target)
70
- actor.vector.pitch += @clock.elapsed_time * rate
87
+ actor.vector.pitch += delta
71
88
  end
72
89
  end
73
90
 
74
91
 
75
92
  #Approaches the target, but obeys law of inertia.
76
- class ApproachAction < Action
93
+ class ApproachAction < TimedAction
77
94
  #Direction/speed actor is accelerating in.
78
95
  attr_accessor :heading
79
96
  #Degrees per second the direction of acceleration can change.
80
- attr_accessor :turn_rate
81
- def initialize(heading = Vector.new, turn_rate = 360)
82
- self.heading, self.turn_rate = heading, turn_rate
83
- @clock = Clock.new
84
- end
85
- #Begin tracking time between actions.
86
- def start(actor, target)
97
+ attr_accessor :rate
98
+ def initialize(rate = 360, heading = Vector.new)
87
99
  super
88
- @clock.reset_elapsed_time
100
+ self.heading = heading
101
+ end
102
+ #Make a deep copy.
103
+ def copy
104
+ copy = super
105
+ copy.heading = @heading.copy
106
+ copy
89
107
  end
90
108
  #Accelerate toward the target, but limited by turn rate.
91
109
  def do(actor, target)
@@ -98,7 +116,7 @@ class ApproachAction < Action
98
116
  turn_angle += 360.0
99
117
  end
100
118
  #The creature can only turn as fast as the elapsed time, of course.
101
- maximum_turn = turn_rate * @clock.elapsed_time
119
+ maximum_turn = delta
102
120
  if turn_angle.abs > maximum_turn
103
121
  if turn_angle > 0
104
122
  turn_angle = maximum_turn
@@ -115,19 +133,20 @@ end
115
133
 
116
134
 
117
135
  #Flees from the target, but obeys law of inertia.
118
- class FleeAction < Action
136
+ class FleeAction < TimedAction
119
137
  #Direction/speed actor is accelerating in.
120
138
  attr_accessor :heading
121
139
  #Degrees per second the direction of acceleration can change.
122
- attr_accessor :turn_rate
123
- def initialize(heading = Vector.new, turn_rate = 360)
124
- self.heading, self.turn_rate = heading, turn_rate
125
- @clock = Clock.new
126
- end
127
- #Begin tracking time between actions.
128
- def start(actor, target)
140
+ attr_accessor :rate
141
+ def initialize(rate = 360, heading = Vector.new)
129
142
  super
130
- @clock.reset_elapsed_time
143
+ self.heading = heading
144
+ end
145
+ #Make a deep copy.
146
+ def copy
147
+ copy = super
148
+ copy.heading = @heading.copy
149
+ copy
131
150
  end
132
151
  #Accelerate away from the target, but limited by turn rate.
133
152
  def do(actor, target)
@@ -140,7 +159,7 @@ class FleeAction < Action
140
159
  turn_angle += 360.0
141
160
  end
142
161
  #The creature can only turn as fast as the elapsed time, of course.
143
- maximum_turn = turn_rate * @clock.elapsed_time
162
+ maximum_turn = delta
144
163
  if turn_angle.abs > maximum_turn
145
164
  if turn_angle > 0
146
165
  turn_angle = maximum_turn
@@ -166,8 +185,6 @@ class DestroyAction < Action
166
185
  #Remove the target from the environment.
167
186
  def do(actor, target)
168
187
  @environment.objects.delete(target)
169
- #Discontinue action.
170
- return false
171
188
  end
172
189
  end
173
190
 
@@ -213,24 +230,15 @@ end
213
230
 
214
231
 
215
232
  #Pushes target away.
216
- class PushAction < Action
233
+ class PushAction < TimedAction
217
234
  #Units/second to accelerate target by.
218
- attr_accessor :force
219
- def initialize(force = 1)
220
- @force = force
221
- @clock = Clock.new
222
- end
223
- #Begin tracking time between actions.
224
- def start(actor, target)
225
- super
226
- @clock.reset_elapsed_time
227
- end
235
+ attr_accessor :rate
228
236
  #Accelerate the target away from the actor, but limited by elapsed time.
229
237
  def do(actor, target)
230
238
  #Angle to target is also angle of push force.
231
239
  push_angle = Utility.find_angle(actor.location, target.location)
232
240
  #Acceleration will be limited by elapsed time.
233
- push_force = @force * @clock.elapsed_time
241
+ push_force = delta
234
242
  #Apply the force to the creature's movement vector.
235
243
  target.vector += Vector.new(push_force, push_angle)
236
244
  end
@@ -238,28 +246,59 @@ end
238
246
 
239
247
 
240
248
  #Pulls target toward actor.
241
- class PullAction < Action
249
+ class PullAction < TimedAction
242
250
  #Units/second to accelerate target by.
243
- attr_accessor :force
244
- def initialize(force = 1)
245
- @force = force
246
- @clock = Clock.new
247
- end
248
- #Begin tracking time between actions.
249
- def start(actor, target)
250
- super
251
- @clock.reset_elapsed_time
252
- end
251
+ attr_accessor :rate
253
252
  #Accelerate away from the target, but limited by turn rate.
254
253
  def do(actor, target)
255
254
  #Angle from target to actor is also angle of pull force (opposite of that for push).
256
255
  pull_angle = Utility.find_angle(target.location, actor.location)
257
256
  #Acceleration will be limited by elapsed time.
258
- pull_force = @force * @clock.elapsed_time
257
+ pull_force = delta
259
258
  #Apply the force to the creature's movement vector.
260
259
  target.vector += Vector.new(pull_force, pull_angle)
261
260
  end
262
261
  end
263
262
 
264
263
 
264
+ class BreedAction < Action
265
+ DEFAULT_DELAY = 60
266
+ #Environment to place children into.
267
+ attr_accessor :environment
268
+ #Delay between actions.
269
+ attr_accessor :delay
270
+ def initialize(environment, delay = DEFAULT_DELAY)
271
+ self.environment, self.delay = environment, delay
272
+ @clock = Clock.new
273
+ @time_since_last_action = 0
274
+ end
275
+ def do(actor, target)
276
+ #Skip action if target is not a Creature.
277
+ return unless target.is_a?(Creature)
278
+ #Get time since last action, and skip if it hasn't been long enough.
279
+ @time_since_last_action += @clock.elapsed_time
280
+ return unless @time_since_last_action >= @delay
281
+ #Create a child.
282
+ child = Creature.new
283
+ #Combine colors.
284
+ child.color = actor.color + target.color
285
+ #Combine behaviors EXCEPT those with BreedActions.
286
+ behaviors = (actor.behaviors + target.behaviors).find_all do |behavior|
287
+ ! behavior.actions.any?{|action| action.is_a?(BreedAction)}
288
+ end
289
+ behaviors.each {|behavior| child.behaviors << behavior.copy}
290
+ #Location should equal actor's.
291
+ child.location = actor.location.copy
292
+ #Add parents' vectors to get child's vector.
293
+ child.vector = actor.vector + target.vector
294
+ #Child's size should be half the average size of the parents'.
295
+ child.size = ((actor.size + target.size) / 2) / 2
296
+ #Add child to environment.
297
+ @environment.objects << child
298
+ #Reset elapsed time.
299
+ @time_since_last_action = 0
300
+ end
301
+ end
302
+
303
+
265
304
  end #module Zyps
data/lib/zyps.rb CHANGED
@@ -117,7 +117,19 @@ class GameObject
117
117
 
118
118
  def initialize (name = nil, location = Location.new, color = Color.new, vector = Vector.new, age = 0, size = 1, tags = [])
119
119
  self.name, self.location, self.color, self.vector, self.age, self.size, self.tags = name, location, color, vector, age, size, tags
120
- @identifier = rand(99999999) #TODO: Current setup won't necessarily be unique.
120
+ @identifier = generate_identifier
121
+ end
122
+
123
+ #Make a deep copy.
124
+ def copy
125
+ copy = self.clone
126
+ copy.vector = @vector.copy
127
+ copy.color = @color.copy
128
+ copy.location = @location.copy
129
+ copy.tags = @tags.clone
130
+ copy.identifier = generate_identifier
131
+ copy.name = @name ? "Copy of " + @name : nil
132
+ copy
121
133
  end
122
134
 
123
135
  #Size must be positive.
@@ -133,6 +145,19 @@ class GameObject
133
145
  def age; Time.new.to_f - @birth_time; end
134
146
  def age=(age); @birth_time = Time.new.to_f - age; end
135
147
 
148
+ #Set identifier.
149
+ #Not part of API; copy() needs this to make copy's ID unique.
150
+ def identifier=(value) #:nodoc:
151
+ @identifier = value
152
+ end
153
+
154
+ private
155
+
156
+ #Make a unique GameObject identifier.
157
+ def generate_identifier
158
+ rand(99999999) #TODO: Current setup won't necessarily be unique.
159
+ end
160
+
136
161
  end
137
162
 
138
163
 
@@ -149,6 +174,15 @@ class Creature < GameObject
149
174
  self.behaviors = behaviors
150
175
  end
151
176
 
177
+ #Make a deep copy.
178
+ def copy
179
+ copy = super
180
+ #Make deep copy of each behavior.
181
+ copy.behaviors = []
182
+ @behaviors.each {|behavior| copy.behaviors << behavior.copy}
183
+ copy
184
+ end
185
+
152
186
  #Call Behavior.perform(self, environment) on each of the creature's assigned Behaviors.
153
187
  def act(environment)
154
188
  behaviors.each {|behavior| behavior.perform(self, environment)}
@@ -175,15 +209,18 @@ class Action
175
209
  @started = false
176
210
  end
177
211
 
212
+ #Make a deep copy.
213
+ def copy; self.clone; end
214
+
178
215
  #Start the action.
179
216
  #Overriding subclasses must either call "super" or set the @started attribute to true.
180
217
  def start(actor, target)
181
218
  @started = true
182
219
  end
183
220
 
184
- #Perform the action.
185
- #Subclasses should override this.
221
+ #Action subclasses must implement a do(actor, target) instance method.
186
222
  def do(actor, target)
223
+ raise NotImplementedError.new("Action subclasses must implement a do(actor, target) instance method.")
187
224
  end
188
225
 
189
226
  #Stop the action.
@@ -197,8 +234,16 @@ end
197
234
 
198
235
 
199
236
  #A condition for one Creature to act on another.
200
- #Conditions must implement a met?(actor, target) instance method.
201
237
  class Condition
238
+
239
+ #Make a deep copy.
240
+ def copy; self.clone; end
241
+
242
+ #Condition subclasses must implement a met?(actor, target) instance method.
243
+ def met?(actor, target)
244
+ raise NotImplementedError.new("Condition subclasses must implement a met?(actor, target) instance method.")
245
+ end
246
+
202
247
  end
203
248
 
204
249
 
@@ -220,6 +265,18 @@ class Behavior
220
265
  @active_target = nil
221
266
  end
222
267
 
268
+ #Make a deep copy.
269
+ def copy
270
+ copy = self.clone #Currently, we overwrite everything anyway, but we may add some clonable attributes later.
271
+ #Make a deep copy of all actions.
272
+ copy.actions = []
273
+ @actions.each {|action| copy.actions << action.copy}
274
+ #Make a deep copy of all conditions.
275
+ copy.conditions = []
276
+ @conditions.each {|condition| copy.conditions << condition.copy}
277
+ copy
278
+ end
279
+
223
280
  #Test all conditions against each object in the evironment.
224
281
  #For the first object that meets all of them, mark it active (and operate on it first next time).
225
282
  #Then call start() (if applicable) and perform() for all actions against the active target.
@@ -294,6 +351,9 @@ class Color
294
351
  self.red, self.green, self.blue = red, green, blue
295
352
  end
296
353
 
354
+ #Make a deep copy.
355
+ def copy; self.clone; end
356
+
297
357
  #Automatically constrains value to the range 0 - 1.
298
358
  def red=(v); v = 0 if v < 0; v = 1 if v > 1; @red = v; end
299
359
  #Automatically constrains value to the range 0 - 1.
@@ -329,6 +389,9 @@ class Location
329
389
  def initialize (x = 0, y = 0)
330
390
  self.x, self.y = x, y
331
391
  end
392
+
393
+ #Make a deep copy.
394
+ def copy; self.clone; end
332
395
 
333
396
  end
334
397
 
@@ -346,6 +409,9 @@ class Vector
346
409
  self.pitch = pitch
347
410
  end
348
411
 
412
+ #Make a deep copy.
413
+ def copy; self.clone; end
414
+
349
415
  #The angle along the X/Y axes.
350
416
  def pitch; Utility.to_degrees(@pitch); end
351
417
  def pitch=(degrees)
@@ -390,6 +456,9 @@ class Clock
390
456
  reset_elapsed_time
391
457
  end
392
458
 
459
+ #Make a deep copy.
460
+ def copy; self.clone; end
461
+
393
462
  #Returns the time in (fractional) seconds since this method was last called (or on the first call, time since the Clock was created).
394
463
  def elapsed_time
395
464
  time = Time.new.to_f
data/test/test_zyps.rb CHANGED
@@ -61,6 +61,27 @@ class TestGameObject < Test::Unit::TestCase
61
61
  assert_in_delta(1, object.location.x, 0.001)
62
62
  assert_in_delta(1, object.location.y, 0.001)
63
63
  end
64
+
65
+
66
+ def test_copy
67
+ object = GameObject.new
68
+ object.vector = Vector.new(1, 1)
69
+ object.color = Color.new(0.5, 0.5, 0.5)
70
+ object.location = Location.new(3, 3)
71
+ object.tags = ["1", "2"]
72
+ object.name = "name"
73
+ copy = object.copy
74
+ assert_not_same(object.vector, copy.vector, "Copy's vector should not be same object.")
75
+ assert_equal(object.vector.x, copy.vector.x, "Copy's vector should share attributes.")
76
+ assert_not_same(object.color, copy.color, "Copy's color should not be same object.")
77
+ assert_equal(object.color.red, copy.color.red, "Copy's color should share attributes.")
78
+ assert_not_same(object.location, copy.location, "Copy's location should not be same object.")
79
+ assert_equal(object.location.x, copy.location.x, "Copy's location should share attributes.")
80
+ assert_not_same(object.tags, copy.tags, "Copy's tag list should not be same object.")
81
+ assert_equal(object.tags[0], copy.tags[0], "Copy's tag list should share attributes.")
82
+ assert_not_equal(object.identifier, copy.identifier, "Copy's identifier should not be identical.")
83
+ assert_not_equal(object.name, copy.name, "Copy's name should not be identical.")
84
+ end
64
85
 
65
86
 
66
87
  end
@@ -115,6 +136,17 @@ class TestCreature < Test::Unit::TestCase
115
136
  end
116
137
 
117
138
 
139
+ def test_copy
140
+ creature = Creature.new
141
+ behavior1 = Behavior.new
142
+ behavior2 = Behavior.new
143
+ creature.behaviors << behavior1 << behavior2
144
+ copy = creature.copy
145
+ assert_not_same(creature.behaviors, copy.behaviors, "Copy's behavior list should not be same object.")
146
+ assert_not_same(creature.behaviors[0], copy.behaviors[0], "Behaviors in list should not be same objects.")
147
+ end
148
+
149
+
118
150
  end
119
151
 
120
152
 
@@ -476,7 +508,7 @@ class TestUtility < Test::Unit::TestCase
476
508
 
477
509
  end
478
510
 
479
- #TODO: Rewrite to pass environment.
511
+
480
512
  class TestBehavior < Test::Unit::TestCase
481
513
 
482
514
  #Always true.
@@ -551,5 +583,18 @@ class TestBehavior < Test::Unit::TestCase
551
583
 
552
584
  end
553
585
 
586
+ def test_copy
587
+ original = Behavior.new
588
+ action = TagAction.new("tag")
589
+ original.actions << action
590
+ condition = TagCondition.new("tag")
591
+ original.conditions << condition
592
+ copy = original.copy
593
+ assert_not_same(original.actions, copy.actions, "Copy's action list should not be same object.")
594
+ assert_not_same(original.actions[0], copy.actions[0], "Actions in list should not be same objects.")
595
+ assert_not_same(original.conditions, copy.conditions, "Copy's condition list should not be same object.")
596
+ assert_not_same(original.conditions[0], copy.conditions[0], "Conditions in list should not be same objects.")
597
+ end
598
+
554
599
 
555
600
  end
@@ -40,9 +40,9 @@ class TestActions < Test::Unit::TestCase
40
40
 
41
41
  #Create and populate an environment.
42
42
  def setup
43
- @actor = Creature.new('name', Location.new(0, 0))
44
- @target1 = GameObject.new('name', Location.new(1, 1))
45
- @target2 = GameObject.new('name', Location.new(-2, -2))
43
+ @actor = Creature.new('actor', Location.new(0, 0))
44
+ @target1 = Creature.new('target1', Location.new(1, 1))
45
+ @target2 = Creature.new('target2', Location.new(-2, -2))
46
46
  #Create an environment, and add the objects.
47
47
  @environment = Environment.new
48
48
  #Order is important - we want to act on target 1 first.
@@ -90,7 +90,7 @@ class TestActions < Test::Unit::TestCase
90
90
 
91
91
  #Create an ApproachAction with a 0-degree vector, turn rate of 40 degrees/sec.
92
92
  @actor.vector = Vector.new(0, 0)
93
- action = ApproachAction.new(Vector.new(1, 0), 40)
93
+ action = ApproachAction.new(40, Vector.new(1, 0))
94
94
  add_action(action, @actor)
95
95
  #Act.
96
96
  @environment.interact
@@ -109,7 +109,7 @@ class TestActions < Test::Unit::TestCase
109
109
  #Create an ApproachAction with a 0-degree vector, turn rate high enough to turn more than 45 degrees in 0.1 seconds.
110
110
  #Action should only turn as far as target.
111
111
  @actor.vector = Vector.new(0, 0)
112
- action = ApproachAction.new(Vector.new(1, 0), 500)
112
+ action = ApproachAction.new(500, Vector.new(1, 0))
113
113
  add_action(action, @actor)
114
114
  #Act.
115
115
  @environment.interact
@@ -129,7 +129,7 @@ class TestActions < Test::Unit::TestCase
129
129
 
130
130
  #Create a FleeAction with a 0-degree vector, turn rate of 40 degrees/sec.
131
131
  @actor.vector = Vector.new(0, 0)
132
- action = FleeAction.new(Vector.new(1, 0), 40)
132
+ action = FleeAction.new(40, Vector.new(1, 0))
133
133
  add_action(action, @actor)
134
134
  #Act.
135
135
  @environment.interact
@@ -147,7 +147,7 @@ class TestActions < Test::Unit::TestCase
147
147
  #Create a FleeAction with a 0-degree vector, turn rate high enough to turn more than 135 degrees in 0.1 seconds.
148
148
  #Action should turn directly away from target, but no farther.
149
149
  @actor.vector = Vector.new(0, 0)
150
- action = FleeAction.new(Vector.new(1, 0), 1400)
150
+ action = FleeAction.new(1400, Vector.new(1, 0))
151
151
  add_action(action, @actor)
152
152
  #Act.
153
153
  @environment.interact
@@ -253,4 +253,31 @@ class TestActions < Test::Unit::TestCase
253
253
  assert_in_delta(225.0, @target1.vector.pitch, REQUIRED_ACCURACY, "@target1's angle should be facing toward @actor.")
254
254
  end
255
255
 
256
+
257
+ #A BreedAction creates a new Creature by combining the actor's color and behaviors with another creature.
258
+ def test_breed_action
259
+ #Create two creatures with different colors and behaviors.
260
+ @actor.color = Color.new(1, 1, 1)
261
+ @target1.color = Color.new(0, 0, 0)
262
+ add_action(TagAction.new("1"), @actor)
263
+ add_action(TagAction.new("2"), @target1)
264
+ #Set actor's location to a non-standard place.
265
+ @actor.location = Location.new(33, 33)
266
+ #Create a BreedAction using the Environment, and act.
267
+ add_action(BreedAction.new(@environment, 0.2), @actor) #0.1 delay ensures modified Clock will trigger action on second operation.
268
+ @environment.interact
269
+ @environment.interact #Act twice to trigger action on actor (and only actor).
270
+ #Find child.
271
+ child = @environment.objects.last
272
+ #Ensure child's color is a mix of parents'.
273
+ assert_equal(Color.new(0.5, 0.5, 0.5), child.color)
274
+ #Ensure child's behaviors combine the parents', but exclude those with BreedActions.
275
+ assert_equal("1", child.behaviors[0].actions.first.tag)
276
+ assert_equal("2", child.behaviors[1].actions.first.tag)
277
+ assert_equal(2, child.behaviors.length)
278
+ #Ensure child appears at actor's location.
279
+ assert_equal(@actor.location.x, child.location.x)
280
+ assert_equal(@actor.location.y, child.location.y)
281
+ end
282
+
256
283
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: zyps
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.6.2
7
- date: 2007-11-01 00:00:00 -07:00
6
+ version: 0.6.3
7
+ date: 2007-11-08 00:00:00 -07:00
8
8
  summary: A game library for Ruby
9
9
  require_paths:
10
10
  - lib