zyps 0.1.1

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.
@@ -0,0 +1,80 @@
1
+ == Synopsis
2
+
3
+ Zyps - A simulation/game with autonomous creatures.
4
+
5
+
6
+ == Description
7
+
8
+ Zyps are small creatures with minds of their own. You can create dozens of Zyps, then decide how each will act. (Will it run away from the others? Follow them? Eat them for lunch?) You can combine rules and actions to create very complex behaviors.
9
+
10
+
11
+ == Requirements
12
+
13
+ * Ruby: http://www.ruby-lang.org
14
+ * Ruby-GNOME2: http://ruby-gnome2.sourceforge.jp
15
+ * Rake (to build from source): http://rake.rubyforge.org
16
+
17
+
18
+ == Installation
19
+
20
+ Make sure you have administrative privileges, then type the following at a command line:
21
+
22
+ gem install zyps
23
+
24
+
25
+ == Usage
26
+
27
+ Ensure Ruby-GNOME2 is installed and working.
28
+
29
+ To see the demo, at a command line, type:
30
+
31
+ zyps_demo
32
+
33
+
34
+ == Development
35
+
36
+ Source code and documentation are available via the project site (http://jay.mcgavren.com/zyps).
37
+
38
+ Once downloaded, change into the project directory. For a list of targets, run:
39
+
40
+ rake -T
41
+
42
+ To build a gem:
43
+
44
+ rake
45
+
46
+ To see the demo:
47
+
48
+ rake demo
49
+
50
+ To create a "doc" subdirectory with API documentation:
51
+
52
+ rake rdoc
53
+
54
+ Also see "bin/zyps_demo" and the "test" subfolder in the project directory for sample code.
55
+
56
+
57
+ == Thanks
58
+
59
+ The GUI is provided via Ruby-GNOME2. Thanks to its authors for their considerable effort in making their library easy to use.
60
+
61
+
62
+ == Author
63
+
64
+ Copyright 2007 Jay McGavren, jay@mcgavren.com
65
+
66
+
67
+ == License
68
+
69
+ Zyps is free software; you can redistribute it and/or modify
70
+ it under the terms of the GNU Lesser General Public License as published by
71
+ the Free Software Foundation; either version 3 of the License, or
72
+ (at your option) any later version.
73
+
74
+ This program is distributed in the hope that it will be useful,
75
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
76
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
77
+ GNU General Public License for more details.
78
+
79
+ You should have received a copy of the GNU Lesser General Public License
80
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,41 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ # Copyright 2007 Jay McGavren, jay@mcgavren.com.
4
+ #
5
+ # This file is part of Zyps.
6
+ #
7
+ # Zyps is free software; you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+
21
+ begin
22
+ require 'zyps'
23
+ require 'zyps/views/trails'
24
+ rescue LoadError
25
+ require 'rubygems'
26
+ require 'zyps'
27
+ require 'zyps/views/trails'
28
+ end
29
+
30
+
31
+ begin
32
+
33
+ puts "This is just a placeholder for the final application."
34
+ puts "To see a demonstration of the library, please run 'zyps_demo'."
35
+
36
+ rescue => exception
37
+
38
+ #Print error to STDERR and exit with an abnormal status.
39
+ abort "Error: " + exception.message + exception.backtrace.join("\n")
40
+
41
+ end
@@ -0,0 +1,525 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ # Copyright 2007 Jay McGavren, jay@mcgavren.com.
4
+ #
5
+ # This file is part of Zyps.
6
+ #
7
+ # Zyps is free software; you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+
21
+ begin
22
+ require 'zyps'
23
+ require 'zyps/views/trails'
24
+ rescue LoadError
25
+ require 'rubygems'
26
+ require 'zyps'
27
+ require 'zyps/views/trails'
28
+ end
29
+
30
+
31
+
32
+
33
+ class Demo
34
+
35
+ #The view width.
36
+ WIDTH = 400
37
+ #The view height.
38
+ HEIGHT = 300
39
+ #Number of frames to draw per demo.
40
+ FRAME_COUNT = 80
41
+
42
+ #Set up a window, a canvas, and an object environment, then run the given block.
43
+ def demo
44
+
45
+ #Create a window, and set GTK up to quit when it is closed.
46
+ window = Gtk::Window.new
47
+ window.signal_connect("delete_event") {false}
48
+ window.signal_connect("destroy") {Gtk.main_quit}
49
+
50
+ #Add view to window.
51
+ @view = TrailsView.new(WIDTH, HEIGHT)
52
+ window.add(@view.canvas)
53
+ window.show_all
54
+
55
+ #Create environment.
56
+ @environment = Environment.new
57
+
58
+ #Point view at environment.
59
+ @environment.add_observer(@view)
60
+
61
+ #Run the given block.
62
+ yield
63
+
64
+ #Activate the GUI.
65
+ Gtk.main
66
+
67
+ #A divider for explanation text between demos.
68
+ say("-" * 30)
69
+
70
+ end
71
+
72
+
73
+ #Animate an environment for a given number of frames.
74
+ def animate(frame_count)
75
+ begin
76
+ (1..frame_count).each do |i|
77
+ @environment.interact
78
+ #Delay 1/60th second to avoid screen flicker.
79
+ sleep 1.0 / 60.0
80
+ end
81
+ rescue Exception => exception
82
+ puts exception, exception.backtrace
83
+ end
84
+ end
85
+
86
+
87
+ #Populate an environment with the given number of creatures.
88
+ def populate(environment, count = 50)
89
+ count.times do |i|
90
+ multiplier = i / count.to_f
91
+ environment.objects << Creature.new(
92
+ i,
93
+ Location.new(multiplier * @view.width, multiplier * @view.height),
94
+ Color.new(multiplier, 1 - multiplier, multiplier / 2 + 0.5),
95
+ Vector.new(100 * multiplier, multiplier * 360)
96
+ )
97
+ end
98
+ end
99
+
100
+
101
+ #Explain what's going on to the user.
102
+ def say(phrase)
103
+ puts phrase
104
+ end
105
+
106
+
107
+ #Demonstrates drawing an environment and changing its size.
108
+ def test_render
109
+
110
+ thread = Thread.new do
111
+
112
+ say("The things that populate an environment are called GameObjects. Each object has:")
113
+ object = GameObject.new
114
+ say("...a name")
115
+ object.name = "Clancy"
116
+ say("...a Location with x and y coordiates")
117
+ object.location = Location.new(@view.width/2, @view.height/2)
118
+ say("...a Color with red, green and blue components ranging from 0 to 1")
119
+ object.color = Color.new(1, 0, 0)
120
+ say("...and a Vector giving its speed and an angle from 0 to 360.")
121
+ object.vector = Vector.new(10, 45)
122
+
123
+ say("Once your object is ready, add it to the environment.")
124
+ @environment.objects << object
125
+
126
+ say("Call a view's render(objects) method to draw the objects.")
127
+ say("Call an environment's interact() method to have the objects in it move around.")
128
+ say("Our demo's animate() method does both these things for us.")
129
+ animate(FRAME_COUNT)
130
+
131
+ say("Let's add a couple more objects with different colors and vectors.")
132
+ @environment.objects << GameObject.new(nil, Location.new(@view.width/2, @view.height/2), Color.new(0, 1, 0), Vector.new(20, 135))
133
+ @environment.objects << GameObject.new(nil, Location.new(@view.width/2, @view.height/2), Color.new(0, 0, 1), Vector.new(30, 225))
134
+ animate(FRAME_COUNT)
135
+
136
+ say("The viewing area can be resized at any time via its width and height attributes.")
137
+ @view.width += 100
138
+ @view.height += 100
139
+ animate(FRAME_COUNT)
140
+
141
+ say("TrailsView lets you set the thickness and length of the trails as well.")
142
+ @view.trail_width = 10
143
+ @view.trail_length = 10
144
+ animate(FRAME_COUNT)
145
+
146
+ end
147
+
148
+ end
149
+
150
+
151
+ #Demonstrates environmental factors by adding gravity to the environment.
152
+ def test_environmental_factors
153
+
154
+ populate(@environment)
155
+
156
+ thread = Thread.new do
157
+
158
+ say("Without gravity, objects just travel on forever.")
159
+ animate(FRAME_COUNT)
160
+
161
+ say("Let's add a new EnvironmentalFactor to simulate gravity.")
162
+ gravity = EnvironmentalFactor.new
163
+
164
+ say("We create a Behavior the EnvironmentalFactor will follow.")
165
+ accelerate = Behavior.new
166
+ say("The behavior will have a single action: to accelerate objects toward the 'ground' at 9.8 meters/second.")
167
+ accelerate.actions << lambda {|gravity, target| target.vector.y += 9.8}
168
+ say("We add the behavior to the EnvironmentalFactor.")
169
+ gravity.behaviors << accelerate
170
+ say("Then we add the EnvironmentalFactor to the Environment.")
171
+ @environment.environmental_factors << gravity
172
+
173
+ say("Everything immediately drops.")
174
+ animate(FRAME_COUNT)
175
+
176
+ end
177
+
178
+ end
179
+
180
+
181
+ #Demonstrates creature behaviors.
182
+ def test_behaviors
183
+
184
+ populate(@environment)
185
+
186
+ thread = Thread.new do
187
+
188
+ say("Let's add a behavior to our creatures.")
189
+ chase = Behavior.new
190
+
191
+ say("We'll have them head straight toward their target.")
192
+ chase.actions << lambda do |creature, target|
193
+ angle_to_target = Utility.find_angle(creature.location, target.location)
194
+ creature.vector.pitch = angle_to_target
195
+ end
196
+
197
+ say("So that they don't target every creature on the screen, we'll add a condition to the behavior saying the target must have the label 'food'.")
198
+ chase.conditions << lambda {|creature, target| target.tags.include?('food')}
199
+
200
+ say("We'll apply this behavior to all creatures currently in the environment.")
201
+ @environment.objects.each {|creature| creature.behaviors << chase}
202
+ animate(FRAME_COUNT)
203
+
204
+ say("Then we'll toss a piece of food (a GameObject with the label 'food') into the environment.")
205
+ @environment.objects << GameObject.new(
206
+ "target",
207
+ Location.new(@view.width / 2, @view.height / 2),
208
+ Color.new(1, 1, 1),
209
+ Vector.new(50, 315),
210
+ 0, #Age.
211
+ ["food"] #Tags.
212
+ )
213
+
214
+ say("Let's see what the creatures do.")
215
+ animate(FRAME_COUNT)
216
+
217
+ end
218
+
219
+ end
220
+
221
+
222
+ #A Creature that changes the colors of other objects.
223
+ class Morpher < Creature
224
+ def initialize(*arguments)
225
+ super
226
+ morph = Behavior.new
227
+ #Shift the target's color to match the creature's.
228
+ morph.actions << lambda do |creature, target|
229
+ target.color.red += 0.1 if target.color.red < creature.color.red
230
+ target.color.green += 0.1 if target.color.green < creature.color.green
231
+ target.color.blue += 0.1 if target.color.blue < creature.color.blue
232
+ end
233
+ #Act only on nearby targets.
234
+ morph.conditions << lambda {|creature, target| Utility.find_distance(creature.location, target.location) < 50}
235
+ @behaviors << morph
236
+ end
237
+ end
238
+
239
+ #Demonstrates changing object colors.
240
+ def test_change_color
241
+
242
+ populate(@environment)
243
+
244
+ say("Creatures can influence any attribute of their target, such as its color.")
245
+ say("This demo includes a Morpher class, which is a type of Creature.")
246
+ say("Morphers are created with a single behavior, which shifts the color of any nearby target to match the Morpher's color.")
247
+
248
+ say("Let's place a red Morpher...")
249
+ @environment.objects << Morpher.new(nil, Location.new(0, 100), Color.new(1, 0, 0), Vector.new(100, 0))
250
+ say("a green one...")
251
+ @environment.objects << Morpher.new(nil, Location.new(0, 150), Color.new(0, 1, 0), Vector.new(200, 0))
252
+ say("and a blue one...")
253
+ @environment.objects << Morpher.new(nil, Location.new(0, 200), Color.new(0, 0, 1), Vector.new(300, 0))
254
+
255
+ say("And see what they do.")
256
+ thread = Thread.new {animate(FRAME_COUNT)}
257
+
258
+ end
259
+
260
+
261
+ #Demonstrates altering object speed.
262
+ def test_accelerate
263
+
264
+ populate(@environment)
265
+
266
+ thread = Thread.new do
267
+
268
+
269
+ say("Many actions need to happen smoothly over time, such as accelerating at a given rate.")
270
+
271
+ say("Here are some Creatures, just plodding along.")
272
+ animate(FRAME_COUNT / 4)
273
+ say("We're going to have them pick up the pace.")
274
+
275
+ say("A Clock helps track the passage of time (obviously).")
276
+ say("We'll create a separate Clock object for each creature.")
277
+ clocks = Hash.new {|h, k| h[k] = Clock.new}
278
+
279
+ say("Our acceleration behavior is going to check to see how long it's been since it last took effect.")
280
+ say("It will multiply that time by a given rate, say 100 meters/second.")
281
+ say("Let's say it's been 0.1 seconds since this creature last accelerated.")
282
+ say("100 times 0.1 is 10.")
283
+ say("So we should add 10 meters/second to the creature's speed.")
284
+ accelerate = Behavior.new
285
+ accelerate.actions << lambda do |creature, target|
286
+ #Accelerate the appropriate amount for the elapsed time.
287
+ creature.vector.speed += 100 * clocks[creature].elapsed_time
288
+ end
289
+
290
+ say("We add acceleration to all the creatures...")
291
+ @environment.objects.each {|creature| creature.behaviors << accelerate}
292
+
293
+ say("And watch them rocket away.")
294
+ animate(FRAME_COUNT)
295
+
296
+ end
297
+
298
+ end
299
+
300
+
301
+ #Demonstrates altering object vectors.
302
+ def test_turn
303
+
304
+ populate(@environment, 20)
305
+
306
+ thread = Thread.new do
307
+
308
+ say("Turning smoothly requires tracking the rate of the turn as well.")
309
+ animate(FRAME_COUNT / 2)
310
+
311
+ say("Again, we keep a separate Clock for each Creature.")
312
+ clocks = Hash.new {|h, k| h[k] = Clock.new}
313
+
314
+ say("Our turn behavior follows the same principle as accelerating.")
315
+ say("We see how many seconds have elapsed, then multiply that by the turn rate.")
316
+ say("We add the result to the Vector angle.")
317
+ turn = Behavior.new
318
+ turn.actions << lambda do |creature, target|
319
+ #Turn the appropriate amount for the elapsed time.
320
+ creature.vector.pitch += 100 * clocks[creature].elapsed_time
321
+ end
322
+
323
+ say("We add the behavior to each Creature...")
324
+ @environment.objects.each {|creature| creature.behaviors << turn}
325
+
326
+ say("And watch things spiral out of control.")
327
+ animate(FRAME_COUNT)
328
+
329
+ end
330
+
331
+ end
332
+
333
+
334
+ #Demonstrates adding vectors.
335
+ def test_approach
336
+
337
+ populate(@environment, 50)
338
+
339
+ say("When your car skids on ice, you might steer in a different direction, but you're going to keep following your original vector for a while.")
340
+ say("Adding vectors together lets us simulate this.")
341
+
342
+ say("We'll keep a separate Vector for each Creature to track the direction it's 'steering' in.")
343
+ headings = Hash.new {|h, k| h[k] = Vector.new(k.vector.speed, k.vector.pitch)}
344
+
345
+ say("We'll only allow them to turn a maximum of 20 degrees in either direction.")
346
+ max_turn_angle = 20
347
+
348
+ say("We create a Behavior which adds the Vector the creature WANTS to follow to the Vector it's ACTUALLY following.")
349
+ approach = Behavior.new
350
+ approach.actions << lambda do |creature, target|
351
+
352
+ #Find the difference between the current heading and the angle to the target.
353
+ turn_angle = Utility.find_angle(creature.location, target.location) - headings[creature].pitch
354
+
355
+ #If the angle is the long way around from the current heading, change it to the smaller angle.
356
+ if turn_angle > 180 then
357
+ turn_angle -= 360.0
358
+ elsif turn_angle < -180 then
359
+ turn_angle += 360.0
360
+ end
361
+
362
+ #If turn angle is greater than allowed turn speed, reduce it.
363
+ turn_angle = Utility.constrain_value(turn_angle, max_turn_angle)
364
+
365
+ #Turn the appropriate amount.
366
+ headings[creature].pitch += turn_angle
367
+
368
+ #Apply the heading to the creature's movement vector.
369
+ creature.vector += headings[creature]
370
+
371
+ end
372
+
373
+ say("We add a condition that it should only target its prey.")
374
+ approach.conditions << lambda do |creature, target|
375
+ target.tags.include?('prey')
376
+ end
377
+
378
+ say("We add the behavior to all creatures...")
379
+ @environment.objects.each {|creature| creature.behaviors << approach}
380
+
381
+ say("Add a target...")
382
+ @environment.objects << Creature.new(
383
+ "target",
384
+ Location.new(@view.width / 2, @view.height / 2),
385
+ Color.new(1, 1, 1),
386
+ Vector.new(3, 0),
387
+ 0, #Age.
388
+ ["prey"] #Tags.
389
+ )
390
+
391
+ say("And watch them all TRY to catch it.")
392
+ thread = Thread.new {animate(FRAME_COUNT)}
393
+
394
+ end
395
+
396
+
397
+ #Demonstrates turning away from an object, instead of toward it.
398
+ def test_flee
399
+
400
+ populate(@environment, 20)
401
+
402
+ say("Fleeing from something is just like approaching it, but we head in the OPPOSITE direction.")
403
+ say("Just get the angle toward the object, then add 180 degrees.")
404
+
405
+ #Keep a separate heading for each object.
406
+ headings = Hash.new {|h, k| h[k] = Vector.new(k.vector.speed, k.vector.pitch)}
407
+
408
+ #Create a behavior.
409
+ max_turn_angle = 20
410
+ flee = Behavior.new
411
+ flee.actions << lambda do |creature, target|
412
+
413
+ #Find the difference between the current heading and the angle AWAY from the target.
414
+ turn_angle = Utility.find_angle(creature.location, target.location) - headings[creature].pitch + 180
415
+
416
+ #If the angle is the long way around from the current heading, change it to the smaller angle.
417
+ if turn_angle > 180 then
418
+ turn_angle -= 360.0
419
+ elsif turn_angle < -180 then
420
+ turn_angle += 360.0
421
+ end
422
+
423
+ #If turn angle is greater than allowed turn speed, reduce it.
424
+ turn_angle = Utility.constrain_value(turn_angle, max_turn_angle)
425
+
426
+ #Turn the appropriate amount for the elapsed time.
427
+ headings[creature].pitch += turn_angle
428
+
429
+ #Apply the heading to the creature's movement vector.
430
+ creature.vector += headings[creature]
431
+
432
+ end
433
+
434
+ #Flee from only the creature's predator.
435
+ flee.conditions << lambda do |creature, target|
436
+ target.tags.include?('predator')
437
+ end
438
+
439
+ #Add behavior to creatures.
440
+ @environment.objects.each {|creature| creature.behaviors << flee}
441
+
442
+ #Add a target.
443
+ @environment.objects << Creature.new(
444
+ "lion",
445
+ Location.new(@view.width / 2, @view.height / 2),
446
+ Color.new(1, 1, 1),
447
+ Vector.new(3, 0),
448
+ 0, #Age.
449
+ ["predator"] #Tags.
450
+ )
451
+
452
+ thread = Thread.new {animate(FRAME_COUNT)}
453
+
454
+ end
455
+
456
+
457
+ #A Behavior that destroys the target.
458
+ class Destroy < Behavior
459
+ #Environment from which targets will be removed.
460
+ attr_accessor :environment
461
+ def initialize(actions = [], conditions = [], environment = Environment.new)
462
+ super(actions, conditions)
463
+ @environment = environment
464
+ #"Kill" target.
465
+ self.actions << lambda do |creature, target|
466
+ @environment.objects.delete(target)
467
+ end
468
+ #Act only if target is close.
469
+ self.conditions << lambda do |creature, target|
470
+ Utility.find_distance(creature.location, target.location) < 25
471
+ end
472
+ end
473
+ end
474
+
475
+
476
+ #Demonstrates keeping a reference to an Environment so a Creature can alter it.
477
+ def test_destroy
478
+
479
+ populate(@environment)
480
+
481
+ say("Most games are all about destruction, but there hasn't been much so far.")
482
+ say("Let's create a creature that causes some havoc.")
483
+ delinquent = Creature.new(nil, Location.new(0, 150), Color.new(0, 1, 0), Vector.new(200, 0))
484
+
485
+ say("This demo code includes a subclass of Behavior, called Destroy.")
486
+ destroy = Destroy.new
487
+
488
+ say("We'll destroy our targets by removing them from their environment.")
489
+ say("Creatures and their Behaviors normally know nothing about the Environment they belong to, so we added an environment attribute to Destroy.")
490
+ say("Destroy finds the target in Environment.objects and removes it.")
491
+ destroy.environment = @environment
492
+
493
+ say("Add the Destroy instance to the creature's behaviors...")
494
+ delinquent.behaviors << destroy
495
+
496
+ say("Drop the creature into the actual environment...")
497
+ @environment.objects << delinquent
498
+
499
+ say("And - chomp!")
500
+ thread = Thread.new {animate(FRAME_COUNT)}
501
+
502
+ end
503
+
504
+
505
+ #Run all the demos.
506
+ def main
507
+ say "After each demo, close the window to proceed."
508
+ say("-" * 30)
509
+ demo {test_render}
510
+ demo {test_environmental_factors}
511
+ demo {test_behaviors}
512
+ demo {test_change_color}
513
+ demo {test_accelerate}
514
+ demo {test_turn}
515
+ demo {test_approach}
516
+ demo {test_flee}
517
+ demo {test_destroy}
518
+ end
519
+
520
+
521
+ end
522
+
523
+
524
+ #Run the demos.
525
+ Demo.new.main