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,25 @@
1
+ class NilClass
2
+ def nullsafe_registry_id; self; end
3
+ end
4
+
5
+ module Registerable
6
+ def registry_id?
7
+ @registry_id
8
+ end
9
+
10
+ def registry_id
11
+ @registry_id or raise("No ID set for #{self}")
12
+ end
13
+ def nullsafe_registry_id; registry_id; end
14
+
15
+ # For use in to_s
16
+ def registry_id_safe
17
+ @registry_id || :NO_ID
18
+ end
19
+
20
+ def registry_id=(id)
21
+ raise "#{self}: Already have ID #{@registry_id}, cannot set to #{id}" if @registry_id
22
+ raise "#{self}: Invalid ID #{id}" unless id
23
+ @registry_id = id.to_sym
24
+ end
25
+ end
@@ -0,0 +1,69 @@
1
+ class NilClass
2
+ def as_json; self end
3
+ end
4
+
5
+ module Serializable
6
+ # Flat list of all object state
7
+ # For sorting purposes, most significant goes first
8
+ def all_state
9
+ []
10
+ end
11
+
12
+ # Based on all_state
13
+ def <=>(other)
14
+ self.all_state <=> other.all_state
15
+ end
16
+ def ==(other)
17
+ other.class.equal?(self.class) && other.all_state == self.all_state
18
+ end
19
+ def hash; self.class.hash ^ all_state.hash; end
20
+ def eql?(other); self == other; end
21
+
22
+ # Returns a hash which becomes the JSON
23
+ def self.as_json(thing)
24
+ { :class => thing.class.to_s }
25
+ end
26
+
27
+ # Based on as_json
28
+ def to_json(*args)
29
+ as_json.to_json(*args)
30
+ end
31
+
32
+ # Make our state match what's in the hash
33
+ def update_from_json(hash)
34
+ end
35
+
36
+ def to_s
37
+ self.class.name
38
+ end
39
+
40
+ def self.from_json(json, generate_id=false)
41
+ return nil unless json
42
+ class_name = json[:class]
43
+ binding.pry unless class_name
44
+ raise "Suspicious class name: #{class_name}" unless
45
+ (class_name == 'Player') ||
46
+ (class_name.start_with? 'Entity::') ||
47
+ (class_name.start_with? 'Move::')
48
+ require "game_2d/#{class_name.pathize}"
49
+ clazz = constant(class_name)
50
+ it = clazz.new
51
+
52
+ # A registry ID must be specified either in the JSON or by the caller, but
53
+ # not both
54
+ if it.is_a? Registerable
55
+ if generate_id
56
+ fail("Entity #{it} (from #{json.inspect}) already has " +
57
+ "ID #{it.registry_id}, cannot generate") if it.registry_id?
58
+ # Leave it nil - it will be populated when added to a space
59
+ else
60
+ it.registry_id = json[:registry_id]
61
+ end
62
+ elsif generate_id
63
+ fail("#{clazz} is not Registerable")
64
+ end
65
+
66
+ it.update_from_json(json)
67
+ end
68
+
69
+ end
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+ require 'game_2d/hash'
3
+
4
+ # An instance of this class is created by ServerPort whenever an
5
+ # incoming connection is accepted.
6
+
7
+ class ServerConnection
8
+
9
+ def initialize(port, game, server, id, remote_addr)
10
+ @port, @game, @server, @id, @remote_addr = port, game, server, id, remote_addr
11
+ puts "ServerConnection: New connection #{id} from #{remote_addr}"
12
+ end
13
+
14
+ def answer_handshake(handshake)
15
+ player_name = handshake[:player_name]
16
+ player = @game.add_player(player_name)
17
+ @player_id = player.registry_id
18
+ @port.register_player @player_id, self
19
+
20
+ response = {
21
+ :you_are => @player_id,
22
+ :world => {
23
+ :world_name => @game.world_name,
24
+ :world_id => @game.world_id,
25
+ :highest_id => @game.world_highest_id,
26
+ :cell_width => @game.world_cell_width,
27
+ :cell_height => @game.world_cell_height,
28
+ },
29
+ :add_players => @game.get_all_players,
30
+ :add_npcs => @game.get_all_npcs,
31
+ :at_tick => @game.tick,
32
+ }
33
+ puts "#{player} logs in from #{@remote_addr} at <#{@game.tick}>"
34
+ send_record response, true # answer handshake reliably
35
+ end
36
+
37
+ def player
38
+ @game[@player_id]
39
+ end
40
+
41
+ def answer_ping(ping)
42
+ send_record :pong => ping
43
+ end
44
+
45
+ def add_npc(npc, at_tick)
46
+ send_record :add_npcs => [ npc ], :at_tick => at_tick
47
+ end
48
+
49
+ def add_player(player, at_tick)
50
+ send_record :add_players => [ player ], :at_tick => at_tick
51
+ end
52
+
53
+ def delete_entity(entity, at_tick)
54
+ send_record :delete_entities => [ entity.registry_id ], :at_tick => at_tick
55
+ end
56
+
57
+ def update_entities(entities, at_tick)
58
+ send_record :update_entities => entities, :at_tick => at_tick
59
+ end
60
+
61
+ # Not called yet...
62
+ def update_score(player, at_tick)
63
+ send_record :update_score => { player.registry_id => player.score }, :at_tick => at_tick
64
+ end
65
+
66
+ def close
67
+ @port.deregister_player @player_id
68
+ toast = player
69
+ puts "#{toast} -- #{@remote_addr} disconnected at <#{@game.tick}>"
70
+ @game.delete_entity toast
71
+ end
72
+
73
+ def on_packet(data, channel)
74
+ hash = JSON.parse(data).fix_keys
75
+ debug_packet('Received', hash)
76
+ if (handshake = hash[:handshake])
77
+ answer_handshake(handshake)
78
+ elsif (hash[:save])
79
+ @game.save
80
+ elsif (ping = hash[:ping])
81
+ answer_ping ping
82
+ else
83
+ @game.add_player_action @player_id, hash
84
+ @port.broadcast_player_action @id,
85
+ hash.merge(:player_id => @player_id),
86
+ channel
87
+ end
88
+ end
89
+
90
+ def send_record(hash, reliable=false, channel=0)
91
+ debug_packet('Sending', hash)
92
+ send_str = hash.to_json
93
+ # Send data to the client (client ID, data, reliable or not, channel ID)
94
+ @server.send_packet(@id, send_str, reliable, channel)
95
+ @server.flush
96
+ end
97
+
98
+ def debug_packet(direction, hash)
99
+ return unless $debug_traffic
100
+ at_tick = hash[:at_tick] || 'NO TICK'
101
+ keys = hash.keys - [:at_tick]
102
+ puts "#{direction} #{keys.join(', ')} <#{at_tick}>"
103
+ end
104
+ end
@@ -0,0 +1,74 @@
1
+ require 'renet'
2
+ require 'json'
3
+ require 'game_2d/server_connection'
4
+
5
+ class ServerPort
6
+ def initialize(game, port_number, max_clients)
7
+ @game = game
8
+ @server = _create_enet_server port_number, max_clients, 2, 0, 0
9
+ puts "ENet server listening on #{port_number}"
10
+
11
+ @clients = {}
12
+ @player_connections = {}
13
+
14
+ @server.on_connection method(:on_connection)
15
+ @server.on_packet_receive method(:on_packet_receive)
16
+ @server.on_disconnection method(:on_disconnection)
17
+ end
18
+
19
+ def _create_enet_server(*args)
20
+ ENet::Server.new *args
21
+ end
22
+
23
+ def on_connection(id, ip)
24
+ puts "New ENet connection #{id} from #{ip}"
25
+ @clients[id] = ServerConnection.new(self, @game, @server, id, ip)
26
+ end
27
+
28
+ def on_packet_receive(id, data, channel)
29
+ @clients[id].on_packet(data, channel)
30
+ end
31
+
32
+ def on_disconnection(id)
33
+ puts "ENet connection #{id} disconnected"
34
+ gone = @clients.delete id
35
+ gone.close
36
+ puts "Remaining connection IDs: #{@clients.keys.sort.join(', ')}"
37
+ end
38
+
39
+ def broadcast(data, reliable=false, channel=1)
40
+ @server.broadcast_packet data.to_json, reliable, channel
41
+ @server.flush
42
+ end
43
+
44
+ def register_player(player_id, conn)
45
+ @player_connections[player_id] = conn
46
+ end
47
+
48
+ def deregister_player(player_id)
49
+ @player_connections.delete player_id
50
+ end
51
+
52
+ def player_connection(player_id)
53
+ @player_connections[player_id]
54
+ end
55
+
56
+ # Re-broadcast to everyone except the original sender
57
+ def broadcast_player_action(sender_id, hash, channel)
58
+ data = hash.to_json
59
+ @clients.keys.each do |id|
60
+ @server.send_packet(id, data, false, channel) unless id == sender_id
61
+ end
62
+ @server.flush
63
+ end
64
+
65
+ def update(timeout=0) # non-blocking by default
66
+ @server.update(timeout)
67
+ end
68
+
69
+ def update_until(stop_time)
70
+ while Time.now.to_r < stop_time do
71
+ update
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,42 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'game_2d/hash'
4
+
5
+ class Storage
6
+ def self.in_home_dir(name)
7
+ Storage.new "#{Dir.home}/#{name}"
8
+ end
9
+
10
+ def initialize(dir)
11
+ @dir = dir
12
+ FileUtils.mkdir_p @dir
13
+ end
14
+
15
+ def dir(subdir)
16
+ Storage.new("#{@dir}/#{subdir}")
17
+ end
18
+
19
+ def [](name)
20
+ Settings.new("#{@dir}/#{name}")
21
+ end
22
+
23
+ def to_s; "Storage(#{@dir})"; end
24
+ end
25
+
26
+ class Settings
27
+ def initialize(name)
28
+ @name = name
29
+ @values = File.exist?(name) ? JSON.parse(IO.read(name)).fix_keys : {}
30
+ end
31
+
32
+ def save
33
+ puts "Writing to #{self}"
34
+ File.open(@name, 'w') {|f| f.write(@values.to_json) }
35
+ end
36
+
37
+ def [](key); @values[key]; end
38
+ def []=(key, value); @values[key] = value; end
39
+ def empty?; @values.empty?; end
40
+
41
+ def to_s; "Settings(#{@name})"; end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Game2d
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,21 @@
1
+ require 'game_2d/entity'
2
+
3
+ class Wall < Entity
4
+ def initialize(space, cell_x, cell_y)
5
+ super(cell_x * Entity::WIDTH, cell_y * Entity::HEIGHT)
6
+ self.space = space
7
+ end
8
+
9
+ def moving?; false; end
10
+ def moving=(moving); end
11
+
12
+ def sleep_now?; true; end
13
+ def wake!; end
14
+
15
+ def registry_id; "Wall[#{left_cell_x}x#{top_cell_y}]"; end
16
+ def to_s
17
+ "Wall at #{left_cell_x}x#{top_cell_y} (#{x}x#{y})"
18
+ end
19
+
20
+ def all_state; [x, y]; end
21
+ end
@@ -0,0 +1,3 @@
1
+ module ZOrder
2
+ Background, Objects, Pellet, Player, Highlight, Text, Cursor = *0..6
3
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,235 @@
1
+ $LOAD_PATH << '.'
2
+ require 'game'
3
+ require 'server_port'
4
+ require 'client_engine'
5
+ require 'client_connection'
6
+
7
+ IP_ADDRESS = '1.1.1.1'
8
+ CONNECTION_ID = 666
9
+
10
+ class FakeENetServer
11
+ def initialize(*args)
12
+ $stderr.puts "FakeENetServer.new(#{args.inspect})"
13
+ @queue = []
14
+ end
15
+ def broadcast_packet(data, reliable, channel)
16
+ $stderr.puts "FakeENetServer.broadcast_packet"
17
+ $fake_client.xmit data, channel
18
+ end
19
+ def send_packet(id, data, reliable, channel)
20
+ $fake_client.xmit data, channel
21
+ end
22
+ def update(timeout)
23
+ $stderr.puts "FakeENetServer.update(#{timeout}) - processing #{@queue.size} packets"
24
+ @queue.each do |data, channel|
25
+ $fake_server_port.on_packet_receive(CONNECTION_ID, data, channel)
26
+ end
27
+ @queue.clear
28
+ end
29
+ def on_connection(a_method) # accepts (id, ip)
30
+ end
31
+ def on_packet_receive(a_method) # accepts (id, data, channel)
32
+ end
33
+ def on_disconnection(a_method) # accepts (id)
34
+ end
35
+ def flush
36
+ $stderr.puts "FakeENetServer.flush"
37
+ end
38
+
39
+ def xmit(data, channel)
40
+ $stderr.puts "Client->Server on ##{channel}: #{data}"
41
+ @queue << [data, channel]
42
+ end
43
+ end
44
+
45
+ class FakeServerPort < ServerPort
46
+ def _create_enet_server(*args)
47
+ $fake_server_port = self
48
+ $fake_server = FakeENetServer.new *args
49
+ end
50
+ end
51
+
52
+ class FakeGame < Game
53
+ attr_reader :space
54
+
55
+ def _create_server_port(*args)
56
+ FakeServerPort.new *args
57
+ end
58
+ end
59
+
60
+ class FakeENetConnection
61
+ def initialize(*args)
62
+ $stderr.puts "FakeENetConnection.new(#{args.inspect})"
63
+ @queue = []
64
+ end
65
+ def connect(timeout)
66
+ $stderr.puts "FakeENetConnection.connect(#{timeout})"
67
+ $fake_server_port.on_connection(CONNECTION_ID, IP_ADDRESS)
68
+ $fake_client_conn.on_connect
69
+ end
70
+ def send_packet(data, reliable, channel)
71
+ $fake_server.xmit data, channel
72
+ end
73
+ def update(timeout)
74
+ $stderr.puts "FakeENetConnection.update(#{timeout}) - processing #{@queue.size} packets"
75
+ @queue.each do |data, channel|
76
+ $fake_client_conn.on_packet(data, channel)
77
+ end
78
+ @queue.clear
79
+ end
80
+ def on_connection(a_method) # accepts no args
81
+ end
82
+ def on_packet_receive(a_method) # accepts (data, channel)
83
+ end
84
+ def on_disconnection(a_method) # accepts no args
85
+ end
86
+ def flush
87
+ $stderr.puts "FakeENetConnection.flush"
88
+ end
89
+
90
+ def xmit(data, channel)
91
+ $stderr.puts "Server->Client on ##{channel}: #{data}"
92
+ @queue << [data, channel]
93
+ end
94
+ end
95
+
96
+ class FakeClientConnection < ClientConnection
97
+ def _create_connection(*args)
98
+ $fake_client_conn = self
99
+ $fake_client = FakeENetConnection.new(*args)
100
+ end
101
+ end
102
+
103
+ class FakeGameWindow
104
+ attr_accessor :player_id, :conn, :engine
105
+
106
+ def initialize(host, port, player_name)
107
+ @conn = FakeClientConnection.new(host, port, self, player_name)
108
+ @conn.engine = @engine = ClientEngine.new(self)
109
+ end
110
+
111
+ def space
112
+ @engine.space
113
+ end
114
+
115
+ def player
116
+ space[@player_id]
117
+ end
118
+
119
+ def update
120
+ @conn.update
121
+ @engine.update
122
+ end
123
+
124
+ def generate_move(move)
125
+ @conn.send_move move
126
+ end
127
+ end
128
+
129
+ describe FakeGame do
130
+ let(:hostname) { 'localhost' }
131
+ let(:port_number) { 9998 }
132
+ let(:player_name) { 'Ed' }
133
+ let(:max_clients) { 2 }
134
+ let(:storage) { '.test' }
135
+ let(:level) { 'test-level' }
136
+ let(:cell_width) { 3 }
137
+ let(:cell_height) { 3 }
138
+ let(:self_check) { false }
139
+ let(:profile) { false }
140
+
141
+ let(:game) { FakeGame.new(
142
+ :port => port_number,
143
+ :max_clients => max_clients,
144
+ :storage => storage,
145
+ :level => level,
146
+ :width => cell_width,
147
+ :height => cell_height,
148
+ :self_check => self_check,
149
+ :profile => profile,
150
+ :registry_broadcast_every => registry_broadcast_every
151
+ ) }
152
+ let(:window) { game; FakeGameWindow.new(hostname, port_number, player_name) }
153
+
154
+ def update_both
155
+ game.update
156
+ window.update
157
+ end
158
+
159
+ def expect_spaces_to_match
160
+ expect(window.space).to eq(game.space)
161
+ end
162
+
163
+ context "with default registry syncs" do
164
+ let(:registry_broadcast_every) { nil }
165
+ it "is in sync after one update" do
166
+ window
167
+
168
+ update_both
169
+
170
+ expect_spaces_to_match
171
+ end
172
+ it "is in sync after a fall and a build" do
173
+ window
174
+
175
+ expect(game.tick).to eq(-1)
176
+ expect(window.engine.tick).to be_nil
177
+
178
+ 28.times do |n|
179
+ update_both
180
+ expect(game.tick).to eq(n)
181
+ expect(window.engine.tick).to eq(n)
182
+ expect_spaces_to_match
183
+ end
184
+
185
+ expect(game.space.players.size).to eq(1)
186
+ expect(game.space.npcs.size).to eq(0)
187
+
188
+ plr = game.space.players.first
189
+ expect(plr.y).to eq(800)
190
+
191
+ # Command generated at tick 27, scheduled for tick 33
192
+ window.generate_move :build
193
+
194
+ 5.times do # ticks 28 - 32
195
+ update_both
196
+ expect_spaces_to_match
197
+ expect(game.space.npcs.size).to eq(0)
198
+ end
199
+
200
+ # tick 33
201
+ update_both
202
+ expect(game.tick).to eq(33)
203
+ expect(game.space.npcs.size).to eq(1)
204
+
205
+ expect_spaces_to_match
206
+
207
+ # Command generated at tick 33, scheduled for tick 39
208
+ window.generate_move :rise_up
209
+ 5.times do # ticks 34 - 38
210
+ update_both
211
+ $stderr.puts "TICK ##{game.tick}"
212
+ expect_spaces_to_match
213
+ expect(plr.y).to eq(800)
214
+ end
215
+ 41.times do |n| # ticks 39 - 79
216
+ update_both
217
+ $stderr.puts "TICK ##{game.tick}"
218
+ expect(plr.y).to eq(800 - (10 * n))
219
+ cplr = window.engine.space.players.first
220
+ binding.pry unless cplr == plr
221
+ expect(cplr).to eq(plr)
222
+ expect_spaces_to_match
223
+ end
224
+ end
225
+ end
226
+ context "with no registry syncs" do
227
+ let(:registry_broadcast_every) { 0 }
228
+ it "is in sync after one update" do
229
+ window
230
+ update_both
231
+
232
+ expect_spaces_to_match
233
+ end
234
+ end
235
+ end