Created
June 27, 2012 17:48
-
-
Save jlogsdon/3005649 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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