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,15 @@
1
+ *.swp
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in game_2d.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Greg Meyers
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,84 @@
1
+ # Game2d
2
+
3
+ A 2D sandbox game, using Gosu and REnet.
4
+
5
+ There's not much here yet, in terms of actual gameplay. At this point I'm laying the foundation. What I have so far:
6
+
7
+ * The server runs at 60 frames a second, to match Gosu.
8
+ * A GameSpace holds a copy of the game's state. This is primarily a grid populated by Entities.
9
+ * The dimensions of the GameSpace (in cells) are specified on server startup.
10
+ * Each cell in the grid is 40x40 pixels, but is represented as 400x400 internally. This allows for motion at slow speeds (1 pixel every 10 ticks == 6 pixels per second).
11
+ * Entity positions and velocities are integers. Apart from player input, all entities' behaviors are predictable. This allows the client to make accurate predictions of the server state.
12
+ * New Entities are assigned IDs in order, again, to allow the client to predict which ID will be assigned to each new entity.
13
+ * Clients connect using REnet. The handshaking process creates a Player object to represent the client.
14
+ * Keyboard commands control the player:
15
+ * A and D, or LeftArrow and RightArrow: Hold down to accelerate.
16
+ * LeftControl or RightControl: Hold down to brake (slow down).
17
+ * S, or DownArrow: Hold down to build (the longer you hold, the stronger the block).
18
+ * W, or UpArrow:
19
+ * When not building: Flip (turn 180 degrees).
20
+ * When building: Rise up (ascend to the top of the built block).
21
+ * Left button: Fire a pellet. Mouse click determines the direction and speed -- closer to the player means slower speed.
22
+ * Right button: Build a block where you click. *(This will eventually become part of a level-editing mode.)*
23
+ * A simple menu system is also available. Left-click to select. Esc returns to the top-level.
24
+ * The menus include a Save feature, which tells the server to save the level's state.
25
+ * Levels are persisted in ~/.game_2d/<level name> as JSON text.
26
+ * On subsequent startup with the same name, the level is loaded.
27
+ * The client and server communicate using JSON text. This is undoubtedly inefficient, but aids in debugging at this early stage.
28
+ * Every frame is numbered by the server, and is referred to as a 'tick'.
29
+ * Player actions are both executed locally, and sent to the server.
30
+ * If other players are connected, the server sends everyone's actions to everyone else.
31
+ * Player actions are dated six ticks in the future, to give everyone a chance to get the message early. That way everyone can execute the action at the same time.
32
+ * The server broadcasts the complete GameSpace four times a second, just in case a client gets out of sync.
33
+ * The client predicts the server state, but treats its own copy as advisory only -- the server's is definitive.
34
+ * If the server sends something conflicting, the client discards its wrong prediction.
35
+ * This is intended to compensate for dropped packets or lag, but much more testing is needed.
36
+
37
+ The physics is (intentionally) pretty simple. Unsupported objects fall, accelerating downward at a rate of 1/10 pixel per tick per tick. Blocks assume one of the following forms depending on how many hit points they possess:
38
+
39
+ * Dirt blocks (1-5 HP) fall unless something is underneath.
40
+ * Brick blocks (6-10 HP) can stay up if supported from both left and right.
41
+ * Cement blocks (11-15 HP) can stay up if supported from *either* left *or* right.
42
+ * Steel blocks (16-20 HP) can stay up if touching anything else, even above.
43
+ * Unlikelium blocks (21-25 HP) never fall.
44
+
45
+ When building a block, the longer you hold the S or DownArrow key, the more HP will be awarded to the block. Hitting a block with a pellet reduces its HP, until it degrades to a lower form. Dirt blocks with 1 HP will be entirely destroyed when hit.
46
+
47
+ Whether an object is supported depends exclusively on its immediate surroundings. That means two objects can support each other, and hang suspended. For example, a dirt block sitting on a steel block will support each other. A horizontal row of brick, capped at either end with cement, will also be free-standing.
48
+
49
+ Pellets are fired from the player's center, and are affected by gravity. They damage whatever they hit first, and disappear. Pellets won't hit the player who fired them.
50
+
51
+ There are also Titanium entities, which never fall, and are indestructible. These are intended for use in designing levels with specific shapes. They can only be created by using the menu to select Titanium, and then right-clicking.
52
+
53
+ A player is considered to be supported if their "feet" are touching a block. Unsupported players will turn feet-downward and fall, until they land on something. Supported players may slide left or right, and will follow any edges they reach -- going up and down walls, or hanging under ceilings. The "flip" maneuver swaps head and feet; this becomes useful if the player's head is exactly touching another block. At all other times, a flip leads to a fall (which can be useful too).
54
+
55
+ When building a block, the player and the new block occupy the same space. This is allowed until the player moves off of the block. After that, the block is considered opaque as usual. While the block is still "inside" the player, the Rise Up maneuver may be used. This moves the player headward (which may not be "up"--it depends which way the player is turned) to sit "on top" of the block. It's possible to use this maneuver to construct a horizontal row of bricks, carefully.
56
+
57
+ The GameSpace is bounded by invisible, indestructible Wall entities. These can also support blocks, or the player.
58
+
59
+
60
+ ## Installation
61
+
62
+ Install it with:
63
+
64
+ $ gem install game_2d
65
+
66
+ ## Usage
67
+
68
+ This needs streamlining.
69
+
70
+ Start a server in one window:
71
+
72
+ $ game_2d_server.rb -w 50 -h 50 --level example
73
+
74
+ And a client in another window:
75
+
76
+ $ game_2d_client.rb --name Bender --hostname 127.0.0.1
77
+
78
+ ## Contributing
79
+
80
+ 1. Fork it ( https://github.com/sereneiconoclast/game_2d/fork )
81
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
82
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
83
+ 4. Push to the branch (`git push origin my-new-feature`)
84
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'trollop'
4
+ require 'game_2d/game_window'
5
+
6
+ opts = Trollop::options do
7
+ opt :name, "Player name", :type => :string, :required => true
8
+ opt :hostname, "Hostname of server", :type => :string, :required => true
9
+ opt :port, "Port number", :default => DEFAULT_PORT
10
+ opt :profile, "Turn on profiling", :type => :boolean
11
+ opt :debug_traffic, "Debug network traffic", :type => :boolean
12
+ end
13
+
14
+ $debug_traffic = opts[:debug_traffic] || false
15
+
16
+ window = GameWindow.new( opts[:name], opts[:hostname], opts[:port], opts[:profile] )
17
+ window.show
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'trollop'
4
+ require 'game_2d/game'
5
+
6
+ opts = Trollop::options do
7
+ opt :level, "Level name", :type => :string, :required => true
8
+ opt :width, "Level width", :type => :integer
9
+ opt :height, "Level height", :type => :integer
10
+ opt :port, "Port number", :type => :integer
11
+ opt :storage, "Data storage dir (in home directory)", :type => :string
12
+ opt :max_clients, "Maximum clients", :type => :integer
13
+ opt :self_check, "Run data consistency checks", :type => :boolean
14
+ opt :profile, "Turn on profiling", :type => :boolean
15
+ opt :debug_traffic, "Debug network traffic", :type => :boolean
16
+ opt :registry_broadcast_every, "Send registry broadcasts every N frames (0 = never)", :type => :integer
17
+ end
18
+
19
+ $debug_traffic = opts[:debug_traffic] || false
20
+
21
+ Game.new(opts).run
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'game_2d/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "game_2d"
8
+ spec.version = Game2d::VERSION
9
+ spec.authors = ["Greg Meyers"]
10
+ spec.email = ["cmdr.samvimes@gmail.com"]
11
+ spec.summary = %q{Client/server sandbox game using Gosu and REnet}
12
+ spec.description = %q{Client/server sandbox game using Gosu and REnet}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+ spec.required_ruby_version = ">= 1.9.3"
21
+
22
+ spec.add_runtime_dependency "facets", [">= 2.9.3"]
23
+ spec.add_runtime_dependency "gosu", [">= 0.8.5"]
24
+ spec.add_runtime_dependency "json", [">= 1.8.1"]
25
+ spec.add_runtime_dependency "renet", [">= 0.1.14"]
26
+ spec.add_runtime_dependency "trollop", [">= 2.0"]
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.7"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rr", "~> 1.1.2"
31
+ spec.add_development_dependency "rspec", "~> 3.1.0"
32
+ end
@@ -0,0 +1,127 @@
1
+ require 'renet'
2
+ require 'json'
3
+ require 'game_2d/hash'
4
+
5
+ # The client creates one of these.
6
+ # It is then used for all communication with the server.
7
+
8
+ class ClientConnection
9
+ attr_reader :player_name
10
+ attr_accessor :engine
11
+
12
+ # We tell the server to execute all actions this many ticks
13
+ # in the future, to give the message time to propagate around
14
+ # the fleet
15
+ ACTION_DELAY = 6 # 1/10 of a second
16
+
17
+ def initialize(host, port, game, player_name, timeout=2000)
18
+ # remote host address, remote host port, channels, download bandwidth, upload bandwidth
19
+ @socket = _create_connection(host, port, 2, 0, 0)
20
+ @game = game
21
+ @player_name = player_name
22
+
23
+ @socket.on_connection(method(:on_connect))
24
+ @socket.on_disconnection(method(:on_close))
25
+ @socket.on_packet_receive(method(:on_packet))
26
+
27
+ @socket.connect(timeout)
28
+ end
29
+
30
+ def _create_connection(*args)
31
+ ENet::Connection.new(*args)
32
+ end
33
+
34
+ def on_connect
35
+ puts "Connected to server - sending handshake"
36
+ # send handshake reliably
37
+ send_record( { :handshake => { :player_name => @player_name } }, true)
38
+ end
39
+
40
+ def on_close
41
+ puts "Client disconnected by server"
42
+ @game.shutdown
43
+ end
44
+
45
+ def on_packet(data, channel)
46
+ hash = JSON.parse(data).fix_keys
47
+ debug_packet('Received', hash)
48
+
49
+ pong = hash[:pong]
50
+ if pong
51
+ stop = Time.now.to_f
52
+ puts "Ping took #{stop - pong[:start]} seconds"
53
+ end
54
+
55
+ at_tick = hash[:at_tick]
56
+ fail "No at_tick in #{hash.inspect}" unless at_tick
57
+
58
+ world = hash[:world]
59
+ if world
60
+ @engine.establish_world(world, at_tick)
61
+ end
62
+
63
+ delta_keys = [
64
+ :add_players, :add_npcs, :delete_entities, :update_entities, :update_score, :move
65
+ ]
66
+ @engine.add_delta(hash) if delta_keys.any? {|k| hash.has_key? k}
67
+
68
+ you_are = hash[:you_are]
69
+ if you_are
70
+ # The 'world' response includes deltas for add_players and add_npcs
71
+ # Need to process those first, as one of the players is us
72
+ @engine.apply_all_deltas(at_tick)
73
+
74
+ @engine.create_local_player you_are
75
+ end
76
+
77
+ registry, highest_id = hash[:registry], hash[:highest_id]
78
+ @engine.sync_registry(registry, highest_id, at_tick) if registry
79
+ end
80
+
81
+ def send_actions_at
82
+ @engine.tick + ACTION_DELAY
83
+ end
84
+
85
+ def send_move(move, args={})
86
+ return unless move
87
+ args[:move] = move.to_s
88
+ delta = { :at_tick => send_actions_at, :move => args }
89
+ send_record delta
90
+ delta[:move][:player_id] = @engine.player_id
91
+ @engine.add_delta delta
92
+ end
93
+
94
+ def send_create_npc(npc)
95
+ delta = { :at_tick => send_actions_at, :create_npc => npc }
96
+ send_record delta
97
+ @engine.add_delta delta
98
+ end
99
+
100
+ def send_save
101
+ send_record :save => true
102
+ end
103
+
104
+ def send_ping
105
+ send_record :ping => { :start => Time.now.to_f }
106
+ end
107
+
108
+ def send_record(data, reliable=false)
109
+ debug_packet('Sending', data)
110
+ @socket.send_packet(data.to_json, reliable, 0)
111
+ @socket.flush
112
+ end
113
+
114
+ def debug_packet(direction, hash)
115
+ return unless $debug_traffic
116
+ at_tick = hash[:at_tick] || 'NO TICK'
117
+ keys = hash.keys - [:at_tick]
118
+ puts "#{direction} #{keys.join(', ')} <#{at_tick}>"
119
+ end
120
+
121
+ def update
122
+ @socket.update(0) # non-blocking
123
+ end
124
+
125
+ def online?; @socket.online?; end
126
+ def disconnect; @socket.disconnect(200); end
127
+ end
@@ -0,0 +1,227 @@
1
+ require 'set'
2
+ require 'game_2d/game_space'
3
+ require 'game_2d/serializable'
4
+
5
+ # Server sends authoritative copy of GameSpace for tick T0.
6
+ # We store that, along with pending moves generated by
7
+ # our player, and pending moves for other players sent to us
8
+ # by the server. Then we calculate further ticks of action.
9
+ # These are predictions and might well be wrong.
10
+ #
11
+ # When the user requests an action at T0, we delay it by 100ms
12
+ # (T6). We tell the server about it immediately, but advise
13
+ # it not to perform the action until T6 arrives. The server
14
+ # rebroadcasts this information to other players. Hopefully,
15
+ # everyone receives all players' actions before T6.
16
+ #
17
+ # We render one tick after another, 60 per second, the same
18
+ # speed at which the server calculates them. But because we
19
+ # may get out of sync, we also watch for full server updates
20
+ # at, e.g., T15. When we get a new full update, we can discard
21
+ # all information about older ticks. Anything we've calculated
22
+ # past the new update must now be recalculated, applying again
23
+ # whatever pending player actions we have heard about.
24
+ class ClientEngine
25
+ # If we haven't received a full update from the server in this
26
+ # many ticks, stop guessing. We're almost certainly wrong by
27
+ # this point.
28
+ MAX_LEAD_TICKS = 30
29
+
30
+ attr_reader :tick
31
+
32
+ def initialize(game_window)
33
+ @game_window, @width, @height = game_window, 0, 0
34
+ @spaces = {}
35
+ @deltas = Hash.new {|h,tick| h[tick] = Array.new}
36
+ @earliest_tick = @tick = @preprocessed = nil
37
+ end
38
+
39
+ def establish_world(world, at_tick)
40
+ @world_name, @world_id = world[:world_name], world[:world_id]
41
+ @width, @height = world[:cell_width], world[:cell_height]
42
+ highest_id = world[:highest_id]
43
+ create_initial_space(at_tick, highest_id)
44
+ end
45
+
46
+ def create_initial_space(at_tick, highest_id)
47
+ @earliest_tick = @tick = at_tick
48
+ space = @spaces[@tick] = GameSpace.new.establish_world(@world_name, @world_id, @width, @height)
49
+ space.highest_id = highest_id
50
+ space
51
+ end
52
+
53
+ def space_at(tick)
54
+ return @spaces[tick] if @spaces[tick]
55
+
56
+ fail "Can't create space at #{tick}; earliest space we know about is #{@earliest_tick}" if tick < @earliest_tick
57
+
58
+ last_space = space_at(tick - 1)
59
+ @spaces[tick] = new_space = GameSpace.new.copy_from(last_space)
60
+
61
+ # Certain deltas, like add_npcs, need to be processed post-update
62
+ # to match the server's behavior. An object created during tick T
63
+ # does not receive its first update until T+1.
64
+ apply_deltas_before_update(tick)
65
+ new_space.update
66
+ apply_deltas_after_update(tick)
67
+
68
+ new_space
69
+ end
70
+
71
+ def update
72
+ return unless @tick # handshake not yet answered
73
+
74
+ # Display the frame we received from the server as-is
75
+ if @preprocessed == @tick
76
+ @preprocessed = nil
77
+ return space_at(@tick)
78
+ end
79
+
80
+ if @tick - @earliest_tick >= MAX_LEAD_TICKS
81
+ $stderr.puts "Lost connection? Running ahead of server?"
82
+ return space_at(@tick)
83
+ end
84
+ space_at(@tick += 1)
85
+ end
86
+
87
+ def space
88
+ @spaces[@tick]
89
+ end
90
+
91
+ def create_local_player(player_id)
92
+ old_player_id = @game_window.player_id
93
+ fail "Already have player #{old_player_id}!?" if old_player_id
94
+
95
+ @game_window.player_id = player_id
96
+ puts "I am player #{player_id}"
97
+ end
98
+
99
+ def player_id
100
+ @game_window.player_id
101
+ end
102
+
103
+ def add_delta(delta)
104
+ at_tick = delta.delete :at_tick
105
+ if at_tick < @tick
106
+ $stderr.puts "Received delta #{@tick - at_tick} ticks late"
107
+ if at_tick <= @earliest_tick
108
+ $stderr.puts "Discarding it - we've received registry sync at <#{@earliest_tick}>"
109
+ return
110
+ end
111
+ # Invalidate old spaces that were generated without this information
112
+ at_tick.upto(@tick) {|old_tick| @spaces.delete old_tick}
113
+ end
114
+ @deltas[at_tick] << delta
115
+ end
116
+
117
+ def apply_deltas_before_update(at_tick)
118
+ space = space_at(at_tick)
119
+
120
+ @deltas[at_tick].each do |hash|
121
+ players = hash[:add_players]
122
+ add_players(space, players) if players
123
+
124
+ doomed = hash[:delete_entities]
125
+ delete_entities(space, doomed) if doomed
126
+
127
+ updated = hash[:update_entities]
128
+ update_entities(space, updated) if updated
129
+
130
+ move = hash[:move]
131
+ apply_move(space, move) if move
132
+
133
+ score_update = hash[:update_score]
134
+ update_score(space, score_update) if score_update
135
+ end
136
+ end
137
+
138
+ def apply_deltas_after_update(at_tick)
139
+ space = space_at(at_tick)
140
+
141
+ @deltas[at_tick].each do |hash|
142
+ npcs = hash[:add_npcs]
143
+ add_npcs(space, npcs) if npcs
144
+ end
145
+
146
+ # Any later spaces are now invalid
147
+ @spaces.delete_if {|key, _| key > at_tick}
148
+ end
149
+
150
+ def apply_all_deltas(at_tick)
151
+ apply_deltas_before_update(at_tick)
152
+ apply_deltas_after_update(at_tick)
153
+ end
154
+
155
+ def add_player(space, hash)
156
+ player = Serializable.from_json(hash)
157
+ puts "Added player #{player}"
158
+ space << player
159
+ player.registry_id
160
+ end
161
+
162
+ def add_players(space, players)
163
+ players.each {|json| add_player(space, json) }
164
+ end
165
+
166
+ def apply_move(space, move)
167
+ player_id = move[:player_id]
168
+ player = space[player_id]
169
+ fail "No such player #{player_id}, can't apply #{move.inspect}" unless player
170
+ player.add_move move
171
+ end
172
+
173
+ def add_npcs(space, npcs)
174
+ npcs.each {|json| space << Serializable.from_json(json) }
175
+ end
176
+
177
+ def add_entity(space, json)
178
+ space << Serializable.from_json(json)
179
+ end
180
+
181
+ # Returns the set of registry IDs updated or added
182
+ def update_entities(space, updated)
183
+ registry_ids = Set.new
184
+ updated.each do |json|
185
+ registry_id = json[:registry_id]
186
+ fail "Can't update #{entity.inspect}, no registry_id!" unless registry_id
187
+ registry_ids << registry_id
188
+
189
+ if my_obj = space[registry_id]
190
+ my_obj.update_from_json(json)
191
+ else
192
+ add_entity(space, json)
193
+ end
194
+ end
195
+
196
+ registry_ids
197
+ end
198
+
199
+ def delete_entities(space, doomed)
200
+ doomed.each do |registry_id|
201
+ dead = space[registry_id]
202
+ next unless dead
203
+ puts "Disconnected: #{dead}" if dead.is_a? Player
204
+ space.doom dead
205
+ end
206
+ space.purge_doomed_entities
207
+ end
208
+
209
+ def update_score(space, update)
210
+ registry_id, score = update.to_a.first
211
+ return unless player = space[registry_id]
212
+ player.score = score
213
+ end
214
+
215
+ # Discard anything we think we know, in favor of the registry
216
+ # we just got from the server
217
+ def sync_registry(server_registry, highest_id, at_tick)
218
+ @spaces.clear
219
+ # Any older deltas are now irrelevant
220
+ @earliest_tick.upto(at_tick - 1) {|old_tick| @deltas.delete old_tick}
221
+ update_entities(create_initial_space(at_tick, highest_id), server_registry)
222
+
223
+ # The server has given us a complete, finished frame. Don't
224
+ # create a new one until this one has been displayed once.
225
+ @preprocessed = at_tick
226
+ end
227
+ end