Working with Ruby
Hi, I am Jan. This is my old Ruby blog. I still post about Ruby, but I now do it on idiosyncratic-ruby.com. You should also install Irbtools to improve your IRB.

Playing with Dijkstra

About a year ago, some students at my university announced a little programming competition for students beginning studying IT, like me. The language could be chosen freely.

At this time, I had already done some C and PHP programming.. but I also had heard of Ruby and that Ruby is sooo cool. So I decided to learn the basics of Ruby by taking part… and it’s been the right decision! I fell in love with Ruby ;).

I publish my solution here. It is a good “try to understand what it does”-exercise for people new to Ruby or programming in general (or people doing Rails only all the time).

Description

The Task was to find a way from A to Z in a little labyrinth like this one:

A;0;0;0;0;0;0;0;0 0;0;0;0;0;0;0;0;0 0;0;0;0;0;0;0;0;0 0;0;0;0;1;0;0;0;0 0;0;0;0;1;0;0;0;0 0;0;0;0;1;0;0;0;0 0;0;0;0;0;0;0;0;0 0;0;0;0;0;0;0;0;0 0;0;0;0;0;0;0;0;Z

On a 0 can be walked, the 1 is a blocker. You can only go one field per step and not diagonally. The A and Z are always at the same positions, but the block structure changed from level to level. The solution had to be a sequence of the starting letters of *u*p, *d*own, *l*eft and *r*ight.

If you are able to speak German, you can look at the original description.

Ruby, Ruby, Ruby!

And here is my solution, using Dijkstra / A* and Backtracking. There are some minor bugs in it and I probably would write it totally different today… but.. everyone changes :).

Download full project: AKMRubyLabyrinth.tgz (recommended if you are trying to run the program, because of the non-ruby-files like the labyrinths). Another note: Ruby 1.8 is not supported.

Source Files

The last snippet is the most interesting one.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# I have written this program to learn ruby... and after reading
# docs & books and hours of listining to great music like Ghosts
# from Nine Inch Nails, and some crazy stuff (do you know the band
# Cynic - they do a addictive jazz/deathmetal/disharmonic/bla mix °_°)
# ... I got the basics ;)
#
# Also... thanks to the orgas of this challenge ;)
#
# OK... lets start ;)

# important program constants
Version = 1.0

# include source files
require 'lpers.rb'
require 'nfig.rb'
require 'nu.rb'
require 'v.rb'
require 'go.rb'

# 'global' function that lets start the solution algo ;)
def start(lab)
  unless lab.instance_of? Env
    raise ArgumentError, 'ease set a labyrinth file!'
  else
    # put out the lab filename again
    puts 'byrinth file:  ' << Config[:file].to_s unless Config[:output]==:clean
    # start timer
    timer = Time.now
    # calculate and display!
    lab.output_path Config[:algo]||:dijkstra, Config[:output]||:console
    # output timer
    puts "me needed for calculation and output: #{ime.now - timer)}econds" unless Config[:output]==:clean
  end
end


# now do what the command line options tell us to do
if Config[:info]
  Menu.info # display info and exit
elsif !Config[:file]
  Menu.idle # go to menu..
elsif Config[:force_menu]
  Menu.idle Env.new Config[:file] # [forced menu! - but file is already set]
else
  start Env.new Config[:file] # ..or start the code if file was set by command line options
end

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# this methods add some functionality to some existing classes - in ruby, every class can be 'reopened'!

# helper: trims the first char of both args and merges them into the object hash
class Hash
  def merge_without_first!(arg,arg_val)
    if arg
      arg = arg[1,arg.length].to_sym
      arg_val = arg_val.empty? ? true : arg_val[1,arg_val.length].to_sym
      self[arg] = arg_val
    end
  end
end

# helper: output currently configured file path
def File.full_path(prm_path,expanded=false)
  if prm_path[0] == '/' or prm_path[0] == '~' or prm_path[1] == ':' # too easy way to detect absolute pahts
    prm_path.to_s
  elsif expanded
    File.expand_path(Config::BasePathLabs + prm_path.to_s)
  else
    Config::BasePathLabs + prm_path.to_s
  end
