snek 0.1.0

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.
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: []