One of the earliest Ruby constructs I learned about was Hash.new {|h, k| h[k] = {} }
.
This small one-liner is explored in many Ruby tutorials and it completely captured my imagination.
I've never been able to use this in a production app as I've always found a better solution.
However, one thing it might be good for is to take you on a short journey.
This journey will be interactive, so you will need to copy snippets of code into an irb or pry console. I recommend using ruby 2.6 or higher as the copy-paste functionality is better (and I havn't debugged on lower versions).
UPDATE: I've now added the caves_of_lemaria.rb
script which will run through the whole adventure!
Before we begin, lets make a slight change to my chosen code snippet...
def map
Hash.new do |places, o| # an outer hash to hold a bunch of places
places[o] =
Hash.new do |destinations, d| # an inner hash to hold all the destinations
destinations[d] = nil
end
end
end
You will play the part of the main character in this journey!
puts "What is your name, epic adventurer?"; our_hero = gets.chomp
So, the story has to start somewhere...
Somewhere = Struct.new(:name, :description, :map)
our_hero
lives in a place called Lemaria...
lemaria = Somewhere.new('Lemaria', 'A beautiful remote village', map)
.
Lemaria has a bit of everything...
home = Somewhere.new('Home', 'A peaceful cottage with a nice warm fire crackling inside.', map)
town = Somewhere.new('Town Center', 'A bustling town with a vibrant and noisy market bazzar', map)
forest = Somewhere.new('Forest', 'A forest with an abundance of flora and fauna', map)
mountains = Somewhere.new('Mountains', 'A towering mountain range with snow covered peaks', map)
cave = Somewhere.new('Cave', 'A dark damp cave with many winding and forking passages', map)
The map of Lemaria looks something like this...
# T <---+
# ^ | H = Home
# v V T = Town Center
# F <-> H F = Forest
# ^ ^ M = Mountains
# ^ | C = Cave
# ^ ^ V
# +--> M ^
# ^ | ^ ^ ^ ^
# | ^ | ^ ^
# C----+ ^ ^ ^
lemaria.map[home][town]
lemaria.map[home][forest]
lemaria.map[town][home]
lemaria.map[town][forest]
lemaria.map[forest][home]
lemaria.map[forest][town]
lemaria.map[forest][mountains]
lemaria.map[mountains][forest]
lemaria.map[mountains][cave]
lemaria.map[cave][mountains]
our_hero
heads to town
and overhears one of the shop owners talking about a group of tourists that have ventured up the
mountains and noone has heard from them for several days.
our_hero
is adventurous and courageous, and volunteers to look for the lost tourists.
our_hero
heads back home
to pack some supplies for the potentially perilous journey ahead.
As our_hero
treks through the forest
, a woodcutter approaches.
The woodcutter confirms the tourists passed by this way, and that they were going to map the passages
in the cave
up the mountain.
our_hero
has travelled up the mountain
many times before and has no trouble finding the cave
.
The cave network is vast, many people have attempted to navigate the cave system, but very few have returned. It's was the site of an old mine, and is even rumoured to have been used for satanic cult worship!
our_hero
has very little idea of what the cave system actually looks like.
The dark dank cave looms before our_hero
:
# I would recommend copying this line bby line to prevent errors
e = "VGhlIGVudHJhbmNlIHRvIHRoZSBjYXZlIHN0YW5kcyBiZWZvcmUgJXM="; a = "QW4gaW5zY3JpcHRpb24gb24gdGhlIHdhbGwgcmVhZHMgIk51bWVyYSBvbW5l\ncyBxdWkgaW5ncmVkaXVudHVyIGNhdmVhbnQi"; cave.map[:entrance][a] = "JXMgZW50ZXJzIHRoZSBjYXZl"; cave.map[a][:entrance] = "JXMgbGVhdmVzIHRoZSBjYXZl"; cave.map[a][19]; cave.map[19][a]; cave.map[a][2]; cave.map[2][a]; cave.map[2][3]; cave.map[3][2]; cave.map[2][4]; cave.map[4][2]; d1 = "JXMgZmFsbHMgZG93biBhIGRlZXAgaG9sZS4="; cave.map[3][d1];
cave.map[3][5] = "JXMgZm91bmQgYSBtYXAhCgogICAgICAgICAgICAqCiAgICAgICAgICAgIHwK\nICAgICAgIE8gICAgKi0tKi0tKiAgICAgICAgICAgICotLS0qLS0tKgogICAg\nICAgfCAgICB8ICAgICAgICAgICAgICAgICAgfCAgICAgICB8CiAgICAgICAq\nLS0tLSotLSotLUAgICAgKiAgICAgICA9ICAgICAgIHwKICAgICAgIHwgICAg\nfCAgfCAgICAgICB8ICAgICAgIHwgICAgICAgfAogICAgICAgfCAgICB8ICAq\nICAgICotLSogICAgICAgKiAgICAgICB8CiAgICAgICAqLS0tLSogIHwgICAg\nICAgfCAgICAgICB8ICAgICAgIHwKICAgICAgIHwgICAgICAgKi0tLSotLS0q\nLS0tKi0tLSogICAgICAgfCAgICAgICAqCmUgLS0tLSAqICAgICAgICAgICAg\nICAgfCAgICAgICB8ICAgICAgIHwgICAgICAgfAogICAgICAgfCAgICAgICAg\nICAgICAgICogICAgICAgKi0tLS0tLS0qLS0tKi0tLSoKICAgICAgIHwgICAg\nICAgICAgICAgICB8ICAgICAgICAgICAgICAgICAgICAgICB8CiAgICAgICB8\nICAgICAgIF9fXyAgICAgfCAgICAgICAgICAgICAgICAgICAgICAgKgogICAg\nICAgKi0tLS0tLXsgX18gICAgIEwgICAgICAgICAgICAgICAgICAgICAgIHwK\nICAgICAgICAgICAgICB8ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\nICB8CiAgICAgICAgICAgICAgfCAgICAgICAgICAgICAgICAgICAgICAgICAg\nICAgICAgKgogICAgICAgICAgICAgICotLS0tIwo=\n";
cave.map[5][3]; cave.map[4][5]; cave.map[5][4]; cave.map[5][8]; cave.map[8][5]; cave.map[5][6]; cave.map[6][5]; a2 = "QW4gYWx0ZXIgc3RhbmRzIGluIHRoZSBjZW50ZXIgb2YgdGhlIHJvb20gd2l0\naCBjcmVlcHkgbXVyYWxzIG9mIGRhZW1vbnM="; cave.map[6][a2]; cave.map[a2][6]; cave.map[6][13]; cave.map[13][6]; h = "QW4gb2xkIG1pbmVycyBoYXQgc2l0cyBieSB0aGUgd2FsbA=="; cave.map[8][h]; cave.map[h][8]; cave.map[8][9]; cave.map[9][8]; cave.map[9][11]; cave.map[11][9]; b = "QW4gb2xkIGJvb3Qgc2l0cyBpbiB0aGUgbWlkZGxlIG9mIHRoZSBjYXZl"; cave.map[13][b]; cave.map[b][13]; cave.map[b][15]; cave.map[15][b]; cave.map[15][16]; cave.map[16][15]; cave.map[16][18]; cave.map[18][16]; b1 = "QXJnISBCYXRz"; cave.map[16][b1]; cave.map[b1][16]; cave.map[16][21]; cave.map[21][16];
cave.map[21][22]; cave.map[22][21]; cave.map[21][39]; cave.map[39][21]; t = "dG91cmlzdHM="; cave.map[39][t]; cave.map[t][39]; c3 = "QXQgdGhlIHRvcCBhIGNsaWZmIGluIGEgbGFyZ2UgY2F2ZXJuLCAlcyBkb2Vz\nbid0IHNlZSB0aGUgZWRnZSBhbmQgZmFsbHM="; cave.map[18][c3]; c1 = "JXMgZW50ZXJzIGEgbGFyZ2UgY2F2ZXJu"; cave.map[19][c1] ; cave.map[c1][19]; cave.map[c1][36]; cave.map[36][c1]; c2 = "JXMgaXMgaW4gYSBsYXJnZSBjYXZlcm4gYXQgdGhlIGJhc2Ugb2YgYSBjbGlm\nZg=="; cave.map[c1][c2]; cave.map[c2][c1]; m = "QW4gYWJhbmRvbmVkIG1pbmUgc2hhZnQ="; cave.map[36][m]; cave.map[m][36]; cave.map[17][23]; cave.map[23][17]; cave.map[23][25]; cave.map[25][23]; cave.map[23][24]; cave.map[24][23]; cave.map[24][30]; cave.map[30][24]; n = "JXMgc3F1ZWV6ZXMgdGhyb3VnaCBhIG5hcnJvdyBwYXNzYWdld2F5"; cave.map[25][n]; cave.map[n][25]; cave.map[n][27];
cave.map[27][n]; cave.map[27][28]; cave.map[28][27]; w = "JXMgd2Fsa3MgaW50byBhIGdpYW50IHNwaWRlcndlYiE="; cave.map[28][w]; cave.map[w][28]; cave.map[w][30]; cave.map[30][w]; cave.map[30][32]; cave.map[32][30]; cave.map[32][33]; cave.map[33][32]; cave.map[33][34]; cave.map[34][33]; cave.map[33][35]; cave.map[35][33]; s = "QSBodW1hbiBza2VsZXRvbi4uLiBwb29yIGd1eQ==";
cave.map[35][s]; cave.map[s][35];
our_hero
decides to come up with a navigation strategy before entering the cave:
def navigation_strategy(graph, start_node)
Enumerator.new do |enum|
visited_rooms = []
unvisited_queue = [start_node]
path = []
while unvisited_queue.any?
room = unvisited_queue.pop
previous_room = path.last
path.push(room) unless path.last == room
selected_room = enum.yield(room, graph[room], visited_rooms, previous_room, path)
path = path[0...path.index(selected_room)]
unvisited_queue.push(selected_room)
visited_rooms.push(room)
end
end
end
our_hero
wants to document the journey incase the worst should happen:
narration = proc do |room, options, visited, previous_room|
require 'base64'
passage_text = cave.map[previous_room][room] if cave.map[previous_room].keys.include?(room)
puts Base64.decode64(passage_text) % our_hero if passage_text
puts
if room == :entrance then puts Base64.decode64("VGhlIGVudHJhbmNlIHRvIHRoZSBjYXZlIHN0YW5kcyBiZWZvcmUgJXMhCg==") % our_hero
elsif options.count == 0 then puts Base64.decode64("JXMgZGllcyE=") % our_hero
elsif options.count == 1 then puts Base64.decode64("QSBkZWFkIGVuZCEgJXMgbXVzdCBoZWFkIGJhY2s=") % our_hero
elsif options.count == 2 then puts Base64.decode64("JXMgY29udGludWVzIGRvd24gYSBwYXNzYWdld2F5") % our_hero
elsif options.count > 2 then puts Base64.decode64("JXMgZW5jb3VudGVycyBhIGZvcmtpbmcgcGF0aCE=") % our_hero
end
puts
if room.is_a?(String)
text = Base64.decode64(room)
if text == 'tourists'
puts Base64.decode64("JXMgZm91bmQgdGhlIHRvdXJpc3RzIQ==") % our_hero
puts Base64.decode64("VGhleSBhcmUgc28gaGFwcHkgdG8gYmUgcmVzY3VlZCE=") % our_hero
else
puts text % our_hero
end
end
puts Base64.decode64("JXMgaGFzIGJlZW4gaGVyZSBiZWZvcmU=") % our_hero if visited.include?(room)
puts
end
our_hero
feels that some devine intervention may be required on this epic quest:
interaction = proc do |room, room_options, visited, previous_room, path|
narration.call(room, room_options, visited, previous_room)
next if room_options.empty?
available_paths = room_options.each_with_index.collect do |(key, _), i|
attributes = []
if key == :entrance
attributes << :entrance
else
attributes << :visited if visited.include?(key)
attributes << :back if previous_room == key
end
text = (i + 1).to_s
text = '%s (%s)' % [text, attributes.join('|')] if attributes.any?
[key, text]
end
puts 'Which way will %s go? Available paths: %s' % [our_hero, available_paths.collect {|_, t| t}.join(', ')]
while (option = gets.chomp.to_i) == 0 || option > room_options.count
puts "I'm sorry %s, I'm afraid you can't do that" % our_hero
end
puts
available_paths[option - 1].first
end
Finally, our_hero
is ready to venture into the cave to resucue the poor tourists.
our_hero
can leave the cave at any time by pressing ctrl+c
.
navigation_strategy(cave.map, :entrance).each(&interaction)
our_hero
saved the day!!
A great feast is held in lemaria
to celbrate the return of our_hero
and the rescued tourists.
I really hope you enjoyed the interactive journey! I wanted to give you something different, yet very truely in the spirit of Ruby.
In closing, my code snippet, Hash.new {|h, k| h[k] = {} }
, did the job, but there were many gotchas and edgecases that
made debugging very hard and confusing. As always, a dedecated object model would have been a better solution. Oh well...
Best regards, Brent