end

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
require 'yaml' # yet another markup language # comment out this line for JRuby 1.2.0 support (see readme!)

 # Config: global class/module which holds all configuration
 # access current program configuration by 'Config[:foo]' # => 'bar'
 # access general configuration (loaded by config file) as constants: 'Config::Foo' # => 'bar'
module Config
  @settings = {};
  def self.[](key); @settings[key]; end # config getter
  def self.[]=(key,val); # config setter
    raise ArgumentError, 'Sorry, the given algorithm is not suppported (see info!)'     if key == :algo   && ![:dijkstra,:astar,:astar_b,:backtracking].member?(val)
    raise ArgumentError, 'Sorry, the given output format is not suppported (see info!)' if key == :output && ![:console,:html,:both,:clean].member?(val)
    @settings[key] = val;
  end

  # This hash will hold program arguments..
  args = {}
  # ..get those now
  while arg = ARGV.shift
    unless arg[0]=='-' # param is no option
      arg = ARGV.shift # go to next argument
    else
      arg_val = '' # start with empty values..
      temp_val = ARGV.shift # (next param)
      while temp_val && temp_val[0]!='-'
        arg_val += ' ' + temp_val #..and add them to current the current arg-value
        temp_val = ARGV.shift # (next param)
      end
      args.merge_without_first!(arg,arg_val) # put options answers in hash
      ARGV.unshift(temp_val) # temp_val is next arg
    end
  end
  args.merge_without_first!(arg,arg_val) # put options answers in hash - also if end was already reached in loop

  # save allowed configuration
  args.each { |arg,arg_val| @settings[arg] = arg_val if [:file,:algo,:output,:force_menu,:info].member? arg}

  # load settings from config file
  config_path = (args[:config] || 'config/default.yaml').to_s
  base = YAML::load(File.read(config_path)) if File.file? config_path # comment out this line for JRuby 1.2.0 support (see readme!)
  # base = nil # remove comment from this line for JRuby 1.2.0 support (see readme!)

  if base and base['base_paths']
    BasePathLabs = base['base_paths']['labs']
    BasePathHtml = base['base_paths']['html']
  else
    BasePathLabs = 'labs/' # std
    BasePathHtml = 'html/' #   paths
  end
end

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# as you might have thought - the program menu
module Menu
  # show the menu
  def self.show(flash=nil)
    system("clear") #print "\e[2J\e[f" - low level function... will not work @ some systems :(
    puts "== AKMRubyLabyrinth #{Version} coded for AKMProgChallenge09 =="
    puts
    puts 'Current configuration'
    puts " LABYRINTH-FILE:\t#{ Config[:file]   || '---'}"
    puts " ALGORITHM:     \t#{(Config[:algo]   || 'Dijkstra [standard]').to_s.capitalize}"
    puts " OUTPUT-FORMAT: \t#{(Config[:output] || 'Console  [standard]').to_s.capitalize}"
    puts
    puts 'Please type one of the commands:'
    puts " info  \t- Give some information"
    puts " file  \t- Read-in the labyrinth-file"
    puts " algo  \t- Select algorithm to use"
    puts " output\t- Select output format to use"
    puts " start \t- Calculate the solution path! [labyrinth file must be loaded]"
    puts " quit  \t- Exit program"
    if flash
      puts
      puts 'Message:'
      puts ' ' << flash
    end
    puts
  end

  # some infos ;)
  def self.info
    system("clear")
    puts '== Info ==
 This program calculates the shortest path in a 9x9 labyrinth. The player
 starts in the top left corner and wants to reach the bottom right one.
 For more info about the task, see <https://akamentor.net/prog/>

 You can choose between following algorithms:
  - Dijkstra
  - Astar
  - Astar_b (variation)
  - Backtracking

 Additionally you can choose between the output formats:
  - Console
  - Html
  - Both
  - Clean (puts out only the solution path)

 The output is a line of commands describing where to go from the start point:
  - u: up
  - d: down
  - l: left
  - r: right

 This program is written in ruby, the shiny great programming language. If you
 do not know it - feel free to study the source of this program and learn it ;)
 If your thoughts the same as mine and you rather like writing nice code than
 super performant, but unreadable c-or-whatever code... than do it!

 Feel free to mail me about the program or ruby: <mail@janlelis.de>

 Some more information about this program can be found in the readme file.

