roflbalt 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +54 -0
- data/bin/roflbalt +5 -0
- data/lib/roflbalt.rb +477 -0
- metadata +50 -0
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
ROFLBALT
|
2
|
+
========
|
3
|
+
|
4
|
+
A Canabalt-inspired sidescroller in ASCII (with ANSI color!) for your console.
|
5
|
+
|
6
|
+
Run it in Terminal.app or iTerm.app with xterm-256color.
|
7
|
+
|
8
|
+
Instructions
|
9
|
+
------------
|
10
|
+
|
11
|
+
With Ruby 1.9 installed, run:
|
12
|
+
|
13
|
+
cd roflbalt
|
14
|
+
./bin/roflbalt
|
15
|
+
|
16
|
+
|
17
|
+
"Screenshot"
|
18
|
+
------------
|
19
|
+
|
20
|
+
Score: 23432
|
21
|
+
|
22
|
+
ROFL:ROFL:LoL:ROFL:ROFL
|
23
|
+
O/ L ____|__
|
24
|
+
/| O ===` []\
|
25
|
+
/ > L \________]
|
26
|
+
.__|____|__/
|
27
|
+
|
28
|
+
==========================================
|
29
|
+
::::::::::::::::::::::::::::::::::::::::::
|
30
|
+
::: :: :: :: ::
|
31
|
+
::: :: :: :: ::
|
32
|
+
==================== ::::::::::::::::::::::::::::::::::::::::::
|
33
|
+
::::::::::::::::::::: ::: :: :: :: ::
|
34
|
+
:: :: :: ::: :: :: :: ::
|
35
|
+
:: :: :: ::::::::::::::::::::::::::::::::::::::::::
|
36
|
+
::::::::::::::::::::: ::: :: :: :: ::
|
37
|
+
:: :: :: ::: :: :: :: ::
|
38
|
+
|
39
|
+
WTF?
|
40
|
+
----
|
41
|
+
|
42
|
+
This was made by [Dennis Hotson][1] and [Paul Annesley][2] at [Rails Camp X][3] in Adelaide.
|
43
|
+
|
44
|
+
[1]: http://dhotson.tumblr.com/
|
45
|
+
[2]: http://paul.annesley.cc/
|
46
|
+
[3]: http://railscamps.com/
|
47
|
+
|
48
|
+
|
49
|
+
License
|
50
|
+
-------
|
51
|
+
|
52
|
+
(c) 2012 Dennis Hotson, Paul Annesley
|
53
|
+
|
54
|
+
Open source: MIT license.
|
data/bin/roflbalt
ADDED
data/lib/roflbalt.rb
ADDED
@@ -0,0 +1,477 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
SCREEN_WIDTH = 120
|
4
|
+
SCREEN_HEIGHT = 40
|
5
|
+
|
6
|
+
class Game
|
7
|
+
def initialize
|
8
|
+
reset
|
9
|
+
end
|
10
|
+
def reset
|
11
|
+
@run = true
|
12
|
+
@world = World.new(SCREEN_WIDTH)
|
13
|
+
@screen = Screen.new(SCREEN_WIDTH, SCREEN_HEIGHT, @world)
|
14
|
+
end
|
15
|
+
def run
|
16
|
+
Signal.trap(:INT) do
|
17
|
+
@run = false
|
18
|
+
end
|
19
|
+
while @run
|
20
|
+
start_time = Time.new.to_f
|
21
|
+
unless @world.tick
|
22
|
+
reset
|
23
|
+
end
|
24
|
+
render start_time
|
25
|
+
end
|
26
|
+
on_exit
|
27
|
+
end
|
28
|
+
def render start_time
|
29
|
+
@world.buildings.each do |building|
|
30
|
+
@screen.draw(building)
|
31
|
+
end
|
32
|
+
@screen.draw(@world.player)
|
33
|
+
@world.misc.each do |object|
|
34
|
+
@screen.draw(object)
|
35
|
+
end
|
36
|
+
@screen.render start_time
|
37
|
+
end
|
38
|
+
def on_exit
|
39
|
+
@screen.on_exit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Screen
|
44
|
+
OFFSET = -20
|
45
|
+
def initialize width, height, world
|
46
|
+
@width = width
|
47
|
+
@height = height
|
48
|
+
@world = world
|
49
|
+
@background = world.background
|
50
|
+
create_frame_buffer
|
51
|
+
%x{stty -icanon -echo}
|
52
|
+
print "\033[0m" # reset
|
53
|
+
print "\033[2J" # clear screen
|
54
|
+
print "\x1B[?25l" # disable cursor
|
55
|
+
end
|
56
|
+
attr_reader :width, :height, :world
|
57
|
+
def create_frame_buffer
|
58
|
+
@fb = Framebuffer.new @background
|
59
|
+
end
|
60
|
+
def draw renderable
|
61
|
+
renderable.each_pixel(world.ticks) do |x, y, pixel|
|
62
|
+
@fb.set x, y, pixel
|
63
|
+
end
|
64
|
+
end
|
65
|
+
def render start_time
|
66
|
+
print "\e[H"
|
67
|
+
buffer = ''
|
68
|
+
previous_pixel = nil
|
69
|
+
(0...height).each do |y|
|
70
|
+
(OFFSET...(width + OFFSET)).each do |x|
|
71
|
+
pixel = @fb.get(x, y)
|
72
|
+
if Pixel === previous_pixel && Pixel === pixel && pixel.color_equal?(previous_pixel)
|
73
|
+
buffer << pixel.char
|
74
|
+
else
|
75
|
+
buffer << pixel.to_s
|
76
|
+
end
|
77
|
+
previous_pixel = pixel
|
78
|
+
end
|
79
|
+
buffer << "\n"
|
80
|
+
end
|
81
|
+
print "\033[0m"
|
82
|
+
|
83
|
+
dt = Time.new.to_f - start_time;
|
84
|
+
target_time = 0.04
|
85
|
+
sleep target_time - dt if dt < target_time
|
86
|
+
|
87
|
+
print buffer
|
88
|
+
create_frame_buffer
|
89
|
+
end
|
90
|
+
def on_exit
|
91
|
+
print "\033[0m" # reset colours
|
92
|
+
print "\x1B[?25h" # re-enable cursor
|
93
|
+
print "\n"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Pixel
|
98
|
+
def initialize char = " ", fg = nil, bg = nil
|
99
|
+
@char = char
|
100
|
+
@fg, @bg = fg, bg
|
101
|
+
end
|
102
|
+
attr_reader :char
|
103
|
+
def fg; @fg || 255 end
|
104
|
+
def bg; @bg || 0 end
|
105
|
+
def to_s
|
106
|
+
"\033[48;5;%dm\033[38;5;%dm%s" % [ bg, fg, @char ]
|
107
|
+
end
|
108
|
+
def color_equal? other
|
109
|
+
fg == other.fg && bg == other.bg
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Background
|
114
|
+
PALETTE = [ 16, 232, 233 ]
|
115
|
+
PERIOD = 16.0
|
116
|
+
SPEED = 0.5
|
117
|
+
BLOCKINESS = 10.0
|
118
|
+
def initialize world
|
119
|
+
@world = world
|
120
|
+
end
|
121
|
+
def pixel x, y, char = " "
|
122
|
+
Pixel.new char, 0, color(x, y)
|
123
|
+
end
|
124
|
+
def color x, y
|
125
|
+
y = (y / BLOCKINESS).round * BLOCKINESS
|
126
|
+
sin = Math.sin((x + @world.distance.to_f * SPEED) / PERIOD + y / PERIOD)
|
127
|
+
PALETTE[(0.9 * sin + 0.9).round]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class WindowColor
|
132
|
+
PALETTE = [ 16, 60 ]
|
133
|
+
PERIOD = 6.0
|
134
|
+
def pixel x, y, char = " "
|
135
|
+
Pixel.new char, 0, color(x, y)
|
136
|
+
end
|
137
|
+
def color x, y
|
138
|
+
sin = Math.sin(x / PERIOD + y / (PERIOD * 0.5))
|
139
|
+
PALETTE[(0.256 * sin + 0.256).round]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class Framebuffer
|
144
|
+
def initialize background
|
145
|
+
@pixels = Hash.new { |h, k| h[k] = {} }
|
146
|
+
@background = background
|
147
|
+
end
|
148
|
+
def set x, y, pixel
|
149
|
+
@pixels[x][y] = pixel
|
150
|
+
end
|
151
|
+
def get x, y
|
152
|
+
@pixels[x][y] || @background.pixel(x, y)
|
153
|
+
end
|
154
|
+
def size
|
155
|
+
@pixels.values.reduce(0) { |a, v| a + v.size }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class World
|
160
|
+
def initialize horizon
|
161
|
+
@ticks = 0
|
162
|
+
@horizon = horizon
|
163
|
+
@building_generator = BuildingGenerator.new(self, WindowColor.new)
|
164
|
+
@background = Background.new(self)
|
165
|
+
@player = Player.new(25, @background)
|
166
|
+
@buildings = [ @building_generator.build(-10, 30, 120) ]
|
167
|
+
@misc = [ Scoreboard.new(self), RoflCopter.new(50, 4, @background) ]
|
168
|
+
@speed = 4
|
169
|
+
@distance = 0
|
170
|
+
end
|
171
|
+
attr_reader :buildings, :player, :horizon, :speed, :misc, :ticks, :distance, :background
|
172
|
+
def tick
|
173
|
+
# TODO: this, but less often.
|
174
|
+
if @ticks % 20 == 0
|
175
|
+
@building_generator.generate_if_necessary
|
176
|
+
@building_generator.destroy_if_necessary
|
177
|
+
end
|
178
|
+
|
179
|
+
@distance += speed
|
180
|
+
|
181
|
+
buildings.each do |b|
|
182
|
+
b.move_left speed
|
183
|
+
end
|
184
|
+
|
185
|
+
if b = building_under_player
|
186
|
+
if player.bottom_y > b.y
|
187
|
+
b.move_left(-speed)
|
188
|
+
@speed = 0
|
189
|
+
@misc << Blood.new(player.x, player.y)
|
190
|
+
@misc << GameOverBanner.new
|
191
|
+
player.die!
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
begin
|
196
|
+
if STDIN.read_nonblock(1)
|
197
|
+
if player.dead?
|
198
|
+
return false
|
199
|
+
else
|
200
|
+
player.jump
|
201
|
+
end
|
202
|
+
end
|
203
|
+
rescue Errno::EAGAIN
|
204
|
+
end
|
205
|
+
|
206
|
+
player.tick
|
207
|
+
|
208
|
+
if b = building_under_player
|
209
|
+
player.walk_on_building b if player.bottom_y >= b.y
|
210
|
+
end
|
211
|
+
|
212
|
+
@ticks += 1
|
213
|
+
end
|
214
|
+
def building_under_player
|
215
|
+
buildings.detect do |b|
|
216
|
+
b.x <= player.x && b.right_x >= player.right_x
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class BuildingGenerator
|
222
|
+
def initialize world, background
|
223
|
+
@world = world
|
224
|
+
@background = background
|
225
|
+
end
|
226
|
+
def destroy_if_necessary
|
227
|
+
while @world.buildings.any? && @world.buildings.first.x < -100
|
228
|
+
@world.buildings.shift
|
229
|
+
end
|
230
|
+
end
|
231
|
+
def generate_if_necessary
|
232
|
+
while (b = @world.buildings.last).x < @world.horizon
|
233
|
+
@world.buildings << build(
|
234
|
+
b.right_x + minimium_gap + rand(24),
|
235
|
+
next_y(b),
|
236
|
+
rand(40) + 40
|
237
|
+
)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
def minimium_gap; 16 end
|
241
|
+
def maximum_height_delta; 10 end
|
242
|
+
def minimum_height_clearance; 12; end
|
243
|
+
def next_y previous_building
|
244
|
+
p = previous_building
|
245
|
+
delta = 0
|
246
|
+
while delta.abs <= 1
|
247
|
+
delta = maximum_height_delta * -1 + rand(2 * maximum_height_delta + 1)
|
248
|
+
end
|
249
|
+
[25, [previous_building.y - delta, minimum_height_clearance].max].min
|
250
|
+
end
|
251
|
+
def build x, y, width
|
252
|
+
Building.new x, y, width, @background
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
module Renderable
|
257
|
+
def each_pixel ticks
|
258
|
+
(y...(y + height)).each do |y|
|
259
|
+
(x...(x + width)).each do |x|
|
260
|
+
rx = x - self.x
|
261
|
+
ry = y - self.y
|
262
|
+
yield x, y, pixel(x, y, rx, ry, ticks)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
def right_x; x + width end
|
267
|
+
end
|
268
|
+
|
269
|
+
class Building
|
270
|
+
include Renderable
|
271
|
+
def initialize x, y, width, background
|
272
|
+
@x, @y = x, y
|
273
|
+
@width = width
|
274
|
+
@background = background
|
275
|
+
@period = rand(4) + 6
|
276
|
+
@window_width = @period - rand(2) - 1
|
277
|
+
@color = (235..238).to_a.shuffle.first # Ruby 1.8
|
278
|
+
@top_color = @color + 4
|
279
|
+
@left_color = @color + 2
|
280
|
+
end
|
281
|
+
attr_reader :x, :y, :width
|
282
|
+
def move_left distance
|
283
|
+
@x -= distance
|
284
|
+
end
|
285
|
+
def height; SCREEN_HEIGHT - @y end
|
286
|
+
def pixel x, y, rx, ry, ticks
|
287
|
+
if ry == 0
|
288
|
+
if rx == width - 1
|
289
|
+
Pixel.new " "
|
290
|
+
else
|
291
|
+
Pixel.new "=", 234, @top_color
|
292
|
+
end
|
293
|
+
elsif rx == 0 || rx == 1
|
294
|
+
Pixel.new ":", @left_color + 1, @left_color
|
295
|
+
elsif rx == 2
|
296
|
+
Pixel.new ":", 236, 236
|
297
|
+
elsif rx == width - 1
|
298
|
+
Pixel.new ":", 236, 236
|
299
|
+
else
|
300
|
+
if rx % @period >= @period - @window_width && ry % 5 >= 2
|
301
|
+
Pixel.new(" ", 255, @background.color(rx + x/2, ry))
|
302
|
+
else
|
303
|
+
Pixel.new(":", 235, @color)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
class Player
|
310
|
+
include Renderable
|
311
|
+
def initialize y, background
|
312
|
+
@y = y
|
313
|
+
@background = background
|
314
|
+
@velocity = 1
|
315
|
+
@walking = false
|
316
|
+
end
|
317
|
+
def x; 0; end
|
318
|
+
def width; 3 end
|
319
|
+
def height; 3 end
|
320
|
+
def pixel x, y, rx, ry, ticks
|
321
|
+
Pixel.new char(rx, ry, ticks), 255, @background.color(x, y)
|
322
|
+
end
|
323
|
+
|
324
|
+
def char rx, ry, ticks
|
325
|
+
if dead?
|
326
|
+
[
|
327
|
+
' @ ',
|
328
|
+
'\+/',
|
329
|
+
' \\\\',
|
330
|
+
][ry][rx]
|
331
|
+
elsif !@walking
|
332
|
+
[
|
333
|
+
' O/',
|
334
|
+
'/| ',
|
335
|
+
'/ >',
|
336
|
+
][ry][rx]
|
337
|
+
else
|
338
|
+
[
|
339
|
+
[
|
340
|
+
' O ',
|
341
|
+
'/|v',
|
342
|
+
'/ >',
|
343
|
+
],
|
344
|
+
[
|
345
|
+
' 0 ',
|
346
|
+
',|\\',
|
347
|
+
' >\\',
|
348
|
+
],
|
349
|
+
][ticks / 4 % 2][ry][rx]
|
350
|
+
end
|
351
|
+
end
|
352
|
+
def acceleration
|
353
|
+
if @dead
|
354
|
+
0.05
|
355
|
+
else
|
356
|
+
0.35
|
357
|
+
end
|
358
|
+
end
|
359
|
+
def tick
|
360
|
+
@y += @velocity
|
361
|
+
@velocity += acceleration
|
362
|
+
@walking = false
|
363
|
+
end
|
364
|
+
def y; @y.round end
|
365
|
+
def bottom_y; y + height end
|
366
|
+
def walk_on_building b
|
367
|
+
@y = b.y - height
|
368
|
+
@velocity = 0
|
369
|
+
@walking = true
|
370
|
+
end
|
371
|
+
def jump
|
372
|
+
jump! if @walking
|
373
|
+
end
|
374
|
+
def jump!
|
375
|
+
@velocity = -2.5
|
376
|
+
end
|
377
|
+
def die!
|
378
|
+
@dead = true
|
379
|
+
@velocity = 0
|
380
|
+
end
|
381
|
+
def dead?
|
382
|
+
@dead
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
class Blood < Struct.new(:x, :y)
|
387
|
+
include Renderable
|
388
|
+
def height; 4 end
|
389
|
+
def width; 2 end
|
390
|
+
def x; super + 2 end
|
391
|
+
def pixel x, y, rx, ry, ticks
|
392
|
+
Pixel.new ":", 124, 52
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
class Scoreboard
|
397
|
+
include Renderable
|
398
|
+
def initialize world
|
399
|
+
@world = world
|
400
|
+
end
|
401
|
+
def height; 3 end
|
402
|
+
def width; 20 end
|
403
|
+
def x; -18 end
|
404
|
+
def y; 1 end
|
405
|
+
def template
|
406
|
+
[
|
407
|
+
' ',
|
408
|
+
' Score: %9s ' % [ @world.distance],
|
409
|
+
' '
|
410
|
+
]
|
411
|
+
end
|
412
|
+
def pixel x, y, rx, ry, ticks
|
413
|
+
Pixel.new template[ry][rx], 244, 234
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class GameOverBanner
|
418
|
+
FG = 16
|
419
|
+
BG = 244
|
420
|
+
include Renderable
|
421
|
+
def x; 28 end
|
422
|
+
def y; 14 end
|
423
|
+
def width; 28 end
|
424
|
+
def height; 3 end
|
425
|
+
def template
|
426
|
+
[
|
427
|
+
' ',
|
428
|
+
' YOU DIED. LOL. ',
|
429
|
+
' ',
|
430
|
+
]
|
431
|
+
end
|
432
|
+
def pixel x, y, rx, ry, ticks
|
433
|
+
Pixel.new template[ry][rx], FG, BG
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
class RoflCopter
|
438
|
+
include Renderable
|
439
|
+
def initialize x, y, background
|
440
|
+
@x, @y = x, y
|
441
|
+
@background = background
|
442
|
+
@frames = [
|
443
|
+
[
|
444
|
+
' :LoL:ROFL:ROFL',
|
445
|
+
' L ____|__ ',
|
446
|
+
' O ===` []\ ',
|
447
|
+
' L \________] ',
|
448
|
+
' .__|____|__/ ',
|
449
|
+
],
|
450
|
+
[
|
451
|
+
' ROFL:ROFL:LoL: ',
|
452
|
+
' ____|__ ',
|
453
|
+
' LOL===` []\ ',
|
454
|
+
' \________] ',
|
455
|
+
' .__|____|__/ ',
|
456
|
+
],
|
457
|
+
]
|
458
|
+
end
|
459
|
+
def width; 24 end
|
460
|
+
def height; 5 end
|
461
|
+
def y
|
462
|
+
range = 1.5
|
463
|
+
@y + (range * Math.sin(Time.new.to_f * 1.5)).round
|
464
|
+
end
|
465
|
+
def x
|
466
|
+
range = 20
|
467
|
+
@x + (range * Math.sin(Time.new.to_f * 0.7)).round
|
468
|
+
end
|
469
|
+
def pixel x, y, rx, ry, ticks
|
470
|
+
Pixel.new char(rx, ry, ticks), 246, @background.color(x, y)
|
471
|
+
end
|
472
|
+
def char rx, ry, ticks
|
473
|
+
@frames[ticks % 2][ry][rx] || " "
|
474
|
+
rescue
|
475
|
+
" " # Roflcopter crashes from time to time..
|
476
|
+
end
|
477
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: roflbalt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paul Annesley
|
9
|
+
- Dennis Hotson
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-01-18 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description: ASCII side-scrolling game, with ANSI color!
|
16
|
+
email:
|
17
|
+
- paul@annesley.cc
|
18
|
+
executables:
|
19
|
+
- roflbalt
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- bin/roflbalt
|
24
|
+
- lib/roflbalt.rb
|
25
|
+
- README.md
|
26
|
+
homepage: https://github.com/pda/roflbalt
|
27
|
+
licenses: []
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 1.8.11
|
47
|
+
signing_key:
|
48
|
+
specification_version: 3
|
49
|
+
summary: Canabalt-inspired ASCII side-scroller for your terminal, with ANSI color!
|
50
|
+
test_files: []
|