Basics of Crystal

Priskribo de la ĉi-paĝa enhavo

Konverto de malnovaj BASIC-programoj al Crystal por lerni la fundamentojn de ĉi-tiu lingvo.

Etikedoj:

3D Plot

# 3D Plot

# Original version in BASIC:
#   Creative Computing (Morristown, New Jersey, USA), ca. 1980.

# This version in Crystal:
#   Copyright (c) 2023, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written on 2023-09, 2023-11.
#
# Last modified: 20231104T0055+0100.

SPACE = ' '
DOT   = '*'
WIDTH = 56

def clear
  print "\e[0;0H\e[2J"
end

def print_credits
  puts "3D Plot\n"
  puts "Original version in BASIC:"
  puts "    Creative computing (Morristown, New Jersey, USA), ca. 1980.\n"
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair\n"
  puts "Press Enter to start the program."
  gets
end

def a(z)
  return 30 * Math.exp(-z * z / 100)
end

def draw
  x = -30.0
  while x <= 30
    c = 0
    line = Array.new WIDTH, SPACE
    l = 0
    y1 = 5 * (Math.sqrt(900 - x * x) / 5).to_i
    y = y1
    while y >= -y1
      z = (25 + a(Math.sqrt(x * x + y * y)) - 0.7 * y).to_i
      if z > l
        l = z
        line[z] = DOT
      end
      y += -5
    end
    c = 0
    line.each do |item|
      print item
    end
    puts
    x += 1.5
  end
end

clear
print_credits
clear
draw

Bagels

# Bagels

# Original version in BASIC:
#   D. Resek, P. Rowe, 1978.
#   Creative Computing (Morristown, New Jersey, USA), 1978.

# This version in Crystal:
#   Copyright (c) 2023, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2023-04, 2023-09, 2023-10.
#
# Last modified: 20250513T0043+0200.

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear
  print "\e[2J"
  home
end

# Prompts the user to enter a command and returns it.
def command(prompt = "> ") : String
  s = nil
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  s
end

# Prints the given prompt and waits until the user enters an empty string.
def press_enter(prompt : String)
  until command(prompt) == ""
  end
end

# Returns `true` if the given string is "yes" or a synonym.
def is_yes?(answer : String) : Bool
  ["ok", "yeah", "yes", "y"].any? { |yes| answer.downcase == yes }
end

# Returns `true` if the given string is "no" or a synonym.
def is_no?(answer : String) : Bool
  ["no", "nope", "n"].any? { |no| answer.downcase == no }
end

# Prints the given prompt, waits until the user enters a valid yes/no
# string, and returns `true` for "yes" or `false` for "no".
def yes?(prompt : String) : Bool
  answer = ""
  while !(is_yes?(answer) || is_no?(answer))
    answer = command(prompt)
  end
  is_yes?(answer)
end

# Clears the screen, displays the credits and waits for a keypress.
def print_credits
  clear
  puts "Bagels"
  puts "Number guessing game\n"
  puts "Original source unknown but suspected to be:"
  puts "    Lawrence Hall of Science, U.C. Berkely.\n"
  puts "Original version in BASIC:"
  puts "    D. Resek, P. Rowe, 1978."
  puts "    Creative computing (Morristown, New Jersey, USA), 1978.\n"
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair\n"
  press_enter "Press Enter to read the instructions. "
end

# Clears the screen, prints the instructions and waits for a keypress.
def print_instructions
  clear
  puts "Bagels"
  puts "Number guessing game\n"
  puts "I am thinking of a three-digit number that has no two digits the same."
  puts "Try to guess it and I will give you clues as follows:\n"
  puts "   PICO   - one digit correct but in the wrong position"
  puts "   FERMI  - one digit correct and in the right position"
  puts "   BAGELS - no digits correct"
  press_enter "\nPress Enter to start. "
end

DIGITS =  3
TRIES  = 20

# Returns three random digits.
def random : {Int32, Int32, Int32}
  d = [] of Int32
  while d.size < DIGITS
    digit = rand(10)
    while digit.in?(d)
      digit = rand(10)
    end
    d << digit
  end
  {d[0], d[1], d[2]}
end

def other_than_digits?(text : String) : Bool
  text.each_char do |char|
    if !char.number?
      return true
    end
  end
  false
end

# Prints the given prompt and gets a three-digit number from the user.
def input(prompt : String) : {Int32, Int32, Int32}
  user_digit = [] of Int32 # digit value
  while true
    input = command(prompt)
    if input.size != DIGITS
      puts "Remember it's a #{DIGITS}-digit number."
      next
    elsif other_than_digits?(input)
      puts "What?"
      next
    end
    user_digit.clear
    input.each_char do |c|
      user_digit << c.to_i
    end
    if user_digit.uniq.size != user_digit.size
      puts "Remember my number has no two digits the same."
      next
    end
    break
  end
  {user_digit[0], user_digit[1], user_digit[2]}
end

# Inits and runs the game loop.
def play
  computer_number = [DIGITS] of Int32 # random number
  user_number = [DIGITS] of Int32     # user guess
  DIGITS.times do
    computer_number << 0
    user_number << 0
  end
  score = 0
  fermi = 0  # counter
  pico = 0   # counter
  while true # game loop
    clear
    computer_number[0], computer_number[1], computer_number[2] = random
    puts "O.K.  I have a number in mind."
    (0...TRIES).each do |guess|
      user_number[0], user_number[1], user_number[2] = input("Guess ##{guess.to_s.rjust(2, '0')}: ")
      fermi = 0
      pico = 0
      (0...DIGITS).each do |i|
        (0...DIGITS).each do |j|
          if computer_number[i] == user_number[j]
            if i == j
              fermi += 1
            else
              pico += 1
            end
          end
        end
      end
      if pico + fermi == 0
        print "BAGELS"
      else
        print "PICO " * pico
        print "FERMI " * fermi
      end
      puts
      if fermi == DIGITS
        break
      end
    end # tries loop
    if fermi == DIGITS
      puts "You got it!!!"
      score += 1
    else
      puts "Oh well."
      print "That's #{TRIES} guesses.  My number was "
      print 100 * computer_number[0] + 10 * computer_number[1] + computer_number[2], "\n"
    end
    if !yes?("Play again? ")
      break
    end
  end # game loop
  if score != 0
    puts "A #{score}-point bagels, buff!!"
  end
  puts "Hope you had fun.  Bye."
end

print_credits
print_instructions
play

Bug

# Bug

# Original version in BASIC:
#   Brian Leibowitz, 1978.
#   Creative Computing (Morristown, New Jersey, USA), 1978.

# This version in Crystal:
#   Copyright (c) 2023, 2024, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2023-09, 2023-10, 2023-11, 2024-07.

# Last modified: 20251228T0052+0100.

class Bug
  property body : Bool
  property neck : Bool
  property head : Bool
  property feelers : Int32
  property feeler_type : Char
  property tail : Bool
  property legs : Int32

  def initialize
    @body = false
    @neck = false
    @head = false
    @feelers = 0
    @feeler_type = '|'
    @tail = false
    @legs = 0
  end
end

class Player
  property pronoun : String
  property possessive : String
  property bug : Bug

  def initialize
    @pronoun = ""
    @possessive = ""
    @bug = Bug.new
  end
end

# Bug body parts.
enum Part
  Body   = 1
  Neck
  Head
  Feeler
  Tail
  Leg
end

FIRST_PART = Part::Body.value
LAST_PART  = Part::Leg.value

PART_NAME     = ["body", "neck", "head", "feeler", "tail", "leg"]
PART_QUANTITY = [1, 1, 1, 2, 1, 6]

# Bug body attributes.
BODY_HEIGHT   = 2
FEELER_LENGTH = 4
LEG_LENGTH    = 2
MAX_FEELERS   = 2
MAX_LEGS      = 6
NECK_LENGTH   = 2

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear
  print "\e[2J"
  home
end

# Moves the cursor up by the given number of rows (defaults to 1), without
# changing the column position.
def cursor_up(rows = 1)
  print "\e[#{rows}A"
end

# Erases the current line, without moving the cursor position.
def erase_line
  print "\e[2K"
end

# Moves the cursor to the previous row, without changing the column position,
# and erases its line.
def erase_previous_line
  cursor_up
  erase_line
end

# Prompts the user to enter a command and returns it.
def command(prompt = "> ") : String
  s = nil
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  s
end

# Prints the given prompt and waits until the user enters an empty string.
def press_enter(prompt : String)
  until command(prompt) == ""
  end
end

# Clears the screen, displays the credits and waits for the Enter key to be
# pressed.
def print_credits
  clear
  puts "Bug"
  puts
  puts "Original version in BASIC:"
  puts "    Brian Leibowitz, 1978."
  puts "    Creative computing (Morristown, New Jersey, USA), 1978."
  puts
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, 2024, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair"
  puts
  press_enter "Press Enter to read the instructions. "
end

INSTRUCTIONS = "
The object is to finish your bug before I finish mine. Each number
stands for a part of the bug body.

I will roll the die for you, tell you what I rolled for you, what the
number stands for, and if you can get the part. If you can get the
part I will give it to you. The same will happen on my turn.

If there is a change in either bug I will give you the option of
seeing the pictures of the bugs. The numbers stand for parts as
follows:
"

COLUMNS           = 3
COLUMN_WIDTH      = 8
COLUMN_SEPARATION = 2

# Prints a table with the bug parts' description.
def print_parts_table
  # Headers
  header = ["Number", "Part", "Quantity"]
  (0...COLUMNS).each do |i|
    print header[i].ljust(COLUMN_WIDTH + COLUMN_SEPARATION)
  end
  puts

  # Rulers
  (0...COLUMNS).each do |i|
    print "-" * COLUMN_WIDTH, i == COLUMNS - 1 ? "" : " " * COLUMN_SEPARATION
  end
  puts

  # Data
  (FIRST_PART..LAST_PART).each do |n|
    print \
      n.to_s.ljust(COLUMN_WIDTH + COLUMN_SEPARATION),
      PART_NAME[n - 1].capitalize.ljust(COLUMN_WIDTH + COLUMN_SEPARATION),
      PART_QUANTITY[n - 1], "\n"
  end
end

# Clears the screen, prints the instructions and waits for a keypress.
def print_instructions
  clear
  puts "Bug"
  puts INSTRUCTIONS, "\n"
  print_parts_table
  press_enter "\nPress Enter to start. "
end

# Prints a bug head.
def print_head
  puts "        HHHHHHH"
  puts "        H     H"
  puts "        H O O H"
  puts "        H     H"
  puts "        H  V  H"
  puts "        HHHHHHH"
end

# Prints the given bug.
def print_bug(bug : Bug)
  if bug.feelers > 0
    (0...FEELER_LENGTH).each do |i|
      print "        "
      (0...bug.feelers).each do |j|
        print " #{bug.feeler_type}"
      end
      puts
    end
  end
  if bug.head
    print_head
  end
  if bug.neck
    (0...NECK_LENGTH).each do |i|
      puts "          N N"
    end
  end
  if bug.body
    puts "     BBBBBBBBBBBB"
    (0...BODY_HEIGHT).each do |i|
      puts "     B          B"
    end
    if bug.tail
      puts "TTTTTB          B"
    end
    puts "     BBBBBBBBBBBB"
  end
  if bug.legs > 0
    (0...LEG_LENGTH).each do |i|
      print "    "
      (0...bug.legs).each do |j|
        print " L"
      end
      puts
    end
  end
end

# Returns `true` if the given bug is finished; otherwise returns `false`.
def finished?(bug : Bug) : Bool
  bug.feelers == MAX_FEELERS && bug.tail && bug.legs == MAX_LEGS
end

# Array to convert a number to its equilavent text.
AS_TEXT = ["no", "a", "two", "three", "four", "five", "six"]

# Returns a string containing the given number and noun in their proper form.
def plural(number : Int, noun : String) : String
  AS_TEXT[number] + " " + noun + (number > 1 ? "s" : "")
end

# Adds the given part to the given player's bug.
def add_part(part : Int32, player : Player) : Bool
  changed = false
  case part
  when Part::Body.value
    if player.bug.body
      puts ", but #{player.pronoun} already have a body."
    else
      puts "; #{player.pronoun} now have a body:"
      player.bug.body = true
      changed = true
    end
  when Part::Neck.value
    if player.bug.neck
      puts ", but #{player.pronoun} already have a neck."
    elsif !player.bug.body
      puts ", but #{player.pronoun} need a body first."
    else
      puts ";  #{player.pronoun} now have a neck:"
      player.bug.neck = true
      changed = true
    end
  when Part::Head.value
    if player.bug.head
      puts ", but  #{player.pronoun} already have a head."
    elsif !player.bug.neck
      puts ", but  #{player.pronoun} need a a neck first."
    else
      puts ";  #{player.pronoun} now have a head:"
      player.bug.head = true
      changed = true
    end
  when Part::Feeler.value
    if player.bug.feelers == MAX_FEELERS
      puts ", but  #{player.pronoun} have two feelers already."
    elsif !player.bug.head
      puts ", but  #{player.pronoun} need a head first."
    else
      player.bug.feelers += 1
      print "; #{player.pronoun} now have #{plural(player.bug.feelers, "feeler")}"
      puts ":"
      changed = true
    end
  when Part::Tail.value
    if player.bug.tail
      puts ", but  #{player.pronoun} already have a tail."
    elsif !player.bug.body
      puts ", but  #{player.pronoun} need a body first."
    else
      puts ";  #{player.pronoun} now have a tail:"
      player.bug.tail = true
      changed = true
    end
  when Part::Leg.value
    if player.bug.legs == MAX_LEGS
      print ", but #{player.pronoun} have #{AS_TEXT[MAX_LEGS]} feet already."
    elsif !player.bug.body
      puts ", but  #{player.pronoun} need a body first."
    else
      player.bug.legs += 1
      print "; #{player.pronoun} now have #{plural(player.bug.legs, "leg")}"
      puts ":"
      changed = true
    end
  end
  changed
