game_2d 0.0.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.
Files changed (53) hide show
  1. data/.gitignore +15 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +84 -0
  5. data/Rakefile +1 -0
  6. data/bin/game_2d_client.rb +17 -0
  7. data/bin/game_2d_server.rb +21 -0
  8. data/game_2d.gemspec +32 -0
  9. data/lib/game_2d/client_connection.rb +127 -0
  10. data/lib/game_2d/client_engine.rb +227 -0
  11. data/lib/game_2d/complex_move.rb +45 -0
  12. data/lib/game_2d/entity.rb +371 -0
  13. data/lib/game_2d/entity/block.rb +73 -0
  14. data/lib/game_2d/entity/owned_entity.rb +29 -0
  15. data/lib/game_2d/entity/pellet.rb +27 -0
  16. data/lib/game_2d/entity/titanium.rb +11 -0
  17. data/lib/game_2d/entity_constants.rb +14 -0
  18. data/lib/game_2d/game.rb +213 -0
  19. data/lib/game_2d/game_space.rb +462 -0
  20. data/lib/game_2d/game_window.rb +260 -0
  21. data/lib/game_2d/hash.rb +11 -0
  22. data/lib/game_2d/menu.rb +82 -0
  23. data/lib/game_2d/move/rise_up.rb +77 -0
  24. data/lib/game_2d/player.rb +251 -0
  25. data/lib/game_2d/registerable.rb +25 -0
  26. data/lib/game_2d/serializable.rb +69 -0
  27. data/lib/game_2d/server_connection.rb +104 -0
  28. data/lib/game_2d/server_port.rb +74 -0
  29. data/lib/game_2d/storage.rb +42 -0
  30. data/lib/game_2d/version.rb +3 -0
  31. data/lib/game_2d/wall.rb +21 -0
  32. data/lib/game_2d/zorder.rb +3 -0
  33. data/media/Beep.wav +0 -0
  34. data/media/Space.png +0 -0
  35. data/media/Star.png +0 -0
  36. data/media/Starfighter.bmp +0 -0
  37. data/media/brick.gif +0 -0
  38. data/media/cement.gif +0 -0
  39. data/media/crosshair.gif +0 -0
  40. data/media/dirt.gif +0 -0
  41. data/media/pellet.png +0 -0
  42. data/media/pellet.xcf +0 -0
  43. data/media/player.png +0 -0
  44. data/media/player.xcf +0 -0
  45. data/media/rock.png +0 -0
  46. data/media/rock.xcf +0 -0
  47. data/media/steel.gif +0 -0
  48. data/media/tele.gif +0 -0
  49. data/media/titanium.gif +0 -0
  50. data/media/unlikelium.gif +0 -0
  51. data/spec/client_engine_spec.rb +235 -0
  52. data/spec/game_space_spec.rb +347 -0
  53. metadata +246 -0
