diff --git a/02 Amazing/ruby/README.md b/02 Amazing/ruby/README.md index fb32811e..8bf7896b 100644 --- a/02 Amazing/ruby/README.md +++ b/02 Amazing/ruby/README.md @@ -1,3 +1,9 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Ruby](https://www.ruby-lang.org/en/) + +Converted to Ruby (with tons of inspiration from the Python version) by @marcheiligers + +Run `ruby amazing.rb`. + +Run `DEBUG=1 ruby amazing.ruby` to see how it works (requires at least Ruby 2.7). diff --git a/02 Amazing/ruby/amazing.rb b/02 Amazing/ruby/amazing.rb new file mode 100644 index 00000000..d9a4f9d0 --- /dev/null +++ b/02 Amazing/ruby/amazing.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +DEBUG = !ENV['DEBUG'].nil? + +require 'io/console' if DEBUG + +# BASIC arrays are 1-based, unlike Ruby 0-based arrays, +# and this class simulates that. BASIC arrays are zero-filled, +# which is also done here. While we could easily update the +# algorithm to work with zero-based arrays, this class makes +# the problem easier to reason about, row or col 1 are the +# first row or column. +class BasicArrayTwoD + def initialize(rows, cols) + @val = Array.new(rows) { Array.new(cols, 0) } + end + + def [](row, col = nil) + if col + @val[row - 1][col - 1] + else + @val[row - 1] + end + end + + def []=(row, col, n) + @val[row - 1][col - 1] = n + end + + def to_s(width: max_width, row_hilite: nil, col_hilite: nil) + @val.map.with_index do |row, row_index| + row.map.with_index do |val, col_index| + if row_hilite == row_index + 1 && col_hilite == col_index + 1 + "[#{val.to_s.center(width)}]" + else + val.to_s.center(width + 2) + end + end.join + end.join("\n") + end + + def max_width + @val.flat_map { |row| row.map { |val| val.to_s.length } }.sort.last + end +end + +class Maze + EXIT_DOWN = 1 + EXIT_RIGHT = 2 + + # Set up a constant hash for directions + # The values represent the direction of the move as changes to row, col + # and the type of exit when moving in that direction + DIRECTIONS = { + left: { row: 0, col: -1, exit: EXIT_RIGHT }, + up: { row: -1, col: 0, exit: EXIT_DOWN }, + right: { row: 0, col: 1, exit: EXIT_RIGHT }, + down: { row: 1, col: 0, exit: EXIT_DOWN } + }.freeze + + attr_reader :width, :height, :used, :walls, :entry + + def initialize(width, height) + @width = width + @height = height + + @used = BasicArrayTwoD.new(height, width) + @walls = BasicArrayTwoD.new(height, width) + + create + end + + def draw + # Print the maze + draw_top(entry, width) + (1..height - 1).each do |row| + draw_row(walls[row]) + end + draw_bottom(walls[height]) + end + + private + + def create + # entry represents the location of the opening + @entry = (rand * width).round + 1 + + # Set up our current row and column, starting at the top and the locations of the opening + row = 1 + col = entry + c = 1 + used[row, col] = c # This marks the opening in the first row + c += 1 + + while c != width * height + 1 do + debug walls, row, col + # remove possible directions that are blocked or + # hit cells that we have already processed + possible_dirs = DIRECTIONS.reject do |dir, change| + nrow = row + change[:row] + ncol = col + change[:col] + nrow < 1 || nrow > height || ncol < 1 || ncol > width || used[nrow, ncol] != 0 + end.keys + + # If we can move in a direction, move and make opening + if possible_dirs.size != 0 + direction = possible_dirs.sample + change = DIRECTIONS[direction] # pick a random direction + if %i[left up].include?(direction) + row += change[:row] + col += change[:col] + walls[row, col] = change[:exit] + else + walls[row, col] += change[:exit] + row += change[:row] + col += change[:col] + end + used[row, col] = c + c = c + 1 + # otherwise, move to the next used cell, and try again + else + loop do + if col != width + col += 1 + elsif row != height + row += 1 + col = 1 + else + row = col = 1 + end + break if used[row, col] != 0 + debug walls, row, col + end + end + end + + # Add a random exit + walls[height, (rand * width).round] += 1 + end + + def draw_top(entry, width) + (1..width).each do |i| + if i == entry + print i == 1 ? '┏ ' : '┳ ' + else + print i == 1 ? '┏━━' : '┳━━' + end + end + + puts '┓' + end + + def draw_row(row) + print '┃' + row.each.with_index do |val, col| + print val < 2 ? ' ┃' : ' ' + end + puts + row.each.with_index do |val, col| + print val == 0 || val == 2 ? (col == 0 ? '┣━━' : '╋━━') : (col == 0 ? '┃ ' : '┫ ') + end + puts '┫' + end + + def draw_bottom(row) + print '┃' + row.each.with_index do |val, col| + print val < 2 ? ' ┃' : ' ' + end + puts + row.each.with_index do |val, col| + print val == 0 || val == 2 ? (col == 0 ? '┗━━' : '┻━━') : (col == 0 ? '┗ ' : '┻ ') + end + puts '┛' + end + + def debug(walls, row, col) + return unless DEBUG + + STDOUT.clear_screen + puts walls.to_s(row_hilite: row, col_hilite: col) + sleep 0.1 + end +end + +class Amazing + def run + draw_header + + width, height = ask_dimensions + while width <= 1 || height <= 1 + puts "MEANINGLESS DIMENSIONS. TRY AGAIN." + width, height = ask_dimensions + end + + maze = Maze.new(width, height) + puts "\n" * 3 + maze.draw + end + + def draw_header + puts ' ' * 28 + 'AMAZING PROGRAM' + puts ' ' * 15 + 'CREATIVE COMPUTING MORRISTOWN, NEW JERSEY' + puts "\n" * 3 + end + + def ask_dimensions + print 'WHAT ARE YOUR WIDTH AND HEIGHT? ' + width = gets.to_i + print '?? ' + height = gets.to_i + [width, height] + end +end + +Amazing.new.run \ No newline at end of file