end

# Asks the user to press the Enter key, waits for the input, then erases the
# prompt text.
def prompt
  press_enter "Press Enter to roll the dice. "
  erase_previous_line
end

# Plays one turn for the given player, rolling the dice and updating his bug.
def turn(player : Player)
  prompt
  part = rand(FIRST_PART..LAST_PART) # dice
  print "#{player.pronoun.capitalize} rolled a #{part} (#{PART_NAME[part - 1]})"
  if add_part(part, player)
    puts
    print_bug(player.bug)
  end
  puts
end

# Prints a message about the winner.
def print_winner(p1, p2 : Player)
  if finished?(p1.bug) && finished?(p2.bug)
    puts "Both of our bugs are finished in the same number of turns!"
  elsif finished?(p1.bug)
    puts "#{p1.possessive} bug is finished."
  elsif finished?(p2.bug)
    puts "#{p2.possessive} bug is finished."
  end
end

# Inits the data of a player.
def init(player : Player, pronoun : String, possessive : String, feeler_type : Char)
  player.pronoun = pronoun
  player.possessive = possessive
  player.bug.feeler_type = feeler_type
end

# Executes the game loop.
def play
  clear
  computer = Player.new
  human = Player.new
  init human, "you", "your", 'A'
  init computer, "I", "My", 'F'
  while !(finished?(human.bug) || finished?(computer.bug))
    turn human
    turn computer
  end
  print_winner human, computer
end

print_credits
print_instructions
play
puts "I hope you enjoyed the game, play it again soon!!"

Bunny

# Bunny

# Original version in BASIC:
#   Creative Computing (Morristown, New Jersey, USA), 1978.

# This version in Crystal:
#   Copyright (c) 2023, 2024, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2023-09, 2024-09.
#
# Last modified: 20250731T1954+0200.

# Clears the terminal and moves the cursor to the top left position.
def clear_screen
  print "\e[0;0H\e[2J"
end

# Prints the credits and waits for the Enter key.
def print_credits
  puts "Bunny\n"
  puts "Original version in BASIC:"
  puts "    Creative Computing (Morristown, New Jersey, USA), 1978.\n"
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, 2024, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair\n"
  puts "Press Enter to start the program."
  gets
end

WIDTH = 53

# Clears the given line buffer array with spaces.
def clear_line(line)
  (0...WIDTH).each do |n|
    line[n] = ' '
  end
end

# Prints the given line buffer array.
def print_line(line)
  line.each do |item|
    print item
  end
  puts
end

END_OF_LINE = -1 # end of row identifier

# Draws the graphic.
def draw
  letter = ['B', 'U', 'N', 'N', 'Y']
  letters = letter.size

  data = [
    1, 2, END_OF_LINE, 0, 2, 45, 50, END_OF_LINE, 0, 5, 43, 52, END_OF_LINE, 0, 7, 41, 52, END_OF_LINE,
    1, 9, 37, 50, END_OF_LINE, 2, 11, 36, 50, END_OF_LINE, 3, 13, 34, 49, END_OF_LINE, 4, 14,
    32, 48, END_OF_LINE, 5, 15, 31, 47, END_OF_LINE, 6, 16, 30, 45, END_OF_LINE, 7, 17, 29, 44,
    END_OF_LINE, 8, 19, 28, 43, END_OF_LINE, 9, 20, 27, 41, END_OF_LINE, 10, 21, 26, 40, END_OF_LINE,
    11, 22, 25, 38, END_OF_LINE, 12, 22, 24, 36, END_OF_LINE, 13, 34, END_OF_LINE, 14, 33, END_OF_LINE,
    15, 31, END_OF_LINE, 17, 29, END_OF_LINE, 18, 27, END_OF_LINE, 19, 26, END_OF_LINE, 16, 28, END_OF_LINE,
    13, 30, END_OF_LINE, 11, 31, END_OF_LINE, 10, 32, END_OF_LINE, 8, 33, END_OF_LINE, 7, 34, END_OF_LINE, 6,
    13, 16, 34, END_OF_LINE, 5, 12, 16, 35, END_OF_LINE, 4, 12, 16, 35, END_OF_LINE, 3, 12, 15,
    35, END_OF_LINE, 2, 35, END_OF_LINE, 1, 35, END_OF_LINE, 2, 34, END_OF_LINE, 3, 34, END_OF_LINE, 4, 33,
    END_OF_LINE, 6, 33, END_OF_LINE, 10, 32, 34, 34, END_OF_LINE, 14, 17, 19, 25, 28, 31, 35,
    35, END_OF_LINE, 15, 19, 23, 30, 36, 36, END_OF_LINE, 14, 18, 21, 21, 24, 30, 37, 37,
    END_OF_LINE, 13, 18, 23, 29, 33, 38, END_OF_LINE, 12, 29, 31, 33, END_OF_LINE, 11, 13, 17,
    17, 19, 19, 22, 22, 24, 31, END_OF_LINE, 10, 11, 17, 18, 22, 22, 24, 24, 29,
    29, END_OF_LINE, 22, 23, 26, 29, END_OF_LINE, 27, 29, END_OF_LINE, 28, 29, END_OF_LINE,
  ]

  line = [] of Char # line buffer
  (0...WIDTH).each do
    line << ' '
  end

  data_index = 0
  while data_index < data.size
    first_column = data[data_index]
    data_index += 1
    if first_column == END_OF_LINE
      print_line line
      clear_line line
    else
      last_column = data[data_index]
      data_index += 1
      (first_column..last_column).each do |column|
        line[column] = letter[column % letters]
      end
    end
  end
end

clear_screen
print_credits
clear_screen
draw

Chase

# Chase

# Original version in BASIC:
#
#   Anonymous.
#   Published in 1977 in "The Best of Creative Computing", Volume 2.
#
#   https://www.atariarchives.org/bcc2/showpage.php?page=253

# This version in Crystal:
#
#   Copyright (c) 2024, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2024-11-27/29.
#
# Last modified: 20250511T2305+0200.

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print "\e[2J"
  home
end

def set_color(color : Int)
  print "\e[#{color}m"
end

def set_attribute(attr : Int)
  print "\e[0;#{attr}m"
end

# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int)
  print "\e[#{line};#{column}H"
end

def erase_line_right
  print "\e[K"
end

def hide_cursor
  print "\e[?25l"
end

def show_cursor
  print "\e[?25h"
end

# Globals {{{1
# =============================================================

# Colors

DEFAULT_INK      = FOREGROUND + WHITE
INPUT_INK        = FOREGROUND + BRIGHT + GREEN
INSTRUCTIONS_INK = FOREGROUND + YELLOW
TITLE_INK        = FOREGROUND + BRIGHT + RED

# Arena

ARENA_WIDTH  = 20
ARENA_HEIGHT = 10
ARENA_LAST_X = ARENA_WIDTH - 1
ARENA_LAST_Y = ARENA_HEIGHT - 1
ARENA_ROW    = 3

EMPTY   = " "
FENCE   = "X"
MACHINE = "m"
HUMAN   = "@"

FENCES = 15 # inner obstacles, not the border

# The end

enum End
  Not_Yet
  Quit
  Electrified
  Killed
  Victory
end

the_end : End

# Machines

# XXX TODO
# class Machine
#   property x : Int32
#   property y : Int32
#   property operative : Bool
#
#   def initialize
#     @x = 0
#     @y = 0
#     @operative = true
#   end
# end

MACHINES      = 5
MACHINES_DRAG = 2 # probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.

class Global
  class_property arena = Array(Array(String)).new(ARENA_HEIGHT) { Array(String).new(ARENA_WIDTH, "") }
  class_property machine_x : Array(Int32) = Array.new(MACHINES, 0)
  class_property machine_y : Array(Int32) = Array.new(MACHINES, 0)
  class_property operative : Array(Bool) = Array.new(MACHINES, true)
  class_property destroyed_machines = 0 # counter
  class_property human_x = 0
  class_property human_y = 0
end

# User input {{{1
# =============================================================

# Print the given prompt and wait until the user enters a String.
def input_string(prompt : String = "") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  set_color(DEFAULT_INK)
  s
end

# Print the given prompt and wait until the user presses Enter.
def press_enter(prompt : String)
  input_string(prompt)
end

# Return `true` if the given String is "yes" or a synonym.
def is_yes?(answer : String) : Bool
  ["ok", "yeah", "yes", "y"].any? { |yes| answer.downcase == yes }
end

# Return `true` if the given String is "no" or a synonym.
def is_no?(answer : String) : Bool
  ["no", "nope", "n"].any? { |no| answer.downcase == no }
end

# Print the given prompt, wait until the user enters a valid yes/no
# String, and return `true` for "yes" or `false` for "no".
def yes?(prompt : String) : Bool
  while true
    answer = input_string(prompt)
    if is_yes?(answer)
      return true
    end
    if is_no?(answer)
      return false
    end
  end
end

# Title, credits and instructions {{{1
# =============================================================

TITLE = "Chase"

# Print the title at the current cursor position.
def print_title
  set_color(TITLE_INK)
  puts TITLE
  set_color(DEFAULT_INK)
end

def print_credits
  print_title
  puts
  puts "Original version in BASIC:"
  puts "    Anonymous."
  puts "    Published in \"The Best of Creative Computing\", Volume 2, 1977."
  puts "    https://www.atariarchives.org/bcc2/showpage.php?page=253"
  puts
  puts "This version in Crystal:"
  puts "    Copyright (c) 2024, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair"
end

# Print the game instructions and wait for a key.
def print_instructions
  print_title
  set_color(INSTRUCTIONS_INK)
  puts "\nYou (#{HUMAN}) are in a high voltage maze with #{MACHINES}"
  puts "security machines (#{MACHINE}) trying to kill you."
  puts "You must maneuver them into the maze (#{FENCE}) to survive."
  puts
  puts "Good luck!"
  puts
  puts "The movement commands are the following:"
  puts
  puts "    ↖  ↑  ↗"
  puts "    NW N NE"
  puts "  ←  W   E  →"
  puts "    SW S SE"
  puts "    ↙  ↓  ↘"
  puts "\nPlus 'Q' to end the game."
  set_color(DEFAULT_INK)
end

# Arena {{{1
# =============================================================

# Display the arena at the top left corner of the screen.
def print_arena
  set_cursor_position(ARENA_ROW, 1)
  (0..ARENA_LAST_Y).each do |y|
    (0..ARENA_LAST_X).each do |x|
      print Global.arena[y][x]
    end
    puts
  end
end

# If the given arena coordinates `y` and `x` are part of the border arena (i.e.
# its surrounding fence), return `true`, otherwise return `false`.
def is_border?(y : Int, x : Int) : Bool
  return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X)
end

# Return a random integer in the given inclusive range.
def random_int_in_inclusive_range(min, max : Int) : Int
  return rand(max - min + 1) + min
end

# Place the given String at a random empty position of the arena and return
# the coordinates.
def place(s : String) : Tuple
  while true
    y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1)
    x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1)
    if Global.arena[y][x] == EMPTY
      break
    end
  end
  Global.arena[y][x] = s
  {y, x}
end

# Inhabit the arena with the machines, the inner fences and the human.
def inhabit_arena
  (0...MACHINES).each do |m|
    Global.machine_y[m], Global.machine_x[m] = place(MACHINE)
    Global.operative[m] = true
  end
  (0...FENCES).each do
    place(FENCE)
  end
  Global.human_y, Global.human_x = place(HUMAN)
end

# Clean the arena, i.e. empty it and add a surrounding fence.
def clean_arena
  (0..ARENA_LAST_Y).each do |y|
    (0..ARENA_LAST_X).each do |x|
      Global.arena[y][x] = is_border?(y, x) ? FENCE : EMPTY
    end
  end
end

# Game {{{1
# =============================================================

# Move the given machine.
def move_machine(m : Int)
  maybe : Int32

  Global.arena[Global.machine_y[m]][Global.machine_x[m]] = EMPTY

  maybe = rand(2)
  if Global.machine_y[m] > Global.human_y
    Global.machine_y[m] -= maybe
  elsif Global.machine_y[m] < Global.human_y
    Global.machine_y[m] += maybe
  end

  maybe = rand(2)
  if (Global.machine_x[m] > Global.human_x)
    Global.machine_x[m] -= maybe
  elsif (Global.machine_x[m] < Global.human_x)
    Global.machine_x[m] += maybe
  end

  if Global.arena[Global.machine_y[m]][Global.machine_x[m]] == EMPTY
    Global.arena[Global.machine_y[m]][Global.machine_x[m]] = MACHINE
  elsif Global.arena[Global.machine_y[m]][Global.machine_x[m]] == FENCE
    Global.operative[m] = false
    Global.destroyed_machines += 1
    if Global.destroyed_machines == MACHINES
      the_end = End::Victory
    end
  elsif Global.arena[Global.machine_y[m]][Global.machine_x[m]] == HUMAN
    the_end = End::Killed
  end
end

# Maybe move the given operative machine.
def maybe_move_machine(m : Int)
  if rand(MACHINES_DRAG) == 0
    move_machine(m)
  end
end

# Move the operative machines.
def move_machines
  (0...MACHINES).each do |m|
    if Global.operative[m]
      maybe_move_machine(m)
    end
  end
end

