Skip to content

Instantly share code, notes, and snippets.

@jlogsdon
Created June 27, 2012 17:48
Show Gist options
  • Save jlogsdon/3005649 to your computer and use it in GitHub Desktop.
Save jlogsdon/3005649 to your computer and use it in GitHub Desktop.
require 'curses'
module Sokoban
class Cell
attr_accessor :x, :y
def initialize(board, x=0, y=0)
@board = board
@x = x
@y = y
end
def <=>(cell)
return 1 if below.include?(cell.to_s)
return -1 if cell.below.include?(self.to_s)
return 0
end
def location=(coords)
self.x = coords.first
self.y = coords.last
end
def location
[x, y]
end
def adjacent
Hash[@board.adjacent_to(self).map do |cell|
[cell.relative_to(self), cell]
end]
end
def feel(direction)
adjacent[direction]
end
def adjacent_to?(cell)
if self.x == cell.x
return (self.y == cell.y + 1) || (self.y == cell.y - 1)
elsif self.y == cell.y
return (self.x == cell.x + 1) || (self.x == cell.x - 1)
end
end
def relative_to(cell)
if self.x == cell.x
return :up if self.y == cell.y - 1
return :down if self.y == cell.y + 1
elsif self.y == cell.y
return :left if self.x == cell.x - 1
return :right if self.x == cell.x + 1
end
end
def string_for(char)
below.include?(char) ? char : to_s
end
def below
[]
end
def inspect
"#<#{self.class.name} #{self.x}x#{self.y}>"
end
private
def coords_for(direction)
case direction
when :up then [x, y - 1]
when :down then [x, y + 1]
when :left then [x - 1, y]
when :right then [x + 1, y]
end
end
end
class Moveable < Cell
def move(direction, move_neighbor = true)
move_to = coords_for(direction)
if neighbor = feel(direction)
if neighbor.is_a?(Moveable)
return false unless move_neighbor && neighbor.move(direction, false)
else
return false unless neighbor.is_a?(Storage)
end
end
self.location = move_to
return true
end
end
class Player < Moveable
def to_s; '@'; end
def below; ['#']; end
end
class Box < Moveable
def to_s; 'o'; end
def below; ['#', '@']; end
end
class Storage < Cell
def to_s; '.'; end
def below; ['#', '@', 'o']; end
end
class Wall < Cell
def to_s; '#'; end
end
class Board
attr_reader :width, :height, :level, :player, :moves, :cells
def initialize(level)
@level = level
reset!
end
def reset!
@cells = []
@player = Player.new(self)
@height = 0
@width = 0
@moves = 0
read_map
end
def adjacent_to(cell)
@cells.select { |c| c.adjacent_to?(cell) }.sort.uniq { |c| c.location }
end
def at(cell)
@cells.select { |c| c != cell && c.location == cell.location }.sort.first
end
def boxes
@cells.select { |c| c.is_a?(Box) }
end
def stored
boxes.select { |box| at(box).is_a?(Storage) }
end
def move(direction)
@moves += 1
@player.move(direction)
end
def to_grid
map = Array.new(@height)
@cells.each do |cell|
map[cell.y] ||= Array.new(@width, ' ')
map[cell.y][cell.x] = cell.string_for(map[cell.y][cell.x])
end
map[@player.y][@player.x] = @player.to_s
map
end
def level_path
File.join(File.dirname(__FILE__), "level_#{@level}.txt")
end
private
def read_map
y = 0
File.foreach(level_path) do |row|
x = 0
row.each_char do |char|
x += 1
case char
when '@' then @player.location = [x, y]
when 'o' then @cells << Box.new(self, x, y)
when '.' then @cells << Storage.new(self, x, y)
when '#' then @cells << Wall.new(self, x, y)
end
end
@width = x if @width < x
y += 1
end
@height = y - 1
end
end
class DebugWindow
include Curses
def initialize(board)
@board = board
@screen = Curses::Window.new(5, 35, 0, 0)
end
def close
@screen.close
end
def render
@screen.clear
@screen.setpos(0, 0)
@screen << @board.player.inspect + "\n"
@board.player.adjacent.each do |direction, cell|
@screen << direction.to_s + "\t" + cell.inspect + "\n"
end
@screen.refresh
end
end
class Display
include Curses
def initialize(board)
@board = board
end
def init
@screen = init_screen
noecho
cbreak
curs_set(0)
start_color
init_pair(COLOR_RED, COLOR_RED, COLOR_BLACK)
init_pair(COLOR_YELLOW, COLOR_YELLOW, COLOR_BLACK)
init_pair(COLOR_CYAN, COLOR_CYAN, COLOR_BLACK)
@debug = DebugWindow.new(@board)
@screen.keypad = true
end
def close
@debug.close
close_screen
end
def key_buffer(initial_char)
c = initial_char
buffer = []
printable = (33..126).to_a
while printable.include?(c) && c.chr =~ /\d/
buffer << c.chr.to_i
c = @screen.getch
end
return c, buffer
end
def render
@screen.clear
top = (@screen.maxy / 2) - (@board.height / 2)
left = (@screen.maxx / 2) - (@board.width / 2)
@board.to_grid.each do |row|
row.each do |char|
@screen.setpos(top, left)
colorize(char)
left += 1
end
left = (@screen.maxx / 2) - (@board.width / 2)
top += 1
end
@screen.setpos(top, left)
@screen.addstr("Turns: #{@board.moves}, Score: #{@board.stored.size} / #{@board.boxes.size}")
@screen.refresh
@debug.render
end
def color_for(char)
case char
when '@' then color_pair(COLOR_CYAN)
when 'o' then color_pair(COLOR_YELLOW)
when '.' then color_pair(COLOR_RED)
end
end
def colorize(char)
print = lambda { @screen.addch(char) }
if color = color_for(char)
@screen.attron(color|A_BOLD) { print.call }
else
print.call
end
end
def parse_command
c = @screen.getch
c, buffer = key_buffer(c)
multiplier = buffer.empty? ? 1 : buffer.join.to_i
direction = nil
case c
when Curses::Key::UP, ?k then direction = :up
when Curses::Key::DOWN, ?j then direction = :down
when Curses::Key::LEFT, ?h then direction = :left
when Curses::Key::RIGHT, ?l then direction = :right
when ?r then @board.reset!
when ?q then exit
end
if direction
Curses.beep unless @board.move(direction)
end
end
end
def self.run(level)
board = Board.new(level)
display = Display.new(board)
begin
display.init
loop do
display.render
display.parse_command
end
ensure
display.close
end
end
end
if ARGV.first == 'test'
board = Sokoban::Board.new(1)
puts board.cells.sort.map(&:inspect)
else
Sokoban.run(ARGV.empty? ? 1 : ARGV.first)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment