Basics of Crystal
Description of the page content
Conversion of old BASIC programs to Crystal in order to learn the basics of this language.
Tags:
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"