# Read a user command; update `the_end` accordingly and return the direction
# increments.
def get_move : Tuple
  y_inc = 0
  x_inc = 0
  puts
  erase_line_right
  command = input_string("Command: ").downcase
  case command
  when "q" ; the_end = End::Quit
  when "sw"; y_inc = +1; x_inc = -1
  when "s" ; y_inc = +1
  when "se"; y_inc = +1; x_inc = +1
  when "w" ; x_inc = -1
  when "e" ; x_inc = +1
  when "nw"; y_inc = -1; x_inc = -1
  when "n" ; y_inc = -1
  when "ne"; y_inc = -1; x_inc = +1
  end
  return y_inc, x_inc
end

def play
  y_inc : Int32
  x_inc : Int32
  moving : Bool

  while true # game loop

    clear_screen
    print_title

    # init game
    clean_arena
    inhabit_arena
    Global.destroyed_machines = 0
    the_end = End::Not_Yet

    while true # action loop
      print_arena
      y_inc, x_inc = get_move
      if the_end == End::Not_Yet
        if y_inc != 0 || x_inc != 0
          Global.arena[Global.human_y][Global.human_x] = EMPTY
          if Global.arena[Global.human_y + y_inc][Global.human_x + x_inc] == FENCE
            the_end = End::Electrified
          elsif Global.arena[Global.human_y + y_inc][Global.human_x + x_inc] == MACHINE
            the_end = End::Killed
          else
            Global.arena[Global.human_y][Global.human_x] = EMPTY
            Global.human_y = Global.human_y + y_inc
            Global.human_x = Global.human_x + x_inc
            Global.arena[Global.human_y][Global.human_x] = HUMAN
            print_arena
            move_machines
          end
        end
      end
      if the_end != End::Not_Yet
        break
      end
    end # action loop

    puts
    case the_end
    when End::Quit
      puts "Sorry to see you quit."
    when End::Electrified
      puts "Zap! You touched the fence!"
    when End::Killed
      puts "You have been killed by a lucky machine."
    when End::Victory
      puts "You are lucky, you destroyed all machines."
    end

    if !yes?("\nDo you want to play again? ")
      break
    end
  end # game loop

  puts "\nHope you don't feel fenced in."
  puts "Try again sometime."
end

# Main {{{1
# =============================================================

set_color(DEFAULT_INK)

clear_screen
print_credits

