--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--