Dynamic database manipulation: a "Sokoban" implementation

Macro file: sokoban.lym

This toy application dynamically changes the database to realize a game arena. As a trial application, it implements one level of the famous "Sokoban" game.

Source code

module Examples

  # ---------------------------------------------------------------------------
  #  A class that simplifies the creation of new menu entries by
  #  allowing a more "Ruby-style" callbacks 
  
  class MenuHandler < RBA::Action
    def initialize(t, k, &action) 
      self.title = t
      self.shortcut = k
      self.on_triggered = action
    end
  end
  
  # ---------------------------------------------------------------------------
  #  The base class for objects that inhabit the arena
  
  class GameObject
    
    #  Constructor: each object must have a position
    def initialize(x, y)
      @x = x
      @y = y
    end
  
    #  Helper method: create a cell ("game" is the game controller object)
    def create_cell(game, name)
      if game.layout.has_cell?(name)
        @cell_index = game.layout.cell_by_name(name)
      else
        @cell_index = game.layout.add_cell(name)
        build_cell(game)
      end
    end
  
    #  Instantiate our cell in the top cell ("game" is the game controller object)
    def instantiate(game)
      t = RBA::Trans::new(RBA::Point::new(@x*1000, @y*1000))
      inst = RBA::CellInstArray::new(@cell_index, t)
      game.topcell.insert(inst)
    end
  
    #  Predicate telling if we can move
    #  Reimplemented by the derived classes
    def can_move?(level, x, y)
      return true
    end
  
    #  Check, if we are at the given position
    def is_at?(x, y)
      return x == @x && y == @y
    end
  
    #  Predicate, telling if we are at the guy
    def is_guy? 
      return false
    end
  
    #  Predicate, telling if we are an obstacle (i.e. a piece of the wall)
    def is_obstacle? 
      return false
    end
  
    #  Predicate, telling if we are a target 
    def is_target? 
      return false
    end
  
    #  Predicate, telling if we are a diamond (the "load" to move around) 
    def is_diamond?
      return false
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  A piece of the wall
  
  class Wall < GameObject
  
    def construct(game)
      create_cell(game, "wall")
    end
  
    def build_cell(game)
  
      lay1 = game.create_layer("wall.1", 0xc00000, 0xffc280, 0)
  
      ystep = 125
      width = 250
      (1000 / ystep).times do |n|
        x = (n % 2 == 1) ? -width / 2 : 0
        while x < 1000
          brick = RBA::Box::new(x < 0 ? 0 : x, n * ystep, x + width > 1000 ? 1000 : x + width, (n + 1) * ystep)
          game.layout.cell(@cell_index).shapes(lay1).insert_box(brick)
          x += width
        end
      end
  
    end
  
    def is_obstacle?
      return true
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  A target
  
  class Target < GameObject
  
    def construct(game)
      create_cell(game, "target")
    end
  
    def build_cell(game)
  
      lay2 = game.create_layer("target.2", 0x80ff8d, 0x80ff8d, 0)
      lay1 = game.create_layer("target.1", 0x01c04b, 0x01c04b, 0)
  
      [ [ 0, 50, lay1 ], [ 50, 100, lay2 ], [ 100, 150, lay1 ], [ 150, 200, lay2 ],
        [ 200, 250, lay1 ], [ 250, 300, lay2 ], [ 300, 350, lay1 ] ].each do |r|
        pointlist = []
        n = 32
        n.times do |a| 
          x = 500 + r[1] * Math::cos((2 * Math::PI * a) / n)
          y = 500 + r[1] * Math::sin((2 * Math::PI * a) / n)
          pointlist.push(RBA::Point::new(x, y))
        end
        shape = RBA::Polygon::new(pointlist)
        if r[0] > 0 
          pointlist = []
          n = 32
          n.times do |a| 
            x = 500 + r[0] * Math::cos((2 * Math::PI * a) / n)
            y = 500 + r[0] * Math::sin((2 * Math::PI * a) / n)
            pointlist.push(RBA::Point::new(x, y))
          end
          shape.insert_hole(pointlist)
        end
        game.layout.cell(@cell_index).shapes(r[2]).insert_polygon(shape)
      end
  
    end
  
    def is_target? 
      return true
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  A diamond
  
  class Diamond < GameObject
  
    def construct(game)
      create_cell(game, "diamond")
    end
  
    def build_cell(game)
  
      lay1 = game.create_layer("diamond.1", 0x80fffb, 0x8000ff, 0, 2)
  
      pts = [ [ [ 300, 900 ], [ 700, 900 ], [ 600, 870 ], [ 400, 870 ] ],
              [ [ 700, 900 ], [ 900, 730 ], [ 680, 800 ], [ 600, 870 ] ],
              [ [ 680, 800 ], [ 900, 730 ], [ 660, 520 ], [ 600, 720 ] ],
              [ [ 660, 520 ], [ 600, 720 ], [ 400, 720 ], [ 340, 520 ] ],
              [ [ 320, 800 ], [ 100, 730 ], [ 340, 520 ], [ 400, 720 ] ],
              [ [ 300, 900 ], [ 100, 730 ], [ 320, 800 ], [ 400, 870 ] ],
              [ [ 400, 870 ], [ 600, 870 ], [ 680, 800 ], [ 600, 720 ], [ 400, 720 ], [ 320, 800 ] ],
              [ [ 100, 730 ], [ 500, 125 ], [ 340, 520 ] ],
              [ [ 340, 520 ], [ 500, 125 ], [ 660, 520 ] ],
              [ [ 660, 520 ], [ 500, 125 ], [ 900, 730 ] ] ]
  
      pts.each do |pp|
        pointlist = []
        pp.each { |p| pointlist.push(RBA::Point::new(p[0], p[1])) }
        shape = RBA::Polygon::new(pointlist)
        game.layout.cell(@cell_index).shapes(lay1).insert_polygon(shape)
      end
  
    end
  
    def can_move?(level, x, y)
      level.each_object { |o|
        if o.is_at?(@x + x, @y + y)
          if o.is_obstacle? || o.is_diamond?
            return false
          end
        end
      }
      return true
    end
  
    def move(level, x, y)
      @x += x
      @y += y
      @in_target = false
      level.each_object { |o|
        if o.is_target? && o.is_at?(@x, @y)
          @in_target = true
        end
      }
    end
  
    def is_diamond?
      return true
    end
  
    def in_target?
      return @in_target
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  The guy
  
  class Guy < GameObject
  
    def construct(game)
      create_cell(game, "guy")
    end
  
    def build_cell(game)
  
      lay1 = game.create_layer("guy.1", 0x805000, 0xffffff, 0)
  
      pts = [ [ [ 400, 880 ], [ 420, 940 ], [ 580, 940 ], [ 600, 880 ], [ 550, 750 ], [ 450, 750 ] ],
              [ [ 350, 740 ], [ 630, 740 ], [ 710, 640 ], [ 710, 350 ], [ 630, 350 ], [ 630, 610 ],
                [ 620, 610 ], [ 620, 100 ], [ 700, 100 ], [ 700, 50 ], [ 505, 50 ], [ 505, 400 ],
                [ 495, 400 ], [ 495, 50 ], [ 300, 50 ], [ 300, 100 ], [ 380, 100 ], [ 380, 610 ],
                [ 370, 610 ], [ 370, 350 ], [ 290, 350 ], [ 290, 640 ] ] ]
  
      pts.each do |pp|
        pointlist = []
        pp.each { |p| pointlist.push(RBA::Point::new(p[0], p[1])) }
        shape = RBA::Polygon::new(pointlist)
        game.layout.cell(@cell_index).shapes(lay1).insert_polygon(shape)
      end
  
    end
  
    def can_move?(level, x, y)
      level.each_object { |o|
        if o.is_at?(@x + x, @y + y)
          if o.is_obstacle? 
            return false
          elsif o.is_diamond? && !o.can_move?(level, x, y)
            return false
          end
        end
      }
      return true
    end
  
    def move(level, x, y)
      @x += x
      @y += y
      level.each_object { |o|
        if o.is_at?(@x, @y) && o.is_diamond?
          o.move(level, x, y)
        end
      }
      return true
    end
  
    def is_guy?
      return true
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  The arena which is inhabitated by GameObjects
  
  class Level 
    
    def initialize()
  
      #  This is one example for an arena
      arena = [ 
        '   ####',
        '####  #',
        '#     ####',
        '# $ #  . ##',
        '#  #   .  #',
        '## #$$#.  #',
        '##    #####',
        '# @ ###',
        '#   #',
        '#####',
      ]
    
      @objs = []
  
      y = arena.size - 1
      arena.each { |l| 
        x = 0
        l.split("").each { |o|
          if o == '#' 
            @objs.push(Wall.new(x, y))
          elsif o == '.'
            @objs.push(Target.new(x, y))
          elsif o == '$'
            @objs.push(Diamond.new(x, y))
          elsif o == '@'
            @guy = Guy.new(x, y)
            @objs.push(@guy)
          end
          x += 1
        }
        y -= 1
      }
  
    end
  
    #  iterate over all objects in the arena
    def each_object(&action)
      @objs.each { |o| action.call(o) }
    end
  
    #  get the object representing the guy
    def guy
      return @guy
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  The game controller
  
  class Game
  
    def initialize()
  
      #  Get the reference to the application object, the main window and the menu
      app = RBA::Application.instance
      mw = app.main_window
      menu = mw.menu
  
      #  create the menu handlers
      #  IMPORTANT: in order to keep the references (which is not done on C++ side)
      #  we need to assign the reference to member variables
      @down_handler  = MenuHandler.new("Down", "Down")   { move(0, -1) }
      @left_handler  = MenuHandler.new("Left", "Left")   { move(-1, 0) }
      @right_handler = MenuHandler.new("Right", "Right") { move(1, 0) }
      @up_handler    = MenuHandler.new("Up", "Up")       { move(0, 1) }
      @restart_handler = MenuHandler.new("Restart", "")  { restart }
  
      #  add new menu entries into the toolbar and bind them to our action handlers
      menu.insert_separator("@toolbar.end", "name")
      menu.insert_item("@toolbar.end", "sokoban_down", @down_handler)
      menu.insert_item("@toolbar.end", "sokoban_left", @left_handler)
      menu.insert_item("@toolbar.end", "sokoban_right", @right_handler)
      menu.insert_item("@toolbar.end", "sokoban_up", @up_handler)
      menu.insert_item("@toolbar.end", "sokoban_restart", @restart_handler)
  
      #  create a new layout and store a reference to it's view objects, layout handle
      #  and a reference to the top cell
      mw.create_layout("", 0)
      @view = mw.current_view
      @view.set_config("bitmap-oversampling", "3")
      @layout = @view.cellview(0).layout 
      @topcell = @layout.add_cell("game")
  
      #  initialize the layer list: so far we do not have layers
      @layers = {}
  
      #  create and initialize some dummy objects so it is guaranteed that the layers are
      #  created in the right order.
      dummy_objs = [ Wall.new(0, 0), Target.new(0, 0), Diamond.new(0, 0), Guy.new(0, 0) ]
      dummy_objs.each { |o| o.construct(self) }
  
      #  instantiate the level and create 
      @level = Level.new
      @level.each_object { |o| o.construct(self) }
      @level.each_object { |o| o.instantiate(self) }
  
      #  set up the viewer window: select the new cell for top cell, update cell hierarchy browser
      #  and layer list, fit all and show all levels of hierarchy
      @view.select_cell_path([@topcell], 0)
      @view.update_content
      @view.zoom_fit
      @view.max_hier
  
    end
  
    #  start over 
    def restart
      @level = Level.new
      @level.each_object { |o| o.construct(self) }
      redraw
    end
  
    #  refresh the layout with the current arena setup
    def redraw
  
      #  IMPORTANT: always stop the redraw thread before applying changes
      @view.stop_redraw
  
      #  empty the top cell and recreate the instances to the game objects 
      #  so they appear at their position
      topcell.clear_insts
      @level.each_object { |o| o.instantiate(self) }
      @view.select_cell_path([@topcell], 0)
  
      #  force an update and redraw of the content
      @view.update_content
      RBA::Application.instance.main_window.redraw
  
    end
  
    #  move the guy by the specified distance
    def move(dx, dy)
  
      #  IMPORTANT: because the user may have closed the view panel or the layout, 
      #  we need to check, if we still have a valid object
      if ! @view.destroyed?
  
        #  check, if we can move the guy and do so.
        if @level.guy.can_move?(@level, dx, dy)
          @level.guy.move(@level, dx, dy)
        end
  
        #  update the arena view
        redraw
  
        #  check, if all objects have been moved into their targets
        all_in_target = true
        @level.each_object { |o| 
          if o.is_diamond? && !o.in_target?
            all_in_target = false
          end
        }
        if all_in_target
          RBA::MessageBox::info("Done", "Congratulations! Level done.", RBA::MessageBox::b_ok)
          @level = Level.new
          @level.each_object { |o| o.construct(self) }
          redraw
        end
  
      end
  
    end
  
    #  retrieve the top cell handle
    def topcell 
      return @layout.cell(@topcell)
    end
  
    #  retrieve the layout handle
    def layout 
      return @layout
    end
  
    #  create a layer with the given properties
    def create_layer(name, color, frame_color, stipple, width = 1)
  
      if @layers[name] == nil 
  
        linfo = RBA::LayerInfo.new 
        lid = @layout.insert_layer(linfo)
        @layers[name] = lid
  
        lpp = @view.end_layers
        ln = RBA::LayerPropertiesNode::new
        ln.dither_pattern = stipple
        ln.fill_color = color
        ln.frame_color = frame_color
        ln.width = width
        ln.source_layer_index = lid
        @view.insert_layer(lpp, ln)
  
      else
        lid = @layers[name]
      end
  
      return lid
  
    end
  
  end
  
  # ---------------------------------------------------------------------------
  #  Main application
  
  #  instantiate the game controller
  @sokoban_game = Game.new

end