seh 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,6 +1,5 @@
1
1
  *.gem
2
2
  .bundle
3
- Gemfile.lock
4
3
  pkg/*
5
4
  *~
6
5
  .yardoc/*
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- group :development, :test do
4
- gem 'rspec'
3
+ group :development do
4
+ gem "rspec", "~> 2.12.0"
5
5
  end
6
6
 
7
7
  gemspec
@@ -0,0 +1,25 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ seh (0.2.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.1.3)
10
+ rspec (2.12.0)
11
+ rspec-core (~> 2.12.0)
12
+ rspec-expectations (~> 2.12.0)
13
+ rspec-mocks (~> 2.12.0)
14
+ rspec-core (2.12.2)
15
+ rspec-expectations (2.12.1)
16
+ diff-lcs (~> 1.1.3)
17
+ rspec-mocks (2.12.1)
18
+
19
+ PLATFORMS
20
+ java
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ rspec (~> 2.12.0)
25
+ seh!
data/README.md CHANGED
@@ -1,13 +1,87 @@
1
1
 
2
- # seh: structured event handler
2
+ # Seh
3
+
4
+ Structured event handler. Pure ruby event handling similar to w3c dom events. Check out `examples/epic_battle.rb`
5
+
6
+ ## Why Seh?
7
+
8
+ Seh was developed as a dependency of my ruby game engine. I couldn't find another ruby event library with the features I wanted.
9
+
10
+ You might want to use Seh if:
11
+ * You need events/hooks/triggers/slots+signals
12
+ * You need a synchronous system, because an event must be fully resolved before things can continue
13
+ * An object/target/actor/thing might be affected by other things it doesn't know about
14
+ * Your objects have underlying hierarchical / graph / observer relationships
15
+ * You're hoping for *complex emergent behaviors*
16
+
17
+ You probably don't want to use Seh if:
18
+ * You need distributed or asynchronous events/hooks/triggers/slots+signals
19
+ * You're thinking "gee, this seems complicated, I really just need simple hooks" -> Seh is probably over-engineering for you
20
+
21
+ ## Features
22
+
23
+ Seh is driven by `Seh::Event` objects targeting objects of type `Seh::EventTarget`. Events are typed using `Seh::EventType` (which is mostly used implicitly).
24
+
25
+ In most event systems, such as w3c dom events in a web browser, events are handled by child nodes before the event bubbles to ancestor nodes. In Seh, each `EventTarget` which sees the `Event` may add callbacks to the event, *before any callbacks on the event are executed*. After each `EventTarget` has been visited, the `Event` executes its callbacks. This allows an ancestor object to affect an outcome on a descendant.
26
+
27
+ Major Features
28
+ * `EventTarget` is a mixin allowing any object to be the target of an `Event`
29
+ * Create an `Event`, add some targets - objects which mixin `EventTarget` - and dispatch the event. Each `Event` is one-use-only
30
+ * `Event` has a list of types, e.g. :snow, :bad_weather, :sunshine; Types may be any object which defines equality ==
31
+ * Bind callbacks to an `EventTarget`, e.g. `my_target.bind(:snow) { "It's snowing!" }`
32
+ * Bind target callbacks using a type boolean expression, e.g. `my_target.bind( Seh::or :hurricane, Seh::and(:rain, :sunshine, Seh::not(:cold)) ) {
33
+ "It's either a hurricane, or sunny and raining and not cold." }`
34
+ * Disconnect callback binds, `my_bind = my_target.bind(:only_needed) { "for awhile" }; my_bind.disconnect`
35
+ * `EventTarget#observers` defaults to `[]`, and can be overridden to create an object graph. Each `EventTarget` recursively reachable by `EventTarget#observers` receives the event as if it was a top-level target.
36
+ * `Event` makes it easy for events to "inherit" from one another, so that you may develop a rich event hierarchy on your application side. Note that there's no real ruby class inheritance, e.g. in `examples/event/`, the `Event::damage()` method calls `Event::hostile()`, making each damage event a hostile event.
37
+ * `Event` inherits from `OpenStruct` to easily define attributes on the event
38
+ * Event stages provide fine-grained control over callbacks
39
+
40
+ ## Understanding Event Stages & Callbacks
41
+
42
+ See `Seh::Event#dispatch`. When `#dispach` is called:
43
+
44
+ 1. determine the full set of targets affected by this event
45
+ 2. run callbacks on targets which match this event's types
46
+ 3. run stage callbacks contained in this event; typically targets will append stage callbacks to this event using Event#bind, #start, #finish
47
+ 4. Callback execution order: (1) start callbacks; (2) stage callbacks - in the order stages were added to the event; (3) finish callbacks
48
+
49
+ ```ruby
50
+ target = Seh::EventTarget::Default.new # Default includes EventTarget
51
+ target.bind(:fireball) do |event|
52
+ puts "1"
53
+ event.finish { puts "4" }
54
+ end
55
+
56
+ event = Seh::Event.new
57
+ event.type :fireball
58
+ event.target target
59
+ event.start { puts "2" }
60
+
61
+ event.add_stage :burn_enemy
62
+ event.bind(:burn_enemy) { puts "3" }
63
+
64
+ event.dispatch
65
+ ```
66
+
67
+ The output of the above code is:
68
+
69
+ ```ruby
70
+ 1
71
+ 2
72
+ 3
73
+ 4
74
+ ```
3
75
 
4
76
  ## Roadmap
5
- * documentation
6
- * unit tests (my bad)
7
- * examples
8
77
 
9
- ## v0.1.0
10
- seh API is fully implemented :)
78
+ I'm working on efficiency, so tens of thousands of events can be used each second in a game engine. There's a toy benchmark in 'rake benchmark'.
79
+
80
+ ## Release Notes
81
+
82
+ * v0.3.0 - updated api, significantly expanded test coverage, documentation, examples, released under the [BSD license](http://opensource.org/licenses/BSD-3-Clause).
83
+ * v0.1.0 - an older version of the API that's not backwards-compatible
84
+
85
+ ## License
11
86
 
12
- ## Contact
13
- Please send (welcome) feedback and bug reports to ryan.berckmans@gmail.com.
87
+ Seh is released under the [BSD license](http://opensource.org/licenses/BSD-3-Clause).
data/Rakefile CHANGED
@@ -1 +1,32 @@
1
+ require 'rake'
1
2
  require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
9
+ desc "run a simple Seh benchmark"
10
+ task :benchmark do
11
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/examples")
12
+ require 'seh'
13
+ require_relative 'examples/mob'
14
+ require_relative 'examples/event/damage'
15
+
16
+ bob = Mob.new
17
+ fred = Mob.new
18
+
19
+ damage_shield = Seh::EventTarget::Default.new
20
+ damage_shield.bind(:damage) { |event| event.start { event.damage_add -= 0 } }
21
+ bob.observers << damage_shield
22
+
23
+ fred.bind(:hostile) { "oh no, hostile on fred" }
24
+
25
+ start_time = Time.now
26
+ 100000.times do
27
+ e = Seh::Event.new
28
+ Event::damage e, bob, fred, 0
29
+ e.dispatch
30
+ end
31
+ puts "took: #{Time.now - start_time}"
32
+ end
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'seh'
4
+ require_relative 'event'
5
+ require_relative 'event_color'
6
+
7
+ # run ruby examples/epic_battle.rb
8
+ #
9
+ # The output is colorized to help visualize the Seh::Event objects.
10
+ #
11
+ # This example uses most of the Seh API and simulates a battle between two combatants, Fred and Jim.
12
+ # Fred and Jim will continue melee-attacking each other until one is dead. Each has abilities which dynamically influence the result of the combat.
13
+ #
14
+ # examples/event/hostile.rb is a template for a Seh::Event representing a hostile action
15
+ #
16
+ # examples/event/damage.rb is a template for a Seh::Event representing one combatant doing damage to another. damage.rb calls hostile.rb, making each damage event also a hostile event.
17
+ #
18
+ # examples/event/melee_attack.rb is a template for a Seh::Event representing an attempted melee attack by one combatant on another. A successfully melee attack will create and dispach a new damage event, representing the damage inflicted by the strike.
19
+ #
20
+ # A melee attack may be thwarted by a dodge or riposte, which are modeled as callbacks on melee_attack events. The melee attack event has no direct knowledge that it may be affected by a dodge or riposte.
21
+ #
22
+ # Damage may be reduced by the shield of the ancients ability, or reflected by the reflexive barrier ability. The damage event has no direct knowledge of these abilities, which are modeled as callbacks.
23
+ #
24
+ # The battle is orated by BenevolentOverlord, a singleton which is wired to Combatant#observers. This causes BenevolentOverlord to receive each event targeting any instance of Combatant.
25
+ #
26
+
27
+ # An observer which reports on the battle. BenevolentOverlord sees each event affecting any Combatant due to Combatant#observers
28
+ class BenevolentOverlord
29
+ include Seh::EventTarget
30
+ def initialize
31
+ melee_battle_damage_callbacks
32
+ melee_battle_hostile_callbacks
33
+ melee_battle_melee_attack_callbacks
34
+ end
35
+
36
+ def melee_battle_melee_attack_callbacks
37
+ bind :melee_attack do |event|
38
+ event.bind :melee_miss do
39
+ puts_color_event event.object_id, "#{event.attacker} misses #{event.defender}. (event id #{event.object_id})"
40
+ end
41
+
42
+ event.bind :melee_hit do
43
+ puts_color_event event.object_id, "#{event.attacker} successfully hits #{event.defender} for an initial damage of #{event.attack_damage}! (event id #{event.object_id})"
44
+ end
45
+ end
46
+ end
47
+
48
+ def melee_battle_damage_callbacks
49
+ bind :damage do |event|
50
+ event.start { puts_color_event event.object_id, "#{event.damager} tries to do #{event.damage} damage to #{event.receiver} (event id #{event.object_id})" }
51
+ event.finish { puts_color_event event.object_id, "#{event.damager} did #{event.damage > 0 ? event.damage : 0} damage to #{event.receiver} (event id #{event.object_id})" }
52
+ end
53
+ end
54
+
55
+ def melee_battle_hostile_callbacks
56
+ bind :hostile do |event|
57
+ # hostile is pretty spammy so comment it out
58
+ # event.start { puts_color_event event.object_id, "hostile start: #{event.aggressor} on #{event.aggressee} (event id #{event.object_id})" }
59
+ # event.finish { puts_color_event event.object_id, "hostile finish: #{event.aggressor} on #{event.aggressee} (event id #{event.object_id})" }
60
+ end
61
+ end
62
+
63
+ INSTANCE = BenevolentOverlord.new
64
+ end
65
+
66
+ class Combatant
67
+ include Seh::EventTarget
68
+
69
+ attr_accessor :name, :hp, :affects
70
+ def initialize name
71
+ @name = name
72
+ @hp = 40
73
+ @affects = {}
74
+ end
75
+
76
+ def observers
77
+ [BenevolentOverlord::INSTANCE]
78
+ end
79
+
80
+ def to_s
81
+ @name
82
+ end
83
+ end
84
+
85
+ def coin_flip
86
+ Random.rand(2) > 0
87
+ end
88
+
89
+ def roll_damage
90
+ Random.rand(40) + 1
91
+ end
92
+
93
+ class CombatantDied < Exception
94
+ attr_reader :combatant
95
+ def initialize combatant
96
+ @combatant = combatant
97
+ end
98
+ end
99
+
100
+ def melee_attack attacker, defender
101
+ melee_attack_event = Seh::Event.new
102
+ Event::melee_attack melee_attack_event, attacker, defender, coin_flip, roll_damage
103
+ melee_attack_event.dispatch
104
+ raise CombatantDied, attacker if attacker.hp < 1
105
+ raise CombatantDied, defender if defender.hp < 1
106
+ end
107
+
108
+ ############################
109
+ # rpg/effects.rb
110
+
111
+ # grant combatant the ability to dodge melee attacks
112
+ def dodge combatant
113
+ puts "#{combatant} gains the ability to dodge"
114
+ combatant.bind :melee_attack do |event|
115
+ event.bind :melee_determine_hit do
116
+ if event.defender == combatant && event.attack_hit && coin_flip
117
+ event.attack_hit = false
118
+ event.abort
119
+ puts_color_event event.object_id, "#{event.attacker} narrowly misses #{event.defender}, who dodges! (event id #{event.object_id})"
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ # grant combatant the ability to riposte, i.e. to block and counterattack a melee attack
126
+ def riposte combatant
127
+ puts "#{combatant} gains the ability to riposte"
128
+ combatant.bind :melee_attack do |event|
129
+ event.bind :melee_determine_hit do
130
+ if event.defender == combatant && event.attack_hit && coin_flip && coin_flip
131
+ event.attack_hit = false
132
+ event.abort
133
+ puts_color_event event.object_id, "#{event.attacker} attacks #{event.defender}, who parries and responds with a lightning-fast riposte! (event id #{event.object_id})"
134
+ melee_attack event.defender, event.attacker
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # reduce incoming damage
141
+ SHIELD_AMOUNT = 12
142
+ def shield_of_the_ancients combatant
143
+ return if combatant.affects.key? :shield_of_ancients
144
+ puts "#{combatant} casts shield of the ancients"
145
+ combatant.affects[:shield_of_ancients] = combatant.bind(:damage) do |event|
146
+ event.bind :damage_modify do
147
+ next unless event.receiver == combatant
148
+ event.damage_reduction += SHIELD_AMOUNT
149
+ puts_color_event event.object_id, "#{combatant}'s shield of the ancients reduces his damage taken by #{SHIELD_AMOUNT} (event id #{event.object_id})"
150
+ end
151
+ end
152
+ end
153
+
154
+ # for one damage event, reverse the damage back to the damager
155
+ def reflexive_barrier combatant
156
+ return if combatant.affects.key? :reflexive_barrier
157
+ puts "#{combatant} casts reflexive barrier"
158
+ combatant.affects[:reflexive_barrier] = combatant.bind(:damage) do |event|
159
+ event.bind :damage_targets do
160
+ next unless event.receiver == combatant
161
+ temp = event.damager
162
+ event.damager = event.receiver
163
+ event.receiver = temp
164
+ puts_color_event event.object_id, "#{combatant} uses his reflexive barrier to reflect damage back at #{event.receiver} (event id #{event.object_id})"
165
+ combatant.affects[:reflexive_barrier].disconnect
166
+ combatant.affects.delete :reflexive_barrier
167
+ end
168
+ end
169
+ end
170
+
171
+ # disable the next hostile action when the aggressee is the combatant
172
+ def serenity combatant
173
+ return if combatant.affects.key? :serenity
174
+ puts "#{combatant}: casting serenity"
175
+ combatant.affects[:serenity] = combatant.bind :hostile do |event|
176
+ next unless event.aggressee == combatant
177
+ event.abort
178
+ puts_color_event event.object_id, "#{combatant}'s serenity prevents hostile action by #{event.aggressor} (event id #{event.object_id})"
179
+ combatant.affects[:serenity].disconnect
180
+ combatant.affects.delete :serenity
181
+ end
182
+ end
183
+
184
+ def permanent_affects combatant
185
+ shield_of_the_ancients combatant if coin_flip
186
+ dodge combatant if coin_flip
187
+ riposte combatant if coin_flip
188
+ end
189
+
190
+ def onetime_affects combatant
191
+ reflexive_barrier combatant if coin_flip && coin_flip && coin_flip
192
+ serenity combatant if coin_flip && coin_flip && coin_flip
193
+ end
194
+
195
+ ############################
196
+ # put example all together
197
+
198
+ def run
199
+ def status *combatants
200
+ s = ''
201
+ combatants.each { |m| s += m.to_s + ": " + m.hp.to_s + 'hp; ' }
202
+ puts s
203
+ end
204
+
205
+ 2.times do
206
+ fred = Combatant.new "Fred"
207
+ jim = Combatant.new "Jim"
208
+ puts "#{fred} and #{jim} rush into furious combat!"
209
+
210
+ permanent_affects fred
211
+ permanent_affects jim
212
+
213
+ begin
214
+ status fred, jim
215
+
216
+ onetime_affects fred
217
+ onetime_affects jim
218
+
219
+ if coin_flip
220
+ first = fred
221
+ second = jim
222
+ else
223
+ first = jim
224
+ second = fred
225
+ end
226
+
227
+ melee_attack first, second
228
+ melee_attack second, first
229
+ melee_attack first, second if coin_flip
230
+ melee_attack second, first if coin_flip
231
+ rescue CombatantDied => died
232
+ puts "#{died.combatant} died!\n\n"
233
+ break
234
+ end while true
235
+ puts "Myerael, arch-angel of battle, reincarnates #{fred} and #{jim}.."
236
+ end
237
+ puts "..and promptly slays them. The end."
238
+ end
239
+
240
+ run if $0 == __FILE__
@@ -0,0 +1 @@
1
+ Dir[File.dirname(__FILE__) + '/event/*.rb'].each {|file| require file }
@@ -0,0 +1,31 @@
1
+ require "seh"
2
+ require_relative "hostile"
3
+
4
+ module Event
5
+ class << self
6
+ def damage event, damager, receiver, damage
7
+ hostile event, damager, receiver
8
+
9
+ event.target damager, receiver
10
+ event.type :damage
11
+ event.damager = damager
12
+ event.receiver = receiver
13
+
14
+ event.damage = damage
15
+ event.damage_add = 0 # bonus damage
16
+ event.damage_multiply = 1.0 # % bonus damage and stacks with damage_add
17
+ event.damage_reduction = 0 # directly subtracted from semifinal damage
18
+
19
+ event.add_stage :damage_targets # use stage to modify event.damager/receiver
20
+ event.add_stage :damage_modify # use stage to modify event.damage_add, damage_multiply, damage_reduction
21
+ event.add_stage :damage_apply # damage is applied during this stage, do not interfere
22
+
23
+ event.bind :damage_apply do
24
+ final_damage = ((event.damage + event.damage_add) * event.damage_multiply - event.damage_reduction).round
25
+ event.receiver.hp -= final_damage if final_damage > 0 # as a design decision, don't allow negative damage to heal
26
+ event.damage = final_damage
27
+ end
28
+ event
29
+ end
30
+ end
31
+ end