Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 00aa71bf63
478 lines (451 sloc) 9.981 kb
# encoding: utf-8
SCREEN_WIDTH = 120
SCREEN_HEIGHT = 40
class Game
def initialize
reset
end
def reset
@run = true
@world = World.new(SCREEN_WIDTH)
@screen = Screen.new(SCREEN_WIDTH, SCREEN_HEIGHT, @world)
end
def run
Signal.trap(:INT) do
@run = false
end
while @run
start_time = Time.new.to_f
unless @world.tick
reset
end
render start_time
end
on_exit
end
def render start_time
@world.buildings.each do |building|
@screen.draw(building)
end
@screen.draw(@world.player)
@world.misc.each do |object|
@screen.draw(object)
end
@screen.render start_time
end
def on_exit
@screen.on_exit
end
end
class Screen
OFFSET = -20
def initialize width, height, world
@width = width
@height = height
@world = world
@background = world.background
create_frame_buffer
%x{stty -icanon -echo}
print "\033[0m" # reset
print "\033[2J" # clear screen
print "\x1B[?25l" # disable cursor
end
attr_reader :width, :height, :world
def create_frame_buffer
@fb = Framebuffer.new @background
end
def draw renderable
renderable.each_pixel(world.ticks) do |x, y, pixel|
@fb.set x, y, pixel
end
end
def render start_time
print "\e[H"
buffer = ''
previous_pixel = nil
(0...height).each do |y|
(OFFSET...(width + OFFSET)).each do |x|
pixel = @fb.get(x, y)
if Pixel === previous_pixel && Pixel === pixel && pixel.color_equal?(previous_pixel)
buffer << pixel.char
else
buffer << pixel.to_s
end
previous_pixel = pixel
end
buffer << "\n"
end
print "\033[0m"
dt = Time.new.to_f - start_time;
target_time = 0.04
sleep target_time - dt if dt < target_time
print buffer
create_frame_buffer
end
def on_exit
print "\033[0m" # reset colours
print "\x1B[?25h" # re-enable cursor
print "\n"
end
end
class Pixel
def initialize char = " ", fg = nil, bg = nil
@char = char
@fg, @bg = fg, bg
end
attr_reader :char
def fg; @fg || 255 end
def bg; @bg || 0 end
def to_s
"\033[48;5;%dm\033[38;5;%dm%s" % [ bg, fg, @char ]
end
def color_equal? other
fg == other.fg && bg == other.bg
end
end
class Background
PALETTE = [ 16, 232, 233 ]
PERIOD = 16.0
SPEED = 0.5
BLOCKINESS = 10.0
def initialize world
@world = world
end
def pixel x, y, char = " "
Pixel.new char, 0, color(x, y)
end
def color x, y
y = (y / BLOCKINESS).round * BLOCKINESS
sin = Math.sin((x + @world.distance.to_f * SPEED) / PERIOD + y / PERIOD)
PALETTE[(0.9 * sin + 0.9).round]
end
end
class WindowColor
PALETTE = [ 16, 60 ]
PERIOD = 6.0
def pixel x, y, char = " "
Pixel.new char, 0, color(x, y)
end
def color x, y
sin = Math.sin(x / PERIOD + y / (PERIOD * 0.5))
PALETTE[(0.256 * sin + 0.256).round]
end
end
class Framebuffer
def initialize background
@pixels = Hash.new { |h, k| h[k] = {} }
@background = background
end
def set x, y, pixel
@pixels[x][y] = pixel
end
def get x, y
@pixels[x][y] || @background.pixel(x, y)
end
def size
@pixels.values.reduce(0) { |a, v| a + v.size }
end
end
class World
def initialize horizon
@ticks = 0
@horizon = horizon
@building_generator = BuildingGenerator.new(self, WindowColor.new)
@background = Background.new(self)
@player = Player.new(25, @background)
@buildings = [ @building_generator.build(-10, 30, 120) ]
@misc = [ Scoreboard.new(self), RoflCopter.new(50, 4, @background) ]
@speed = 4
@distance = 0
end
attr_reader :buildings, :player, :horizon, :speed, :misc, :ticks, :distance, :background
def tick
# TODO: this, but less often.
if @ticks % 20 == 0
@building_generator.generate_if_necessary
@building_generator.destroy_if_necessary
end
@distance += speed
buildings.each do |b|
b.move_left speed
end
if b = building_under_player
if player.bottom_y > b.y
b.move_left(-speed)
@speed = 0
@misc << Blood.new(player.x, player.y)
@misc << GameOverBanner.new
player.die!
end
end
begin
if STDIN.read_nonblock(1)
if player.dead?
return false
else
player.jump
end
end
rescue Errno::EAGAIN
end
player.tick
if b = building_under_player
player.walk_on_building b if player.bottom_y >= b.y
end
@ticks += 1
end
def building_under_player
buildings.detect do |b|
b.x <= player.x && b.right_x >= player.right_x
end
end
end
class BuildingGenerator
def initialize world, background
@world = world
@background = background
end
def destroy_if_necessary
while @world.buildings.any? && @world.buildings.first.x < -100
@world.buildings.shift
end
end
def generate_if_necessary
while (b = @world.buildings.last).x < @world.horizon
@world.buildings << build(
b.right_x + minimium_gap + rand(24),
next_y(b),
rand(40) + 40
)
end
end
def minimium_gap; 16 end
def maximum_height_delta; 10 end
def minimum_height_clearance; 12; end
def next_y previous_building
p = previous_building
delta = 0
while delta.abs <= 1
delta = maximum_height_delta * -1 + rand(2 * maximum_height_delta + 1)
end
[25, [previous_building.y - delta, minimum_height_clearance].max].min
end
def build x, y, width
Building.new x, y, width, @background
end
end
module Renderable
def each_pixel ticks
(y...(y + height)).each do |y|
(x...(x + width)).each do |x|
rx = x - self.x
ry = y - self.y
yield x, y, pixel(x, y, rx, ry, ticks)
end
end
end
def right_x; x + width end
end
class Building
include Renderable
def initialize x, y, width, background
@x, @y = x, y
@width = width
@background = background
@period = rand(4) + 6
@window_width = @period - rand(2) - 1
@color = (235..238).to_a.shuffle.first # Ruby 1.8
@top_color = @color + 4
@left_color = @color + 2
end
attr_reader :x, :y, :width
def move_left distance
@x -= distance
end
def height; SCREEN_HEIGHT - @y end
def pixel x, y, rx, ry, ticks
if ry == 0
if rx == width - 1
Pixel.new " "
else
Pixel.new "=", 234, @top_color
end
elsif rx == 0 || rx == 1
Pixel.new ":", @left_color + 1, @left_color
elsif rx == 2
Pixel.new ":", 236, 236
elsif rx == width - 1
Pixel.new ":", 236, 236
else
if rx % @period >= @period - @window_width && ry % 5 >= 2
Pixel.new(" ", 255, @background.color(rx + x/2, ry))
else
Pixel.new(":", 235, @color)
end
end
end
end
class Player
include Renderable
def initialize y, background
@y = y
@background = background
@velocity = 1
@walking = false
end
def x; 0; end
def width; 3 end
def height; 3 end
def pixel x, y, rx, ry, ticks
Pixel.new char(rx, ry, ticks), 255, @background.color(x, y)
end
def char rx, ry, ticks
if dead?
[
' @ ',
'\+/',
' \\\\',
][ry][rx]
elsif !@walking
[
' O/',
'/| ',
'/ >',
][ry][rx]
else
[
[
' O ',
'/|v',
'/ >',
],
[
' 0 ',
',|\\',
' >\\',
],
][ticks / 4 % 2][ry][rx]
end
end
def acceleration
if @dead
0.05
else
0.35
end
end
def tick
@y += @velocity
@velocity += acceleration
@walking = false
end
def y; @y.round end
def bottom_y; y + height end
def walk_on_building b
@y = b.y - height
@velocity = 0
@walking = true
end
def jump
jump! if @walking
end
def jump!
@velocity = -2.5
end
def die!
@dead = true
@velocity = 0
end
def dead?
@dead
end
end
class Blood < Struct.new(:x, :y)
include Renderable
def height; 4 end
def width; 2 end
def x; super + 2 end
def pixel x, y, rx, ry, ticks
Pixel.new ":", 124, 52
end
end
class Scoreboard
include Renderable
def initialize world
@world = world
end
def height; 3 end
def width; 20 end
def x; -18 end
def y; 1 end
def template
[
' ',
' Score: %9s ' % [ @world.distance],
' '
]
end
def pixel x, y, rx, ry, ticks
Pixel.new template[ry][rx], 244, 234
end
end
class GameOverBanner
FG = 16
BG = 244
include Renderable
def x; 28 end
def y; 14 end
def width; 28 end
def height; 3 end
def template
[
' ',
' YOU DIED. LOL. ',
' ',
]
end
def pixel x, y, rx, ry, ticks
Pixel.new template[ry][rx], FG, BG
end
end
class RoflCopter
include Renderable
def initialize x, y, background
@x, @y = x, y
@background = background
@frames = [
[
' :LoL:ROFL:ROFL',
' L ____|__ ',
' O ===` []\ ',
' L \________] ',
' .__|____|__/ ',
],
[
' ROFL:ROFL:LoL: ',
' ____|__ ',
' LOL===` []\ ',
' \________] ',
' .__|____|__/ ',
],
]
end
def width; 24 end
def height; 5 end
def y
range = 1.5
@y + (range * Math.sin(Time.new.to_f * 1.5)).round
end
def x
range = 20
@x + (range * Math.sin(Time.new.to_f * 0.7)).round
end
def pixel x, y, rx, ry, ticks
Pixel.new char(rx, ry, ticks), 246, @background.color(x, y)
end
def char rx, ry, ticks
@frames[ticks % 2][ry][rx] || " "
rescue
" " # Roflcopter crashes from time to time..
end
end
Jump to Line
Something went wrong with that request. Please try again.