> Press <Enter>'
  end

  # and the main method... idling in the menu till user decides to quit
  def self.idle(lab=nil)
    @lab = lab if lab
    show
    print '>> '; input = gets.strip.downcase.to_sym # get user command
    until input == :quit; begin # extra block for rescueing Err
      case input
        when :file
          temp_file = get_setting "Please enter the path to the labyrinth file\n (base path is: \"#{File.expand_path(Config::BasePathLabs)}\")"
          @lab = Env.new  temp_file # try to load specified file (throws exception if not sucessful)
          Config[:file] = temp_file
          show
        when :algo   then Config[:algo]   = get_setting 'Please enter the algorithm to use';     show
        when :output then Config[:output] = get_setting 'Please enter the output format to use'; show
        when :info   then info;       gets; show
        when :start  then start @lab; gets; show # 'global' method - start the solution!
      end
    rescue ArgumentError => e # error! inform user what he did wrong.
    #  show e.message
    puts '> ' << e.message
    else # no errors
    #  show
    ensure # do this always [errors or no errors]
      print '>> '; input = gets.strip.downcase.to_sym # - get next command
    end; end
  end

  # helper method which gets the settings that we want!
  def self.get_setting(instruction='Please enter a value for this setting') # gets the value of one setting
    print '> ' << instruction << ': '; gets.strip.downcase.to_sym
  end
end

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
require 'set' # std lib for sets [alternative to arrays]

