--Multipart=_Thu__4_Nov_2004_01_16_41_+0100_5zxzf2Ra/y=QHLOJ
Content-Type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 7bit
Again, very late, but here is my solution. A central game core and two front ends, a curses one and and FXRuby one.
Thomas Leitner
--Multipart=_Thu__4_Nov_2004_01_16_41_+0100_5zxzf2Ra/y=QHLOJ
Content-Type: text/x-ruby;
name="curses.rb"
Content-Disposition: attachment;
filename="curses.rb"
Content-Transfer-Encoding: 8bit
require 'curses'
require 'sokoban'
sokoban = Sokoban.new
sokoban.load_levels( File.read( 'sokoban_levels.txt' ) )
puts "Welcome to Curses-Sokoban!"
print "Select the level (0..#{sokoban.levels.length-1}): "
sokoban.select_level( gets.to_i )
Curses::init_screen
Curses::noecho
width = sokoban.cur_level.map.width + 4
height = sokoban.cur_level.map.height + 4
win = Curses::Window.new( height, width, (Curses::lines - height) / 2 , (Curses::cols - width) / 2 )
win.box( ?|, ?- )
win.keypad = true
begin
y = 2
sokoban.cur_level.map.each_row do |item|
win.setpos( y, 2 )
win.addstr( item.pack('C*') )
y += 1
end
win.refresh
char = win.getch
case char
when ?w, Curses::Key::UP then sokoban.cur_level.move( :up )
when ?s, Curses::Key::DOWN then sokoban.cur_level.move( :down )
when ?a, Curses::Key::LEFT then sokoban.cur_level.move( :left )
when ?d, Curses::Key::RIGHT then sokoban.cur_level.move( :right )
end
end while char != ?q && !sokoban.cur_level.level_finished?
win.close
Curses::close_screen
if sokoban.cur_level.level_finished?
puts "You are the greatest player in history!!!"
else
puts "You have given up too easily!!!"
end
--Multipart=_Thu__4_Nov_2004_01_16_41_+0100_5zxzf2Ra/y=QHLOJ
Content-Type: text/x-ruby;
name="fox.rb"
Content-Disposition: attachment;
filename="fox.rb"
Content-Transfer-Encoding: 8bit
require 'fox'
require 'fox/colors'
require 'sokoban'
include Fox
class SokobanWindow < FXMainWindow
def initialize( app )
super( app, "Sokoban for Ruby Quiz #5", nil, nil, DECOR_ALL, 0, 0, 300, 300 )
menubar = FXMenubar.new( self )
filemenu = FXMenuPane.new( self )
levelmenu = FXMenuPane.new( self )
FXMenuTitle.new( menubar, "&File", nil, filemenu )
FXMenuTitle.new( menubar, "&Levels", nil, levelmenu )
FXMenuCommand.new( filemenu, "&Quit\tCtl-Q", nil, getApp(), FXApp::ID_QUIT )
@sokoban = Sokoban.new
@sokoban.load_levels( File.read( 'sokoban_levels.txt' ) )
@sokoban.select_level( 0 )
menu = nil
@sokoban.levels.each_with_index do |level, index|
if index % 15 == 0
menu = FXMenuPane.new( self )
FXMenuCascade.new( levelmenu, "#{index}++", nil, menu )
end
icon = FXIcon.new( app, nil, 0, IMAGE_KEEP | IMAGE_OPAQUE, 50, 50 )
icon.create
FXDCWindow.new( icon ) do |dc|
paint_map( 50, 50, dc, level.map )
end
item = FXMenuCommand.new( menu, nil, icon )
item.connect( SEL_COMMAND, method( :on_level_chosen ) )
item.userData = Struct.new(:level, :index).new( level, index )
end
@canvas = FXCanvas.new( self, nil, 0, LAYOUT_FILL_X | LAYOUT_FILL_Y )
@canvas.connect( SEL_PAINT, method( :on_canvas_repaint ) )
@canvas.connect( SEL_KEYPRESS, method( :on_canvas_keypress ) )
end
def create
super
show
end
def drawMan(dc, x, y, delta )
dc.foreground = FXColor::Green
dc.lineWidth = 2
dc.drawLine( x*delta + 1, y*delta + 1, x*delta + delta -1, y*delta + delta - 1 )
dc.drawLine( x*delta + delta - 1, y*delta + 1, x*delta + 1, y*delta + delta - 1 )
end
def drawCrate(dc, x, y, delta )
dc.foreground = FXColor::Blue
dc.fillRectangle( x*delta + delta/4, y*delta + delta/4, delta/2, delta/2 )
end
def drawStorage(dc, x, y, delta )
dc.foreground = FXColor::Red
dc.fillCircle( x*delta + delta/2, y*delta + delta/2, delta/2 )
end
def paint_map( width, height, dc, map )
dx = width / map.width
dy = height / map.height
delta = ( dx > dy ? dy : dx )
dc.foreground = FXColor::White
dc.fillRectangle( 0, 0, width, height )
y = 0
map.each_row do |row|
row.each_with_index do |cell, x|
case cell
when Map::Wall
dc.foreground = FXColor::SandyBrown
dc.fillRectangle( x*delta, y*delta, delta, delta )
when Map::Storage
drawStorage( dc, x, y, delta )
when Map::Crate
drawCrate( dc, x, y, delta )
when Map::Man
drawMan( dc, x, y, delta )
when Map::CrateOnStorage
drawStorage( dc, x, y, delta )
drawCrate( dc, x, y, delta )
when Map::ManOnStorage
drawStorage( dc, x, y, delta )
drawMan( dc, x, y, delta )
end
end
y += 1
end
end
def on_level_chosen( sender, sel, event )
@sokoban.select_level( sender.userData.index )
@canvas.focus
end
def on_menu_levels_paint( sender, sel, event )
dc = FXDCWindow.new( sender )
paint_map( sender.width, sender.height, dc, sender.userData.level.map )
dc = nil
GC.start
end
def on_canvas_repaint( sender, sel, event )
dc = FXDCWindow.new( sender )
paint_map( sender.width, sender.height, dc, @sokoban.cur_level.map) if @sokoban.cur_level != nil
dc.foreground = FXColor::Red
dc.drawText( 10, 10, 'Level finished!!!' ) if @sokoban.cur_level.level_finished?
dc = nil
GC.start
end
def on_canvas_keypress( sender, sel, event )
case event.code
when KEY_Left then @sokoban.cur_level.move( :left )
when KEY_Right then @sokoban.cur_level.move( :right )
when KEY_Up then @sokoban.cur_level.move( :up )
when KEY_Down then @sokoban.cur_level.move( :down )
when KEY_Escape then @sokoban.cur_level.reset
end
@canvas.update
end
end
app = FXApp.new( "Sokoban", "Sokoban" )
SokobanWindow.new( app )
app.create
app.run
--Multipart=_Thu__4_Nov_2004_01_16_41_+0100_5zxzf2Ra/y=QHLOJ
Content-Type: text/x-ruby;
name="listener.rb"
Content-Disposition: attachment;
filename="listener.rb"
Content-Transfer-Encoding: 8bit
module Listener
# Adds a new message listener for the object. The message +msgName+
# will be dispatched to either the given +callableObject+ (has to respond
# to +call+) or the given block. If both are specified the +callableObject+
# is used.
def add_msg_listener( msgName, callableObject = nil, &block )
return unless defined?( @msgNames ) && @msgNames.has_key?( msgName )
if !callableObject.nil?
raise NoMethodError, "listener needs to respond to 'call'" unless callableObject.respond_to? :call
@msgNames[msgName].push callableObject
elsif !block.nil?
@msgNames[msgName].push block
else
raise "you have to provide a callback object or a block"
end
end
# Removes the given object from the dispatcher queue of the message +msgName+.
def del_msg_listener( msgName, object )
@msgNames[msgName].delete object if defined? @msgNames
end
#######
private
#######
# Adds a new message target called +msgName+
def add_msg_name( msgName )
@msgNames = {} unless defined? @msgNames
@msgNames[msgName] = [] unless @msgNames.has_key? msgName
end
# Deletes the message target +msgName+.
def del_msg_name( msgName )
@msgNames.delete msgName if defined? @msgNames
end
# Dispatches the message +msgName+ to all listeners for this message,
# providing the given arguments
def dispatch_msg( msgName, *args )
if defined? @msgNames and @msgNames.has_key? msgName
@msgNames[msgName].each do |obj|
obj.call( *args )
end
end
end
end
--Multipart=_Thu__4_Nov_2004_01_16_41_+0100_5zxzf2Ra/y=QHLOJ
Content-Type: text/x-ruby;
name="sokoban.rb"
Content-Disposition: attachment;
filename="sokoban.rb"
Content-Transfer-Encoding: 8bit
require 'listener'
Position = Struct.new( :x, :y )
LevelChange = Struct.new( :object, :old_pos, :new_pos )
class Map
Man = ?@
Crate = ?o
Wall = ?#
Storage = ?.
Floor = ?\s
ManOnStorage = ?+
CrateOnStorage = ?*
include Enumerable
attr_reader :width
attr_reader :height
def initialize( str_map )
@map = str_map.split( /\n/ ).collect {|row| row.unpack( 'C*' ) }
@width = @map.max {|a,b| a.length <=> b.length}.length
@height = @map.length
end
def set_pos( pos, item )
@map[pos.y][pos.x] = item
end
def get_pos( pos )
@map[pos.y][pos.x]
end
def each
@map.each {|row| row.each {|field| yield field } }
end
def each_row
@map.each {|row| yield row}
end
def each_with_pos
@map.each_with_index do |row, y|
row.each_with_index do |cell, x|
yield cell, Position.new( x, y )
end
end
end
end
class Level
include Listener
attr_reader :map
attr_reader :man_pos
def initialize( map )
@original_map = Map.new( map )
reset
add_msg_name :level_changed
add_msg_name :move_impossible
add_msg_name :level_finished
end
def move( direction )
move_possible = true
newpos = Level.new_pos( @man_pos, direction )
case @map.get_pos( newpos )
when Map::Floor, Map::Storage
old_man_pos = @man_pos
move_man( newpos )
dispatch_msg( :level_changed, [LevelChange.new( :man, old_man_pos, newpos )] )
when Map::Wall
dispatch_msg( :move_impossible, [LevelChange.new( :man, @man_pos, newpos )] )
move_possible = false
when Map::Crate, Map::CrateOnStorage
crate_new_pos = Level.new_pos( newpos, direction )
case @map.get_pos( crate_new_pos )
when Map::Wall, Map::Crate, Map::CrateOnStorage
dispatch_msg( :move_impossible, [LevelChange.new( :man, @man_pos, newpos )] )
move_possible = false
else
move_crate( newpos, crate_new_pos )
old_man_pos = @man_pos
move_man( newpos )
dispatch_msg( :level_changed, [LevelChange.new( :man, old_man_pos, newpos ), LevelChange.new( :crate, newpos, crate_new_pos )] )
end
end
dispatch_msg( :level_finished ) if level_finished?
return move_possible
end
def move_man( newpos )
case @map.get_pos( newpos )
when Map::Floor then @map.set_pos( newpos, Map::Man )
when Map::Storage then @map.set_pos( newpos, Map::ManOnStorage )
end
case @map.get_pos( @man_pos )
when Map::Man then @map.set_pos( @man_pos, Map::Floor )
when Map::ManOnStorage then @map.set_pos( @man_pos, Map::Storage )
end
@man_pos = newpos
end
def move_crate( oldpos, newpos )
case @map.get_pos( newpos )
when Map::Floor then @map.set_pos( newpos, Map::Crate )
when Map::Storage then @map.set_pos( newpos, Map::CrateOnStorage )
end
case @map.get_pos( oldpos )
when Map::Crate then @map.set_pos( oldpos, Map::Floor )
when Map::CrateOnStorage then @map.set_pos( oldpos, Map::Storage )
end
end
def level_finished?
!( @map.any? {|item| item == Map::Storage } )
end
def reset
@map = Marshal.load( Marshal.dump( @original_map ) )
@man_pos = Level.find_man( @original_map )
end
def Level.find_man( map )
map.each_with_pos {|cell, pos| return pos if cell == Map::Man || cell == Map::ManOnStorage }
end
def Level.new_pos( pos, direction )
case direction
when :left then Position.new( pos.x - 1, pos.y )
when :right then Position.new( pos.x + 1, pos.y )
when :up then Position.new( pos.x, pos.y - 1 )
when :down then Position.new( pos.x, pos.y + 1)
end
end
end
class Sokoban
attr_reader :levels
attr_reader :cur_level
def load_levels( str )
@levels = str.split( /\n\n/ ).collect {|levelStr| Level.new( levelStr )}
end
def select_level( index )
@cur_level = @levels[index]
@cur_level.reset
end
end
--Multipart=_Thu__4_Nov_2004_01_16_41_+0100_5zxzf2Ra/y=QHLOJ--