@@ -0,0 +1,45 @@
1
+ require 'facets/kernel/try'
2
+ require 'game_2d/serializable'
3
+
4
+ # A complex move is any move that has its own state.
5
+ # Moves that span multiple ticks are complex, because
6
+ # the server may have to tell the client how much of
7
+ # the complex move has been completed by a player.
8
+ class ComplexMove
9
+ include Serializable
10
+
11
+ attr_reader :actor_id
12
+
13
+ def initialize(actor=nil)
14
+ self.actor_id = actor.nullsafe_registry_id
15
+ end
16
+
17
+ def actor_id=(id)
18
+ @actor_id = id.try(:to_sym)
19
+ end
20
+
21
+ # Execute one tick of the move.
22
+ # Return true if there is more work to do,
23
+ # false if the move has completed.
24
+ def update(actor)
25
+ false
26
+ end
27
+
28
+ # Take a final action after the complex move
29
+ # is done
30
+ def on_completion(actor); end
31
+
32
+ def all_state
33
+ [actor_id]
34
+ end
35
+ def as_json
36
+ Serializable.as_json(self).merge!(:actor_id => actor_id)
37
+ end
38
+ def update_from_json(json)
39
+ self.actor_id = json[:actor_id]
40
+ self
41
+ end
42
+ def to_s
43
+ self.class.name
44
+ end
45
+ end
@@ -0,0 +1,371 @@
1
+ require 'facets/string/pathize'
2
+ require 'facets/kernel/constant'
3
+ require 'game_2d/registerable'
4
+ require 'game_2d/serializable'
5
+ require 'game_2d/entity_constants'
6
+
7
+ class NilClass
8
+ # Ignore this
9
+ def wake!; end
10
+ end
11
+
12
+ class Entity
13
+ include Serializable
14
+ include Registerable
15
+ include EntityConstants
16
+
17
+ # X and Y position of the top-left corner
18
+ attr_accessor :x, :y, :moving, :space
19
+
20
+ attr_reader :a, :x_vel, :y_vel
21
+
22
+ # space: the game space
23
+ # x, y: position in sub-pixels of the upper-left corner
24
+ # a: angle, with 0 = up, 90 = right
25
+ # x_vel, y_vel: velocity in sub-pixels
26
+ def initialize(x = 0, y = 0, a = 0, x_vel = 0, y_vel = 0)
27
+ @x, @y, self.a = x, y, a
28
+ self.x_vel, self.y_vel = x_vel, y_vel
29
+ @moving = true
30
+ end
31
+
32
+ def a=(angle); @a = angle % 360; end
33
+
34
+ # Velocity is constrained to the range -MAX_VELOCITY .. MAX_VELOCITY
35
+ def x_vel=(xv)
36
+ @x_vel = [[xv, MAX_VELOCITY].min, -MAX_VELOCITY].max
37
+ end
38
+ def y_vel=(yv)
39
+ @y_vel = [[yv, MAX_VELOCITY].min, -MAX_VELOCITY].max
40
+ end
41
+
42
+ # True if we need to update this entity
43
+ def moving?; @moving; end
44
+
45
+ def doomed?; @space.doomed?(self); end
46
+
47
+ # True if this entity can go to sleep now
48
+ # Only called if update() fails to produce any motion
49
+ # Default: Sleep if we're not moving and not falling
50
+ def sleep_now?
51
+ self.x_vel == 0 && self.y_vel == 0 && !should_fall?
52
+ end
53
+
54
+ def should_fall?
55
+ raise "should_fall? undefined"
56
+ end
57
+
58
+ # Notify this entity that it must take action
59
+ def wake!
60
+ @moving = true
61
+ end
62
+
63
+ # Give this entity a chance to perform clean-up upon destruction
64
+ def destroy!; end
65
+
66
+ # X positions near this entity's
67
+ # Position in pixels of the upper-left corner
68
+ def pixel_x; @x / PIXEL_WIDTH; end
69
+ def pixel_y; @y / PIXEL_WIDTH; end
70
+
71
+ # Left-most cell X position occupied
72
+ def self.left_cell_x_at(x); x / WIDTH; end
73
+ # TODO: Find a more elegant way to call class methods in Ruby...
74
+ def left_cell_x(x = @x); self.class.left_cell_x_at(x); end
75
+
76
+ # Right-most cell X position occupied
77
+ # If we're exactly within a column (@x is an exact multiple of WIDTH),
78
+ # then this equals left_cell_x. Otherwise, it's one higher
79
+ def self.right_cell_x_at(x); (x + WIDTH - 1) / WIDTH; end
80
+ def right_cell_x(x = @x); self.class.right_cell_x_at(x); end
81
+
82
+ # Top-most cell Y position occupied
83
+ def self.top_cell_y_at(y); y / HEIGHT; end
84
+ def top_cell_y(y = @y); self.class.top_cell_y_at(y); end
85
+
86
+ # Bottom-most cell Y position occupied
87
+ def self.bottom_cell_y_at(y); (y + HEIGHT - 1) / HEIGHT; end
88
+ def bottom_cell_y(y = @y); self.class.bottom_cell_y_at(y); end
89
+
90
+ # Returns an array of one, two, or four cell-coordinate tuples
91
+ # E.g. [[4, 5], [4, 6], [5, 5], [5, 6]]
92
+ def occupied_cells(x = @x, y = @y)
93
+ x_array = (left_cell_x(x) .. right_cell_x(x)).to_a
94
+ y_array = (top_cell_y(y) .. bottom_cell_y(y)).to_a
95
+ x_array.product(y_array)
96
+ end
97
+
98
+ # Apply acceleration
99
+ def accelerate(x_accel, y_accel)
100
+ self.x_vel = @x_vel + x_accel
101
+ self.y_vel = @y_vel + y_accel
102
+ end
103
+
104
+ # Override to make particular entities transparent to each other
105
+ def transparent_to_me?(other)
106
+ other == self
107
+ end
108
+
109
+ def opaque(others)
110
+ others.delete_if {|obj| transparent_to_me?(obj)}
111
+ end
112
+
113
+ # Wrapper around @space.entities_overlapping
114
+ # Allows us to remove any entities that are transparent
115
+ # to us
116
+ def entities_obstructing(new_x, new_y)
117
+ fail "No @space set!" unless @space
118
+ opaque(@space.entities_overlapping(new_x, new_y))
119
+ end
120
+
121
+ # Process one tick of motion, horizontally only
122
+ def move_x
123
+ return if doomed?
124
+ return if @x_vel.zero?
125
+ new_x = @x + @x_vel
126
+ impacts = entities_obstructing(new_x, @y)
127
+ if impacts.empty?
128
+ @x = new_x
129
+ return
130
+ end
131
+ @x = if @x_vel > 0 # moving right
132
+ # X position of leftmost candidate(s)
133
+ impact_at_x = impacts.collect(&:x).min
134
+ impacts.delete_if {|e| e.x > impact_at_x }
135
+ impact_at_x - WIDTH
136
+ else # moving left
137
+ # X position of rightmost candidate(s)
138
+ impact_at_x = impacts.collect(&:x).max
139
+ impacts.delete_if {|e| e.x < impact_at_x }
140
+ impact_at_x + WIDTH
141
+ end
142
+ self.x_vel = 0
143
+ i_hit(impacts)
144
+ end
145
+
146
+ # Process one tick of motion, vertically only
147
+ def move_y
148
+ return if doomed?
149
+ return if @y_vel.zero?
150
+ new_y = @y + @y_vel
151
+ impacts = entities_obstructing(@x, new_y)
152
+ if impacts.empty?
153
+ @y = new_y
154
+ return
155
+ end
156
+ @y = if @y_vel > 0 # moving down
157
+ # Y position of highest candidate(s)
158
+ impact_at_y = impacts.collect(&:y).min
159
+ impacts.delete_if {|e| e.y > impact_at_y }
160
+ impact_at_y - HEIGHT
161
+ else # moving up
162
+ # Y position of lowest candidate(s)
163
+ impact_at_y = impacts.collect(&:y).max
164
+ impacts.delete_if {|e| e.y < impact_at_y }
165
+ impact_at_y + HEIGHT
166
+ end
167
+ self.y_vel = 0
168
+ i_hit(impacts)
169
+ end
170
+
171
+ # Process one tick of motion. Only called when moving? is true
172
+ def move
173
+ # Force evaluation of both update_x and update_y (no short-circuit)
174
+ # If we're moving faster horizontally, do that first
175
+ # Otherwise do the vertical move first
176
+ moved = @space.process_moving_entity(self) do
177
+ if @x_vel.abs > @y_vel.abs then move_x; move_y
178
+ else move_y; move_x
179
+ end
180
+ end
181
+
182
+ # Didn't move? Might be time to go to sleep
183
+ @moving = false if !moved && sleep_now?
184
+
185
+ moved
186
+ end
187
+
188
+ # Handle any behavior specific to this entity
189
+ # Default: Accelerate downward if the subclass says we should fall
190
+ def update
191
+ accelerate(0, 1) if should_fall?
192
+ move
193
+ end
194
+
195
+ # Update position/velocity/angle data, and tell the space about it
196
+ def warp(x, y, x_vel, y_vel, angle=self.a, moving=@moving)
197
+ blk = proc do
198
+ @x, @y, self.x_vel, self.y_vel, self.a, @moving =
199
+ x, y, x_vel, y_vel, angle, moving
200
+ end
201
+ if @space
202
+ @space.process_moving_entity(self, &blk)
203
+ else
204
+ blk.call
205
+ end
206
+ end
207
+
208
+ def i_hit(other)
209
+ # TODO
210
+ puts "#{self} hit #{other.inspect}"
211
+ end
212
+
213
+ def harmed_by(other); end
214
+
215
+ # Return any entities adjacent to this one in the specified direction
216
+ def next_to(angle, x=@x, y=@y)
217
+ points = case angle % 360
218
+ when 0 then
219
+ [[x, y - 1], [x + WIDTH - 1, y - 1]]
220
+ when 90 then
221
+ [[x + WIDTH, y], [x + WIDTH, y + HEIGHT - 1]]
222
+ when 180 then
223
+ [[x, y + HEIGHT], [x + WIDTH - 1, y + HEIGHT]]
224
+ when 270 then
225
+ [[x - 1, y], [x - 1, y + HEIGHT - 1]]
226
+ else puts "Trig unimplemented"; []
227
+ end
228
+ @space.entities_at_points(points)
229
+ end
230
+
231
+ def empty_underneath?; opaque(next_to(180)).empty?; end
232
+ def empty_on_left?; opaque(next_to(270)).empty?; end
233
+ def empty_on_right?; opaque(next_to(90)).empty?; end
234
+ def empty_above?; opaque(next_to(0)).empty?; end
235
+
236
+ def angle_to_vector(angle, amplitude=1)
237
+ case angle % 360
238
+ when 0 then [0, -amplitude]
239
+ when 90 then [amplitude, 0]
240
+ when 180 then [0, amplitude]
241
+ when 270 then [-amplitude, 0]
242
+ else raise "Trig unimplemented"
243
+ end
244
+ end
245
+
246
+ # Convert x/y to an angle
247
+ def vector_to_angle(x_vel=@x_vel, y_vel=@y_vel)
248
+ if x_vel == 0 && y_vel == 0
249
+ return puts "Zero velocity, no angle"
250
+ end
251
+ if x_vel != 0 && y_vel != 0
252
+ return puts "Diagonal velocity (#{x_vel}x#{y_vel}), no angle"
253
+ end
254
+
255
+ if x_vel.zero?
256
+ (y_vel > 0) ? 180 : 0
257
+ else
258
+ (x_vel > 0) ? 90 : 270
259
+ end
260
+ end
261
+
262
+ # Given a vector with a diagonal, drop the smaller component, returning a
263
+ # vector that is strictly either horizontal or vertical.
264
+ def drop_diagonal(x_vel, y_vel)
265
+ (y_vel.abs > x_vel.abs) ? [0, y_vel] : [x_vel, 0]
266
+ end
267
+
268
+ # Is the other entity basically above us, below us, or on the left or the
269
+ # right? Returns the angle we should face if we want to face that entity.
270
+ def direction_to(other_x, other_y)
271
+ vector_to_angle(*drop_diagonal(other_x - @x, other_y - @y))
272
+ end
273
+
274
+ # Given our current position and velocity (and only if our velocity is not
275
+ # on a diagonal), are we about to move past the entity at the specified
276
+ # coordinates? If so, returns:
277
+ #
278
+ # 1) The X/Y position of the empty space just past the entity. Assuming the
279
+ # other entity is adjacent to us, this spot touches corners with the other
280
+ # entity.
281
+ #
282
+ # 2) How far we'd go to reach that point.
283
+ #
284
+ # 3) How far past that spot we would go.
285
+ #
286
+ # 4) Which way we'd have to turn (delta angle) if moving around the other
287
+ # entity. Either +90 or -90.
288
+ def going_past_entity(other_x, other_y)
289
+ return if @x_vel == 0 && @y_vel == 0
290
+ return if @x_vel != 0 && @y_vel != 0
291
+
292
+ if @x_vel.zero?
293
+ # Moving vertically. Find target height
294
+ y_pos = (@y_vel > 0) ? other_y + HEIGHT : other_y - HEIGHT
295
+ distance = (@y - y_pos).abs
296
+ overshoot = @y_vel.abs - distance
297
+ turn = if @y_vel > 0
298
+ # Going down: Turn left if it's on our right
299
+ direction_to(other_x, other_y) == 90 ? -90 : 90
300
+ else
301
+ # Going up: Turn right if it's on our right
302
+ direction_to(other_x, other_y) == 90 ? 90 : -90
303
+ end
304
+ return [[@x, y_pos], distance, overshoot, turn] if overshoot >= 0
305
+ else
306
+ # Moving horizontally. Find target column
307
+ x_pos = (@x_vel > 0) ? other_x + WIDTH : other_x - WIDTH
308
+ distance = (@x - x_pos).abs
309
+ overshoot = @x_vel.abs - distance
310
+ turn = if @x_vel > 0
311
+ # Going right: Turn right if it's below us
312
+ direction_to(other_x, other_y) == 180 ? 90 : -90
313
+ else
314
+ # Going left: Turn left if it's below us
315
+ direction_to(other_x, other_y) == 180 ? -90 : 90
316
+ end
317
+ return [[x_pos, @y], distance, overshoot, turn] if overshoot >= 0
318
+ end
319
+ end
320
+
321
+ def as_json
322
+ Serializable.as_json(self).merge!(
323
+ :class => self.class.to_s,
324
+ :registry_id => registry_id,
325
+ :position => [ self.x, self.y ],
326
+ :velocity => [ self.x_vel, self.y_vel ],
327
+ :angle => self.a,
328
+ :moving => self.moving?
329
+ )
330
+ end
331
+
332
+ def update_from_json(json)
333
+ new_x, new_y = json[:position]
334
+ new_x_vel, new_y_vel = json[:velocity]
335
+ new_angle = json[:angle]
336
+ new_moving = json[:moving]
337
+
338
+ warp(new_x, new_y, new_x_vel, new_y_vel, new_angle, new_moving)
339
+ self
340
+ end
341
+
342
+ def image_filename
343
+ raise "No image filename defined"
344
+ end
345
+
346
+ def draw_zorder; ZOrder::Objects end
347
+
348
+ def draw(window)
349
+ anim = window.animation[window.media(image_filename)]
350
+ img = anim[Gosu::milliseconds / 100 % anim.size]
351
+ # Entity's pixel_x/pixel_y is the location of the upper-left corner
352
+ # draw_rot wants us to specify the point around which rotation occurs
353
+ # That should be the center
354
+ img.draw_rot(
355
+ self.pixel_x + CELL_WIDTH_IN_PIXELS / 2,
356
+ self.pixel_y + CELL_WIDTH_IN_PIXELS / 2,
357
+ draw_zorder, self.a)
358
+ # 0.5, 0.5, # rotate around the center
359
+ # 1, 1, # scaling factor
360
+ # @color, # modify color
361
+ # :add) # draw additively
362
+ end
363
+
364
+ def to_s
365
+ "#{self.class} (#{registry_id_safe}) at #{x}x#{y}"
366
+ end
367
+
368
+ def all_state
369
+ [registry_id_safe, @x, @y, @a, @x_vel, @y_vel, @moving]
370
+ end
371
+ end
@@ -0,0 +1,73 @@
1
+ require 'game_2d/entity/owned_entity'
2
+
3
+ class Entity
4
+
5
+ class Block < OwnedEntity
6
+ MAX_LEVEL = 5
7
+ HP_PER_LEVEL = 5
8
+ MAX_HP = MAX_LEVEL * HP_PER_LEVEL
9
+
10
+ attr_reader :hp
11
+
12
+ def hp=(p); @hp = [[p, MAX_HP].min, 0].max; end
13
+
14
+ def all_state; super.push(hp); end
15
+ def as_json; super.merge!(:hp => hp); end
16
+
17
+ def update_from_json(json)
18
+ self.hp = json[:hp] if json[:hp]
19
+ super
20
+ end
21
+
22
+ def should_fall?
23
+ return false if owner || !empty_underneath?
24
+
25
+ case level
26
+ when 0
27
+ true
28
+ when 1
29
+ empty_on_left? || empty_on_right?
30
+ when 2
31
+ empty_on_left? && empty_on_right?
32
+ when 3
33
+ empty_on_left? && empty_on_right? && empty_above?
34
+ when 4
35
+ false
36
+ end
37
+ end
38
+
39
+ def update
40
+ if should_fall?
41
+ accelerate(0, 1)
42
+ else
43
+ self.x_vel = self.y_vel = 0
44
+ end
45
+ move
46
+ end
47
+
48
+ def transparent_to_me?(other)
49
+ super ||
50
+ (other.registry_id == owner_id) ||
51
+ (other.is_a?(Pellet) && other.owner_id == owner_id)
52
+ end
53
+
54
+ def harmed_by(other)
55
+ puts "#{self}: Ouch!"
56
+ self.hp -= 1
57
+ @space.doom(self) if hp <= 0
58
+ end
59
+
60
+ def destroy!
61
+ owner.disown_block if owner
62
+ end
63
+
64
+ def level; (hp - 1) / HP_PER_LEVEL; end
65
+
66
+ def level_name
67
+ %w(dirt brick cement steel unlikelium)[level]
68
+ end
69
+
70
+ def image_filename; "#{level_name}.gif"; end
71
+ end
72
+
73
+ end