Sokoban
false
false
true
macros_menu.examples>end("Examples").end
ruby
# @title Dynamic database manipulation: a "Sokoban" implementation
#
# 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.
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