press_enter("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions

press_enter("\nPress the Enter key to start. ")
play

Diamond

# Diamond

# Original version in BASIC:
#     Example included in Vintage BASIC 1.0.3.
#     http://www.vintage-basic.net

# This version in Crystal:
#     Copyright (c) 2023, Marcos Cruz (programandala.net)
#     SPDX-License-Identifier: Fair
#
# Written in 2023-09.
#
# Last modified: 20231104T0010+0100.

LINES = 17

(1..(LINES / 2 + 1)).each do |i|
  (1..((LINES + 1) / 2 - i + 1)).each do
    print ' '
  end
  (1..(i * 2 - 1)).each do
    print '*'
  end
  puts
end

(1..(LINES / 2)).each do |i|
  (1..(i + 1)).each do
    print ' '
  end
  (1..(((LINES + 1) / 2 - i) * 2 - 1)).each do
    print '*'
  end
  puts
end

Hammurabi

# Hammurabi
#
# Description:
#   A simple text-based simulation game set in the ancient kingdom of Sumeria.
#
# Original program:
#   Written in FOCAL on a DEP PDP-8 by Rick Merrill, 1969.
#
# BASIC port:
#   Ported from FOCAL and modified for Edusystem 70 by David Ahl, c. 1973.
#   Modified for 8K Microsoft BASIC by Peter Turnbull, c. 1978.
#
# More details:
#   - https://en.wikipedia.org/wiki/Hamurabi_(video_game)
#   - https://www.mobygames.com/game/22232/hamurabi/
#
# This improved remake in Crystal:
#   Copyright (c) 2025, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written on 2025-04-21.
#
# Last modified: 20250421T2026+0200.
#
# Acknowledgment:
#   The following Python port was used as a reference of the original
#   variables: <https://github.com/jquast/hamurabi.py>.
#

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print("\e[2J\e[H")
end

def set_color(color : Int32)
  print("\e[#{color}m")
end

# Globals {{{1
# ==============================================================================

ACRES_A_BUSHEL_CAN_SEED             =  2 # yearly
ACRES_A_PERSON_CAN_SEED             = 10 # yearly
ACRES_PER_PERSON                    = 10 # to calculate the initial acres of the city
BUSHELS_TO_FEED_A_PERSON            = 20 # yearly
IRRITATION_LEVELS                   =  5 # after the switch in `show_irritation`
IRRITATION_STEP                     = (MAX_IRRITATION / IRRITATION_LEVELS).to_i
MAX_HARVESTED_BUSHELS_PER_ACRE      = MIN_HARVESTED_BUSHELS_PER_ACRE + RANGE_OF_HARVESTED_BUSHELS_PER_ACRE - 1
MIN_HARVESTED_BUSHELS_PER_ACRE      =   17
MAX_IRRITATION                      =   16
PLAGUE_CHANCE                       = 0.15 # 15% yearly
RANGE_OF_HARVESTED_BUSHELS_PER_ACRE =   10
YEARS                               =   10 # goverment period

DEFAULT_INK      = WHITE + FOREGROUND
INPUT_INK        = BRIGHT + GREEN + FOREGROUND
INSTRUCTIONS_INK = YELLOW + FOREGROUND
RESULT_INK       = BRIGHT + CYAN + FOREGROUND
SPEECH_INK       = BRIGHT + MAGENTA + FOREGROUND
TITLE_INK        = BRIGHT + WHITE + FOREGROUND
WARNING_INK      = BRIGHT + RED + FOREGROUND

enum Result
  Very_Good
  Not_Too_Bad
  Bad
  Very_Bad
end

class Global
  class_property acres : Int32 = 0
  class_property bushels_eaten_by_rats : Int32 = 0
  class_property bushels_harvested : Int32 = 0
  class_property bushels_harvested_per_acre : Int32 = 0
  class_property bushels_in_store : Int32 = 0
  class_property bushels_to_feed_with : Int32 = 0
  class_property dead : Int32 = 0
  class_property infants : Int32 = 0
  class_property irritation : Int32 = 0 # counter (0 .. 99)
  class_property population : Int32 = 0
  class_property starved_people_percentage : Int32 = 0
  class_property total_dead : Int32 = 0
end

# User input {{{1
# ==============================================================================

# Prints the given prompt and waits until the user enters a string.
#
def input_string(prompt = "") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print(prompt)
    s = gets
  end
  set_color(DEFAULT_INK)
  return s
end

# Print the given prompt in the correspondent color, wait until the user
# enters a string and return it parsed as an `Int32`; return also an ok flag
# which is `false` if no appropriate value could be found, or if the input
# string contained more than just the number. Restore the default color.
#
def input_int(prompt : String) : Tuple(Int32, Bool)
  begin
    n = input_string(prompt).to_i
  rescue
    return 0, false
  end
  return n, true
end

def pause(prompt : String = "> ")
  input_string(prompt)
end

# Credits and instructions {{{1
# ==============================================================================

CREDITS =
  "Hammurabi

Original program:
  Written in FOCAL on a DEP PDP-8 by Rick Merrill, 1969.

BASIC port:
  Ported from FOCAL and modified for Edusystem 70 by David Ahl, c. 1973.
  Modified for 8K Microsoft BASIC by Peter Turnbull, c. 1978.

This improved remake in Crystal:
  Copyright (c) 2025, Marcos Cruz (programandala.net)
  SPDX-License-Identifier: Fair"

def print_credits
  set_color(TITLE_INK)
  print("#{CREDITS}\n")
  set_color(DEFAULT_INK)
end

def instructions : String
  return sprintf(
    "Hammurabi is a simulation game in which you, as the ruler of the ancient
kingdom of Sumeria, Hammurabi, manage the resources.

You may buy and sell land with your neighboring city-states for bushels of
grain ― the price will vary between %d and %d bushels per acre.  You also must
use grain to feed your people and as seed to plant the next year's crop.

You will quickly find that a certain number of people can only tend a certain
amount of land and that people starve if they are not fed enough.  You also
have the unexpected to contend with such as a plague, rats destroying stored
grain, and variable harvests.

You will also find that managing just the few resources in this game is not a
trivial job.  The crisis of population density rears its head very rapidly.

Try your hand at governing ancient Sumeria for a %d-year term of office.",

    MIN_HARVESTED_BUSHELS_PER_ACRE, MAX_HARVESTED_BUSHELS_PER_ACRE, YEARS)
end

def print_instructions
  set_color(INSTRUCTIONS_INK)
  print("#{instructions}\n")
  set_color(DEFAULT_INK)
end

# Game {{{1
# ==============================================================================

# Return a random number from 1 to 5 (inclusive).
#
def random_1_to_5 : Int32
  return Random.rand(5) + 1
end

# Return the proper wording for `n` persons, using the given or default words
# for singular and plural forms.
#
def persons(
  n : Int32,
  singular = "person",
  plural = "people",
) : String
  case n
  when 0; return "nobody"
  when 1; return "one " + singular
  else    return n.to_s + plural
  end
end

def ordinal_suffix(n : Int32) : String
  case n
  when 1; return "st"
  when 2; return "nd"
  when 3; return "rd"
  else    return "th"
  end
end

# Return the description of the given year as the previous one.
#
def previous(year : Int32) : String
  if year == 0
    return "the previous year"
  else
    return sprintf("your %v%v year", year, ordinal_suffix(year))
  end
end

def print_annual_report(year : Int32)
  clear_screen
  set_color(SPEECH_INK)
  print("Hammurabi, I beg to report to you.\n")
  set_color(DEFAULT_INK)

  print("\nIn #{previous(year)}, #{persons(Global.dead)} starved and #{persons(Global.infants, "infant", "infants")} #{Global.infants > 1 ? "were" : "was"} born.\n")

  Global.population += Global.infants

  if year > 0 && Random.rand <= PLAGUE_CHANCE
    Global.population = (Global.population / 2).to_i
    set_color(WARNING_INK)
    print("A horrible plague struck!  Half the people died.\n")
    set_color(DEFAULT_INK)
  end

  print("The population is #{Global.population}.\n")
  print("The city owns #{Global.acres} acres.\n")
  print("You harvested #{Global.bushels_harvested} bushels (#{Global.bushels_harvested_per_acre} per acre).\n")
  if Global.bushels_eaten_by_rats > 0
    print("The rats ate #{Global.bushels_eaten_by_rats} bushels.\n")
  end
  print("You have #{Global.bushels_in_store} bushels in store.\n")
  Global.bushels_harvested_per_acre =
    (RANGE_OF_HARVESTED_BUSHELS_PER_ACRE * Random.rand).to_i +
      MIN_HARVESTED_BUSHELS_PER_ACRE
  print("Land is trading at #{Global.bushels_harvested_per_acre} bushels per acre.\n\n")
end

def say_bye
  set_color(DEFAULT_INK)
  print("\nSo long for now.\n\n")
end

def quit_game
  say_bye
  exit(0)
end

def relinquish
  set_color(SPEECH_INK)
  print("\nHammurabi, I am deeply irritated and cannot serve you anymore.\n")
  print("Please, get yourself another steward!\n")
  set_color(DEFAULT_INK)
  quit_game
end

def increase_irritation
  Global.irritation += 1 + Random.rand(IRRITATION_STEP)
  if Global.irritation >= MAX_IRRITATION
    relinquish # this never returns
  end
end

def print_irritated(adverb : String)
  print("The steward seems #{adverb} irritated.\n")
end

def show_irritation
  case
  when Global.irritation < IRRITATION_STEP
  when Global.irritation < IRRITATION_STEP * 2; print_irritated("slightly")
  when Global.irritation < IRRITATION_STEP * 3; print_irritated("quite")
  when Global.irritation < IRRITATION_STEP * 4; print_irritated("very")
  else                                          print_irritated("profoundly")
  end
end

# Print a message begging to repeat an ununderstandable input.
#
def beg_repeat
  increase_irritation # this may never return
  set_color(SPEECH_INK)
  print("I beg your pardon?  I did not understand your order.\n")
  set_color(DEFAULT_INK)
  show_irritation
end

# Print a message begging to repeat a wrong input, because there's only `n`
# items of `name`.
#
def beg_think_again(n : Int32, name : String)
  increase_irritation # this may never return
  set_color(SPEECH_INK)
  print("I beg your pardon?  You have only #{n} #{name}.  Now then…\n")
  set_color(DEFAULT_INK)
  show_irritation
end

# Buy or sell land.
#
def trade
  acres_to_buy : Int32
  acres_to_sell : Int32
  ok : Bool

  while true
    acres_to_buy, ok = input_int("How many acres do you wish to buy? (0 to sell): ")
    if !ok || acres_to_buy < 0
      beg_repeat # this may never return
      next
    end
    if Global.bushels_harvested_per_acre * acres_to_buy <= Global.bushels_in_store
      break
    end
    beg_think_again(Global.bushels_in_store, "bushels of grain")
  end

  if acres_to_buy != 0
    print("You buy #{acres_to_buy} acres.\n")
    Global.acres += acres_to_buy
    Global.bushels_in_store -= Global.bushels_harvested_per_acre * acres_to_buy
    print("You now have #{Global.acres} acres and #{Global.bushels_in_store} bushels.\n")
  else
    while true
      acres_to_sell, ok = input_int("How many acres do you wish to sell?: ")
      if !ok || acres_to_sell < 0
        beg_repeat # this may never return
        next
      end
      if acres_to_sell < Global.acres
        break
      end
      beg_think_again(Global.acres, "acres")
    end

    if acres_to_sell > 0
      print("You sell #{acres_to_sell} acres.\n")
      Global.acres -= acres_to_sell
      Global.bushels_in_store += Global.bushels_harvested_per_acre * acres_to_sell
      print("You now have #{Global.acres} acres and #{Global.bushels_in_store} bushels.\n")
    end
  end
end

# Feed the people.
#
def feed
  ok : Bool

  while true
    Global.bushels_to_feed_with, ok = input_int("How many bushels do you wish to feed your people with?: ")
    if !ok || Global.bushels_to_feed_with < 0
      beg_repeat # this may never return
      next
    end
    # Trying to use more grain than is in silos?
    if Global.bushels_to_feed_with <= Global.bushels_in_store
      break
    end
    beg_think_again(Global.bushels_in_store, "bushels of grain")
  end

  print("You feed your people with #{Global.bushels_to_feed_with} bushels.\n")
  Global.bushels_in_store -= Global.bushels_to_feed_with
  print("You now have #{Global.bushels_in_store} bushels.\n")
end

# Seed the land.
#
def seed
  acres_to_seed : Int32
  ok : Bool

  while true
    acres_to_seed, ok = input_int("How many acres do you wish to seed?: ")
    if !ok || acres_to_seed < 0
      beg_repeat # this may never return
      next
    end
    if acres_to_seed == 0
      break
    end

    # Trying to seed more acres than you own?
    if acres_to_seed > Global.acres
      beg_think_again(Global.acres, "acres")
      next
    end

    # Enough grain for seed?
    if (acres_to_seed / ACRES_A_BUSHEL_CAN_SEED).to_i > Global.bushels_in_store
      beg_think_again(
        Global.bushels_in_store,
        sprintf(
          "bushels of grain,\nand one bushel can seed %v acres",
          ACRES_A_BUSHEL_CAN_SEED
        )
      )
      next
    end

    # Enough people to tend the crops?
    if acres_to_seed <= ACRES_A_PERSON_CAN_SEED * Global.population
      break
    end

    beg_think_again(
      Global.population,
      sprintf(
        "people to tend the fields,\nand one person can seed %v acres",
        ACRES_A_PERSON_CAN_SEED
      )
    )
  end

  bushels_used_for_seeding = (acres_to_seed / ACRES_A_BUSHEL_CAN_SEED).to_i
  print("You seed #{acres_to_seed} acres using #{bushels_used_for_seeding} bushels.\n")
  Global.bushels_in_store -= bushels_used_for_seeding
  print("You now have #{Global.bushels_in_store} bushels.\n")

  # A bountiful harvest!
  Global.bushels_harvested_per_acre = random_1_to_5
  Global.bushels_harvested = acres_to_seed * Global.bushels_harvested_per_acre
  Global.bushels_in_store += Global.bushels_harvested
end

def is_even(n : Int32) : Bool
  return n % 2 == 0
end

def check_rats
  rat_chance = random_1_to_5
  Global.bushels_eaten_by_rats = is_even(rat_chance) ? (Global.bushels_in_store / rat_chance).to_i : 0
  Global.bushels_in_store -= Global.bushels_eaten_by_rats
end

# Set the variables to their values in the first year.
#
def init
  Global.dead = 0
  Global.total_dead = 0
  Global.starved_people_percentage = 0
  Global.population = 95
  Global.infants = 5
  Global.acres = ACRES_PER_PERSON * (Global.population + Global.infants)
  Global.bushels_harvested_per_acre = 3
  Global.bushels_harvested = Global.acres * Global.bushels_harvested_per_acre
  Global.bushels_eaten_by_rats = 200
  Global.bushels_in_store = Global.bushels_harvested - Global.bushels_eaten_by_rats
  Global.irritation = 0
end

def print_result(result : Result)
  set_color(RESULT_INK)

  case result
  when Result::Very_Good
    print("A fantastic performance!  Charlemagne, Disraeli and Jefferson combined could\n")
    print("not have done better!\n")
  when Result::Not_Too_Bad
    print("Your performance could have been somewat better, but really wasn't too bad at\n")
    print("all. #{((Global.population).to_f * 0.8 * Random.rand).to_i} people would dearly like to see you assassinated, but we all have our\n")
    print("trivial problems.\n")
  when Result::Bad
    print("Your heavy-handed performance smacks of Nero and Ivan IV.  The people\n")
    print("(remaining) find you an unpleasant ruler and, frankly, hate your guts!\n")
  when Result::Very_Bad
    print("Due to this extreme mismanagement you have not only been impeached and thrown\n")
    print("out of office but you have also been declared national fink!!!\n")
  end

  set_color(DEFAULT_INK)
end

def print_final_report
  clear_screen

  if Global.starved_people_percentage > 0
    print("In your #{YEARS}-year term of office, #{Global.starved_people_percentage} percent of the\n")
    print("population starved per year on the average, i.e., a total of #{Global.total_dead} people died!\n\n")
  end

  acres_per_person = Global.acres / Global.population
  print("You started with #{ACRES_PER_PERSON} acres per person and ended with #{acres_per_person}.\n\n")

  case
  when Global.starved_people_percentage > 33, acres_per_person < 7; print_result(Result::Very_Bad)
  when Global.starved_people_percentage > 10, acres_per_person < 9; print_result(Result::Bad)
  when Global.starved_people_percentage > 3, acres_per_person < 10; print_result(Result::Not_Too_Bad)
  else                                                              print_result(Result::Very_Good)
  end
end

def check_starvation(year : Int32)
  # How many people has been fed?
  fed_people = (Global.bushels_to_feed_with / BUSHELS_TO_FEED_A_PERSON).to_i

  if Global.population > fed_people
    Global.dead = Global.population - fed_people
    Global.starved_people_percentage = (((year - 1) * Global.starved_people_percentage + Global.dead * 100 / Global.population) / year).to_i
    Global.population -= Global.dead
    Global.total_dead += Global.dead

    # Starve enough for impeachment?
    if Global.dead > (0.45 * (Global.population).to_f).to_i
      set_color(WARNING_INK)
      print("\nYou starved #{Global.dead} people in one year!!!\n\n")
      set_color(DEFAULT_INK)
      print_result(Result::Very_Bad)
      quit_game
    end
  end
end

def govern
  init

  print_annual_report(0)

  (1..YEARS).each do |year|
    trade
    feed
    seed
    check_rats

    # Let's have some babies
    Global.infants = (random_1_to_5 * (20 * Global.acres + Global.bushels_in_store) / Global.population / 100 + 1).to_i

    check_starvation(year)

    pause("\nPress the Enter key to read the annual report. ")
    print_annual_report(year)
  end
end

# Main {{{1
# ==============================================================================

clear_screen
print_credits

pause("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions

pause("\nPress the Enter key to start. ")
govern

pause("Press the Enter key to read the final report. ")
print_final_report
say_bye

High Noon

# High Noon

# Original version in BASIC:
#   Designed and programmed by Chris Gaylo, Syosset High School, New York, 1970-09-12.
#   http://mybitbox.com/highnoon-1970/
#   http://mybitbox.com/highnoon/
#
# Transcriptions:
#   https://github.com/MrMethor/Highnoon-BASIC/
#   https://github.com/mad4j/basic-highnoon/
#
# Version modified for QB64:
#   By Daniele Olmisani, 2014.
#   https://github.com/mad4j/basic-highnoon/

# This improved remake in Crystal:
#   Copyright (c) 2024, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2024-11-27/28.
#
# Last modified: 20250421T0020+0200.

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print "\e[2J"
  home
end

def set_color(color : Int)
  print "\e[#{color}m"
end

def set_attribute(attr : Int)
  print "\e[0;#{attr}m"
end

# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int)
  print "\e[#{line};#{column}H"
end

def erase_line_right
  print "\e[K"
end

def hide_cursor
  print "\e[?25l"
end

def show_cursor
  print "\e[?25h"
end

# Globals {{{1
# =============================================================

DEFAULT_INK      = FOREGROUND + WHITE
INPUT_INK        = FOREGROUND + BRIGHT + GREEN
INSTRUCTIONS_INK = FOREGROUND + YELLOW
TITLE_INK        = FOREGROUND + BRIGHT + RED

INITIAL_DISTANCE     = 100
INITIAL_BULLETS      =   4
MAX_WATERING_TROUGHS =   3

class Global
  class_property distance : Int32 = INITIAL_DISTANCE # distance between both gunners, in paces
  class_property strategy : String = ""              # player's strategy
  class_property player_bullets : Int32 = INITIAL_BULLETS
  class_property opponent_bullets : Int32 = INITIAL_BULLETS
end

# User input {{{1
# =============================================================

# Prints the given prompt and waits until the user enters an integer; if the
# user input is not a valid integer, returns 0 instead.
def input_int(prompt : String = "") : Int
  set_color(INPUT_INK)
  print prompt
  number = gets.not_nil!.to_i rescue 0
  set_color(DEFAULT_INK)
  return number
end

# Prints the given prompt and waits until the user enters a string.
def input_string(prompt = "") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  set_color(DEFAULT_INK)
  s
end

# Prints the given prompt and waits until the user presses Enter.
def press_enter(prompt : String)
  input_string(prompt)
end

# Returns `true` if the given string is "yes" or a synonym.
def is_yes?(s : String) : Bool
  return s.downcase.in?(["ok", "y", "yeah", "yes"])
end

# Returns `true` if the given string is "no" or a synonym.
def is_no?(s : String) : Bool
  return s.downcase.in?(["n", "no", "nope"])
end

# Prints the given prompt, waits until the user enters a valid yes/no
# string, and returns `true` for "yes" or `false` for "no".
def yes?(prompt : String) : Bool
  while true
    answer = input_string(prompt)
    if is_yes?(answer)
      return true
    end
    if is_no?(answer)
      return false
    end
  end
end

# Title, instructions and credits {{{1
# =============================================================

# Print the title at the current cursor position.
def print_title
  set_color(TITLE_INK)
  puts "High Noon"
  set_color(DEFAULT_INK)
end

def print_credits
  print_title
  puts "\nOriginal version in BASIC:"
  puts "    Designed and programmend by Chris Gaylo, 1970."
  puts "    http://mybitbox.com/highnoon-1970/"
  puts "    http://mybitbox.com/highnoon/"
  puts "Transcriptions:"
  puts "    https://github.com/MrMethor/Highnoon-BASIC/"
  puts "    https://github.com/mad4j/basic-highnoon/"
  puts "Version modified for QB64:"
  puts "    By Daniele Olmisani, 2014."
  puts "    https://github.com/mad4j/basic-highnoon/"
  puts "This improved remake in Crystal:"
  puts "    Copyright (c) 2024, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair"
end

def print_instructions
  print_title
  set_color(INSTRUCTIONS_INK)
  puts "\nYou have been challenged to a showdown by Black Bart, one of"
  puts "the meanest desperadoes west of the Allegheny mountains."
  puts "\nWhile you are walking down a dusty, deserted side street,"
  puts "Black Bart emerges from a saloon one hundred paces away."
  puts "\nBy agreement, you each have #{INITIAL_BULLETS} bullets in your six-guns."
  puts "Your marksmanship equals his. At the start of the walk nei-"
  puts "ther of you can possibly hit the other, and at the end of"
  puts "the walk, neither can miss. the closer you get, the better"
  puts "your chances of hitting black Bart, but he also has beter"
  puts "chances of hitting you."
  set_color(DEFAULT_INK)
end

# Game loop {{{1
# =============================================================

def plural_suffix(n : Int) : String
  case n
  when 1; return ""
  else    return "s"
  end
end

def print_shells_left
  if Global.player_bullets == Global.opponent_bullets
    puts "Both of you have #{Global.player_bullets} bullets."
  else
    print "You now have #{Global.player_bullets} bullet#{plural_suffix(Global.player_bullets)} "
    puts "to Black Bart's #{Global.opponent_bullets} bullet#{plural_suffix(Global.opponent_bullets)}."
  end
end

def print_check
  puts "******************************************************"
  puts "*                                                    *"
  puts "*                 BANK OF DODGE CITY                 *"
  puts "*                  CASHIER'S RECEIT                  *"
  puts "*                                                    *"
  puts "* CHECK NO. #{rand(1000..9999)}                   AUGUST #{10 + rand(10)}TH, 1889 *"
  puts "*                                                    *"
  puts "*                                                    *"
  puts "*       PAY TO THE BEARER ON DEMAND THE SUM OF       *"
  puts "*                                                    *"
  puts "* TWENTY THOUSAND DOLLARS-------------------$20,000  *"
  puts "*                                                    *"
  puts "******************************************************"
end

def get_reward
  puts "As mayor of Dodge City, and on behalf of its citizens,"
  puts "I extend to you our thanks, and present you with this"
  puts "reward, a check for $20,000, for killing Black Bart."
  puts
  puts
  print_check
  puts "\n\nDon't spend it all in one place."
end

def move_the_opponent
  paces = 2 + rand(8)
  puts "Black Bart moves #{paces} paces."
  Global.distance -= paces
end

# Maybe move the opponent; if so, return `true`, otherwise return `false`.
# A true `silent` flag allows to omit the message when the opponent doesn't move.
def maybe_move_the_opponent(silent = false) : Bool
  if rand(2) == 0 # 50% chances
    move_the_opponent()
    return true
  else
    if !silent
      puts "Black Bart stands still."
    end
    return false
  end
end

def missed_shot : Bool
  # XXX TODO check
  return rand(10) <= Global.distance // 10
end

# Handle the opponent's shot and return a flag with the result: if the
# opponent kills the player, return `true`; otherwise return `false`.
def the_opponent_fires_and_kills : Bool
  puts "Black Bart fires…"
  Global.opponent_bullets -= 1
  if missed_shot()
    puts "A miss…"
    case Global.opponent_bullets
    when 3
      puts "Whew, were you lucky. That bullet just missed your head."
    when 2
      puts "But Black Bart got you in the right shin."
    when 1
      puts "Though Black Bart got you on the left side of your jaw."
    when 0
      puts "Black Bart must have jerked the trigger."
    end
  else
    if Global.strategy == "j"
      puts "That trick just saved yout life. Black Bart's bullet"
      puts "was stopped by the wood sides of the trough."
    else
      puts "Black Bart shot you right through the heart that time."
      puts "You went kickin' with your boots on."
      return true
    end
  end
  return false
end

# Handle the opponent's strategy and return a flag with the result: if the
# opponent runs or kills the player, return `true`; otherwise return `false`.
def the_opponent_kills_or_runs : Bool
  if Global.distance >= 10 || Global.player_bullets == 0
    if maybe_move_the_opponent(silent = true)
      return false
    end
  end
  if Global.opponent_bullets > 0
    return the_opponent_fires_and_kills()
  else
    if Global.player_bullets > 0
      if rand(2) == 0 # 50% chances
        puts "Now is your chance, Black Bart is out of bullets."
      else
        puts "Black Bart just hi-tailed it out of town rather than face you"
        puts "without a loaded gun. You can rest assured that Black Bart"
        puts "won't ever show his face around this town again."
        return true
      end
    end
  end
  return false
end

def play
  watering_troughs = 0

  while true # showdown loop

    puts "You are now #{Global.distance} paces apart from Black Bart."
    print_shells_left()
    set_color(INSTRUCTIONS_INK)
    puts "\nStrategies:"
    puts "  [A]dvance"
    puts "  [S]tand still"
    puts "  [F]ire"
    puts "  [J]ump behind the watering trough"
    puts "  [G]ive up"
    puts "  [T]urn tail and run"
    set_color(DEFAULT_INK)

    Global.strategy = input_string("\nWhat is your strategy? ").upcase

    case Global.strategy
    when "A" # advance

      while true
        paces = input_int("How many paces do you advance? ")
        if paces < 0
          puts "None of this negative stuff, partner, only positive numbers."
        elsif paces > 10
          puts "Nobody can walk that fast."
        else
          Global.distance -= paces
          break
        end
      end
    when "S" # stand still

      puts "That move made you a perfect stationary target."
    when "F" # fire

      if Global.player_bullets == 0
        puts "You don't have any bullets left."
      else
        Global.player_bullets -= 1
        if missed_shot()
          case Global.player_bullets
          when 2
            puts "Grazed Black Bart in the right arm."
          when 1
            puts "He's hit in the left shoulder, forcing him to use his right"
            puts "hand to shoot with."
          end
          puts "What a lousy shot."
          if Global.player_bullets == 0
            puts "Nice going, ace, you've run out of bullets."
            if Global.opponent_bullets != 0
              puts "Now Black Bart won't shoot until you touch noses."
              puts "You better think of something fast (like run)."
            end
          end
        else
          puts "What a shot, you got Black Bart right between the eyes."
          press_enter("\nPress the Enter key to get your reward. ")
          clear_screen
          get_reward
          break
        end
      end
    when "J" # jump

      if watering_troughs == MAX_WATERING_TROUGHS
        puts "How many watering troughs do you think are on this street?"
        Global.strategy = ""
      else
        watering_troughs += 1
        puts "You jump behind the watering trough."
        puts "Not a bad maneuver to threw Black Bart's strategy off."
      end
    when "G" # give up

      puts "Black Bart accepts. The conditions are that he won't shoot you"
      puts "if you take the first stage out of town and never come back."
      if yes?("Agreed? ")
        puts "A very wise decision."
        break
      else
        puts "Oh well, back to the showdown."
      end
    when "T" # turn tail and run

      # The more bullets of the opponent, the less chances to escape.
      if rand(Global.opponent_bullets + 2) == 0 # XXX TODO check rand.int_max
        puts "Man, you ran so fast even dogs couldn't catch you."
      else
        case Global.opponent_bullets
        when 0
          puts "You were lucky, Black Bart can only throw his gun at you, he"
          puts "doesn't have any bullets left. You should really be dead."
        when 1
          puts "Black Bart fires his last bullet…"
          puts "He got you right in the back. That's what you deserve, for running."
        when 2
          puts "Black Bart fires and got you twice: in your back"
          puts "and your ass. Now you can't even rest in peace."
        when 3
          puts "Black Bart unloads his gun, once in your back"
          puts "and twice in your ass. Now you can't even rest in peace."
        when 4
          puts "Black Bart unloads his gun, once in your back"
          puts "and three times in your ass. Now you can't even rest in peace."
        end
        Global.opponent_bullets = 0
      end
      break
    else
      puts "You sure aren't going to live very long if you can't even follow directions."
      puts
    end # strategy case

    if the_opponent_kills_or_runs()
      break
    end

    if Global.player_bullets + Global.opponent_bullets == 0
      puts "The showdown must end, because nobody has bullets left."
      break
    end

    puts
  end # showdown loop

end

# Main {{{1
# =============================================================

clear_screen
print_credits

press_enter("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions

press_enter("\nPress the Enter key to start. ")
clear_screen
play

Math

# Math

# Original version in BASIC:
#     Example included in Vintage BASIC 1.0.3.
#     http://www.vintage-basic.net

# This version in Crystal:
#     Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
#     SPDX-License-Identifier: Fair
#
# Written in 2023-09, 2025-05.
#
# Last modified: 20250513T0055+0200.

while true
  puts "Enter a number: "
  begin
    n = gets.not_nil!.to_f
    break
  rescue
    puts "That wasn't a valid number."
  end
end

puts "ABS(#{n}) --> #{n}.abs --> #{n.abs}"
puts "ATN(#{n}) --> Math.atan(#{n}) --> #{Math.atan(n)}"
puts "COS(#{n}) --> Math.cos(#{n}) --> #{Math.cos(n)}"
puts "EXP(#{n}) --> Math.exp(#{n}) --> #{Math.exp(n)}"
puts "INT(#{n}) --> #{n}.to_i --> #{n.to_i}"
puts "LOG(#{n}) --> Math.log(#{n}) --> #{Math.log(n)}"
puts "SGN(#{n}) --> #{n}.sign --> #{n.sign}"
puts "SQR(#{n}) --> Math.sqrt(#{n}) --> #{Math.sqrt(n)}"
puts "TAN(#{n}) --> Math.tan(#{n}) --> #{Math.tan(n)}"

Mugwump

# Mugwump

# Original version in BASIC:
#   Written by Bud Valenti's students of Project SOLO (Pittsburg, Pennsylvania, USA).
#   Slightly modified by Bob Albrecht of People's Computer Company.
#   Published by Creative Computing (Morristown, New Jersey, USA), 1978.
#   - https://www.atariarchives.org/basicgames/showpage.php?page=114
#   - http://vintage-basic.net/games.html

# This version in Crystal:
#   Copyright (c) 2023, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2023-09, 2023-10, 2023-11.
#
# Last modified: 20231104T0057+0100.

GRID_SIZE = 10
TURNS     = 10
MUGWUMPS  =  4

class Mugwump
  property hidden : Bool
  property x : Int32
  property y : Int32

  def initialize
    @hidden = true
    @x = rand(GRID_SIZE)
    @y = rand(GRID_SIZE)
  end

  def init
    initialize
  end

  def reveal
    @hidden = false
  end
end

# Moves the cursor to the top left position of the terminal.
def home
  print "\e[H"
end

# Clears the terminal and moves the cursor to the top left position.
def clear
  print "\e[2J"
  home
end

# Prints the given prompt, waits until the user enters a valid integer and
# returns it.
def get_number(prompt : String) : Int
  while true
    print prompt
    begin
      n = gets.not_nil!.to_i
      break
    rescue
      puts "Invalid number."
    end
  end
  n
end

# Prompts the user to enter a command and returns it.
def command(prompt = "> ") : String
  s = nil
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  s
end

# Prints the given prompt and waits until the user enters an empty string.
def press_enter(prompt : String)
  until command(prompt) == ""
  end
end

# Returns `true` if the given string is "yes" or a synonym.
def is_yes?(answer : String) : Bool
  ["ok", "yeah", "yes", "y"].any? { |yes| answer.downcase == yes }
end

# Returns `true` if the given string is "no" or a synonym.
def is_no?(answer : String) : Bool
  ["no", "nope", "n"].any? { |no| answer.downcase == no }
end

# Prints the given prompt, waits until the user enters a valid yes/no
# string, and returns `true` for "yes" or `false` for "no".
def yes?(prompt : String) : Bool
  answer = ""
  while !(is_yes?(answer) || is_no?(answer))
    answer = command(prompt)
  end
  is_yes?(answer)
end

# Clears the screen, prints the credits and asks the user to press enter.
def print_credits
  clear
  puts "Mugwump\n"
  puts "Original version in BASIC:"
  puts "    Written by Bud Valenti's students of Project SOLO (Pittsburg, Pennsylvania, USA)."
  puts "    Slightly modified by Bob Albrecht of People's Computer Company."
  puts "    Published by Creative Computing (Morristown, New Jersey, USA), 1978."
  puts "    - https://www.atariarchives.org/basicgames/showpage.php?page=114"
  puts "    - http://vintage-basic.net/games.html\n"
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair\n"
  press_enter("Press Enter to read the instructions. ")
end

# Clears the screen, prints the instructions and asks the user to press enter.
def print_instructions
  clear
  puts "Mugwump\n"
  puts "The object of this game is to find four mugwumps"
  puts "hidden on a 10 by 10 grid.  Homebase is position 0,0."
  puts "Any guess you make must be two numbers with each"
  puts "number between 0 and 9, inclusive.  First number"
  puts "is distance to right of homebase and second number"
  puts "is distance above homebase.\n"
  puts "You get #{TURNS} tries.  After each try, you will see"
  puts "how far you are from each mugwump.\n"
  press_enter("Press Enter to start. ")
end

# Prints the given prompt, then waits until the user enters a valid coord and
# returns it.
def get_coord(prompt : String) : Int
  while true
    coord = get_number(prompt)
    if coord < 0 || coord >= GRID_SIZE
      puts "Invalid value #{coord}: not in range [0, #{GRID_SIZE - 1}]."
    else
      break
    end
  end
  coord
end

# Returns `true` if the given mugwump is hidden in the given coords.
def is_here?(m : Mugwump, x : Int, y : Int) : Bool
  m.hidden && m.x == x && m.y == y
end

# Returns the distance between the given mugwump and the given coords.
def distance(m : Mugwump, x : Int, y : Int) : Int
  Math.sqrt((m.x - x) ** 2 + (m.y - y) ** 2).to_i
end

# If the given number is greater than 1, this method returns a plural ending
# (default: "s"); otherwise it returns a singular ending (default: an empty
# string).
def plural(n : Int, plural = "s", singular = "") : String
  n > 1 ? plural : singular
end

# Prints the mugwumps found in the given coordinates and returns the count.
def found(mugwump : Array(Mugwump), x, y) : Int
  found = 0
  (0...MUGWUMPS).each do |m|
    if is_here?(mugwump[m], x, y)
      mugwump[m].reveal
      found += 1
      puts "You have found mugwump #{m}!"
    end
  end
  found
end

# Runs the game.
def play
  found : Int32 # counter
  turn : Int32  # counter
  mugwump = [] of Mugwump
  (0...MUGWUMPS).each do |m|
    mugwump << Mugwump.new
  end
  while true # game
    clear
    (0...MUGWUMPS).each do |m|
      mugwump[m].init
    end
    found = 0
    turn = 1
    while turn < TURNS
      puts "Turn number #{turn}\n"
      puts "What is your guess (in range [0, #{GRID_SIZE - 1}])?"
      x = get_coord("Distance right of homebase (x-axis): ")
      y = get_coord("Distance above homebase (y-axis): ")
      puts "\nYour guess is (#{x}, #{y})."
      found += found(mugwump, x, y)
      if found == MUGWUMPS
        break # turns
      else
        (0...MUGWUMPS).each do |m|
          if mugwump[m].hidden
            puts "You are #{distance(mugwump[m], x, y)} units from mugwump #{m}."
          end
        end
        puts
      end
      turn += 1
    end # turns
    if found == MUGWUMPS
      puts "\nYou got them all in #{turn} turn#{plural(turn)}!\n"
      puts "That was fun! let's play again…"
      puts "Four more mugwumps are now in hiding."
    else
      puts "\nSorry, that's #{TURNS} tr#{plural(TURNS, "ies", "y")}.\n"
      puts "Here is where they're hiding:"
      (0...MUGWUMPS).each do |m|
        if mugwump[m].hidden
          puts "Mugwump #{m} is at (#{mugwump[m].x}, #{mugwump[m].y})."
        end
      end
    end
    puts
    if !yes?("Do you want to play again? ")
      break # game
    end
  end # game
end

print_credits
print_instructions
play

Name

# Name

# Original version in BASIC:
#     Example included in Vintage BASIC 1.0.3.
#     http://www.vintage-basic.net

# This version in Crystal:
#     Copyright (c) 2023, Marcos Cruz (programandala.net)
#     SPDX-License-Identifier: Fair
#
# Written on 2023-09-30.
#
# Last modified: 20231104T0010+0100.

def getn(prompt : String) : Float64
  while true
    print prompt
    begin
      number = gets.not_nil!.to_f
      break
    rescue
      puts "Number expected."
    end
  end
  number
end

print "What is your name? "
name = gets.not_nil!

n = getn "Enter a number: "

n.to_i.times do
  puts "Hello, #{name}!"
end

Poetry

# Poetry

# Original version in BASIC:
#   Unknown author.
#   Modified and reworked by Jim Bailey, Peggy Ewing, and Dave Ahl at DEC.
#   Published in "BASIC Computer Games", Creative Computing (Morristown, New Jersey, USA), 1978.
#   https://archive.org/details/Basic_Computer_Games_Microcomputer_Edition_1978_Creative_Computing
#   https://github.com/chaosotter/basic-games/tree/master/games/BASIC%20Computer%20Games/Poetry
#   http://vintage-basic.net/games.html

# This improved remake in Crystal:
#   Copyright (c) 2024, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written on 2024-11-26, 2024-12-12.
#
# Last modified: 20250421T0020+0200.

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print "\e[2J"
  home
end

def set_color(color : Int)
  print "\e[#{color}m"
end

def set_attribute(attr : Int)
  print "\e[0;#{attr}m"
end

DEFAULT_INK = FOREGROUND + WHITE
INPUT_INK   = FOREGROUND + BRIGHT + GREEN
TITLE_INK   = FOREGROUND + BRIGHT + RED

# User input {{{1
# =============================================================

# Prompts the user to enter a command and returns it.
def input_string(prompt = "> ") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  set_color(DEFAULT_INK)
  s
end

# Prints the given prompt and wait until the user presses Enter.
def press_enter(prompt : String)
  input_string(prompt)
end

# Title and credits {{{1
# =============================================================

# Prints the title at the current cursor position.
def print_title
  set_color(TITLE_INK)
  puts "Poetry"
  set_color(DEFAULT_INK)
end

# Prints the credits at the current cursor position.
def print_credits
  print_title
  puts
  puts "Original version in BASIC:"
  puts "    Unknown author."
  puts "    Published in \"BASIC Computer Games\","
  puts "    Creative Computing (Morristown, New Jersey, USA), 1978."
  puts
  puts "This improved remake in Crystal:"
  puts "    Copyright (c) 2024, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair"
end

# Main {{{1
# =============================================================

# Is the given integer even?
def is_even(n : Int) : Bool
  return n % 2 == 0
end

MAX_PHRASES_AND_VERSES = 20

def play
  action = 0
  phrase = 0
  phrases_and_verses = 0
  verse_chunks = 0

  while true
    manage_the_verse_continuation = true
    maybe_add_comma = true

    case action
    when 0, 1
      case phrase
      when 0; print "MIDNIGHT DREARY"
      when 1; print "FIERY EYES"
      when 2; print "BIRD OR FIEND"
      when 3; print "THING OF EVIL"
      when 4; print "PROPHET"
      end
    when 2
      case phrase
      when 0; print "BEGUILING ME"; verse_chunks = 2
      when 1; print "THRILLED ME"
      when 2; print "STILL SITTING…"; maybe_add_comma = false
      when 3; print "NEVER FLITTING"; verse_chunks = 2
      when 4; print "BURNED"
      end
    when 3
      case phrase
      when 0; print "AND MY SOUL"
      when 1; print "DARKNESS THERE"
      when 2; print "SHALL BE LIFTED"
      when 3; print "QUOTH THE RAVEN"
      when 4
        if verse_chunks != 0
          print "SIGN OF PARTING"
        end
      end
    when 4
      case phrase
      when 0; print "NOTHING MORE"
      when 1; print "YET AGAIN"
      when 2; print "SLOWLY CREEPING"
      when 3; print "…EVERMORE"
      when 4; print "NEVERMORE"
      end
    when 5
      action = 0
      puts
      if phrases_and_verses > MAX_PHRASES_AND_VERSES
        puts
        verse_chunks = 0
        phrases_and_verses = 0
        action = 2
        next
      else
        manage_the_verse_continuation = false
      end
    end

    if manage_the_verse_continuation
      sleep 250.milliseconds

      if maybe_add_comma && !(verse_chunks == 0 || rand > 0.19)
        print ","
        verse_chunks = 2
      end

      if rand > 0.65
        puts
        verse_chunks = 0
      else
        print " "
        verse_chunks += 1
      end
    end

    action += 1
    phrase = rand(5)
    phrases_and_verses += 1

    if !(verse_chunks > 0 || is_even(action))
      print "     "
    end
  end
end

clear_screen
print_credits
press_enter("\nPress the Enter key to start. ")
clear_screen
play

Russian Roulette

# Russian Roulette

# Original version in BASIC:
#   Creative Computing (Morristown, New Jersey, USA), ca. 1980.

# This version in Crystal:
#   Copyright (c) 2023, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2023-09, 2023-10.
#
# Last modified: 20231104T0010+0100.

# Clears the terminal and moves the cursor to the top left position.
def clear_screen
  print "\e[2J\e[H"
end

# Prompts the user to enter a command and returns it.
def command(prompt = "> ") : String
  print prompt
  gets.not_nil!
end

def press_enter_to_start
  command("Press Enter to start. ")
end

def print_credits
  clear_screen
  puts "Russian Roulette\n"
  puts "Original version in BASIC:"
  puts "    Creative Computing (Morristown, New Jersey, USA), ca. 1980.\n"
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair\n"
  press_enter_to_start
end

def print_instructions
  clear_screen
  puts "Here is a revolver."
  puts "Type 'f' to spin chamber and pull trigger."
  puts "Type 'g' to give up, and play again."
  puts "Type 'q' to quit.\n"
end

def play? : Bool
  while true # game loop
    print_instructions
    times = 0
    while true # play loop
      case command
      when "f" # fire
        if rand(100) > 83
          puts "Bang! You're dead!"
          puts "Condolences will be sent to your relatives."
          break
        else
          times += 1
          if times > 10
            puts "You win!"
            puts "Let someone else blow his brains out."
            break
          else
            puts "Click."
          end
        end
      when "g" # give up
        puts "Chicken!"
        break
      when "q" # quit
        return false
      end # case
    end   # play loop
    press_enter_to_start
  end  # game loop
  true # play again, do not quit
end

print_credits
while play?
end
puts "Bye!"

Seance

# Seance

# Original version in BASIC:
#   By Chris Oxlade, 1983.
#   https://archive.org/details/seance.qb64
#   https://github.com/chaosotter/basic-games

# This version in Crystal:
#   Copyright (c) 2024, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written on 2024-11-27.
#
# Last modified: 20250421T0020+0200.

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print "\e[2J"
  home
end

def set_color(color : Int)
  print "\e[#{color}m"
end

def set_attribute(attr : Int)
  print "\e[0;#{attr}m"
end

# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int)
  print "\e[#{line};#{column}H"
end

def erase_line_right
  print "\e[K"
end

def hide_cursor
  print "\e[?25l"
end

def show_cursor
  print "\e[?25h"
end

# Input {{{1
# ==============================================================================

# Prints the given prompt and waits until the user enters a string.
def input(prompt = "") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  set_color(DEFAULT_INK)
  s
end

# Prints the given prompt and waits until the user presses Enter.
def press_enter(prompt : String)
  input(prompt)
end

# Globals {{{1
# ==============================================================================

MAX_SCORE = 50

MAX_MESSAGE_LENGTH = 6
MIN_MESSAGE_LENGTH = 3

BASE_CHARACTER = '@'.ord
PLANCHETTE     = '*'

FIRST_LETTER_NUMBER =  1
LAST_LETTER_NUMBER  = 26

BOARD_INK          = FOREGROUND + BRIGHT + CYAN
DEFAULT_INK        = FOREGROUND + WHITE
INPUT_INK          = FOREGROUND + BRIGHT + GREEN
INSTRUCTIONS_INK   = FOREGROUND + YELLOW
MISTAKE_EFFECT_INK = FOREGROUND + BRIGHT + RED
PLANCHETTE_INK     = FOREGROUND + YELLOW
TITLE_INK          = FOREGROUND + BRIGHT + RED

INPUT_X = BOARD_X
INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4

MESSAGES_Y = INPUT_Y

MISTAKE_EFFECT_PAUSE = 3 # seconds

BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD # screen columns
BOARD_BOTTOM_Y     = BOARD_HEIGHT + 1            # relative to the board
BOARD_HEIGHT       =  5                          # characters displayed on the left and right borders
BOARD_PAD          =  1                          # blank characters separating the board from its left and right borders
BOARD_WIDTH        =  8                          # characters displayed on the top and bottom borders
BOARD_X            = 29                          # screen column
BOARD_Y            =  5                          # screen line

TITLE = "Seance"

# Output {{{1
# ==============================================================================

# Returns the x coordinate to print the given text centered on the board.
def board_centered_x(text : String) : Int
  return (BOARD_X + (BOARD_ACTUAL_WIDTH - text.size) // 2)
end

# Prints the given text on the given row, centered on the board.
def print_board_centered(text : String, y : Int)
  set_cursor_position(y, board_centered_x(text))
  puts text
end

# Prints the title at the current cursor position.
def print_title
  set_color(TITLE_INK)
  puts TITLE
  set_color(DEFAULT_INK)
end

# Prints the title on the given row, centered on the board.
def print_board_centered_title(y : Int)
  set_color(TITLE_INK)
  print_board_centered(TITLE, y)
  set_color(DEFAULT_INK)
end

# Information {{{1
# ==============================================================================

def print_credits
  print_title
  puts
  puts "Original version in BASIC:"
  puts "    Written by Chris Oxlade, 1983."
  puts "    https://archive.org/details/seance.qb64"
  puts "    https://github.com/chaosotter/basic-games"
  puts
  puts "This version in Crystal:"
  puts "    Copyright (c) 2024, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair"
end

def print_instructions
  print_title
  set_color(INSTRUCTIONS_INK)
  puts "\nMessages from the Spirits are coming through, letter by letter.  They want you"
  puts "to remember the letters and type them into the computer in the correct order."
  puts "If you make mistakes, they will be angry ― very angry…"
  puts
  puts "Watch for stars on your screen ― they show the letters in the Spirits'"
  puts "messages."
  set_color(DEFAULT_INK)
end

# Board {{{1
# ==============================================================================

# Prints the given letter at the given board coordinates.
def print_character(y, x : Int, a : Char)
  set_cursor_position(y + BOARD_Y, x + BOARD_X)
  print a
end

def print_board
  set_color(BOARD_INK)
  (1..BOARD_WIDTH).each do |i|
    print_character(0, i + 1, (BASE_CHARACTER + i).chr)                                                      # top border
    print_character(BOARD_BOTTOM_Y, i + 1, (BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1).chr) # bottom border
  end
  (1..BOARD_HEIGHT).each do |i|
    print_character(i, 0, (BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1).chr)    # left border
    print_character(i, 3 + BOARD_WIDTH, (BASE_CHARACTER + BOARD_WIDTH + i).chr) # right border
  end
  puts
  set_color(DEFAULT_INK)
end

# Erases the given line to the right of the given column.
def erase_line_right_from(line, column : Int)
  set_cursor_position(line, column)
  erase_line_right
end

# Prints the given mistake effect, waits a configured number of seconds and finally erases it.
def print_mistake_effect(effect : String)
  x = board_centered_x(effect)
  hide_cursor
  set_cursor_position(MESSAGES_Y, x)
  set_color(MISTAKE_EFFECT_INK)
  print effect, "\n"
  set_color(DEFAULT_INK)
  sleep MISTAKE_EFFECT_PAUSE.seconds
  erase_line_right_from(MESSAGES_Y, x)
  show_cursor
end

# Returns a new message of the given length, after marking its letters on the board.
def message(length : Int) : String
  y = 0
  x = 0
  message = ""
  hide_cursor
  (1..length).each do |i|
    letter_number = rand(FIRST_LETTER_NUMBER..LAST_LETTER_NUMBER)
    letter = (BASE_CHARACTER + letter_number).chr
    message = "#{message}#{letter}"
    case
    when letter_number <= BOARD_WIDTH
      # top border
      y = 1
      x = letter_number + 1
    when letter_number <= BOARD_WIDTH + BOARD_HEIGHT
      # right border
      y = letter_number - BOARD_WIDTH
      x = 2 + BOARD_WIDTH
    when letter_number <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH
      # bottom border
      y = BOARD_BOTTOM_Y - 1
      x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letter_number
    else
      # left border
      y = 1 + LAST_LETTER_NUMBER - letter_number
      x = 1
    end
    set_color(PLANCHETTE_INK)
    print_character(y, x, PLANCHETTE)
    set_color(DEFAULT_INK)
    sleep 1.seconds
    print_character(y, x, ' ')
  end
  show_cursor
  return message
end

# Main {{{1
# ==============================================================================

# Accepts a string from the user, erases it from the screen and returns it.
def message_understood : String
  set_cursor_position(INPUT_Y, INPUT_X)
  erase_line_right_from(INPUT_Y, INPUT_X)
  return input("? ").upcase
end

def play
  score = 0
  mistakes = 0
  print_board_centered_title(1)
  print_board

  while true
    message_length = rand(MIN_MESSAGE_LENGTH..MAX_MESSAGE_LENGTH)
    if message(message_length) != message_understood
      mistakes += 1
      case mistakes
      when 1
        print_mistake_effect("The table begins to shake!")
      when 2
        print_mistake_effect("The light bulb shatters!")
      when 3
        print_mistake_effect("Oh, no!  A pair of clammy hands grasps your neck!")
        return
      end
    else
      score += message_length
      if score >= MAX_SCORE
        print_board_centered("Whew!  The spirits have gone!", MESSAGES_Y)
        print_board_centered("You live to face another day!", MESSAGES_Y + 1)
        return
      end
    end
  end
end

set_color(DEFAULT_INK)
clear_screen
print_credits

press_enter("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions

press_enter("\nPress the Enter key to start. ")
clear_screen
play
puts
puts

Sine Wave

# Sine Wave

# Original version in BASIC:
#     Creative Computing (Morristown, New Jersey, USA), ca. 1980.

# This version in Crystal:
#     Copyright (c) 2023, Marcos Cruz (programandala.net)
#     SPDX-License-Identifier: Fair
#
# Written in 2023-09, 2023-11.
#
# Last modified: 20241213T2300+0100.

def clear_screen
  print "\e[0;0H\e[2J"
end

def print_credits
  puts "Sine Wave"
  puts
  puts "Original version in BASIC:"
  puts "    Creative computing (Morristown, New Jersey, USA), ca. 1980."
  puts
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair"
  puts
  print "Press Enter to start the program. "
  gets
end

# Returns a string array containing 2 words typed by the user.
def words : Array(String)
  word = ["", ""]
  (0..1).each do |n|
    while word[n] == ""
      print "Enter the ", n == 0 ? "first" : "second", " word: "
      word[n] = gets.not_nil!
    end
  end
  word
end

# Returns the y position correspondent to the given angle.
def y(angle : Float) : Int
  (26 + 25 * Math.sin angle).to_i
end

# Draws the wave using the given array containing 2 strings.
def draw(word : Array(String))
  even = true
  angle = 0.0
  while angle <= 40
    print " " * y(angle), word[even ? 0 : 1], "\n"
    even = !even
    angle += 0.25
  end
end

clear_screen
print_credits
clear_screen
draw words

Slots

# Slots
#   A slot machine simulation.

# Original version in BASIC:
#   Creative Computing (Morristown, New Jersey, USA).
#   Produced by Fred Mirabelle and Bob Harper on 1973-01-29.

# This version in Crystal:
#   Copyright (c) 2023, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2023-09.
#
# Last modified: 20250407T2050+0200.

# Config {{{1
# ==============================================================================

REELS   = 3
IMAGE   = [" BAR  ", " BELL ", "ORANGE", "LEMON ", " PLUM ", "CHERRY"]
IMAGES  = IMAGE.size
BAR     =   0 # position of "BAR" in `IMAGE`.
MIN_BET =   1
MAX_BET = 100

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Moves the cursor to the home position.
def home
  print "\e[H"
end

# Clears the screen and moves the cursor to the home position.
def clear
  print "\e[2J"
  home
end

def set_color(color : Int)
  print "\e[#{color}m"
end

def set_attribute(attr : Int)
  print "\e[0;#{attr}m"
end

def hide_cursor
  print "\e[?25l"
end

def show_cursor
  print "\e[?25h"
end

# User Input {{{1
# ==============================================================================

# Prompts the user to enter a command and returns it.
def command(prompt = "> ") : String
  s = nil
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  s
end

# Prints the given prompt, waits until the user enters a integer and returns it.
# If the input is not a valid integer, returns zero instead.
def get_integer_or_0(prompt : String) : Int
  while true
    print prompt
    begin
      return gets.not_nil!.to_i
      break
    rescue
      return 0
    end
  end
end

# Prints the given prompt and waits until the user enters an empty string.
def press_enter(prompt : String)
  until command(prompt) == ""
  end
end

# Credits and instructions {{{1
# ==============================================================================

def print_credits
  clear
  puts "Slots"
  puts "A slot machine simulation.\n"
  puts "Original version in BASIC:"
  puts "    Creative computing (Morristown, New Jersey, USA)."
  puts "    Produced by Fred Mirabelle and Bob Harper on 1973-01-29.\n"
  puts "This version in Crystal:"
  puts "    Copyright (c) 2023, Marcos Cruz (programandala.net)"
  puts "    SPDX-License-Identifier: Fair\n"
  press_enter("Press Enter for instructions. ")
end

def print_instructions
  clear
  puts "You are in the H&M casino, in front of one of our"
  puts "one-arm bandits. Bet from #{MIN_BET} to #{MAX_BET} USD (or 0 to quit).\n"
  press_enter("Press Enter to start. ")
end

# Main {{{1
# ==============================================================================

def won(prize : Int, bet : Int) : Int
  case prize
  when   2; puts "DOUBLE!"
  when   5; puts "*DOUBLE BAR*"
  when  10; puts "**TOP DOLLAR**"
  when 100; puts "***JACKPOT***"
  end
  puts "You won!"
  return (prize + 1) * bet
end

def show_standings(usd : Int)
  puts "Your standings are #{usd} USD."
end

def print_reels(reel : Array(Int))
  home

  color = [
    FOREGROUND + WHITE,
    FOREGROUND + CYAN,
    FOREGROUND + YELLOW,
    FOREGROUND + BRIGHT + YELLOW,
    FOREGROUND + BRIGHT + WHITE,
    FOREGROUND + BRIGHT + RED,
  ]

  reel.each do |r|
    set_color(color[r])
    print "[#{IMAGE[r]}] "
  end

  set_attribute(NORMAL)
  puts
end

def init_reels(reel : Array(Int))
  reel.each_index do |i|
    reel[i] = rand(IMAGES)
  end
end

SPINS = 24000

def spin_reels(reel : Array(Int))
  hide_cursor
  SPINS.times do
    reel.each_index do |i|
      reel[i] = rand(IMAGES)
    end
    print_reels(reel)
  end
  show_cursor
end

def prize(reel : Array(Int)) : Tuple
  # XXX TODO rewrite, make the counting independent from the number of reels
  equals = 0 # number of equals in the 3 reels
  bars = 0   # number of bars in the 3 reels
  case
  when reel[0] == reel[1] && reel[1] == reel[2]
    equals = 3
  when reel[0] == reel[1] || reel[1] == reel[2] || reel[0] == reel[2]
    equals = 2
  end
  (0...REELS).each do |i|
    bars += reel[i] == BAR ? 1 : 0
  end
  return equals, bars
end

def play
  standings = 0
  reel = [0, 0, 0]
  equals = 0
  bars = 0
  init_reels(reel)
  betting = true
  playing = true
  while playing
    bet = 0
    while playing && betting
      clear
      print_reels(reel)
      bet = get_integer_or_0("Your bet (or 0 to quit): ")
      case
      when bet > MAX_BET
        puts "House limits are #{MAX_BET} USD."
        press_enter("Press Enter to try again. ")
      when bet < MIN_BET
        if command("Type \"q\" to confirm you want to quit: ") == "q"
          playing = false
          betting = false
        end
      else
        betting = false
      end # bet check
    end   # betting
    if playing
      clear
      spin_reels(reel)
      equals, bars = prize(reel)
      case
      when equals == REELS
        if bars == REELS
          standings += won(100, bet)
        else
          standings += won(10, bet)
        end
      when equals == 2
        if bars == 2
          standings += won(5, bet)
        else
          standings += won(2, bet)
        end
      else
        puts "You lost."
        standings -= bet
      end # prize check
      show_standings(standings)
      press_enter("Press Enter to continue. ")
      betting = true
    end
  end # play loop
  show_standings(standings)
  case
  when standings < 0
    puts "Pay up!  Please leave your money on the terminal."
  when standings == 0
    puts "Hey, you broke even."
  when standings > 0
    puts "Collect your winnings from the H&M cashier."
  end
end

print_credits
print_instructions
play

Stars

# Stars

# Original version in BASIC:
#     Example included in Vintage BASIC 1.0.3.
#     http://www.vintage-basic.net

# This version in Crystal:
#     Copyright (c) 2023, Marcos Cruz (programandala.net)
#     SPDX-License-Identifier: Fair
#
# Written on 2023-09-30.
#
# Last modified: 20231104T0010+0100.

def getn(prompt : String) : Float64
  while true
    print prompt
    begin
      number = gets.not_nil!.to_f
      break
    rescue
      puts "Number expected."
    end
  end
  number
end

def yes_or_no(prompt : String) : String
  answer = ""
  until answer != ""
    print prompt
    answer = gets.not_nil!
  end
  answer
end

print "What is your name? "
name = gets.not_nil!
puts "Hello, #{name}."

while true
  n = getn "How many stars do you want? "
  puts "*" * n.to_i
  answer = yes_or_no("Do you want more stars? ")
  if !["ok", "yeah", "yes", "y"].any? { |yes| answer.downcase == yes }
    puts "Goodbye, #{name}."
    break
  end
end

Strings

# Strings

# Original version in BASIC:
#     Example included in Vintage BASIC 1.0.3.
#     http://www.vintage-basic.net

# This version in Crystal:
#     Copyright (c) 2023, Marcos Cruz (programandala.net)
#     SPDX-License-Identifier: Fair
#
# Written in 2023-09.
#
# Last modified: 20241226T0148+0100.

def get_integer : Int
  while true
    print "Enter an integer: "
    begin
      return gets.not_nil!.to_i
      break
    rescue
      puts "That wasn't a valid integer."
    end
  end
end

print "Enter a string: "
s = gets.not_nil!
n = get_integer

print "ASC(\"#{s}\") --> "
print "\"#{s}\".[0].ord --> "
print s.[0].ord, "\n"

print "CHR$(#{n}) --> "
print "#{n}.chr --> "
print "'", n.chr, "'\n"

print "LEFT$(\"#{s}\", #{n}) --> "
print "\"#{s}\".[0...#{n}] --> "
print "\"", s.[0...n], "\"\n"

print "MID$(\"#{s}\", #{n}) --> "
print "\"s\".[#{n} - 1..] --> "
print "\"", s.[n - 1..], "\"\n"

print "MID$(\"#{s}\", #{n}, 3) --> "
print "\"s\".[(#{n} - 1)...(#{n} - 1 + 3)] --> "
print "\"", s.[(n - 1)...(n - 1 + 3)], "\"\n"

print "RIGHT$(\"#{s}\", #{n}) --> "
print "\"s\".[#{-n}..] --> "
print "\"", s.[-n..], "\"\n"

print "LEN(\"#{s}\") --> "
print "\"#{s}\".size --> "
print s.size, "\n"

print "VAL(\"#{s}\") --> "
print "(\"#{s}\".to_f rescue 0.0) --> "
print (s.to_f rescue 0.0), "\n"

print "STR$(#{n}) --> "
print "#{n}.to_s --> "
print "\"#{n.to_s}\"\n"

print "SPC(#{n}) --> "
print " \" \" * #{n} --> "
print "\"", (" " * n), "\"\n"

Xchange

# Xchange

# Original version in BASIC:
#   Written by Thomas C. McIntire, 1979.
#   Published in "The A to Z Book of Computer Games", 1979.
#   https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
#   https://github.com/chaosotter/basic-games

# This improved remake in Crystal:
#   Copyright (c) 2024, 2025, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2024-12, 2025-04.
#
# Last modified: 20250421T0020+0200.

# Terminal {{{1
# ==============================================================================

# Screen colors
BLACK   = 0
RED     = 1
GREEN   = 2
YELLOW  = 3
BLUE    = 4
MAGENTA = 5
CYAN    = 6
WHITE   = 7
DEFAULT = 9

# Screen attributes
NORMAL = 0

# Screen color offsets
FOREGROUND = +30
BRIGHT     = +60

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print "\e[2J\e[H"
end

def set_color(color : Int32)
  print "\e[#{color}m"
end

# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int32)
  print "\e[#{line};#{column}H"
end

def set_cursor_coord(coord : Tuple(Int32, Int32))
  line, column = coord
  set_cursor_position(line, column)
end

# Erases the screen from the current line down to the bottom of the screen.
def erase_screen_down
  print "\e[J"
end

def erase_line_right
  print "\e[K"
end

# Globals {{{1
# =============================================================

BOARD_INK        = BRIGHT + CYAN + FOREGROUND
DEFAULT_INK      = WHITE + FOREGROUND
INPUT_INK        = BRIGHT + GREEN + FOREGROUND
INSTRUCTIONS_INK = YELLOW + FOREGROUND
TITLE_INK        = BRIGHT + RED + FOREGROUND

BLANK = "*"

GRID_HEIGHT = 3 # cell rows
GRID_WIDTH  = 3 # cell columns

CELLS = GRID_WIDTH * GRID_HEIGHT

GRIDS_ROW    =  3 # screen row where the grids are printed
GRIDS_COLUMN =  5 # screen column where the left grid is printed
CELLS_GAP    =  2 # distance between the grid cells, in screen rows or columns
GRIDS_GAP    = 16 # screen columns between equivalent cells of the grids

MAX_PLAYERS = 4

QUIT_COMMAND = "X"

class Global
  class_property grid = Array(Array(String)).new(MAX_PLAYERS, Array(String).new(CELLS, ""))
  class_property is_playing = Array(Bool).new(MAX_PLAYERS, false)
  class_property players : Int32 = 0
  class_property pristine_grid = Array(String).new(CELLS)
end

# User input {{{1
# =============================================================

# Prints the given prompt, waits until the user enters a integer and returns it.
# If the input is not a valid integer, returns zero instead.
def input_int(prompt = "") : Int32
  while true
    print prompt
    begin
      return gets.not_nil!.to_i
      break
    rescue
      return 0
    end
  end
end

# Prints the given prompt and waits until the user enters a string.
#
def input_string(prompt = "") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  set_color(DEFAULT_INK)
  return s
end

# Prints the given prompt and waits until the user presses Enter.
#
def press_enter(prompt : String)
  input_string(prompt)
end

# Returns `true` if the given string is "yes" or a synonym.
#
def is_yes?(s : String) : Bool
  return s.downcase.in?(["ok", "y", "yeah", "yes"])
end

# Returns `true` if the given string is "no" or a synonym.
#
def is_no?(s : String) : Bool
  return s.downcase.in?(["n", "no", "nope"])
end

# Prints the given prompt, waits until the user enters a valid yes/no string,
# and returns `true` for "yes" or `false` for "no".
#
def yes?(prompt : String) : Bool
  while true
    answer = input_string(prompt)
    if is_yes?(answer)
      return true
    end
    if is_no?(answer)
      return false
    end
  end
end

# Title, instructions and credits {{{1
# =============================================================

# Prints the title at the current cursor position.
#
def print_title
  set_color(TITLE_INK)
  print "Xchange\n"
  set_color(DEFAULT_INK)
end

# Prints the credits at the current cursor position.
#
def print_credits
  print_title
  print "\nOriginal version in BASIC:\n"
  print "    Written by Thomas C. McIntire, 1979.\n"
  print "    Published in \"The A to Z Book of Computer Games\", 1979.\n"
  print "    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up\n"
  print "    https://github.com/chaosotter/basic-games\n"
  print "This improved remake in Crystal:\n"
  print "    Copyright (c) 2024, 2025, Marcos Cruz (programandala.net)\n"
  print "    SPDX-License-Identifier: Fair\n"
end

# Prints the instructions at the current cursor position.
#
def print_instructions
  print_title
  set_color(INSTRUCTIONS_INK)
  print "\nOne or two may play.  If two, you take turns.  A grid looks like this:\n\n"
  set_color(BOARD_INK)
  print "    F G D\n"
  print "    A H #{BLANK}\n"
  print "    E B C\n\n"
  set_color(INSTRUCTIONS_INK)
  print "But it should look like this:\n\n"
  set_color(BOARD_INK)
  print "    A B C\n"
  print "    D E F\n"
  print "    G H #{BLANK}\n\n"
  set_color(INSTRUCTIONS_INK)
  print "You may exchange any one letter with the '#{BLANK}', but only one that's adjacent:\n"
  print "above, below, left, or right.  Not all puzzles are possible, and you may enter\n"
  print "'#{QUIT_COMMAND}' to give up.\n\n"
  print "Here we go...\n"
  set_color(DEFAULT_INK)
end

# Grids {{{1
# =============================================================

# Prints the given player's grid title.
#
def print_grid_title(player : Int32)
  set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP))
  print "Player #{player + 1}"
end

# Returns the cursor position of the given player's grid cell.
#
def cell_position(player, cell : Int32) : Tuple(Int32, Int32)
  grid_row : Int32 = cell // GRID_HEIGHT
  grid_column : Int32 = cell % GRID_WIDTH
  grid_columns : Int32 = GRID_HEIGHT * (CELLS_GAP + 1)
  title_margin = Global.players > 1 ? 2 : 0
  row = GRIDS_ROW + title_margin + grid_row
  column = GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)
  return row, column
end

# Returns the cursor position of the given player's grid prompt.
#
def grid_prompt_position(player : Int32) : Tuple(Int32, Int32)
  grid_row : Int32 = CELLS // GRID_HEIGHT
  grid_column : Int32 = CELLS % GRID_WIDTH
  grid_columns : Int32 = GRID_HEIGHT * (CELLS_GAP + 1)
  title_margin = Global.players > 1 ? 2 : 0
  rows = GRIDS_ROW + title_margin + grid_row + 1
  cols = GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)
  return rows, cols
end

# Prints the given player's grid, in the given or default color.
#
def print_grid(player : Int32, color : Int32 = BOARD_INK)
  if Global.players > 1
    print_grid_title(player)
  end
  set_color(color)
  (0...CELLS).each do |cell|
    set_cursor_coord(cell_position(player, cell))
    print Global.grid[player][cell]
  end
  set_color(DEFAULT_INK)
end

# Prints the current players' grids.
#
def print_grids
  (0...Global.players).each do |player|
    if Global.is_playing[player]
      print_grid(player)
    end
  end
  puts
  erase_screen_down
end

# Scrambles the grid of the given player.
#
def scramble_grid(player : Int32)
  (0...CELLS).each do |cell|
    random_cell = rand(CELLS)
    # Exchange the contents of the current cell with that of the random one.
    Global.grid[player][cell], Global.grid[player][random_cell] =
      Global.grid[player][random_cell], Global.grid[player][cell]
  end
end

# Inits the grids.
#
def init_grids
  Global.grid[0] = Global.pristine_grid.dup
  scramble_grid(0)
  (1...Global.players).each do |player|
    Global.grid[player] = Global.grid[0].dup
  end
end

# Messages {{{1
# =============================================================

# Returns a message prefix for the given player.
#
def player_prefix(player : Int32) : String
  return Global.players > 1 ? "Player #{player + 1}: " : ""
end

# Returns the cursor position of the given player's messages, adding the given
# row increment, which defaults to zero.
#
def message_position(player : Int32, row_inc : Int32 = 0) : Tuple(Int32, Int32)
  prompt_row, _ = grid_prompt_position(player)
  return prompt_row + 2 + row_inc, 1
end

# Prints the given message about the given player, adding the given row
# increment, which defaults to zero, to the default cursor coordinates.
#
def print_message(message : String, player : Int32, row_inc : Int32 = 0)
  set_cursor_coord(message_position(player, row_inc))
  print "#{player_prefix(player)}#{message}"
  erase_line_right
  puts
end

# Erases the last message about the given player.
#
def erase_message(player : Int32)
  set_cursor_coord(message_position(player))
  erase_line_right
end

# Game loop {{{1
# =============================================================

# Returns a message with the players range.
#
def players_range_message : String
  if MAX_PLAYERS == 2
    return "1 or 2"
  else
    return "from 1 to #{MAX_PLAYERS}"
  end
end

# Returns the number of players, asking the user if needed.
#
def number_of_players : Int32
  Global.players = 0
  print_title
  puts
  if MAX_PLAYERS == 1
    Global.players = 1
  else
    while Global.players < 1 || Global.players > MAX_PLAYERS
      Global.players = input_int("Number of players (#{players_range_message}): ")
    end
  end
  return Global.players
end

# Is the given cell the first one on a grid row?
#
def is_first_cell_of_a_grid_row?(cell : Int32) : Bool
  return (cell % GRID_WIDTH) == 0
end

# Is the given cell the last one on a grid row?
#
def is_last_cell_of_a_grid_row?(cell : Int32) : Bool
  return ((cell + 1) % GRID_WIDTH) == 0
end

# Are the given cells adjacent?
#
def are_cells_adjacent?(cell_1, cell_2 : Int32) : Bool
  case
  when cell_2 == cell_1 + 1 && !is_first_cell_of_a_grid_row?(cell_2)
    return true
  when cell_2 == cell_1 + GRID_WIDTH
    return true
  when cell_2 == cell_1 - 1 && !is_last_cell_of_a_grid_row?(cell_2)
    return true
  when cell_2 == cell_1 - GRID_WIDTH
    return true
  end
  return false
end

NOWHERE = -1

# If the given player's character cell is a valid move, i.e. it is adjacent to
# the blank cell, returns the blank cell; otherwise returns `nil`.
#
def position_to_cell(player, char_cell : Int32) : (Int32 | Nil)
  (0...CELLS).each do |cell|
    if Global.grid[player][cell] == BLANK
      if are_cells_adjacent?(char_cell, cell)
        return cell
      else
        break
      end
    end
  end
  print_message("Illegal move \"#{Global.grid[player][char_cell]}\".", player)
  return nil
end

# If the given player's command is valid, i.e. a grid character, return its
# position; otherwise return `nil`.
#
def command_to_position(player : Int32, command : String) : (Int32 | Nil)
  if command != BLANK
    (0...CELLS).each do |position|
      if Global.grid[player][position] == command
        return position
      end
    end
  end
  print_message("Invalid character \"#{command}\".", player)
  return nil
end

# Forgets the given player, who quitted.
#
def forget_player(player : Int32)
  Global.is_playing[player] = false
  print_grid(player, DEFAULT_INK)
end

# Plays the turn of the given player.
#
def play_turn(player : Int32)
  blank_position : (Int32 | Nil)
  character_position : (Int32 | Nil)

  if Global.is_playing[player]
    while true
      while true
        coord = grid_prompt_position(player)
        set_cursor_coord(coord)
        erase_line_right
        set_cursor_coord(coord)
        command = input_string("Move: ").rstrip(' ').upcase
        if command == QUIT_COMMAND
          forget_player(player)
          return
        end
        character_position = command_to_position(player, command)
        if character_position.is_a?(Int32)
          break
        end
      end
      blank_position = position_to_cell(player, character_position.as(Int32))
      if blank_position.is_a?(Int32)
        break
      end
    end

    erase_message(player)
    Global.grid[player][blank_position] = Global.grid[player][character_position.as(Int)]
    Global.grid[player][character_position.as(Int)] = BLANK
  end
end

# Plays the turns of all players.
#
def play_turns
  (0...Global.players).each do |player|
    play_turn(player)
  end
end

# Is someone playing?
#
def is_someone_playing : Bool
  (0...Global.players).each do |player|
    if Global.is_playing[player]
      return true
    end
  end
  return false
end

# Has someone won? If so, print a message for every winner and return `true`;
# otherwise just return `false`.
#
def has_someone_won? : Bool
  winners = 0
  (0...Global.players).each do |player|
    if Global.is_playing[player]
      if Global.grid[player] == Global.pristine_grid
        winners += 1
        if winners > 0
          print_message(
            "You're the winner#{winners > 1 ? ", too" : ""}!",
            player,
            row_inc = winners - 1)
        end
      end
    end
  end
  return winners > 0
end

# Inits the game.
#
def init_game
  clear_screen
  Global.players = number_of_players
  (0...Global.players).each do |player|
    Global.is_playing[player] = true
  end
  clear_screen
  print_title
  init_grids
  print_grids
end

# Plays the game.
#
def play
  init_game
  while is_someone_playing
    play_turns
    print_grids
    if has_someone_won?
      break
    end
  end
end

# Main {{{1
# =============================================================

FIRST_CHAR_CODE = 'A'.ord

# Inits the program, i.e. just once before the first game.
#
def init_once
  (0...CELLS).each do |cell|
    Global.pristine_grid << (FIRST_CHAR_CODE + cell).chr.to_s
  end
  Global.pristine_grid[-1] = BLANK
end

# Returns `true` if the player does not want to play another game; otherwise
# returns `false`.
#
def enough : Bool
  row, col = grid_prompt_position(player = 0)
  set_cursor_position(row, col)
  return !yes?("Another game? ")
end

init_once

clear_screen
print_credits

press_enter("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions

press_enter("\nPress the Enter key to start. ")
while true
  play
  if enough
    break
  end
end
print "So long…\n"

Rilataj paĝoj

Basics off
Metaprojekto pri la projektoj «Basics of…».
Basics of 8th
Konverto de malnovaj BASIC-programoj al 8th por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Ada
Konverto de malnovaj BASIC-programoj al Ada por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Arturo
Konverto de malnovaj BASIC-programoj al Arturo por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of C#
Konverto de malnovaj BASIC-programoj al C# por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of C3
Konverto de malnovaj BASIC-programoj al C3 por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Chapel
Konverto de malnovaj BASIC-programoj al Chapel por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Clojure
Konverto de malnovaj BASIC-programoj al Clojure por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of D
Konverto de malnovaj BASIC-programoj al D por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Elixir
Konverto de malnovaj BASIC-programoj al Elixir por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of F#
Konverto de malnovaj BASIC-programoj al F# por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Factor
Konverto de malnovaj BASIC-programoj al Factor por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of FreeBASIC
Konverto de malnovaj BASIC-programoj al FreeBASIC por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Gleam
Konverto de malnovaj BASIC-programoj al Gleam por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Go
Konverto de malnovaj BASIC-programoj al Go por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Hare
Konverto de malnovaj BASIC-programoj al Hare por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Haxe
Konverto de malnovaj BASIC-programoj al Haxe por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Icon
Konverto de malnovaj BASIC-programoj al Icon por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Io
Konverto de malnovaj BASIC-programoj al Io por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Janet
Konverto de malnovaj BASIC-programoj al Janet por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Julia
Konverto de malnovaj BASIC-programoj al Julia por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Kotlin
Konverto de malnovaj BASIC-programoj al Kotlin por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Lobster
Konverto de malnovaj BASIC-programoj al Lobster por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Lua
Konverto de malnovaj BASIC-programoj al Lua por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Nature
Konverto de malnovaj BASIC-programoj al Nature por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Neat
Konverto de malnovaj BASIC-programoj al Neat por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Neko
Konverto de malnovaj BASIC-programoj al Neko por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Nelua
Konverto de malnovaj BASIC-programoj al Nelua por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Nim
Konverto de malnovaj BASIC-programoj al Nim por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Nit
Konverto de malnovaj BASIC-programoj al Nit por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Oberon-07
Konverto de malnovaj BASIC-programoj al Oberon-07 por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of OCaml
Konverto de malnovaj BASIC-programoj al OCaml por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Odin
Konverto de malnovaj BASIC-programoj al Odin por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Pike
Konverto de malnovaj BASIC-programoj al Pike por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Pony
Konverto de malnovaj BASIC-programoj al Pony por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Python
Konverto de malnovaj BASIC-programoj al Python por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Racket
Konverto de malnovaj BASIC-programoj al Racket por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Raku
Konverto de malnovaj BASIC-programoj al Raku por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Retro
Konverto de malnovaj BASIC-programoj al Retro por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Rexx
Konverto de malnovaj BASIC-programoj al Rexx por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Ring
Konverto de malnovaj BASIC-programoj al Ring por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Rust
Konverto de malnovaj BASIC-programoj al Rust por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Scala
Konverto de malnovaj BASIC-programoj al Scala por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Scheme
Konverto de malnovaj BASIC-programoj al Scheme por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Styx
Konverto de malnovaj BASIC-programoj al Styx por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Swift
Konverto de malnovaj BASIC-programoj al Swift por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of V
Konverto de malnovaj BASIC-programoj al V por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Vala
Konverto de malnovaj BASIC-programoj al Vala por lerni la fundamentojn de ĉi-tiu lingvo.
Basics of Zig
Konverto de malnovaj BASIC-programoj al Zig por lerni la fundamentojn de ĉi-tiu lingvo.

Eksteraj rilataj ligiloj