#!/usr/bin/env ruby # # Sokoban for Ruby/Tk < Ruby/GnomeCanvas # # You can get stage data from sokoban.tar.gz # http://ruby-gnome2.sourceforge.jp/hiki.cgi?Sokoban # # You can redistribute it and/or modify it under the terms of # the Ruby's licence. # # 2003.08.17 # start # 2003.08.21 # Mr. nagai's patch [ruby-list:38175] # 2003.09.11 # rename TOP -> UP (from tamura's diary) # add a mechanism for saving last stage require 'tk' require 'tkcanvas' LASTSTAGE = ".sokoban_tk_laststage" # # Charactors # module Actor WIDTH = 20 HEIGHT = 20 def move_arround(dir_x, dir_y) true end end class Passage include Actor end class Storage < TkcRectangle include Actor def initialize(root, row, col) x = Actor::WIDTH + col * WIDTH; y = Actor::HEIGHT + row * HEIGHT super(root, x + 0, y + 0, x + WIDTH, y + HEIGHT) fill "#99ffff" outline "gray" lower end end class Baggage include Actor def initialize(root, map, row, col) @map, @row, @col = map, row, col @canvas = root @items = [] x = Actor::WIDTH + col * WIDTH; y = Actor::HEIGHT + row * HEIGHT @items << TkcOval.new(root, x + 1, y + 2, x + WIDTH - 2, y + HEIGHT - 3) { fill "red" } @items << TkcOval.new(root, x + 5, y + 5, x + 9, y + 9) { fill "white" outline "white" } @pre_obj = Passage.new @storage_area = false end def move(dir_x, dir_y) @items.each { |i| i.move(dir_x, dir_y) } end def move_arround(dir_x, dir_y) obj = @map[@row + dir_y][@col + dir_x] if obj.kind_of? Passage or obj.kind_of? Storage move(dir_x * WIDTH, dir_y * HEIGHT) @map[@row][@col] = @pre_obj @row, @col = @row + dir_y, @col + dir_x @pre_obj = obj @map[@row][@col] = self @storage_area = obj.kind_of?(Storage) true else false end end def storage_area? @storage_area end end class Wall include Actor def initialize(root, row, col) x = Actor::WIDTH + col * WIDTH; y = Actor::HEIGHT + row * HEIGHT TkcRectangle.new(root, x + 0, y + 0, x + WIDTH, y + HEIGHT) { fill "#55dd77" } TkcLine.new(root, x + 0, y + 0, x + 0, y + HEIGHT) { fill "#eeffee" } TkcLine.new(root, x + 0, y + 0, x + WIDTH, y + 0) { fill "#eeffee" } end def move_arround(dir_x, dir_y) false end end class Worker include Actor def initialize(root, map, row, col) @map, @row, @col = map, row, col @moves, @pushes = 0, 0 @canvas = root x = Actor::WIDTH + col * WIDTH; y = Actor::HEIGHT + row * HEIGHT TkcOval.new(root, x + 1, y + 1, x + WIDTH - 1, y + HEIGHT - 1) { tags "worker" fill "yellow" } TkcLine.new(root, x + 5, y + 4, x + 7, y + 7) { tags "worker" fill "black" } TkcLine.new(root, x + WIDTH - 5, y + 4, x + WIDTH - 7, y + 7) { tags "worker" fill "black" } TkcPolygon.new(root, x + 5, y + HEIGHT - 10, x + WIDTH / 2, y + HEIGHT - 5, x + WIDTH - 5, y + HEIGHT - 10) { tags "worker" fill "orange" outline "black" } end def move(dir_x, dir_y) @canvas.move("worker", dir_x, dir_y) end def move_real(dir_x, dir_y) move(dir_x * WIDTH, dir_y * HEIGHT) @row, @col = @row + dir_y, @col + dir_x end def move_arround(dir_x, dir_y) obj = @map[@row + dir_y][@col + dir_x] if obj.move_arround(dir_x, dir_y) move_real(dir_x, dir_y) @undo = [obj, dir_x, dir_y] @moves += 1 @pushes += 1 if obj.kind_of? Baggage else false end end def undo if @undo move_real(- @undo[1], - @undo[2]) @undo[0].move_arround( - @undo[1], - @undo[2]) @undo = nil end end attr_reader :moves, :pushes end # # Main Window # class MainWindow KEYS = [ ['h', -1, 0], ['Left', -1, 0], ['l', 1, 0], ['Right', 1, 0], ['k', 0, -1], ['Up', 0, -1], ['j', 0, 1], ['Down', 0, 1], ] MESSAGE = [ %w(LEFT - h or Left RESTART - r), %w(RIGHT - l or Right NEXT_STAGE - n), %w(UP - k or Up PREVIOUS_STAGE - p), %w(DOWN - j or Down QUIT - q), %w(UNDO - u) ] def create_stage(stage) @stage = stage if @canvas @canvas.destroy end canvas = TkCanvas.new(@base_box) { bg "white" borderwidth 3 relief 'sunken' pack } @map = Array.new @baggages = Array.new @storages = Array.new width = 0 @maps[stage].each_with_index do |line, row| cols = Array.new line.scan(/./).each_with_index do |char, col| case char when '$' baggage = Baggage.new(canvas, @map, row, col) cols << baggage @baggages << baggage when '#' cols << Wall.new(canvas, row, col) when '.' storage = Storage.new(canvas, row, col) cols << storage @storages << storage when '@' @worker = Worker.new(canvas, @map, row, col) cols << Passage.new else cols << Passage.new end end width = cols.size if width < cols.size @map << cols end width = width * Actor::WIDTH + Actor::WIDTH * 2 height = Actor::HEIGHT * @maps[stage].size + Actor::HEIGHT * 2 canvas.width(width) canvas.height(height) update_label_stage @canvas = canvas end def update_label_stage @label_stage.text("[Level: #{@stage + 1}] #{@worker.moves} moves & #{@worker.pushes} pushes") end def quit begin open((ENV['HOME'] || ".") + File::SEPARATOR + LASTSTAGE, "w").print(@stage.to_s) rescue end exit end def initialize(maps, stage = 0) @maps = maps if stage == 0 begin stage = open((ENV['HOME'] || ".") + File::SEPARATOR + LASTSTAGE).gets.to_i rescue end end vbox = TkFrame.new { pack } TkLabel.new(vbox, 'borderwidth'=>0, 'pady'=>1, 'anchor'=>'s') { text "Sokoban" fg "#ff0000" font ( TkFont.new({'family'=>'lucida', 'size'=>26}) ) pack('pady'=>1, 'ipady'=>5) } fnt = nil TkLabel.new(vbox, 'borderwidth'=>0, 'pady'=>0) { text "A sample script for Ruby/Tk" fg "#ff5500" fnt = font pack } TkFrame.new(vbox){|f| msgfont = TkFont.new({'family'=>fnt, 'size'=>12}) MESSAGE.each_with_index{|msg,idx| TkGrid.configure( TkLabel.new(f, 'text'=>msg[0], 'font'=>msgfont, 'fg'=>'darkblue', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkLabel.new(f, 'text'=>msg[1], 'font'=>msgfont, 'fg'=>'darkblue', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkLabel.new(f, 'text'=>msg[2], 'font'=>msgfont, 'fg'=>'#0033ff', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkLabel.new(f, 'text'=>msg[3], 'font'=>msgfont, 'fg'=>'darkblue', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkLabel.new(f, 'text'=>msg[4], 'font'=>msgfont, 'fg'=>'#0033ff', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkFrame.new(f, 'width'=>9), TkLabel.new(f, 'text'=>msg[5], 'font'=>msgfont, 'fg'=>'darkblue', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkLabel.new(f, 'text'=>msg[6], 'font'=>msgfont, 'fg'=>'darkblue', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), TkLabel.new(f, 'text'=>msg[7], 'font'=>msgfont, 'fg'=>'#0033ff', 'borderwidth'=>0, 'pady'=>0, 'padx'=>3), 'sticky'=>'wns') } pack('pady'=>7) } @label_stage = TkLabel.new { pack } @base_box = TkFrame.new { pack } Tk.root.title('Sokoban') # Actions KEYS.each do |key| Tk.root.bind(key[0], proc { move_arround(key[1], key[2]) }) end Tk.root.bind('r', proc { create_stage(@stage) }) Tk.root.bind('n', proc { stage = @stage + 1 stage = 0 if stage == @maps.size create_stage(stage) }) Tk.root.bind('p', proc { stage = @stage - 1 stage = @maps.size - 1 if stage < 0 create_stage(stage) }) Tk.root.bind('u', proc { @worker.undo }) Tk.root.bind('q', proc { quit }) Tk.root.protocol("WM_DELETE_WINDOW", proc { quit }) create_stage(stage) end def move_arround(dir_x, dir_y) obj = @worker.move_arround(dir_x, dir_y) update_label_stage if @baggages.select{|v| v.storage_area?}.size == @storages.size @stage += 1 if @stage == @maps.size Tk.messageBox('type' => 'ok', 'icon' => 'info', 'message' => "All clear! Congratulations!") @stage = 0 else Tk.messageBox('type' => 'ok', 'icon' => 'info', 'message' => "Clear! Go next stage!") end create_stage(@stage) end end end if $0 == __FILE__ i = 1 maps = Array.new loop { path = "data/screen.#{i}" break unless FileTest.exist?(path) maps << File.open(path).readlines i += 1 } MainWindow.new(maps, 0) Tk.mainloop end