# Env = Environment = labyrinth - loads the labyrinth file and do things with it
class Env
  attr_reader :array, :nodes, :edges, :path # getter for instance vars

  # reads file into lab-array
  def initialize(prm_path)
    @array = []

    raise ArgumentError, 'Sorry, could not load the labyrinth file "'<< File.full_path(prm_path,true) << '"' unless File.exist?(File.full_path(prm_path))

    File.open(File.full_path(prm_path)) do |b_file| # Read file..
      b_file.each {|b_line| b_line.rstrip!; @array << b_line.split(';').collect {|a_ele| a_ele.to_i} } # ..and put ele as int into an multi-dim-array [A and Z are converted to 0 by to_i]
    end

    raise ArgumentError, 'Sorry, could not load the labyrinth: the labyrinth file is empty!' if @array.empty? || @array[0].empty?
    raise ArgumentError, 'Sorry, could not load the labyrinth: wrong format!' unless @array.size == 9 # only a weak test
  end


  # get the solution :)
  def output_path(prm_algo=:dijkstra,prm_format=:console)
    # choose algo
    algo = (prm_algo==:backtracking) ? Algo.const_get(prm_algo.to_s.capitalize) : Algo::Dijkstra
    # (currently, the std algo is Dijkstra -- needs to be changed if algos get more complex or more are added!)

    # init calculation
    transform_to_graph if algo::NeedsGraph

    case prm_algo
      when :dijkstra
        algo.init(@nodes,@edges)
      when :astar
        # with a block appended describing the heuristic, the dijkstra module turns into A* :]
        algo.init(@nodes,@edges) do |idx,tupel|
          tupel.d + 16-(idx/9+idx%9) # ...
        end
      when :astar_b
        h = [16, 15, 14, 13, 12, 11, 10, 9, 8, 15, 14, 13, 12, 11, 10, 9, 8, 7, 14, 13, 12, 11, 10, 9, 8, 7, 6, 13, 12, 11, 10, 9, 8, 7, 6, 5, 12, 11, 10, 9, 8, 7, 6, 5, 4, 11, 10, 9,8, 7, 6, 5, 4, 3, 10, 9, 8, 7, 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 1, 8, 7, 6, 5, 4, 3, 2, 1, 0]
        algo.init(@nodes,@edges) do |idx,tupel|
          tupel.d + h[idx] # same as above, only with hardcoded values
        end
      when :backtracking
        algo.init(@array)
    end

    # calculate! :)
    @path = algo.calc

    # put out
    if prm_format == :console or prm_format == :both
      puts "Algorithm:       #{prm_algo.capitalize}"
      puts '_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _'
      if @path and @path.is_a? Array
        puts algo::info
        puts 'Path:'
      end
      puts @path
    end

    # html output in a very ugly way ;)
    if prm_format == :html or prm_format == :both
      # calculate file name
      now_name = "#{Config::BasePathHtml}#{Time.now.strftime("%y%m%d_%H%M%S_" + Config[:file].to_s + "_" + (Config[:algo]||:dijkstra).to_s)}.htm"
      # and create sub dir if not present
      Dir.mkdir 'html' unless File.directory? 'html'

      # write the file ;)
      File.open(now_name,'w') do |out|
        out.puts "<html><head><title>AKMRubyLabyrinth - File: #{Config[:file]} - Algorithm: #{(Config[:algo]||:dijkstra).to_s.capitalize}</title>"
        out.puts '<style type="text/css">
 body 	              {background:#333;font-family:Arial;color:white}
 h1,h3,.info          {background:#777;border:darkred solid 1px}
 h1,h2,h3,.info,.sub  {padding:5px;margin:5px}
 .sub                 {background:#777;border:darkred dashed 1px}
</style></head><body>'# some style blabla
        out.puts '<h1>AKMRubyLabyrinth</h1>'
        out.puts "<h3>File: #{Config[:file]}<br/>"
        out.puts "Algorithm: #{(Config[:algo]||:dijkstra).to_s.capitalize}</h3>"
        out.puts '<div class="info">'
        if path.is_a? Array
          out.puts " <div class=\"sub\">Path length: #{path.size}</div>"
          out.puts " <div class=\"info\">Solution: <strong>"
          path.each {|step| out.print " #{step}"}
        elsif
          out.puts "<div class=\"sub\"><strong>#{path}"
        end
        out.puts ' </strong></div>'
        out.puts '</div>'

        out.puts '</body></html>'
      end
      # (weak) check if it was successful
      if File.file? now_name
        puts 'Output saved in ' << now_name
      else
        puts 'Sorry, could not save the solution in a html document..'
      end
    end

    # or clean °_°
    if prm_format == :clean
      puts @path
    end
  end


  # transforms @array into @nodes and @edges
  def transform_to_graph()
    @edges = Set.new  # start
    @nodes = Set.new  #      values

    9.times do |i| # 0.upto 9
	    9.times do |j|
        if @array[i][j] == 0 # field is valid node
          ni = node_ident(i,j)
          @nodes << ni
          @edges << [node_ident(i,j-1),ni] << [ni,node_ident(i,j-1)] if j>0 and @array[i][j-1] == 0 # valid edge version 1
          @edges << [node_ident(i-1,j),ni] << [ni,node_ident(i-1,j)] if i>0 and @array[i-1][j] == 0 # valid edge version 2
        end
      end
    end
  end

  # helper: takes indexes, returns node ident-number (0-80)
  def node_ident(i,j);  9*i+j;  end
end

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
require 'set'

# now it gets interesting: this module holds the algorithms
module Algo
  # each Algo has its own namespace
  module Dijkstra
   NeedsGraph = true
   class << self # everything are module methods - dont write self. prefix everytime..

    def info # is displayed as console info
      'Shortest path length: ' << @path.length.to_s
    end

    # init dijkstra, if an heuristic is given, how to decide [by min] which sould be the next node, than use it!
    def init(nodes,edges,&heuristic)
      @a, @edges, @heuristic = nodes, edges, heuristic||proc{|sn| sn[1].d} # std heuristic is dijkstra [next node is the one with min. distance]
      @m = {} # marked nodes
      @s = {} # sidenodes
    end

    # calculate the solution!
    def calc()
      # get shortest distance tupel :)
      current = DTupel.new(0,0,0) # 0 is startnode
      until current.n == 80 or @a.empty?
        @a.delete current.n # delete node from @a
        @s.delete current   # .. and from @s
        @m[current.n] = current # add tupel to @m

        new_tupels = create_tupels current # get new sidenode-tupels
        new_tupels.each do |sn|
          @s[sn.n] = sn  if !@s[sn.n] or @s[sn.n].d > sn.d # add sidenode if it does not exist or choose the one with smallest distance
        end
        # check if there are sidenodes
        return '*No solution path is possible in this labyrinth!*' if @s.empty?
        # get minimum for next iteration
        next_node = @s.min_by(&@heuristic) # classical dijkstra: {|sn| sn[1].d }
        current = next_node[1]
        @s.delete next_node[0]
      end

      # get final path
      ret = []
      until current.n == 0
        ret << get_direction(current.n, current.p)
        current = @m[current.p]
      end
      @path = ret.reverse
    end

    # create new sidenode tupels
    def create_tupels(tupel)
      # get open edges
      # comment out next line for JRuby 1.2.0 support (see readme!)
      open_edges = @edges.select {|cur,other| tupel.n==cur and @a.include? other} # connection between current and a still available ones - this is the normal way, working in 1.9
      # remove comment next line for JRuby 1.2.0 support (see readme!)
      # open_edges = []; @edges.each{|cur,other| open_edges << [cur,other] if tupel.n==cur&&@a.include?(other)}
      open_edges.collect {|e| DTupel.new(e[1],tupel.d+1,tupel.n) }
    end

    # calculate direction by node identifier
    def get_direction(n1,n2)
      case
        when n2 - n1 == 1 then 'l' # left
        when n1 - n2 == 1 then 'r' # right
        when n2 - n1 == 9 then 'u' # up
        when n1 - n2 == 9 then 'd' # down
      end
    end

   end

   # One tupel takes @n,@d,@p -> current node, current (shortest) distance, prior node
   class DTupel
     attr_reader :n,:d,:p
     attr_writer    :d,:p

     def initialize(n,d=1.0/0,p=nil) # d = Infinity xD
       @n,@d,@p = n,d,p #node, distance, prior node
     end
   end

  end


 module Backtracking
    NeedsGraph = false
    class << self

      def info
        "This algorithm does not calculate the shortest path!\nIt's calculating *a* path to the finish!\nPath length: #{@path.length}"
      end

      def init(a)
        @a = a;
        @coords = [[0,0]]
        @path   = []
      end

      def calc()
        catch :found do
          search 0,0,@coords # backtrack for solutions (-->coords) and stop! if one is found
          return '*No solution path is possible in this labyrinth!*' if @coords.last != [8,8]
        end

        @coords.each_cons(2) {|old,new| @path << get_direction(old,new)}
        @path
      end

      def consistent(x,y,coords)
        (0..8).member? x and (0..8).member? y and @a[x][y] == 0 and not coords.member?([x,y])
      end

      def search(x,y,coords)
        if x==8&&y==8 # reached finish! abort..
          @coords = coords
          throw :found
        end
        (x+=1;coords << [x,y];search x,y,coords.clone) if consistent(x+1,y,coords)
        (y+=1;coords << [x,y];search x,y,coords.clone) if consistent(x,y+1,coords)
        (x-=1;coords << [x,y];search x,y,coords.clone) if consistent(x-1,y,coords)
        (y-=1;coords << [x,y];search x,y,coords.clone) if consistent(x,y-1,coords)
     end

     def get_direction(old,new) # ...between two points
       if old[0] == new[0]
         if old[1] == new[1]-1 then  'r' # right
         else                        'l' # left
         end
       elsif old[0] == new[0]-1 then 'd' # down
       else                          'u' # up
       end
     end

   end
 end
end
Creative Commons License