snek 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e9b064541769fc02d79b90d8394c226c70d34199
4
+ data.tar.gz: 9271b210952c5ab66e427c656bf2b8fea88aade2
5
+ SHA512:
6
+ metadata.gz: 54624bbbd428c6bf48039183ddab94c0df99988a7b26053a78ae3644cd286dfc68efca675f44bf58cf1a7381187c766096b8255cf909deb477e2049ff6addd75
7
+ data.tar.gz: 91a641b05a9bb3f8d3acbff651e6703b235675dfc17e7436ed9bb5e8a35fc7efa895324a7d51a9405f4218fad2ca8903d360608f84b8798cdc52c2cb195b8785
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ pkg/
3
+ snek.log
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,17 @@
1
+ # SNEK
2
+
3
+ Terminal multiplayer snek game made at Railscamp 19, Adelaide.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install snek
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ snek --head=⛵️
15
+ ```
16
+
17
+ Controls are <kbd>w</kbd>, <kbd>a</kbd>, <kbd>s</kbd>, <kbd>d</kbd>.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new :spec
5
+
6
+ task default: :spec
Binary file
Binary file
Binary file
data/bin/snek ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << './lib'
4
+
5
+ require 'game'
6
+
7
+ options = {}
8
+
9
+ ARGV.each do |arg|
10
+ option, value = arg.split('=')
11
+ options[option] = value
12
+ end
13
+
14
+ head = options['--head'] || options['-h']
15
+
16
+ if !options.empty? && head.nil?
17
+ $stderr.puts "#{$0}: usage: [-h=[char]|--head=[char]]"
18
+ else
19
+ Snek::Game.new(head: head).start
20
+ end
data/lib/game.rb ADDED
@@ -0,0 +1,277 @@
1
+ require 'logger'
2
+ require 'io/console'
3
+
4
+ Dir[File.dirname(__FILE__) + "/**/*.rb"].each { |f| require f }
5
+
6
+ module Snek
7
+ class Game
8
+ def initialize(head:)
9
+ @food = @local_food_position = random_position
10
+ @random_number = rand
11
+ @messages = {}
12
+ @other_sneks = {}
13
+ @food_positions = {}
14
+ @food_eaten_counts = {}
15
+ @food_eaten_count = 0
16
+ @head = head
17
+ end
18
+
19
+ def start
20
+ GameEngine::Sound.play 'startup.wav'
21
+
22
+ @network = GameEngine::Network.new logger: logger
23
+ @network.open_socket
24
+
25
+ reset_snek
26
+
27
+ GameEngine::Frame.setup
28
+
29
+ GameEngine::Engine.tick do |tick|
30
+ @tick = tick
31
+
32
+ render
33
+ update_network
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :frame
40
+
41
+ def update_network
42
+ @network.receive_updates do |data|
43
+ hostname = data[:hostname]
44
+ unless hostname == @network.hostname
45
+ @other_sneks[hostname] = {
46
+ snek: unpack_snek(data[:snek]),
47
+ head: data[:head],
48
+ }
49
+ end
50
+
51
+ update_food_positions(data[:random_number], data[:food_position], data[:food_eaten_count])
52
+ end
53
+ @network.send_update(
54
+ snek: pack_snek(@snek),
55
+ random_number: @random_number,
56
+ food_position: @local_food_position,
57
+ food_eaten_count: @food_eaten_count,
58
+ head: @head,
59
+ )
60
+ end
61
+
62
+ def update_food_positions(id, value, food_eaten_count)
63
+ cached_food_eaten_count = @food_eaten_counts[id]
64
+
65
+ if cached_food_eaten_count != food_eaten_count
66
+ if id != @random_number
67
+ @local_food_position = random_position
68
+ end
69
+ @food_eaten_counts[id] = food_eaten_count
70
+ end
71
+
72
+ @food_positions[id] = value
73
+ @food = max_remote_food_position
74
+ end
75
+
76
+ def pack_snek(snek)
77
+ snek.flatten.join(',')
78
+ end
79
+
80
+ def unpack_snek(snek)
81
+ snek.split(',').map(&:to_i).each_slice(2).to_a
82
+ end
83
+
84
+ def rows
85
+ GameEngine::Window.rows
86
+ 25
87
+ end
88
+
89
+ def columns
90
+ GameEngine::Window.columns
91
+ 80
92
+ end
93
+
94
+ def render
95
+ @frame = GameEngine::Frame.new columns, rows + 10
96
+
97
+ if @tick % 2 == 0 || %w[e w].include?(@snek.direction)
98
+ new_position = move_snek
99
+
100
+ border = draw_border
101
+ draw_messages
102
+ draw_sneks
103
+ draw_food
104
+ draw_scores
105
+
106
+ check_border_collision(border, new_position)
107
+ check_food_collision(new_position)
108
+
109
+ GameEngine::Input.call do |key|
110
+ if key
111
+ return if @tick == @input_in_tick
112
+ @input_in_tick = @tick
113
+ end
114
+
115
+ head = @snek.last
116
+
117
+ case key
118
+ when 'q'.ord, 27, 3 # escape, ctrl-c
119
+ exit
120
+ when 119 # W up
121
+ @snek.direction = 'n' if @snek.direction != 's'
122
+ when 115 # S down
123
+ @snek.direction = 's' if @snek.direction != 'n'
124
+ when 100 # D right
125
+ @snek.direction = 'e' if @snek.direction != 'w'
126
+ when 97 # A left
127
+ @snek.direction = 'w' if @snek.direction != 'e'
128
+ end
129
+ end
130
+
131
+ frame.render
132
+ end
133
+ end
134
+
135
+ def check_border_collision(border, new_position)
136
+ if border.include?(new_position)
137
+ crash_snek
138
+ add_message 'you crashed into a wall'
139
+ return
140
+ end
141
+ end
142
+
143
+ def check_food_collision(new_position)
144
+ if @food == new_position
145
+ GameEngine::Sound.play 'pickup.wav'
146
+ @snek.snek_length += 1
147
+ @local_food_position = random_position
148
+ @food_eaten_count += 1
149
+
150
+ update_food_positions(@random_number, @local_food_position, @food_eaten_count)
151
+ end
152
+ end
153
+
154
+ def max_remote_food_position
155
+ @food_positions.max_by { |k, v| k }.last
156
+ end
157
+
158
+ # TODO: non-conflicting
159
+ def random_position
160
+ x = (1..columns-2).to_a.sample
161
+ y = (1..rows-2).to_a.sample
162
+
163
+ [x,y]
164
+ end
165
+
166
+ def draw_border
167
+ frame.draw 0, 0, "+#{'-' * (columns-2)}+"
168
+ (1..rows-2).each do |y|
169
+ frame.draw 0, y, '|'
170
+ frame.draw columns-1, y, '|'
171
+ end
172
+ frame.draw 0, rows-1, "+#{'-' * (columns-2)}+"
173
+
174
+ border = []
175
+ border << (0..columns-1).map { |x| [x, 0] }
176
+ border << (0..rows-1).map { |y| [0, y] }
177
+ border << (0..rows-1).map { |y| [columns-1, y] }
178
+ border << (0..columns-1).map { |x| [x, rows-1] }
179
+ border.flatten(1)
180
+ end
181
+
182
+ def draw_sneks
183
+ draw_snek @snek, head: (@head || '@')
184
+
185
+ @other_sneks.values.each do |snek|
186
+ draw_snek snek[:snek], head: (snek[:head] || '&')
187
+ end
188
+ end
189
+
190
+ def draw_snek(snek, head:)
191
+ head = head[0]
192
+ snek.each_with_index do |segment, index|
193
+ char = index == snek.count-1 ? head : '*'
194
+ frame.draw *segment, char
195
+ end
196
+ end
197
+
198
+ def draw_food
199
+ frame.draw *@food, '$'
200
+ end
201
+
202
+ def draw_scores
203
+ draw_score (@head || '@'), @network.hostname, @snek.length, rows
204
+
205
+ @other_sneks.each_with_index do |data, i|
206
+ hostname, snek = data
207
+ head = snek[:head] || '&'
208
+ draw_score(head, hostname, snek[:snek].length, rows + i + 1)
209
+ end
210
+ end
211
+
212
+ def draw_score(head, name, score, position)
213
+ frame.draw 0, position, "#{head[0]} #{score} #{name}"
214
+ end
215
+
216
+ def move_snek
217
+ x, y = @snek.last
218
+
219
+ new_position = case @snek.direction
220
+ when 'n'
221
+ [x, y - 1]
222
+ when 's'
223
+ [x, y + 1]
224
+ when 'e'
225
+ [x + 1, y]
226
+ when 'w'
227
+ [x - 1, y]
228
+ end
229
+
230
+ if @snek.include?(new_position)
231
+ crash_snek
232
+ add_message 'you crashed into yourself'
233
+ return
234
+ end
235
+
236
+ @other_sneks.each do |hostname, snek|
237
+ if snek[:snek].include?(new_position)
238
+ crash_snek
239
+ add_message "you crashed into #{hostname}"
240
+ return
241
+ end
242
+ end
243
+
244
+ @snek << new_position
245
+ new_position
246
+ end
247
+
248
+ def logger
249
+ @logger ||= Logger.new('snek.log').tap do |logger|
250
+ logger.formatter = lambda do |severity, datetime, progname, msg|
251
+ "[#{severity}] #{datetime.strftime('%M:%S')}: #{msg}\n"
252
+ end
253
+ end
254
+ end
255
+
256
+ def reset_snek
257
+ @snek = Snek.new([random_position])
258
+ end
259
+
260
+ def add_message(message)
261
+ @messages[message] = @tick + 10
262
+ end
263
+
264
+ def draw_messages
265
+ @messages.each do |message, end_tick|
266
+ if @tick < end_tick
267
+ frame.draw_center rows/2, message
268
+ end
269
+ end
270
+ end
271
+
272
+ def crash_snek
273
+ GameEngine::Sound.play 'explosion.wav'
274
+ reset_snek
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,22 @@
1
+ module GameEngine
2
+ class Engine
3
+ attr_accessor :tick
4
+
5
+ def self.tick(&block)
6
+ self.new(&block).tap(&:call)
7
+ end
8
+
9
+ def initialize(&block)
10
+ @tick = 0
11
+ @block = block
12
+ end
13
+
14
+ def call
15
+ loop do
16
+ @block.call @tick
17
+ @tick += 1
18
+ sleep 0.1
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,84 @@
1
+ module GameEngine
2
+ class Frame
3
+ attr_reader :rows
4
+
5
+ def initialize(width, height)
6
+ @rows = height.times.map { ' ' * width }
7
+ end
8
+
9
+ def width
10
+ @rows.first.size
11
+ end
12
+
13
+ def height
14
+ @rows.size
15
+ end
16
+
17
+ def draw(x, y, sprite)
18
+ lines = sprite.split("\n")
19
+
20
+ lines.each_with_index do |line, i|
21
+ if line.size > 0 && y+lines.size <= self.height && y+i >= 0
22
+ # crop when drawing off left
23
+ if x < 0
24
+ line = line[x.abs..-1]
25
+ x = 0
26
+ end
27
+
28
+ # crop when drawing off right
29
+ if x+line.size-1 >= self.width
30
+ line = line[0..(self.width-1)-(x+line.size)]
31
+ end
32
+
33
+ @rows[y+i][x..x+line.size-1] = line
34
+ end
35
+ end
36
+ end
37
+
38
+ def draw_center(y, sprite)
39
+ sprite_width = sprite.split("\n").first.size
40
+ x = self.width / 2 - sprite_width / 2
41
+ draw x, y, sprite
42
+ end
43
+
44
+ def render
45
+ @rows.each_with_index do |row, i|
46
+ Frame.move_cursor 0, i
47
+ print row
48
+ end
49
+ end
50
+
51
+ def self.move_cursor(x, y)
52
+ print "\033[#{y+1};#{x+1}H"
53
+ end
54
+
55
+ def self.clear_screen
56
+ print "\033[2J"
57
+ end
58
+
59
+ def self.disable_cursor
60
+ print "\x1B[?25l"
61
+ end
62
+
63
+ def self.enable_cursor
64
+ print "\x1B[?25h"
65
+ end
66
+
67
+ def self.setup
68
+ clear_screen
69
+ disable_cursor
70
+
71
+ $stdin.raw!
72
+ at_exit do
73
+ puts "\r"
74
+ enable_cursor
75
+ $stdin.cooked!
76
+ system 'stty sane'
77
+ end
78
+
79
+ trap 'WINCH' do
80
+ clear_screen
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ module GameEngine
2
+ class Input
3
+ def self.call(&block)
4
+ begin
5
+ loop do
6
+ key = $stdin.read_nonblock(1).ord
7
+ block.call key
8
+ end
9
+ rescue Errno::EAGAIN
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,52 @@
1
+ require 'logger'
2
+ require 'socket'
3
+ require 'yaml'
4
+
5
+ module GameEngine
6
+ class Network
7
+ PORT = 47357
8
+
9
+ attr_reader :logger
10
+
11
+ def initialize(logger: Logger.new('/dev/null'))
12
+ @logger = logger
13
+ end
14
+
15
+ def open_socket
16
+ @socket = UDPSocket.new
17
+ @socket.bind '0.0.0.0', PORT
18
+ @socket.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
19
+ @socket.setsockopt Socket::SOL_SOCKET, Socket::SO_BROADCAST, true
20
+ rescue Errno::EADDRINUSE
21
+ $stderr.puts "Game is already running."
22
+ exit 1
23
+ end
24
+
25
+ def receive_updates(&block)
26
+ loop do
27
+ begin
28
+ data, addr = @socket.recvfrom_nonblock 8192
29
+ data = YAML.load(data)
30
+ block.call(data)
31
+ rescue Psych::SyntaxError => error
32
+ logger.error error
33
+ end
34
+ end
35
+ rescue Errno::EAGAIN
36
+ end
37
+
38
+ def send_update(data)
39
+ data = data.merge(hostname: hostname)
40
+
41
+ begin
42
+ @socket.send data.to_yaml, 0, '255.255.255.255', PORT
43
+ rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EMSGSIZE, Errno::ENETDOWN => error
44
+ logger.error error
45
+ end
46
+ end
47
+
48
+ def hostname
49
+ @hostname ||= `hostname -s`.strip
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,12 @@
1
+ module GameEngine
2
+ class Sound
3
+ def self.play(sound)
4
+ case RUBY_PLATFORM
5
+ when /darwin/
6
+ system "afplay assets/sounds/#{sound} &"
7
+ when /linux/
8
+ system "command -v mplayer >/dev/null 2>&1 && mplayer -msglevel all=-1 -nolirc assets/sounds/#{sound} &"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module GameEngine
2
+ class Window
3
+ def self.rows
4
+ $stdin.winsize[0]
5
+ end
6
+
7
+ def self.columns
8
+ $stdin.winsize[1]
9
+ end
10
+ end
11
+ end
data/lib/snek.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'snek/version'
2
+
3
+ module Snek
4
+ class Snek < Array
5
+ DIRECTIONS = %w[n s e w]
6
+
7
+ attr_accessor :snek_length, :direction
8
+
9
+ def initialize(*args)
10
+ @snek_length = 5
11
+ @direction = random_direction
12
+ super
13
+ end
14
+
15
+ def <<(*args)
16
+ shift if count >= snek_length
17
+
18
+ super
19
+ end
20
+
21
+ alias_method :orig_inspect, :inspect
22
+ def inspect
23
+ "#<Snek @snek_length=#{@snek_length.inspect} self=#{orig_inspect.inspect}>"
24
+ end
25
+
26
+ private
27
+
28
+ def random_direction
29
+ DIRECTIONS.sample
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Snek
2
+ VERSION = '0.1.0'
3
+ end
data/snek.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ #: coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'snek/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'snek'
8
+ spec.version = Snek::VERSION
9
+ spec.authors = ['Odin Dutton']
10
+ spec.email = ['odindutton@gmail.com']
11
+
12
+ spec.summary = 'Terminal multiplayer snek game made at Railscamp 19, Adelaide'
13
+ spec.homepage = 'https://github.com/twe4ked/snek'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = 'bin'
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.10'
21
+ spec.add_development_dependency 'rake', '~> 11.0'
22
+ spec.add_development_dependency 'rspec'
23
+ spec.add_development_dependency 'pry'
24
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snek
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Odin Dutton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - odindutton@gmail.com
72
+ executables:
73
+ - snek
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - README.markdown
81
+ - Rakefile
82
+ - assets/sounds/explosion.wav
83
+ - assets/sounds/pickup.wav
84
+ - assets/sounds/startup.wav
85
+ - bin/snek
86
+ - lib/game.rb
87
+ - lib/game_engine/engine.rb
88
+ - lib/game_engine/frame.rb
89
+ - lib/game_engine/input.rb
90
+ - lib/game_engine/network.rb
91
+ - lib/game_engine/sound.rb
92
+ - lib/game_engine/window.rb
93
+ - lib/snek.rb
94
+ - lib/snek/version.rb
95
+ - snek.gemspec
96
+ homepage: https://github.com/twe4ked/snek
97
+ licenses: []
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.4.5
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Terminal multiplayer snek game made at Railscamp 19, Adelaide
119
+ test_files: []