Basics of Hare
Descripción del contenido de la página
Conversión de antiguos programas de BASIC a Hare para aprender los rudimentos de este lenguaje.
Etiquetas:
3D Plot
// 3D Plot
// Original version in BASIC:
// Creative Computing (Morristown, New Jersey, USA), ca. 1980.
// This version in Hare:
// Copyright (c) 2024, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2024-12-14/15, 2025-03-07.
//
// Last modified: 20260213T1645+0100.
use bufio;
use fmt;
use math;
use os;
use strings;
def SPACE = ' ';
def DOT = '*';
def WIDTH = 56;
fn clear_screen() void = {
fmt::print("\x1B[0;0H\x1B[2J")!;
};
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
fn print_credits() void = {
fmt::println("3D Plot\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Creative computing (Morristown, New Jersey, USA), ca. 1980.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2024, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter("Press Enter to start.");
};
fn a(z: f64) f64 = {
return 30.0 * math::expf64(-z * z / 100.0);
};
fn draw() void = {
let l = 0;
let z = 0;
let y1 = 0;
let line: [WIDTH]rune = [SPACE...];
for (let x = -30.0; x <= 30.0; x += 1.5) {
line = [SPACE...];
l = 0;
y1 = 5 * (math::sqrtf64(900.0 - x * x) / 5.0): int;
for (let y = y1; y >= -y1; y += -5) {
z = (25.0
+ a(math::sqrtf64(x * x + (y * y): f64))
- 0.7 * (y: f64)): int;
if (z > l) {
l = z;
line[z] = DOT;
};
};
for (let pos = 0; pos < WIDTH; pos += 1) {
fmt::print(line[pos])!;
};
fmt::println()!;
};
};
export fn main() void = {
clear_screen();
print_credits();
clear_screen();
draw();
};
Bagels
// Bagels
//
// Original version in BASIC:
// D. Resek, P. Rowe, 1978.
// Creative Computing (Morristown, New Jersey, USA), 1978.
//
// This version in Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-14/15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Credits and instructions {{{1
// =============================================================================
// Clear the screen, display the credits and wait for a keypress.
//
fn print_credits() void = {
clear_screen();
fmt::println("Bagels")!;
fmt::println("Number guessing game\n")!;
fmt::println("Original source unknown but suspected to be:")!;
fmt::println(" Lawrence Hall of Science, U.C. Berkely.\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" D. Resek, P. Rowe, 1978.")!;
fmt::println(" Creative computing (Morristown, New Jersey, USA), 1978.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter("Press Enter to read the instructions. ");
};
// Clear the screen, print the instructions and wait for a keypress.
//
fn print_instructions() void = {
clear_screen();
fmt::println("Bagels")!;
fmt::println("Number guessing game\n")!;
fmt::println("I am thinking of a three-digit number that has no two digits the same.")!;
fmt::println("Try to guess it and I will give you clues as follows:\n")!;
fmt::println(" PICO - one digit correct but in the wrong position")!;
fmt::println(" FERMI - one digit correct and in the right position")!;
fmt::println(" BAGELS - no digits correct\n")!;
press_enter("Press Enter to start. ");
};
// Strings {{{1
// =============================================================================
fn repeat(s: str, n: int) str = {
let result = "";
for (let i: int = 0; i < n; i += 1) {
result = strings::concat(result, s)!;
};
return result;
};
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "n", "no", "nope" =>
yield true;
case =>
yield false;
};
};
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt: str) bool = {
for (true) {
let answer = accept_string(prompt);
defer free(answer);
if (is_yes(answer)) {
return true;
};
if (is_no(answer)) {
return false;
};
};
};
// Main {{{1
// =============================================================================
// Print the given prompt and update the array whose address is given with a
// three-digit number from the user.
//
fn get_input(prompt: str, user_digit: *[DIGITS]int) void = {
def ASCII_0 = 48;
for :get_loop (true) {
let input = accept_string(prompt);
defer free(input);
if (len(input) != DIGITS) {
fmt::printfln("Remember it's a {}-digit number.", DIGITS)!;
continue :get_loop;
};
for (let pos = 0; pos < len(input): int; pos += 1) {
let digit = strings::toutf8(input)[pos];
if (ascii::isdigit(digit: rune)) {
user_digit[pos] = digit: int - ASCII_0;
} else {
fmt::println("What?")!;
continue :get_loop;
};
};
if (is_any_repeated(user_digit)) {
fmt::println("Remember my number has no two digits the same.")!;
continue :get_loop;
};
break;
};
};
def DIGITS = 3;
let random_digit: [DIGITS]int = [0 ...];
// Return three random digits.
//
fn random_number() [DIGITS]int = {
for (let i = 0; i < DIGITS; i += 1) {
for :digit_loop (true) {
random_digit[i] = random::u64n(&rand, 10): int;
for (let j = 0; j < i; j += 1) {
if (i != j && random_digit[i] == random_digit[j]) {
continue :digit_loop;
};
};
break;
};
};
return random_digit;
};
// Return `true` if any of the given numbers is repeated; otherwise return
// `false`.
//
fn is_any_repeated(num: []int) bool = {
const length: int = len(num): int;
for (let i0 = 0; i0 < length; i0 += 1) {
for (let n1 .. num[i0 + 1 .. length]) {
if (num[i0] == n1) {
return true;
};
};
};
return false;
};
// Init and run the game loop.
//
fn play() void = {
def TRIES = 20;
let score = 0;
let fermi = 0; // counter
let pico = 0; // counter
let user_number: [DIGITS]int = [0...];
for (true) {
clear_screen();
let computer_number = random_number();
fmt::println("O.K. I have a number in mind.")!;
for (let guess = 1; guess <= TRIES; guess += 1) {
// XXX TMP
// fmt::print("My number: ")!;
// for (let i = 0; i < DIGITS; i += 1) {
// fmt::print(computer_number[i])!;
// };
// fmt::println()!;
get_input(fmt::asprintf("Guess #{:_02}: ", guess)!, &user_number);
fermi = 0;
pico = 0;
for (let i = 0; i < DIGITS; i += 1) {
for (let j = 0; j < DIGITS; j += 1) {
if (user_number[i] == computer_number[j]) {
if (i == j) {
fermi += 1;
} else {
pico += 1;
};
};
};
};
let picos = repeat("PICO ", pico);
defer free(picos);
let fermis = repeat("FERMI ", fermi);
defer free(fermis);
fmt::print(picos)!;
fmt::print(fermis)!;
if (pico + fermi == 0) {
fmt::print("BAGELS")!;
};
fmt::println()!;
if (fermi == DIGITS) {
break;
};
};
if (fermi == DIGITS) {
fmt::println("You got it!!!")!;
score += 1;
} else {
fmt::println("Oh well.")!;
fmt::printf("That's {} guesses. My number was ", TRIES)!;
for (let i = 0; i < DIGITS; i += 1) {
fmt::print(computer_number[i])!;
};
fmt::println(".")!;
};
if (!yes("Play again? ")) {
break;
};
};
if (score != 0) {
fmt::printfln("A {}-point bagels, buff!!", score)!;
};
fmt::println("Hope you had fun. Bye.")!;
};
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn init() void = {
randomize();
};
export fn main() void = {
init();
print_credits();
print_instructions();
play();
};
Bug
// Bug
//
// Original version in BASIC:
// Brian Leibowitz, 1978.
// Creative Computing (Morristown, New Jersey, USA), 1978.
//
// This version in Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-12/15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Data {{{1
// =============================================================================
type Bug = struct {
body: bool,
neck: bool,
head: bool,
feelers: int,
feeler_type: rune,
tail: bool,
legs: int,
};
type Player = struct {
pronoun: str,
possessive: str,
bug: Bug,
};
let computer = Player {
pronoun = "I",
possessive = "My",
bug = Bug {
body = false,
neck = false,
head = false,
feelers = 0,
feeler_type = 'F',
tail = false,
legs = 0,
}
};
let human = Player {
pronoun = "you",
possessive = "Your",
bug = Bug {
body = false,
neck = false,
head = false,
feelers = 0,
feeler_type = 'A',
tail = false,
legs = 0,
}
};
type bug_part = enum int {BODY = 1, NECK, HEAD, FEELER, TAIL, LEG};
def FIRST_PART = bug_part::BODY: int;
def LAST_PART = bug_part::LEG: int;
fn part_quantity(part: bug_part) int = {
return switch (part) {
case bug_part::BODY => yield 1;
case bug_part::NECK => yield 1;
case bug_part::HEAD => yield 1;
case bug_part::FEELER => yield 2;
case bug_part::TAIL => yield 1;
case bug_part::LEG => yield 6;
};
};
fn part_name(part: bug_part) str = {
return switch (part) {
case bug_part::BODY => yield "body";
case bug_part::NECK => yield "neck";
case bug_part::HEAD => yield "head";
case bug_part::FEELER => yield "feeler";
case bug_part::TAIL => yield "tail";
case bug_part::LEG => yield "leg";
};
};
// Bug body attributes.
//
def BODY_HEIGHT = 2;
def FEELER_LENGTH = 4;
def LEG_LENGTH = 2;
def MAX_FEELERS = 2;
def MAX_LEGS = 6;
def NECK_LENGTH = 2;
// Terminal {{{1
// =============================================================================
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn move_cursor_up(lines: int = 1) void = {
fmt::printf("\x1B[{}A", lines)!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Erase the entire current line and move the cursor to the start of the line.
//
fn erase_line() void = {
fmt::print("\x1B[2K")!;
};
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
// Credits and instructions {{{1
// =============================================================================
// Clear the screen, display the credits and wait for a keypress.
//
fn print_credits() void = {
clear_screen();
fmt::println("Bug\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Brian Leibowitz, 1978.")!;
fmt::println(" Creative computing (Morristown, New Jersey, USA), 1978.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter("Press Enter to read the instructions. ");
};
def 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:
`;
// Clear the screen, print the instructions and wait for a keypress.
//
fn print_instructions() void = {
clear_screen();
fmt::println("Bug")!;
fmt::println(INSTRUCTIONS)!;
print_parts_table();
press_enter("\nPress Enter to start. ");
};
// Strings {{{1
// =============================================================================
fn to_capital(s: str) str = {
const initial = ascii::strupper(strings::sub(s, 0, 1))!;
defer free(initial);
return strings::concat(initial, strings::sub(s, 1))!;
};
fn repeat(s: str, n: int) str = {
let result = "";
for (let i: int = 0; i < n; i += 1) {
result = strings::concat(result, s)!;
};
return result;
};
// Pseudo-random numbers {{{1
// =============================================================================
let rand: random::random = 0;
// Return a random number from 1 to 6 (inclusive).
//
fn dice(r: *random::random) int = {
return (random::u32n(r, 6) + 1): int;
};
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
// Main {{{1
// =============================================================================
// Print a table with the description of the bug parts.
//
fn print_parts_table() void = {
def COLUMNS = 3;
def COLUMN_WIDTH = 8;
def COLUMN_SEPARATION = 2;
const format_string = strings::concat(
"{:-",
strconv::itos(COLUMN_WIDTH + COLUMN_SEPARATION),
"}")!;
// Headers
const header: [_]str = ["Number", "Part", "Quantity"];
for (let i = 0; i < COLUMNS; i += 1) {
fmt::printf(format_string, header[i])!;
};
fmt::println()!;
// Rulers
const ruler = repeat("-", COLUMN_WIDTH);
defer free(ruler);
const padding = repeat(" ", COLUMN_SEPARATION - 1);
defer free(padding);
for (let i = 0; i < COLUMNS; i += 1) {
fmt::print(ruler, if (i == COLUMNS - 1) "" else padding)!;
};
fmt::println()!;
// Data
for (let p = FIRST_PART; p <= LAST_PART; p += 1) {
let part = p: bug_part;
fmt::printf(format_string, p)!;
let name = to_capital(part_name(part));
defer free(name);
fmt::printf(format_string, name)!;
fmt::printf(format_string, part_quantity(part))!;
fmt::println()!;
};
};
// Print a bug head.
//
fn print_head() void = {
fmt::println(" HHHHHHH")!;
fmt::println(" H H")!;
fmt::println(" H O O H")!;
fmt::println(" H H")!;
fmt::println(" H V H")!;
fmt::println(" HHHHHHH")!;
};
// Print the given bug.
//
fn print_bug(bug: Bug) void = {
if (bug.feelers > 0) {
for (let i = 0; i < FEELER_LENGTH; i += 1) {
fmt::print(" ")!;
for (let i = 0; i < bug.feelers; i += 1) {
fmt::print(" ", bug.feeler_type)!;
};
fmt::println()!;
};
};
if (bug.head) {
print_head();
};
if (bug.neck) {
for (let i = 0; i < NECK_LENGTH; i += 1) {
fmt::println(" N N")!;
};
};
if (bug.body) {
fmt::println(" BBBBBBBBBBBB")!;
for (let i = 0; i < BODY_HEIGHT; i += 1) {
fmt::println(" B B")!;
};
if (bug.tail) {
fmt::println("TTTTTB B")!;
};
fmt::println(" BBBBBBBBBBBB")!;
};
if (bug.legs > 0) {
for (let i = 0; i < LEG_LENGTH; i += 1) {
fmt::print(" ")!;
for (let i = 0; i < bug.legs; i += 1) {
fmt::print(" L")!;
};
fmt::println()!;
};
};
};
// Return `true` if the given bug is finished; otherwise return `false`.
//
fn finished(bug: Bug) bool = {
return bug.feelers == MAX_FEELERS && bug.tail && bug.legs == MAX_LEGS;
};
// Array to convert a number to its equilavent text.
//
let as_text: [_]str = [
"no",
"a",
"two",
"three",
"four",
"five",
"six" ]; // MAX_LEGS
// Return a string containing the given number and noun in their proper form.
//
fn plural(number: int, noun: str) str = {
return strings::concat(
as_text[number],
" ",
noun,
if (number > 1) "s" else ""
)!;
};
// Add the given part to the given player's bug.
//
fn add_part(part: bug_part, player: *Player) bool = {
let changed: bool = false;
switch (part) {
case bug_part::BODY =>
if (player.bug.body) {
fmt::println(", but", player.pronoun, "already have a body.")!;
} else {
fmt::println(";", player.pronoun, "now have a body:")!;
player.bug.body = true;
changed = true;
};
case bug_part::NECK =>
if (player.bug.neck) {
fmt::println(", but", player.pronoun, "already have a neck.")!;
} else if (!player.bug.body) {
fmt::println(", but", player.pronoun, "need a body first.")!;
} else {
fmt::println(";", player.pronoun, "now have a neck:")!;
player.bug.neck = true;
changed = true;
};
case bug_part::HEAD =>
if (player.bug.head) {
fmt::println(", but", player.pronoun, "already have a head.")!;
} else if (!player.bug.neck) {
fmt::println(", but", player.pronoun, "need a a neck first.")!;
} else {
fmt::println(";", player.pronoun, "now have a head:")!;
player.bug.head = true;
changed = true;
};
case bug_part::FEELER =>
if (player.bug.feelers == MAX_FEELERS) {
fmt::println(", but", player.pronoun, "have two feelers already.")!;
} else if (!player.bug.head) {
fmt::println(", but", player.pronoun, "need a head first.")!;
} else {
player.bug.feelers += 1;
fmt::print(";", player.pronoun, "now have",
plural(player.bug.feelers, "feeler"))!;
fmt::println(":")!;
changed = true;
};
case bug_part::TAIL =>
if (player.bug.tail) {
fmt::println(", but", player.pronoun, "already have a tail.")!;
} else if (!player.bug.body) {
fmt::println(", but", player.pronoun, "need a body first.")!;
} else {
fmt::println(";", player.pronoun, "now have a tail:")!;
player.bug.tail = true;
changed = true;
};
case bug_part::LEG =>
if (player.bug.legs == MAX_LEGS) {
fmt::println(", but", player.pronoun, "have",
as_text[MAX_LEGS], "feet already.")!;
} else if (!player.bug.body) {
fmt::println(", but", player.pronoun, "need a body first.")!;
} else {
player.bug.legs += 1;
fmt::print(";", player.pronoun, "now have",
plural(player.bug.legs, "leg"))!;
fmt::println(":")!;
changed = true;
};
};
return changed;
};
// Play one turn for the given player, rolling the dice and updating his bug.
//
fn turn(player: *Player) void = {
press_enter("Press Enter to roll the dice. ");
move_cursor_up(1);
erase_line();
const number: int = dice(&rand);
const part = number: bug_part;
const pronoun = to_capital(player.pronoun);
defer free(pronoun);
fmt::printf("{} rolled a {} ({})", pronoun, number, part_name(part))!;
if (add_part(part, player)) {
fmt::println()!;
print_bug(player.bug);
};
fmt::println()!;
};
// Print a message about the winner.
//
fn print_winner() void = {
if (finished(human.bug) && finished(computer.bug)) {
fmt::println("Both of our bugs are finished in the same number of turns!")!;
} else if (finished(human.bug)) {
fmt::println(human.possessive, "bug is finished.")!;
} else if (finished(computer.bug)) {
fmt::println(computer.possessive, "bug is finished.")!;
};
};
// Return `true` if either bug is finished, i.e. the game ending condition.
//
fn game_over() bool = {
return finished(human.bug) || finished(computer.bug);
};
// Execute the game loop.
//
fn play() void = {
clear_screen();
for (!game_over()) {
turn(&human);
turn(&computer);
};
print_winner();
};
fn init() void = {
randomize();
};
fn bye() void = {
fmt::println("I hope you enjoyed the game, play it again soon!!")!;
};
export fn main() void = {
init();
print_credits();
print_instructions();
play();
bye();
};
Bunny
// Bunny
//
// Original version in BASIC:
// Creative Computing (Morristown, New Jersey, USA), 1978.
//
// This version in Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-12, 2025-02-15.
//
// Last modified: 20260213T1645+0100.
use bufio;
use fmt;
use os;
use strings;
fn clear_screen() void = {
fmt::print("\x1B[0;0H\x1B[2J")!;
};
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
fn print_credits() void = {
fmt::println("Bunny\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Creative Computing (Morristown, New Jersey, USA), 1978.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter("Press Enter to start the program. ");
};
def WIDTH = 53;
let line: [WIDTH]rune = [' '...]; // buffer
// Clear the line buffer with spaces.
//
fn clear_line() void = {
for (let column: int = 0; column < WIDTH; column += 1) {
line[column] = ' ';
};
};
let letter: [_]rune = ['B', 'U', 'N', 'N', 'Y'];
def LETTERS = len(letter): int;
def EOL = -1; // end of line identifier
let data: [_]int = [
1, 2, EOL, 0, 2, 45, 50, EOL, 0, 5, 43, 52, EOL, 0, 7, 41, 52, EOL,
1, 9, 37, 50, EOL, 2, 11, 36, 50, EOL, 3, 13, 34, 49, EOL, 4, 14,
32, 48, EOL, 5, 15, 31, 47, EOL, 6, 16, 30, 45, EOL, 7, 17, 29, 44,
EOL, 8, 19, 28, 43, EOL, 9, 20, 27, 41, EOL, 10, 21, 26, 40, EOL,
11, 22, 25, 38, EOL, 12, 22, 24, 36, EOL, 13, 34, EOL, 14, 33, EOL,
15, 31, EOL, 17, 29, EOL, 18, 27, EOL, 19, 26, EOL, 16, 28, EOL,
13, 30, EOL, 11, 31, EOL, 10, 32, EOL, 8, 33, EOL, 7, 34, EOL, 6,
13, 16, 34, EOL, 5, 12, 16, 35, EOL, 4, 12, 16, 35, EOL, 3, 12, 15,
35, EOL, 2, 35, EOL, 1, 35, EOL, 2, 34, EOL, 3, 34, EOL, 4, 33,
EOL, 6, 33, EOL, 10, 32, 34, 34, EOL, 14, 17, 19, 25, 28, 31, 35,
35, EOL, 15, 19, 23, 30, 36, 36, EOL, 14, 18, 21, 21, 24, 30, 37, 37,
EOL, 13, 18, 23, 29, 33, 38, EOL, 12, 29, 31, 33, EOL, 11, 13, 17,
17, 19, 19, 22, 22, 24, 31, EOL, 10, 11, 17, 18, 22, 22, 24, 24, 29,
29, EOL, 22, 23, 26, 29, EOL, 27, 29, EOL, 28, 29, EOL ];
def DATA_LEN = len(data): int;
let data_index = 0;
fn datum() int = {
defer data_index += 1;
return data[data_index];
};
fn draw() void = {
clear_line();
for (data_index < DATA_LEN) {
let first_column = datum();
if (first_column == EOL) {
for (let c .. line) {
fmt::print(c)!;
};
fmt::println()!;
clear_line();
} else {
let last_column = datum();
for (let column = first_column; column <= last_column; column += 1) {
line[column] = letter[column % LETTERS];
};
};
};
};
export fn main() void = {
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-17.
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_cursor_position(line: int, column: int) void = {
fmt::printf("\x1B[{};{}H", line, column)!;
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_line_to_end() void = {
fmt::print("\x1B[K")!;
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Config {{{1
// =============================================================
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
// Data {{{1
// =============================================================
// Arena
def ARENA_WIDTH = 20;
def ARENA_HEIGHT = 10;
def ARENA_LAST_X = ARENA_WIDTH - 1;
def ARENA_LAST_Y = ARENA_HEIGHT - 1;
def ARENA_ROW = 3;
let arena: [ARENA_HEIGHT][ARENA_WIDTH]str = [[""...]...];
def EMPTY = " ";
def FENCE = "X";
def MACHINE = "m";
def HUMAN = "@";
def FENCES = 15; // inner obstacles, not the border
// The end
type end = enum {
NOT_YET,
QUIT,
ELECTRIFIED,
KILLED,
VICTORY,
};
let the_end: end = end::NOT_YET;
// Machines
def MACHINES = 5;
def MACHINES_DRAG = 2; // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
let machine_x: [MACHINES]int = [0...];
let machine_y: [MACHINES]int = [0...];
let operative: [MACHINES]bool = [false...];
let destroyed_machines: int = 0; // counter
// Human
let human_x: int = 0;
let human_y: int = 0;
// User input {{{1
// =============================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn get_string(prompt: str = "") str = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return accept_string(prompt);
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "n", "no", "nope" =>
yield true;
case =>
yield false;
};
};
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt: str) bool = {
for (true) {
let answer = get_string(prompt);
defer free(answer);
if (is_yes(answer)) {
return true;
};
if (is_no(answer)) {
return false;
};
};
};
// Title, credits and instructions {{{1
// =============================================================
def TITLE = "Chase";
// Print the title at the current cursor position.
//
fn print_title() void = {
set_style(TITLE_INK);
fmt::println(TITLE)!;
set_style(DEFAULT_INK);
};
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Anonymous.")!;
fmt::println(" Published in \"The Best of Creative Computing\", Volume 2, 1977.")!;
fmt::println(" https://www.atariarchives.org/bcc2/showpage.php?page=253")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
// Print the game instructions and wait for a key.
//
fn print_instructions() void = {
print_title();
set_style(INSTRUCTIONS_INK);
defer set_style(DEFAULT_INK);
fmt::printfln("\nYou ({}) are in a high voltage maze with {}", HUMAN, MACHINES)!;
fmt::printfln("security machines ({}) trying to kill you.", MACHINE)!;
fmt::printfln("You must maneuver them into the maze ({}) to survive.\n", FENCE)!;
fmt::println("Good luck!\n")!;
fmt::println("The movement commands are the following:\n")!;
fmt::println(" ↖ ↑ ↗")!;
fmt::println(" NW N NE")!;
fmt::println(" ← W E →")!;
fmt::println(" SW S SE")!;
fmt::println(" ↙ ↓ ↘")!;
fmt::println("\nPlus 'Q' to end the game.")!;
};
// Random numbers {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn random_int_in_inclusive_range(min: int, max: int) int = {
return random::u32n(&rand, (max - min):u32): int + min;
};
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
fn print_arena() void = {
set_cursor_position(ARENA_ROW, 1);
for (let y = 0; y <= ARENA_LAST_Y; y += 1) {
for (let x = 0; x <= ARENA_LAST_X; x += 1) {
fmt::print(arena[y][x])!;
};
fmt::println()!;
};
};
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
fn is_border(y: int, x: int) bool = {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
};
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
fn place(s: str) (int, int) = {
let y: int = 0;
let x: int = 0;
for (true) {
y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1);
x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1);
if (arena[y][x] == EMPTY) {
break;
};
};
arena[y][x] = s;
return (y, x);
};
// Inhabit the arena with the machines, the inner fences and the human.
//
fn inhabit_arena() void = {
for (let m = 0; m < MACHINES; m += 1) {
let coords = place(MACHINE);
machine_y[m] = coords.0;
machine_x[m] = coords.1;
operative[m] = true;
};
for (let i = 0; i < FENCES; i += 1) {
place(FENCE);
};
let coords = place(HUMAN);
human_y = coords.0;
human_x = coords.1;
};
// Clean the arena, i.e. empty it and add a surrounding fence.
//
fn clean_arena() void = {
for (let y = 0; y <= ARENA_LAST_Y; y += 1) {
for (let x = 0; x <= ARENA_LAST_X; x += 1) {
arena[y][x] = if (is_border(y, x)) FENCE else EMPTY;
};
};
};
// Game {{{1
// =============================================================
// Init the game.
//
fn init_game() void = {
clean_arena();
inhabit_arena();
destroyed_machines = 0;
the_end = end::NOT_YET;
};
// Move the given machine.
//
fn move_machine(m: int) void = {
let maybe: int = 0;
arena[machine_y[m]][machine_x[m]] = EMPTY;
maybe = random::u32n(&rand, 2): int;
if (machine_y[m] > human_y) {
machine_y[m] -= maybe;
} else if (machine_y[m] < human_y) {
machine_y[m] += maybe;
};
maybe = random::u32n(&rand, 2): int;
if (machine_x[m] > human_x) {
machine_x[m] -= maybe;
} else if (machine_x[m] < human_x) {
machine_x[m] += maybe;
};
if (arena[machine_y[m]][machine_x[m]] == EMPTY) {
arena[machine_y[m]][machine_x[m]] = MACHINE;
} else if (arena[machine_y[m]][machine_x[m]] == FENCE) {
operative[m] = false;
destroyed_machines += 1;
if (destroyed_machines == MACHINES) {
the_end = end::VICTORY;
};
} else if (arena[machine_y[m]][machine_x[m]] == HUMAN) {
the_end = end::KILLED;
};
};
// Maybe move the given operative machine.
//
fn maybe_move_machine(m: int) void = {
if (random::u32n(&rand, MACHINES_DRAG) == 0) {
move_machine(m);
};
};
// Move the operative machines.
//
fn move_machines() void = {
for (let m = 0; m < MACHINES; m += 1) {
if (operative[m]) {
maybe_move_machine(m);
};
};
};
// Read a user command; update `the_end` accordingly and return the direction
// increments.
//
fn get_move() (int, int) = {
let y_inc: int = 0;
let x_inc: int = 0;
fmt::println()!;
erase_line_to_end();
let command = ascii::strlower(get_string("Command: "))!;
defer free(command);
switch (command) {
case "q" =>
the_end = end::QUIT;
case "sw" =>
y_inc = 1;
x_inc = -1;
case "s" =>
y_inc = 1;
case "se" =>
y_inc = 1;
x_inc = 1;
case "w" =>
x_inc = -1;
case "e" =>
x_inc = 1;
case "nw" =>
y_inc = -1;
x_inc = -1;
case "n" =>
y_inc = -1;
case "ne" =>
y_inc = -1;
x_inc = 1;
case =>
void;
};
return (y_inc, x_inc);
};
fn play() void = {
let y_inc: int = 0;
let x_inc: int = 0;
for (true) { // game loop
clear_screen();
print_title();
init_game();
for (true) { // action loop
print_arena();
let coords = get_move();
y_inc = coords.0;
x_inc = coords.1;
if (the_end == end::NOT_YET) {
if (y_inc != 0 || x_inc != 0) {
arena[human_y][human_x] = EMPTY;
if (arena[human_y + y_inc][human_x + x_inc] == FENCE) {
the_end = end::ELECTRIFIED;
} else if (arena[human_y + y_inc][human_x + x_inc] == MACHINE) {
the_end = end::KILLED;
} else {
arena[human_y][human_x] = EMPTY;
human_y = human_y + y_inc;
human_x = human_x + x_inc;
arena[human_y][human_x] = HUMAN;
print_arena();
move_machines();
};
};
};
if (the_end != end::NOT_YET) {
break;
};
}; // action loop;
switch (the_end) {
case end::QUIT =>
fmt::println("\nSorry to see you quit.")!;
case end::ELECTRIFIED =>
fmt::println("\nZap! You touched the fence!")!;
case end::KILLED =>
fmt::println("\nYou have been killed by a lucky machine.")!;
case end::VICTORY =>
fmt::println("\nYou are lucky, you destroyed all machines.")!;
case =>
void;
};
if (!yes("\nDo you want to play again? ")) {
break;
};
}; // game loop
fmt::println("\nHope you don't feel fenced in.")!;
fmt::println("Try again sometime.")!;
};
// Main {{{1
// =============================================================
export fn main() void = {
randomize();
set_style(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 Hare (v0.24.2):
// Copyright (c) 2024, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
// Written in 2024-12-14/15.
// Last modified: 20260213T1645+0100.
use fmt;
def LINES = 17;
export fn main() void = {
for (let i = 1; i <= LINES / 2 + 1; i += 1) {
for (let j = 1; j <= (LINES + 1) / 2 - i + 1; j += 1) {
fmt::print(" ")!;
};
for (let j = 1; j <= i * 2 - 1; j += 1) {
fmt::print("*")!;
};
fmt::println()!;
};
for (let i = 1; i <= LINES / 2; i += 1) {
for (let j = 1; j <= i + 1; j += 1) {
fmt::print(" ")!;
};
for (let j = 1; j <= ((LINES + 1) / 2 - i) * 2 - 1; j += 1) {
fmt::print("*")!;
};
fmt::println()!;
};
};
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-19.
//
// Last modified: 20260213T1645+0100.
//
// Acknowledgment:
// The following Python port was used as a reference of the original
// variables: <https://github.com/jquast/hamurabi.py>.
//
// Modules {{{1
// =============================================================
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Data {{{1
// =============================================================
def ACRES_A_BUSHEL_CAN_SEED = 2; // yearly
def ACRES_A_PERSON_CAN_SEED = 10; // yearly
def ACRES_PER_PERSON = 10; // to calculate the initial acres of the city
def BUSHELS_TO_FEED_A_PERSON = 20; // yearly
def IRRITATION_LEVELS = 5; // after the switch in `show_irritation`
def IRRITATION_STEP = MAX_IRRITATION / IRRITATION_LEVELS;
def MAX_HARVESTED_BUSHELS_PER_ACRE = MIN_HARVESTED_BUSHELS_PER_ACRE + RANGE_OF_HARVESTED_BUSHELS_PER_ACRE - 1;
def MIN_HARVESTED_BUSHELS_PER_ACRE = 17;
def MAX_IRRITATION = 16;
def PLAGUE_CHANCE = 0.15; // 15% yearly
def RANGE_OF_HARVESTED_BUSHELS_PER_ACRE = 10;
def YEARS = 10; // goverment period
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def RESULT_INK = FOREGROUND + BRIGHT + CYAN;
def SPEECH_INK = FOREGROUND + BRIGHT + MAGENTA;
def TITLE_INK = FOREGROUND + BRIGHT + WHITE;
def WARNING_INK = FOREGROUND + BRIGHT + RED;
type result = enum {
VERY_GOOD,
NOT_TOO_BAD,
BAD,
VERY_BAD,
};
let acres: int = 0;
let bushels_eaten_by_rats: int = 0;
let bushels_harvested: int = 0;
let bushels_harvested_per_acre: int = 0;
let bushels_in_store: int = 0;
let bushels_to_feed_with: int = 0;
let dead: int = 0;
let infants: int = 0;
let irritation: int = 0; // counter (0 ..= 99)
let population: int = 0;
let starved_people_percentage: int = 0;
let total_dead: int = 0;
// Credits and instructions {{{1
// =============================================================
def CREDITS: str =
`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 Hare:
Copyright (c) 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair`;
fn print_credits() void = {
set_style(TITLE_INK);
fmt::println(CREDITS)!;
set_style(DEFAULT_INK);
};
def INSTRUCTIONS =
`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 {} and {} 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 {}-year term of office.`;
fn print_instructions() void = {
set_style(INSTRUCTIONS_INK);
fmt::printfln(
INSTRUCTIONS,
MIN_HARVESTED_BUSHELS_PER_ACRE,
MAX_HARVESTED_BUSHELS_PER_ACRE,
YEARS
)!;
set_style(DEFAULT_INK);
};
// User input {{{1
// =============================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn pause(prompt: str = "> ") void = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
free(accept_string(prompt));
};
fn accept_integer(prompt: str = "") (int | ...strconv::error) = {
const s = accept_string(prompt);
defer free(s);
return strconv::stoi(s);
};
// Accept an integer from the user with the given prompt; if the input is an
// invalid integer, return -1.
//
fn get_integer(prompt: str) int = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return match (accept_integer(prompt)) {
case let number: int => yield number;
case => yield -1;
};
};
// Random numbers {{{1
// =============================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
// Return a random number from 1 to 5 (inclusive).
//
fn random_1_to_5() int = {
return random::u64n(&rand, 5): int + 1;
};
// Strings {{{1
// =============================================================
// Return a string with the proper wording for `n` persons, using the given or
// default words for singular and plural forms, and a flag indicating whether
// the string must be freed by the caller or not.
//
fn persons(n: int, singular: str = "person", plural: str = "people") (str, bool) = {
switch (n) {
case 0 => return ("nobody", false);
case 1 => return (fmt::asprintf("one {}", singular)!, true);
case => return (fmt::asprintf("{} {}", n, plural)!, true);
};
};
fn ordinal_suffix(n: int) str = {
switch (n) {
case 1 => return "st";
case 2 => return "nd";
case 3 => return "rd";
case => return "th";
};
};
// Game {{{1
// =============================================================================
// Return a string with the description of the given year as the previous one;
// return also a flag indicating whether the string wether must be freed by the
// caller or not.
//
fn previous(year: int) (str, bool) = {
if (year == 0) {
return ("the previous year", false);
} else {
return (
fmt::asprintf(
"your {}{} year",
year,
ordinal_suffix(year)
)!,
true
);
};
};
fn print_annual_report(year: int) void = {
clear_screen();
set_style(SPEECH_INK);
fmt::println("Hammurabi, I beg to report to you.")!;
set_style(DEFAULT_INK);
const (year_text, year_text_must_be_freed) = previous(year);
const (persons_text, persons_text_must_be_freed) = persons(dead);
const (infants_text, infants_text_must_be_freed) = persons(infants);
fmt::printf(
"\nIn {}, {} starved and {} {} born.\n",
year_text,
persons_text,
infants_text,
if (infants > 1) "were" else "was"
)!;
if (year_text_must_be_freed) {
free(year_text);
};
if (persons_text_must_be_freed) {
free(persons_text);
};
if (infants_text_must_be_freed) {
free(infants_text);
};
population += infants;
if (year > 0 && random::f64rand(&rand) <= PLAGUE_CHANCE) {
population = (population / 2): int;
set_style(WARNING_INK);
fmt::println("A horrible plague struck! Half the people died.")!;
set_style(DEFAULT_INK);
};
fmt::printfln("The population is {}.", population)!;
fmt::println("The city owns", acres, "acres.")!;
fmt::printf(
"You harvested {} bushels ({} per acre).\n",
bushels_harvested,
bushels_harvested_per_acre
)!;
if (bushels_eaten_by_rats > 0) {
fmt::println("The rats ate", bushels_eaten_by_rats, "bushels.")!;
};
fmt::println("You have", bushels_in_store, "bushels in store.")!;
bushels_harvested_per_acre =
(RANGE_OF_HARVESTED_BUSHELS_PER_ACRE: f64 * random::f64rand(&rand)): int +
MIN_HARVESTED_BUSHELS_PER_ACRE;
fmt::println("Land is trading at", bushels_harvested_per_acre, "bushels per acre.\n")!;
};
fn say_bye() void = {
set_style(DEFAULT_INK);
fmt::println("\nSo long for now.\n")!;
};
fn quit_game() void = {
say_bye();
os::exit(0);
};
fn relinquish() void = {
set_style(SPEECH_INK);
fmt::println("\nHammurabi, I am deeply irritated and cannot serve you anymore.")!;
fmt::println("Please, get yourself another steward!")!;
set_style(DEFAULT_INK);
quit_game();
};
fn increase_irritation() void = {
irritation += 1 + random::u64n(&rand, IRRITATION_STEP): int;
if (irritation >= MAX_IRRITATION) {
relinquish(); // this never returns
};
};
fn print_irritated(adverb: str) void = {
fmt::printfln("The steward seems {} irritated.", adverb)!;
};
fn show_irritation() void = {
if (irritation < IRRITATION_STEP * 2) {
print_irritated("slightly");
} else if (irritation < IRRITATION_STEP * 3) {
print_irritated("quite");
} else if (irritation < IRRITATION_STEP * 4) {
print_irritated("very");
} else {
print_irritated("profoundly");
};
};
// Print a message begging to repeat an ununderstandable input.
//
fn beg_repeat() void = {
increase_irritation(); // this may never return
set_style(SPEECH_INK);
fmt::println("I beg your pardon? I did not understand your order.")!;
set_style(DEFAULT_INK);
show_irritation();
};
// Print a message begging to repeat a wrong input, because there's only `n`
// items of `name`.
//
fn beg_think_again(n: int, name: str) void = {
increase_irritation(); // this may never return
set_style(SPEECH_INK);
fmt::printfln("I beg your pardon? You have only {} {}. Now then…", n, name)!;
set_style(DEFAULT_INK);
show_irritation();
};
// Buy or sell land.
//
fn trade() void = {
let acres_to_buy: int = 0;
let acres_to_sell: int = 0;
for (true) {
acres_to_buy = get_integer("How many acres do you wish to buy? (0 to sell): ");
if (acres_to_buy < 0) {
beg_repeat(); // this may never return
continue;
};
if (bushels_harvested_per_acre * acres_to_buy <= bushels_in_store) {
break;
};
beg_think_again(bushels_in_store, "bushels of grain");
};
if (acres_to_buy != 0) {
fmt::printfln("You buy {} acres.", acres_to_buy)!;
acres += acres_to_buy;
bushels_in_store -= bushels_harvested_per_acre * acres_to_buy;
fmt::printfln("You now have {} acres and {} bushels.", acres, bushels_in_store)!;
} else {
for (true) {
acres_to_sell = get_integer("How many acres do you wish to sell?: ");
if (acres_to_sell < 0) {
beg_repeat(); // this may never return
continue;
};
if (acres_to_sell < acres) {
break;
};
beg_think_again(acres, "acres");
};
if (acres_to_sell > 0) {
fmt::printfln("You sell {} acres.", acres_to_sell)!;
acres -= acres_to_sell;
bushels_in_store += bushels_harvested_per_acre * acres_to_sell;
fmt::printfln("You now have {} acres and {} bushels.", acres, bushels_in_store)!;
};
};
};
// Feed the people.
//
fn feed() void = {
for (true) {
bushels_to_feed_with = get_integer("How many bushels do you wish to feed your people with?: ");
if (bushels_to_feed_with < 0) {
beg_repeat(); // this may never return
continue;
};
// Trying to use more grain than is in silos?
if (bushels_to_feed_with <= bushels_in_store) {
break;
};
beg_think_again(bushels_in_store, "bushels of grain");
};
fmt::printfln("You feed your people with {} bushels.", bushels_to_feed_with)!;
bushels_in_store -= bushels_to_feed_with;
fmt::printfln("You now have {} bushels.", bushels_in_store)!;
};
// Seed the land.
//
fn seed() void = {
let acres_to_seed:int = 0;
for (true) {
acres_to_seed = get_integer("How many acres do you wish to seed?: ");
if (acres_to_seed < 0) {
beg_repeat(); // this may never return
continue;
};
if (acres_to_seed == 0) {
break;
};
// Trying to seed more acres than you own?
if (acres_to_seed > acres) {
beg_think_again(acres, "acres");
continue;
};
const message = fmt::asprintf(
"bushels of grain,\nand one bushel can seed {} acres",
ACRES_A_BUSHEL_CAN_SEED
)!;
defer free(message);
// Enough grain for seed?
if ((acres_to_seed / ACRES_A_BUSHEL_CAN_SEED): int > bushels_in_store) {
beg_think_again(bushels_in_store, message);
continue;
};
// Enough people to tend the crops?
if (acres_to_seed <= ACRES_A_PERSON_CAN_SEED * population) {
break;
};
const message = fmt::asprintf(
"people to tend the fields,\nand one person can seed {} acres",
ACRES_A_PERSON_CAN_SEED
)!;
defer free(message);
beg_think_again(population, message);
};
let bushels_used_for_seeding = (acres_to_seed / ACRES_A_BUSHEL_CAN_SEED): int;
fmt::printfln("You seed {} acres using {} bushels.", acres_to_seed, bushels_used_for_seeding)!;
bushels_in_store -= bushels_used_for_seeding;
fmt::printfln("You now have {} bushels.", bushels_in_store)!;
// A bountiful harvest!
bushels_harvested_per_acre = random_1_to_5();
bushels_harvested = acres_to_seed * bushels_harvested_per_acre;
bushels_in_store += bushels_harvested;
};
fn is_even(n: int) bool = {
return n % 2 == 0;
};
fn check_rats() void = {
let rat_chance = random_1_to_5();
bushels_eaten_by_rats = if (is_even(rat_chance)) (bushels_in_store / rat_chance): int else 0;
bushels_in_store -= bushels_eaten_by_rats;
};
// Set the variables to their values in the first year.
//
fn init() void = {
dead = 0;
total_dead = 0;
starved_people_percentage = 0;
population = 95;
infants = 5;
acres = ACRES_PER_PERSON * (population + infants);
bushels_harvested_per_acre = 3;
bushels_harvested = acres * bushels_harvested_per_acre;
bushels_eaten_by_rats = 200;
bushels_in_store = bushels_harvested - bushels_eaten_by_rats;
irritation = 0;
};
fn print_result(r: result) void = {
set_style(RESULT_INK);
switch (r) {
case result::VERY_GOOD =>
fmt::println("A fantastic performance! Charlemagne, Disraeli and Jefferson combined could")!;
fmt::println("not have done better!")!;
case result::NOT_TOO_BAD =>
fmt::println("Your performance could have been somewat better, but really wasn't too bad at")!;
fmt::printf(
"all. {} people would dearly like to see you assassinated, but we all have our\n",
((population): f64 * 0.8 * random::f64rand(&rand)): int
)!;
fmt::println("trivial problems.")!;
case result::BAD =>
fmt::println("Your heavy-handed performance smacks of Nero and Ivan IV. The people")!;
fmt::println("(remaining) find you an unpleasant ruler and, frankly, hate your guts!")!;
case result::VERY_BAD =>
fmt::println("Due to this extreme mismanagement you have not only been impeached and thrown")!;
fmt::println("out of office but you have also been declared national fink!!!")!;
};
set_style(DEFAULT_INK);
};
fn print_final_report() void = {
clear_screen();
if (starved_people_percentage > 0) {
fmt::printf(
"In your {}-year term of office, {} percent of the\n",
YEARS,
starved_people_percentage
)!;
fmt::printf(
"population starved per year on the average, i.e., a total of {} people died!\n\n",
total_dead
)!;
};
let acres_per_person = acres / population;
fmt::printf(
"You started with {} acres per person and ended with {}.\n\n",
ACRES_PER_PERSON,
acres_per_person
)!;
if (starved_people_percentage > 33 || acres_per_person < 7) {
print_result(result::VERY_BAD);
} else if (starved_people_percentage > 10 || acres_per_person < 9) {
print_result(result::BAD);
} else if (starved_people_percentage > 3 || acres_per_person < 10) {
print_result(result::NOT_TOO_BAD);
} else {
print_result(result::VERY_GOOD);
};
};
fn check_starvation(year: int) void = {
// How many people has been fed?
let fed_people = (bushels_to_feed_with / BUSHELS_TO_FEED_A_PERSON): int;
if (population > fed_people) {
dead = population - fed_people;
starved_people_percentage = ((year - 1) * starved_people_percentage + dead * 100 / population) / year;
population -= dead;
total_dead += dead;
// Starve enough for impeachment?
if (dead > (0.45 * (population): f64): int) {
set_style(WARNING_INK);
fmt::println("\nYou starved", dead, "people in one year!!!\n")!;
set_style(DEFAULT_INK);
print_result(result::VERY_BAD);
quit_game();
};
};
};
fn govern() void = {
init();
print_annual_report(0);
for (let year = 1; year <= YEARS; year += 1) {
trade();
feed();
seed();
check_rats();
// Let's have some babies
infants = (random_1_to_5() * (20 * acres + bushels_in_store) / population / 100 + 1): int;
check_starvation(year);
pause("\nPress the Enter key to read the annual report. ");
print_annual_report(year);
};
};
// Main {{{1
// =============================================================
export fn main() void = {
randomize();
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-17.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Global variables and constants {{{1
// =============================================================
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
def INITIAL_DISTANCE = 100;
def INITIAL_BULLETS = 4;
def MAX_WATERING_TROUGHS = 3;
let distance: int = 0; // distance between both gunners, in paces
let player_bullets: int = 0;
let opponent_bullets: int = 0;
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
// Print the given prompt and wait until the user enters a string.
//
fn get_string(prompt: str = "") str = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return accept_string(prompt);
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
fn accept_integer() (int | ...strconv::error) = {
const s = accept_string();
defer free(s);
return strconv::stoi(s);
};
fn prompted_integer(prompt: str) (int | ...strconv::error) = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
return accept_integer();
};
fn prompted_integer_or_0(prompt: str) int = {
match (prompted_integer(prompt)) {
case let i: int =>
return i;
case =>
return 0;
};
};
// Print the given prompt and wait until the user enters an integer.
//
fn get_integer(prompt: str = "") int = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return prompted_integer_or_0(prompt);
};
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "n", "no", "nope" =>
yield true;
case =>
yield false;
};
};
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt: str) bool = {
for (true) {
let answer = get_string(prompt);
defer free(answer);
if (is_yes(answer)) {
return true;
};
if (is_no(answer)) {
return false;
};
};
};
// Title, instructions and credits {{{1
// =============================================================
// Print the title at the current cursor position.
//
fn print_title() void = {
set_style(TITLE_INK);
fmt::println("High Noon")!;
set_style(DEFAULT_INK);
};
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Designed and programmend by Chris Gaylo, 1970.")!;
fmt::println(" http://mybitbox.com/highnoon-1970/")!;
fmt::println(" http://mybitbox.com/highnoon/")!;
fmt::println("Transcriptions:")!;
fmt::println(" https://github.com/MrMethor/Highnoon-BASIC/")!;
fmt::println(" https://github.com/mad4j/basic-highnoon/")!;
fmt::println("Version modified for QB64:")!;
fmt::println(" By Daniele Olmisani, 2014.")!;
fmt::println(" https://github.com/mad4j/basic-highnoon/")!;
fmt::println("This improved remake in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
fn print_instructions() void = {
print_title();
set_style(INSTRUCTIONS_INK);
defer set_style(DEFAULT_INK);
fmt::println("\nYou have been challenged to a showdown by Black Bart, one of")!;
fmt::println("the meanest desperadoes west of the Allegheny mountains.")!;
fmt::println("\nWhile you are walking down a dusty, deserted side street,")!;
fmt::println("Black Bart emerges from a saloon one hundred paces away.")!;
fmt::printf("\nBy agreement, you each have {} bullets in your six-guns.", INITIAL_BULLETS)!;
fmt::println("\nYour marksmanship equals his. At the start of the walk nei-")!;
fmt::println("ther of you can possibly hit the other, and at the end of")!;
fmt::println("the walk, neither can miss. the closer you get, the better")!;
fmt::println("your chances of hitting black Bart, but he also has beter")!;
fmt::println("chances of hitting you.")!;
};
// Game loop {{{1
// =============================================================
fn plural_suffix(n: int) str = {
switch (n) {
case 1 => return "";
case => return "s";
};
};
fn print_shells_left() void = {
if (player_bullets == opponent_bullets) {
fmt::printfln("Both of you have {} bullets.", player_bullets)!;
} else {
fmt::printf(
"You now have {} bullet{} to Black Bart's {} bullet{}.\n",
player_bullets,
plural_suffix(player_bullets),
opponent_bullets,
plural_suffix(opponent_bullets))!;
};
};
fn print_check() void = {
fmt::println("******************************************************")!;
fmt::println("* *")!;
fmt::println("* BANK OF DODGE CITY *")!;
fmt::println("* CASHIER'S RECEIT *")!;
fmt::println("* *")!;
fmt::printfln("* CHECK NO. {:_04} AUGUST {}TH, 1889 *",
random::u64n(&rand, 1000),
10: u64 + random::u64n(&rand, 10: u64))!;
fmt::println("* *")!;
fmt::println("* *")!;
fmt::println("* PAY TO THE BEARER ON DEMAND THE SUM OF *")!;
fmt::println("* *")!;
fmt::println("* TWENTY THOUSAND DOLLARS-------------------$20,000 *")!;
fmt::println("* *")!;
fmt::println("******************************************************")!;
};
fn get_reward() void = {
fmt::println("As mayor of Dodge City, and on behalf of its citizens,")!;
fmt::println("I extend to you our thanks, and present you with this")!;
fmt::println("reward, a check for $20,000, for killing Black Bart.\n\n")!;
print_check();
fmt::println("\n\nDon't spend it all in one place.")!;
};
fn move_the_opponent() void = {
let paces = 2 + random::u64n(&rand, 8): int;
fmt::printfln("Black Bart moves {} paces.", paces)!;
distance -= paces;
};
// 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.
//
fn maybe_move_the_opponent(silent: bool = false) bool = {
if (random::u64n(&rand, 2) == 0) { // 50% chances
move_the_opponent();
return true;
} else {
if (! silent) {
fmt::println("Black Bart stands still.")!;
};
return false;
};
};
fn missed_shot() bool = {
return random::f64rand(&rand) * 10.0 <= (distance / 10): f64;
};
// Handle the opponent's shot and return a flag with the result: if the
// opponent kills the player, return `true`; otherwise return `false`.
//
fn the_opponent_fires_and_kills(player_strategy: str) bool = {
fmt::println("Black Bart fires…")!;
opponent_bullets -= 1;
if (missed_shot()) {
fmt::println("A miss…")!;
switch (opponent_bullets) {
case 3 =>
fmt::println("Whew, were you lucky. That bullet just missed your head.")!;
case 2 =>
fmt::println("But Black Bart got you in the right shin.")!;
case 1 =>
fmt::println("Though Black Bart got you on the left side of your jaw.")!;
case 0 =>
fmt::println("Black Bart must have jerked the trigger.")!;
case =>
void;
};
} else {
if (player_strategy == "j") {
fmt::println("That trick just saved yout life. Black Bart's bullet")!;
fmt::println("was stopped by the wood sides of the trough.")!;
} else {
fmt::println("Black Bart shot you right through the heart that time.")!;
fmt::println("You went kickin' with your boots on.")!;
return true;
};
};
return false;
};
// 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`.
//
fn the_opponent_kills_or_runs(player_strategy: str) bool = {
if (distance >= 10 || player_bullets == 0) {
if (maybe_move_the_opponent(true)) {
return false;
};
};
if (opponent_bullets > 0) {
return the_opponent_fires_and_kills(player_strategy);
} else {
if (player_bullets > 0) {
if (random::u64n(&rand, 2) == 0) { // 50% chances
fmt::println("Now is your chance, Black Bart is out of bullets.")!;
} else {
fmt::println("Black Bart just hi-tailed it out of town rather than face you")!;
fmt::println("without a loaded gun. You can rest assured that Black Bart")!;
fmt::println("won't ever show his face around this town again.")!;
return true;
};
};
};
return false;
};
fn play() void = {
distance = INITIAL_DISTANCE;
let watering_troughs = 0;
player_bullets = INITIAL_BULLETS;
opponent_bullets = INITIAL_BULLETS;
for :showdown (true) {
fmt::printfln("You are now {} paces apart from Black Bart.", distance)!;
print_shells_left();
set_style(INSTRUCTIONS_INK);
fmt::println("\nStrategies:")!;
fmt::println(" [A]dvance")!;
fmt::println(" [S]tand still")!;
fmt::println(" [F]ire")!;
fmt::println(" [J]ump behind the watering trough")!;
fmt::println(" [G]ive up")!;
fmt::println(" [T]urn tail and run")!;
set_style(DEFAULT_INK);
let player_strategy = ascii::strlower(get_string("What is your strategy?! "))!;
defer free(player_strategy);
switch (player_strategy) {
case "a" => // advance
for (true) {
let paces = get_integer("How many paces do you advance? ");
if (paces < 0) {
fmt::println("None of this negative stuff, partner, only positive numbers.")!;
} else if (paces > 10) {
fmt::println("Nobody can walk that fast.")!;
} else {
distance -= paces;
break;
};
};
case "s" => // stand still
fmt::println("That move made you a perfect stationary target.")!;
case "f" => // fire
if (player_bullets == 0) {
fmt::println("You don't have any bullets left.")!;
} else {
player_bullets -= 1;
if (missed_shot()) {
switch (player_bullets) {
case 2 =>
fmt::println("Grazed Black Bart in the right arm.")!;
case 1 =>
fmt::println("He's hit in the left shoulder, forcing him to use his right")!;
fmt::println("hand to shoot with.")!;
case =>
void;
};
fmt::println("What a lousy shot.")!;
if (player_bullets == 0) {
fmt::println("Nice going, ace, you've run out of bullets.")!;
if (opponent_bullets != 0) {
fmt::println("Now Black Bart won't shoot until you touch noses.")!;
fmt::println("You better think of something fast (like run).")!;
};
};
} else {
fmt::println("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 :showdown;
};
};
case "j" => // jump
if (watering_troughs == MAX_WATERING_TROUGHS) {
fmt::println("How many watering troughs do you think are on this street?")!;
player_strategy = "";
} else {
watering_troughs += 1;
fmt::println("You jump behind the watering trough.")!;
fmt::println("Not a bad maneuver to threw Black Bart's strategy off.")!;
};
case "g" => // give up
fmt::println("Black Bart accepts. The conditions are that he won't shoot you")!;
fmt::println("if you take the first stage out of town and never come back.")!;
if (yes("Agreed? ")) {
fmt::println("A very wise decision.")!;
break :showdown;
} else {
fmt::println("Oh well, back to the showdown.")!;
};
case "t" => // turn tail and run
// The more bullets of the opponent, the less chances to escape.
if (random::u64n(&rand, (opponent_bullets + 2): u64) == 0) {
fmt::println("Man, you ran so fast even dogs couldn't catch you.")!;
} else {
switch (opponent_bullets) {
case 0 =>
fmt::println("You were lucky, Black Bart can only throw his gun at you, he")!;
fmt::println("doesn't have any bullets left. You should really be dead.")!;
case 1 =>
fmt::println("Black Bart fires his last bullet…")!;
fmt::println("He got you right in the back. That's what you deserve, for running.")!;
case 2 =>
fmt::println("Black Bart fires and got you twice: in your back")!;
fmt::println("and your ass. Now you can't even rest in peace.")!;
case 3 =>
fmt::println("Black Bart unloads his gun, once in your back")!;
fmt::println("and twice in your ass. Now you can't even rest in peace.")!;
case 4 =>
fmt::println("Black Bart unloads his gun, once in your back")!;
fmt::println("and three times in your ass. Now you can't even rest in peace.")!;
case =>
void;
};
opponent_bullets = 0;
};
break :showdown;
case =>
fmt::println("You sure aren't going to live very long if you can't even follow directions.")!;
}; // strategy switch
if (the_opponent_kills_or_runs(player_strategy)) {
break;
};
if (player_bullets + opponent_bullets == 0) {
fmt::println("The showdown must end, because nobody has bullets left.")!;
break;
};
fmt::println()!;
}; // showdown loop
};
// Main {{{1
// =============================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn init() void = {
randomize();
};
export fn main() void = {
init();
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02.
//
// Last modified: 20260213T1645+0100.
use bufio;
use fmt;
use math;
use os;
use strconv;
use strings;
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn accept_f64(prompt: str = "") (f64 | ...strconv::error) = {
const s: str = accept_string(prompt);
defer free(s);
return strconv::stof64(s);
};
fn accept_valid_f64(prompt: str = "", error: str = "Real number expected.") f64 = {
for (true) {
match (accept_f64(prompt)) {
case let n: f64 =>
return n;
case =>
fmt::println(error)!;
};
};
};
export fn main() void = {
fmt::println()!;
const n: f64 = accept_valid_f64("Enter a number: ");
fmt::printfln("ABS({0}) -> math::abs({0}) -> {1}", n, math::absf64(n))!;
fmt::printfln("ATN({0}) -> math::atan({0}) -> {1}", n, math::atanf64(n))!;
fmt::printfln("COS({0}) -> math::cos_f64({0}) -> {1}", n, math::cosf64(n))!;
fmt::printfln("EXP({0}) -> math::expf64({0}) -> {1}", n, math::expf64(n))!;
fmt::printfln("INT({0}) -> ({0}): int -> {1}", n, n: int)!;
fmt::printfln("LOG({0}) -> math::log_f64({0}) -> {1}", n, math::logf64(n))!;
fmt::printfln(
"SGN({0}) -> if ({0} == 0.0) 0 else math::sign_f64({0}) -> {1}",
n,
if (n == 0.0) 0 else math::signf64(n)
)!;
fmt::printfln("SQR({0}) -> math::sqrt_f64({0}) -> {1}", n, math::sqrtf64(n))!;
fmt::printfln("TAN({0}) -> math::tan_f64({0}) -> {1}", n, math::tanf64(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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-14/15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Data {{{1
// =============================================================================
def GRID_SIZE = 10;
def TURNS = 10;
def MUGWUMPS = 4;
type Mugwump = struct {
x: int,
y: int,
hidden: bool,
};
let mugwump: [MUGWUMPS]Mugwump = [Mugwump{x = 0, y = 0, hidden = false}...];
let found: int = 0; // counter
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn accept_integer() (int | ...strconv::error) = {
const s = accept_string();
defer free(s);
return strconv::stoi(s);
};
fn prompted_integer(prompt: str) (int | ...strconv::error) = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
return accept_integer();
};
fn prompted_valid_integer(prompt: str, error: str = "Integer expected.") int = {
let result: int = 0;
for (true) {
match (prompted_integer(prompt)) {
case let i: int =>
result = i;
break;
case =>
fmt::println(error)!;
};
};
return result;
};
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "n", "no", "nope" =>
yield true;
case =>
yield false;
};
};
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt: str) bool = {
for (true) {
let answer = accept_string(prompt);
defer free(answer);
if (is_yes(answer)) {
return true;
};
if (is_no(answer)) {
return false;
};
};
};
// Credits and instructions {{{1
// =============================================================================
// Clear the screen, print the credits and ask the user to press enter.
//
fn print_credits() void = {
clear_screen();
fmt::println("Mugwump\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Written by Bud Valenti's students of Project SOLO (Pittsburg, Pennsylvania, USA).")!;
fmt::println(" Slightly modified by Bob Albrecht of People's Computer Company.")!;
fmt::println(" Published by Creative Computing (Morristown, New Jersey, USA), 1978.")!;
fmt::println(" - https://www.atariarchives.org/basicgames/showpage.php?page=114")!;
fmt::println(" - http://vintage-basic.net/games.html\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
accept_string("Press Enter to read the instructions. ");
};
// Clear the screen, print the instructions and ask the user to press enter.
//
fn print_instructions() void = {
clear_screen();
fmt::println("Mugwump\n")!;
fmt::println("The object of this game is to find four mugwumps")!;
fmt::println("hidden on a 10 by 10 grid. Homebase is position 0,0.")!;
fmt::println("Any guess you make must be two numbers with each")!;
fmt::println("number between 0 and 9, inclusive. First number")!;
fmt::println("is distance to right of homebase and second number")!;
fmt::println("is distance above homebase.\n")!;
fmt::printfln("You get {} tries. After each try, you will see", TURNS)!;
fmt::println("how far you are from each mugwump.\n")!;
accept_string("Press Enter to start. ");
};
// Game {{{1
// =============================================================================
// Init the mugwumps' positions, `hidden` flags and count.
//
fn hide_mugwumps() void = {
for (let m = 0; m < MUGWUMPS; m += 1) {
mugwump[m].x = random::u64n(&rand, GRID_SIZE): int;
mugwump[m].y = random::u64n(&rand, GRID_SIZE): int;
mugwump[m].hidden = true;
};
found = 0; // counter
};
// Print the given prompt, wait until the user enters a valid coord and return
// it.
//
fn get_coord(prompt: str) int = {
let coord: int = 0;
for (true) {
coord = prompted_valid_integer(prompt);
if (coord < 0 || coord >= GRID_SIZE) {
fmt::printfln("Invalid value {}: not in range [0, {}].", coord, GRID_SIZE - 1)!;
} else {
break;
};
};
return coord;
};
// Return `true` if the given mugwump is hidden in the given coords.
//
fn is_here(m: int, x: int, y: int) bool = {
return mugwump[m].hidden && mugwump[m].x == x && mugwump[m].y == y;
};
// Return the distance between the given mugwump and the given coords.
//
fn distance(m: int, x: int, y: int) int = {
return math::sqrtf64(
math::powf64((mugwump[m].x - x): f64, 2.0) +
math::powf64((mugwump[m].y - y): f64, 2.0)): int;
};
// Return a plural suffix (default: "s") if the given number is greater than 1
// otherwise return a singular suffix (default: an empty string).
//
fn plural(n: int, plural_suffix: str = "s", singular_suffix: str = "") str = {
return if (n > 1) plural_suffix else singular_suffix;
};
// Run the game.
//
fn play() void = {
let x: int = 0;
let y: int = 0;
let turn: int = 0; // counter
for (true) { // game
clear_screen();
hide_mugwumps();
for :turns_loop (let turn = 1; turn <= TURNS; turn += 1) {
fmt::printfln("Turn number {}\n", turn)!;
fmt::printfln("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): ");
fmt::printfln("\nYour guess is ({}, {}).", x, y)!;
for (let m = 0; m < MUGWUMPS; m += 1) {
if (is_here(m, x, y)) {
mugwump[m].hidden = false;
found += 1;
fmt::printfln("You have found mugwump {}!", m)!;
if (found == MUGWUMPS) {
break :turns_loop;
};
};
};
for (let m = 0; m < MUGWUMPS; m += 1) {
if (mugwump[m].hidden) {
fmt::printfln("You are {} units from mugwump {}.", distance(m, x, y) , m)!;
};
};
fmt::println()!;
}; // turns
if (found == MUGWUMPS) {
fmt::printfln("\nYou got them all in {} turn{}!\n", turn, plural(turn))!;
fmt::println("That was fun! let's play again…")!;
fmt::println("Four more mugwumps are now in hiding.")!;
} else {
fmt::printfln("\nSorry, that's {} tr{}.\n", TURNS, plural(TURNS, "ies", "y"))!;
fmt::println("Here is where they're hiding:")!;
for (let m = 0; m < MUGWUMPS; m += 1) {
if (mugwump[m].hidden) {
fmt::printfln("Mugwump {} is at ({}, {}).", m, mugwump[m].x, mugwump[m].y)!;
};
};
};
if (!yes("\nDo you want to play again? ")) {
break;
};
}; // game
};
// Main {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn init() void = {
randomize();
};
export fn main() void = {
init();
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-14/15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use bufio;
use fmt;
use os;
use strconv;
use strings;
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str) void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn accept_integer(prompt: str = "") (int | ...strconv::error) = {
const string: str = accept_string(prompt);
defer free(string);
return strconv::stoi(string);
};
fn accept_valid_integer(prompt: str = "", error: str = "Integer expected.") int = {
for (true) {
match (accept_integer(prompt)) {
case let number: int =>
return number;
case =>
fmt::println(error)!;
};
};
};
// Main {{{1
// =============================================================================
export fn main() void = {
const name: str = accept_string("What is your name? ");
defer free(name);
const number: int = accept_valid_integer("Enter a number: ");
for (let i = 0; i < (number): int; i += 1) {
fmt::printfln("Hello, {}!", name)!;
};
};
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
// Title and credits {{{1
// =============================================================================
// Print the title at the current cursor position.
//
fn print_title() void = {
set_style(TITLE_INK);
fmt::println("Poetry")!;
set_style(DEFAULT_INK);
};
// Print the credits at the current cursor position.
//
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Unknown author.")!;
fmt::println(" Published in \"BASIC Computer Games\",")!;
fmt::println(" Creative Computing (Morristown, New Jersey, USA), 1978.\n")!;
fmt::println("This improved remake in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
// Random numbers {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
// Main {{{1
// =============================================================================
// Is the given integer even?
//
fn is_even(n: int) bool = {
return n % 2 == 0;
};
fn play() void = {
randomize();
def MAX_PHRASES_AND_VERSES = 20;
// counters:
let action = 0;
let phrase = 0;
let phrases_and_verses = 0;
let verse_chunks = 0;
for :verse (true) {
let manage_the_verse_continuation = true;
let maybe_add_comma = true;
switch (action) {
case 0, 1 =>
switch (phrase) {
case 0 => fmt::print("MIDNIGHT DREARY")!;
case 1 => fmt::print("FIERY EYES")!;
case 2 => fmt::print("BIRD OR FIEND")!;
case 3 => fmt::print("THING OF EVIL")!;
case 4 => fmt::print("PROPHET")!;
case => void;
};
case 2 =>
switch (phrase) {
case 0 =>
fmt::print("BEGUILING ME")!;
verse_chunks = 2;
case 1 =>
fmt::print("THRILLED ME")!;
case 2 =>
fmt::print("STILL SITTING…")!;
maybe_add_comma = false;
case 3 =>
fmt::print("NEVER FLITTING")!;
verse_chunks = 2;
case 4 =>
fmt::print("BURNED")!;
case =>
void;
};
case 3 =>
switch (phrase) {
case 0 =>
fmt::print("AND MY SOUL")!;
case 1 =>
fmt::print("DARKNESS THERE")!;
case 2 =>
fmt::print("SHALL BE LIFTED")!;
case 3 =>
fmt::print("QUOTH THE RAVEN")!;
case 4 =>
if (verse_chunks != 0) {
fmt::print("SIGN OF PARTING")!;
};
case =>
void;
};
case 4 =>
switch (phrase) {
case 0 => fmt::print("NOTHING MORE")!;
case 1 => fmt::print("YET AGAIN")!;
case 2 => fmt::print("SLOWLY CREEPING")!;
case 3 => fmt::print("…EVERMORE")!;
case 4 => fmt::print("NEVERMORE")!;
case => void;
};
case 5 =>
action = 0;
fmt::println()!;
if (phrases_and_verses > MAX_PHRASES_AND_VERSES) {
fmt::println()!;
verse_chunks = 0;
phrases_and_verses = 0;
action = 2;
continue :verse;
} else {
manage_the_verse_continuation = false;
};
case =>
void;
};
if (manage_the_verse_continuation) {
time::sleep(250 * time::MILLISECOND);
if (maybe_add_comma && !(verse_chunks == 0 || random::f64rand(&rand) > 0.19)) {
fmt::print(",")!;
verse_chunks = 2;
};
if (random::f64rand(&rand) > 0.65) {
fmt::println()!;
verse_chunks = 0;
} else {
fmt::print(" ")!;
verse_chunks += 1;
};
};
action += 1;
phrase = random::u32n(&rand, 5): int;
phrases_and_verses += 1;
if (!(verse_chunks > 0 || is_even(action))) {
fmt::print(" ")!;
};
}; // verse loop
};
export fn main() void = {
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-13, 2025-02-15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use bufio;
use fmt;
use math::random;
use os;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
fn press_enter_to_start() void = {
press_enter("Press Enter to start. ");
};
// Credits and instructions {{{1
// =============================================================================
fn print_credits() void = {
clear_screen();
fmt::println("Russian Roulette\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Creative Computing (Morristown, New Jersey, USA), ca. 1980.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter_to_start();
};
fn print_instructions() void = {
clear_screen();
fmt::println("Here is a revolver.")!;
fmt::println("Type 'f' to spin chamber and pull trigger.")!;
fmt::println("Type 'g' to give up, and play again.")!;
fmt::println("Type 'q' to quit.\n")!;
};
// Main {{{1
// =============================================================================
fn play() void = {
let times = 0;
for (true) { // game loop
print_instructions();
times = 0;
for :play_loop (true) {
let command = accept_string("> ");
defer free(command);
switch (command) {
case "f" => // fire
if (random::u32n(&rand, 100) > 83) {
fmt::println("Bang! You're dead!")!;
fmt::println("Condolences will be sent to your relatives.")!;
break :play_loop;
} else {
times += 1;
if (times > 10) {
fmt::println("You win!")!;
fmt::println("Let someone else blow his brains out.")!;
break :play_loop;
} else {
fmt::println("Click.")!;
};
};
case "g" => // give up
fmt::println("Chicken!")!;
break :play_loop;
case "q" => // quit
return;
case =>
continue;
};
}; // play loop
press_enter_to_start();
}; // game loop
};
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn init() void = {
randomize();
};
fn bye() void = {
fmt::println("Bye!")!;
};
export fn main() void = {
init();
print_credits();
play();
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-17.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_cursor_position(line: int, column: int) void = {
fmt::printf("\x1B[{};{}H", line, column)!;
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_line_to_end() void = {
fmt::print("\x1B[K")!;
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Config {{{1
// =============================================================================
def TITLE = "Seance";
def MAX_SCORE = 50;
def MAX_MESSAGE_LENGTH: int = 6;
def MIN_MESSAGE_LENGTH: int = 3;
def BASE_CHARACTER = '@': int;
def PLANCHETTE = '*';
def FIRST_LETTER_NUMBER = 1;
def LAST_LETTER_NUMBER = 26;
def BOARD_INK = FOREGROUND + BRIGHT + CYAN;
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def MISTAKE_EFFECT_INK = FOREGROUND + BRIGHT + RED;
def PLANCHETTE_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
def INPUT_X: int = BOARD_X;
def INPUT_Y: int = BOARD_Y + BOARD_BOTTOM_Y + 4;
def MESSAGES_Y = INPUT_Y;
def MISTAKE_EFFECT_PAUSE = 3; // seconds
def BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD; // screen columns
def BOARD_BOTTOM_Y = BOARD_HEIGHT + 1; // relative to the board
def BOARD_HEIGHT = 5; // characters displayed on the left and right borders
def BOARD_PAD = 1; // blank characters separating the board from its left and right borders
def BOARD_WIDTH = 8; // characters displayed on the top and bottom borders
def BOARD_X = 29; // screen column
def BOARD_Y = 5; // screen line
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
// Credits and instructions {{{1
// =============================================================================
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Written by Chris Oxlade, 1983.")!;
fmt::println(" https://archive.org/details/seance.qb64")!;
fmt::println(" https://github.com/chaosotter/basic-games")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
fn print_instructions() void = {
print_title();
set_style(INSTRUCTIONS_INK);
defer set_style(DEFAULT_INK);
fmt::println("\nMessages from the Spirits are coming through, letter by letter. They want you")!;
fmt::println("to remember the letters and type them into the computer in the correct order.")!;
fmt::println("If you make mistakes, they will be angry -- very angry...")!;
fmt::println()!;
fmt::println("Watch for stars on your screen -- they show the letters in the Spirits'")!;
fmt::println("messages.")!;
};
// Random numbers {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn random_int_in_inclusive_range(min: int, max: int) int = {
return random::u32n(&rand, (max - min):u32): int + min;
};
// Game {{{1
// =============================================================================
// Return the x coordinate to print the given text centered on the board.
//
fn board_centered_x(text: str) int = {
return (BOARD_X + (BOARD_ACTUAL_WIDTH - len(text)) / 2): int;
};
// Print the given text on the given row, centered on the board.
//
fn print_board_centered(text: str, y: int) void = {
set_cursor_position(y, board_centered_x(text));
fmt::println(text)!;
};
// Print the title at the current cursor position.
//
fn print_title() void = {
set_style(TITLE_INK);
fmt::println(TITLE)!;
set_style(DEFAULT_INK);
};
// Print the title on the given row, centered on the board.
//
fn print_board_centered_title(y: int) void = {
set_style(TITLE_INK);
print_board_centered(TITLE, y);
set_style(DEFAULT_INK);
};
// Print the given letter at the given board coordinates.
//
fn print_character(y: int, x: int, a: rune) void = {
set_cursor_position(y + BOARD_Y, x + BOARD_X);
fmt::print(a)!;
bufio::flush(os::stdout)!;
};
fn print_board() void = {
set_style(BOARD_INK);
defer set_style(DEFAULT_INK);
for (let i = 1; i <= BOARD_WIDTH; i += 1) {
print_character(0, i + 1, (BASE_CHARACTER + i): rune); // top border
print_character(BOARD_BOTTOM_Y, i + 1, (BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1): rune); // bottom border
};
for (let i = 1; i <= BOARD_HEIGHT; i += 1) {
print_character(i , 0, (BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1): rune); // left border
print_character(i , 3 + BOARD_WIDTH, (BASE_CHARACTER + BOARD_WIDTH + i): rune); // right border
};
fmt::println()!;
};
fn erase_line_from(line: int, column: int) void = {
set_cursor_position(line, column);
erase_line_to_end();
};
fn wait_seconds(seconds: int) void = {
time::sleep(seconds * time::SECOND);
};
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
fn print_mistake_effect(effect: str) void = {
let x = board_centered_x(effect);
hide_cursor();
set_cursor_position(MESSAGES_Y, x);
set_style(MISTAKE_EFFECT_INK);
fmt::println(effect)!;
set_style(DEFAULT_INK);
wait_seconds(MISTAKE_EFFECT_PAUSE);
erase_line_from(MESSAGES_Y, x);
show_cursor();
};
// Return a new message of the given length, after marking its letters on the
// board.
//
fn message(length: int) str = {
let y: int = 0;
let x: int = 0;
let letters: []rune = [];
hide_cursor();
for (let i = 0; i <= length; i += 1) {
let letter_number: int = random_int_in_inclusive_range(
FIRST_LETTER_NUMBER,
LAST_LETTER_NUMBER
);
append(letters, (BASE_CHARACTER + letter_number): rune)!;
if (letter_number <= BOARD_WIDTH) {
// top border
y = 1;
x = letter_number + 1;
} else if (letter_number <= BOARD_WIDTH + BOARD_HEIGHT) {
// right border
y = letter_number - BOARD_WIDTH;
x = 2 + BOARD_WIDTH;
} else if (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;
};
set_style(PLANCHETTE_INK);
print_character(y, x, PLANCHETTE);
set_style(DEFAULT_INK);
wait_seconds(1);
print_character(y, x, ' ');
};
show_cursor();
return strings::fromrunes(letters)!;
};
fn accept_message() str = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
set_cursor_position(INPUT_Y, INPUT_X);
defer erase_line_from(INPUT_Y, INPUT_X);
return ascii::strupper(accept_string("? "))!;
};
fn play() void = {
let score = 0;
let mistakes = 0;
print_board_centered_title(1);
print_board();
for (true) {
let message_length = random_int_in_inclusive_range(
MIN_MESSAGE_LENGTH,
MAX_MESSAGE_LENGTH
);
let message_received = message(message_length);
defer free(message_received);
let message_understood = accept_message();
defer free(message_understood);
if (message_received != message_understood) {
mistakes += 1;
switch (mistakes) {
case 1 =>
print_mistake_effect("The table begins to shake!");
case 2 =>
print_mistake_effect("The light bulb shatters!");
case 3 =>
print_mistake_effect("Oh, no! A pair of clammy hands grasps your neck!");
return;
case =>
void;
};
} 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;
};
};
};
};
// Main {{{1
// =============================================================================
export fn main() void = {
randomize();
set_style(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();
fmt::println()!;
};
Sine Wave
// Sine Wave
//
// Original version in BASIC:
// Creative Computing (Morristown, New Jersey, USA), ca. 1980.
//
// This version in Hare:
// Copyright (c) 2024, 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2024-12-15, 2025-02-12, 2025-02-15.
//
// Last modified: 20260213T1645+0100.
use bufio;
use encoding::utf8;
use fmt;
use io;
use math;
use os;
use strings;
fn clear_screen() void = {
fmt::print("\x1B[0;0H\x1B[2J")!;
};
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str = "") void = {
free(accept_string(prompt));
};
fn get_words() [2]str = {
clear_screen();
let word: [2]str = ["", ""];
let order: [2]str = ["first", "second"];
for (let n = 0; n <= 1; n += 1) {
for (word[n] == "") {
fmt::printfln("Enter the {} word: ", order[n])!;
word[n] = accept_string();
};
};
return word;
};
fn print_credits() void = {
clear_screen();
fmt::println("Sine Wave\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Creative Computing (Morristown, New Jersey, USA), ca. 1980.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2024, 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter("Press Enter to start the program. ");
};
fn bool_to_int(flag: bool) int = {
return if (flag) 1 else 0;
};
fn draw(word: [2]str) void = {
clear_screen();
let even = false;
for (let angle = 0.0; angle <= 40.0; angle += 0.25) {
let spaces = (26.0 + math::floorf64(25.0 * math::sinf64(angle))): int;
for (let i = 0; i < spaces; i += 1) {
fmt::print(' ')!;
};
fmt::println(word[bool_to_int(even)])!;
even = !even;
};
};
export fn main() void = {
print_credits();
draw(get_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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-15, 2025-04-07.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Data {{{1
// =============================================================================
def REELS = 3;
const image: [_]str = [" BAR ", " BELL ", "ORANGE", "LEMON ", " PLUM ", "CHERRY"];
def BAR = 0; // position of "BAR" in `image`.
const color: [_]int = [
FOREGROUND + WHITE,
FOREGROUND + CYAN,
FOREGROUND + YELLOW,
FOREGROUND + BRIGHT + YELLOW,
FOREGROUND + BRIGHT + WHITE,
FOREGROUND + BRIGHT + RED ];
def MAX_BET = 100;
def MIN_BET = 1;
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
fn accept_integer() (int | ...strconv::error) = {
const s = accept_string();
defer free(s);
return strconv::stoi(s);
};
fn prompted_integer(prompt: str) (int | ...strconv::error) = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
return accept_integer();
};
fn prompted_integer_or_0(prompt: str) int = {
match (prompted_integer(prompt)) {
case let i: int =>
return i;
case =>
return 0;
};
};
// Credits and instructions {{{1
// =============================================================================
fn print_credits() void = {
clear_screen();
fmt::println("Slots")!;
fmt::println("A slot machine simulation.\n")!;
fmt::println("Original version in BASIC:")!;
fmt::println(" Creative computing (Morristown, New Jersey, USA).")!;
fmt::println(" Produced by Fred Mirabelle and Bob Harper on 1973-01-29.\n")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair\n")!;
press_enter("Press Enter for instructions. ");
};
fn print_instructions() void = {
clear_screen();
fmt::println("You are in the H&M casino, in front of one of our")!;
fmt::printf("one-arm bandits. Bet from {} to {} USD (or 0 to quit).\n\n",
MIN_BET, MAX_BET)!;
press_enter("Press Enter to start. ");
};
// Game {{{1
// =============================================================================
fn won(prize: int, bet: int) int = {
switch (prize) {
case 2 => fmt::println("DOUBLE!")!;
case 5 => fmt::println("*DOUBLE BAR*")!;
case 10 => fmt::println("**TOP DOLLAR**")!;
case 100 => fmt::println("***JACKPOT***")!;
case => void ;
};
fmt::println("You won!")!;
return (prize + 1) * bet;
};
fn show_standings(usd: int) void = {
fmt::printfln("Your standings are {} USD.", usd)!;
};
fn print_reels(reel: *[REELS]int) void = {
move_cursor_home();
for (let r .. reel) {
set_style(color[r]);
fmt::printf("[{}] ", image[r])!;
};
set_style(NORMAL_STYLE);
fmt::println("")!;
};
fn init_reels(reel: *[REELS]int) void = {
let images = (len(image)): int;
for (let i = 0; i < len(reel): int; i += 1) {
reel[i] = random::u32n(&rand, images: u32): int;
};
};
fn spin_reels(reel: *[REELS]int) void = {
def DURATION_IN_SECONDS = 2;
hide_cursor();
let first_second = time::now(time::clock::MONOTONIC).sec;
for ((time::now(time::clock::MONOTONIC).sec - first_second) < DURATION_IN_SECONDS) {
init_reels(reel);
print_reels(reel);
};
show_cursor();
};
fn bool_to_int(flag: bool) int = {
return if (flag) 1 else 0;
};
// Return the number of equals and bars in the given `reel` array.
//
fn prize(reel: *[REELS]int) (int, int) = {
// Count the number of equals
let equals: int = 0;
for (let first = 0; first < len(reel): int; first += 1) {
for (let second = first + 1; second < len(reel): int; second += 1) {
equals += bool_to_int(reel[first] == reel[second]);
};
};
equals += bool_to_int(equals > 0 && equals < len(reel): int);
// Count the number of bars
let bars: int = 0;
for (let i = 0; i < len(reel): int; i += 1) {
bars += bool_to_int(reel[i] == BAR);
};
return (equals, bars);
};
fn play() void = {
let standings = 0;
let bet = 0;
let reel: [REELS]int = [0, 0, 0];
let equals = 0;
let bars = 0;
init_reels(&reel);
for :play_loop (true) {
for :bet_loop (true) {
clear_screen();
print_reels(&reel);
bet = prompted_integer_or_0("Your bet (or 0 to quit): ");
if (bet > MAX_BET) {
fmt::printfln("House limits are {} USD.", MAX_BET)!;
press_enter("Press Enter to try again. ");
} else if (bet < MIN_BET) {
let confirmation = accept_string("Type \"q\" to confirm you want to quit. ");
defer free(confirmation);
if (confirmation == "q" || confirmation == "Q") {
break :play_loop;
};
} else {
break :bet_loop;
}; // bet check
}; // bet loop
clear_screen();
spin_reels(&reel);
let prize_details = prize(&reel);
equals = prize_details.0;
bars = prize_details.1;
switch (equals) {
case 3 =>
if (bars == 3) {
standings += won(100, bet);
} else {
standings += won(10, bet);
};
case 2 =>
if (bars == 2) {
standings += won(5, bet);
} else {
standings += won(2, bet);
};
case =>
fmt::println("You lost.")!;
standings -= bet;
}; // prize check
show_standings(standings);
press_enter("Press Enter to continue. ");
}; // play loop
show_standings(standings);
if (standings < 0) {
fmt::println("Pay up! Please leave your money on the terminal.")!;
} else if (standings > 0) {
fmt::println("Collect your winnings from the H&M cashier.")!;
} else {
fmt::println("Hey, you broke even.")!;
};
};
// Main {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn init() void = {
randomize();
};
export fn main() void = {
init();
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-14/15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use os;
use strconv;
use strings;
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str) void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn accept_integer() (int | ...strconv::error) = {
const s = accept_string();
defer free(s);
return strconv::stoi(s);
};
fn prompted_integer(prompt: str) (int | ...strconv::error) = {
print_prompt(prompt);
return accept_integer();
};
fn prompted_valid_integer(prompt: str, error: str = "Integer expected.") int = {
let result: int = 0;
for (true) {
match (prompted_integer(prompt)) {
case let i: int =>
result = i;
break;
case =>
fmt::println(error)!;
};
};
return result;
};
// Main {{{1
// =============================================================================
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
fn repeat(s: str, n: int) str = {
let result = "";
for (let i: int = 0; i < n; i += 1) {
result = strings::concat(result, s)!;
};
return result;
};
export fn main() void = {
const name: str = accept_string("What is your name? ");
defer free(name);
fmt::printfln("Hello, {}.", name)!;
let number: int = 0;
for (true) {
number = prompted_valid_integer("How many stars do you want? ");
let stars = repeat("*", number);
defer free(stars);
fmt::println(stars)!;
print_prompt("Do you want more stars? ");
let answer: str = accept_string();
defer free(answer);
if (!is_yes(answer)) {
break;
};
};
};
Strings
// Strings
//
// Original version in BASIC:
// Example included in Vintage BASIC 1.0.3.
// http://www.vintage-basic.net
//
// This version in Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-14/15.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use bufio;
use fmt;
use os;
use strconv;
use strings;
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str) void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn accept_integer() (int | ...strconv::error) = {
const s = accept_string();
defer free(s);
return strconv::stoi(s);
};
fn prompted_integer(prompt: str) (int | ...strconv::error) = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
return accept_integer();
};
fn prompted_valid_integer(prompt: str, error: str = "Integer expected.") int = {
let result: int = 0;
for (true) {
match (prompted_integer(prompt)) {
case let i: int =>
result = i;
break;
case =>
fmt::println(error)!;
};
};
return result;
};
// Main {{{1
// =============================================================================
fn repeat(s: str, n: int) str = {
let result = "";
for (let i: int = 0; i < n; i += 1) {
result = strings::concat(result, s)!;
};
return result;
};
export fn main() void = {
let s = accept_string("Enter a string: ");
defer free(s);
let n = prompted_valid_integer("Enter an integer: ");
fmt::println()!;
fmt::printf("ASC(\"{}\") --> ", s)!;
fmt::printf("strings::toutf8(\"{}\"[0]): int --> ", s)!;
fmt::printfln("{}", strings::toutf8(s)[0]: int)!;
fmt::printf("CHR$({}) --> ", n)!;
fmt::printfln("{}: rune --> '{}'", n, n: rune)!;
// XXX FIXME When `n` is greater than the string length, the behaviour
// is different from the original `LEFT$`, `MID$` and `RIGHT$`.
fmt::printf("LEFT$(\"{}\", {}) --> ", s, n)!;
fmt::printf("strings::sub(\"{}\", 0, {}: size) --> ", s, n)!;
fmt::printfln("\"{}\"", strings::sub(s, 0, n: size))!;
fmt::printf("MID$(\"{}\", {}) --> ", s, n)!;
fmt::printf("strings::sub(\"{0}\", ({1} - 1): size, len(\"{0}\")) --> ", s, n)!;
fmt::printfln("\"{}\"", strings::sub(s, (n - 1): size, len(s)))!;
fmt::printf("MID$(\"{}\", {}, 3) --> ", s, n)!;
fmt::printf("strings::sub(\"{0}\", ({1} - 1): size, ({1} - 1 + 3): size) --> ", s, n)!;
fmt::printfln("\"{}\"", strings::sub(s, (n - 1): size, (n - 1 + 3): size))!;
fmt::printf("RIGHT$(\"{}\", {}) --> ", s, n)!;
fmt::printf("strings::sub(\"{0}\", len(\"{0}\") - {1}: size, len(\"{0}\")) --> ", s, n)!;
fmt::printfln("\"{}\"", strings::sub(s, len(s) - n: size, len(s)))!;
fmt::printf("LEN(\"{}\") --> ", s)!;
fmt::printfln("len(\"{}\") --> {}", s, len(s))!;
fmt::printf("VAL(\"{}\") --> ", s)!;
fmt::printf("match (strconv::stof64(\"{}\")) {{ case let i: int => yield i; case => yield 0; }}", s)!;
fmt::printfln(" --> {}",
match (strconv::stof64(s)) {
case let i: f64 =>
yield i;
case =>
yield 0;
}
)!;
fmt::printf("STR$({}) --> ", n)!;
fmt::printfln("fmt::asprint({}) --> \"{}\"", n, fmt::asprint(n)!)!;
fmt::printf("SPC({}) --> ", n)!;
fmt::printfln("repeat(\" \", {}) --> \"{}\"", n, repeat(" ", n))!;
fmt::printf("NB: `repeat` is an ad hoc function.")!;
};
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 Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2025-02-17/18.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_cursor_position(line: int, column: int) void = {
fmt::printf("\x1B[{};{}H", line, column)!;
};
fn set_cursor_coord(coord: (int, int)) void = {
const (line, column) = coord;
set_cursor_position(line, column);
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_line_to_end() void = {
fmt::print("\x1B[K")!;
};
fn erase_screen_to_end() void = {
fmt::print("\x1B[J")!;
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Data {{{1
// =============================================================
def BOARD_INK = FOREGROUND + BRIGHT + CYAN;
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
def BLANK = "*";
def GRID_HEIGHT = 3; // cell rows
def GRID_WIDTH = 3; // cell columns
def CELLS = GRID_WIDTH * GRID_HEIGHT;
const pristine_grid: [CELLS]str = [""...];
def GRIDS_ROW = 3; // screen row where the grids are printed
def GRIDS_COLUMN = 5; // screen column where the left grid is printed
def CELLS_GAP = 2; // distance between the grid cells, in screen rows or columns
def GRIDS_GAP = 16; // screen columns between equivalent cells of the grids
def FIRST_PLAYER = 0;
def MAX_PLAYERS = 4;
let grid: [MAX_PLAYERS][CELLS]str = [[""...]...];
let is_playing: [MAX_PLAYERS]bool = [false...];
let players: int = 0;
def QUIT_COMMAND = "X";
// User input {{{1
// =============================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn accept_integer() (int | ...strconv::error) = {
const s = accept_string();
defer free(s);
return strconv::stoi(s);
};
fn prompted_integer(prompt: str) (int | ...strconv::error) = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
return accept_integer();
};
fn prompted_integer_or_0(prompt: str) int = {
match (prompted_integer(prompt)) {
case let i: int =>
return i;
case =>
return 0;
};
};
fn get_integer(prompt: str = "") int = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return prompted_integer_or_0(prompt);
};
fn get_string(prompt: str = "") str = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return accept_string(prompt);
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "n", "no", "nope" =>
yield true;
case =>
yield false;
};
};
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt: str) bool = {
for (true) {
let answer = get_string(prompt);
if (is_yes(answer)) {
return true;
};
if (is_no(answer)) {
return false;
};
};
};
// Title, instructions and credits {{{1
// =============================================================
fn print_title() void = {
set_style(TITLE_INK);
fmt::println("Xchange")!;
set_style(DEFAULT_INK);
};
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Written by Thomas C. McIntire, 1979.")!;
fmt::println(" Published in \"The A to Z Book of Computer Games\", 1979.")!;
fmt::println(" https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up")!;
fmt::println(" https://github.com/chaosotter/basic-games")!;
fmt::println("This improved remake in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
fn print_instructions() void = {
print_title();
set_style(INSTRUCTIONS_INK);
defer set_style(DEFAULT_INK);
fmt::println("\nOne or two may play. If two, you take turns. A grid looks like this:\n")!;
set_style(BOARD_INK);
fmt::println(" F G D")!;
fmt::printfln(" A H {}", BLANK)!;
fmt::println(" E B C\n")!;
set_style(INSTRUCTIONS_INK);
fmt::println("But it should look like this:\n")!;
set_style(BOARD_INK);
fmt::println(" A B C")!;
fmt::println(" D E F")!;
fmt::printfln(" G H {}\n", BLANK)!;
set_style(INSTRUCTIONS_INK);
fmt::printfln("You may exchange any one letter with the '{}', but only one that's adjacent:", BLANK)!;
fmt::println("above, below, left, or right. Not all puzzles are possible, and you may enter")!;
fmt::printfln("'{}' to give up.\n", QUIT_COMMAND)!;
fmt::println("Here we go...")!;
};
// Grids {{{1
// =============================================================
// Print the given player's grid title.
//
fn print_grid_title(player: int) void = {
set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP));
fmt::printf("Player {}", player + 1)!;
};
// Return the cursor position of the given player's grid cell.
//
fn cell_position(player: int, cell: int) (int, int) = {
const grid_row: int = cell / GRID_HEIGHT;
const grid_column: int = cell % GRID_WIDTH;
const title_margin: int = if (players > 1) 2 else 0;
const row: int = GRIDS_ROW + title_margin + grid_row;
const column: int = GRIDS_COLUMN +
(grid_column * CELLS_GAP) +
(player * GRIDS_GAP);
return (row, column);
};
// Return the cursor position of the given player's grid prompt.
//
fn grid_prompt_position_of(player: int) (int, int) = {
const coord = cell_position(player, CELLS);
const row = coord.0;
const column = coord.1;
return (row + 1, column);
};
// Print the given player's grid, in the given or default color.
//
fn print_grid(player: int, color: int = BOARD_INK) void = {
if (players > 1) {
print_grid_title(player);
};
set_style(color);
defer set_style(DEFAULT_INK);
for (let cell = 0; cell < CELLS; cell += 1) {
set_cursor_coord(cell_position(player, cell));
fmt::print(grid[player][cell])!;
};
};
// Print the current players' grids.
//
fn print_grids() void = {
for (let player = 0; player < players; player += 1) {
if (is_playing[player]) {
print_grid(player);
};
};
fmt::println()!;
erase_screen_to_end();
};
// Scramble the grid of the given player.
//
fn scramble_grid(player: int) void = {
for (let cell = 0; cell < CELLS; cell += 1) {
const random_cell = random::u64n(&rand, CELLS);
// Exchange the contents of the current cell with that of the random one.
const tmp = grid[player][cell];
grid[player][cell] = grid[player][random_cell];
grid[player][random_cell] = tmp;
};
};
// Init the grids.
//
fn init_grids() void = {
grid[0] = pristine_grid;
scramble_grid(0);
for (let player = 1; player < players; player += 1) {
grid[player] = grid[0];
};
};
// Messages {{{1
// =============================================================
// Return a message prefix for the given player.
//
fn player_prefix(player: int) str = {
return if (players > 1) fmt::asprintf("Player {}: ", player + 1)! else "";
};
// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
fn message_position(player: int, row_inc: int = 0) (int, int) = {
let (prompt_row, _) = grid_prompt_position_of(player);
return (prompt_row + 2 + row_inc, 1);
};
// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
fn print_message(message: str, player: int, row_inc: int = 0) void = {
set_cursor_coord(message_position(player, row_inc));
fmt::printf("{}{}", player_prefix(player), message)!;
erase_line_to_end();
fmt::println()!;
};
// Erase the last message about the given player.
//
fn erase_message(player: int) void = {
set_cursor_coord(message_position(player));
erase_line_to_end();
};
// Game loop {{{1
// =============================================================
// Return a message with the players range.
//
fn players_range_message() str = {
if (MAX_PLAYERS == 2) {
return "1 or 2";
} else {
return fmt::asprintf("from 1 to {}", MAX_PLAYERS)!;
};
};
// Return the number of players, asking the user if needed.
//
fn number_of_players() int = {
let players = 0;
print_title();
fmt::println()!;
if (MAX_PLAYERS == 1) {
players = 1;
} else {
for (players < 1 || players > MAX_PLAYERS) {
const prompt = fmt::asprintf("Number of players ({}): ", players_range_message())!;
defer free(prompt);
players = get_integer(prompt);
};
};
return players;
};
// Is the given cell the first one on a grid row?
//
fn is_first_cell_of_a_grid_row(cell: int) bool = {
return cell % GRID_WIDTH == 0;
};
// Is the given cell the last one on a grid row?
//
fn is_last_cell_of_a_grid_row(cell: int) bool = {
return (cell + 1) % GRID_WIDTH == 0;
};
// Are the given cells adjacent?
//
fn are_cells_adjacent(cell_1: int, cell_2: int) bool = {
return (cell_2 == cell_1 + 1 && !is_first_cell_of_a_grid_row(cell_2)) ||
(cell_2 == cell_1 + GRID_WIDTH) ||
(cell_2 == cell_1 - 1 && !is_last_cell_of_a_grid_row(cell_2)) ||
(cell_2 == cell_1 - GRID_WIDTH);
};
type invalid = !void;
// If the given player's character cell is a valid move, i.e. it is adjacent to
// the blank cell, return the blank cell; otherwise return the `invalid` type
// error.
//
fn position_to_cell(player: int, char_cell: int) (int | invalid) = {
for (let cell = 0; cell < CELLS; cell += 1) {
if (grid[player][cell] == BLANK) {
if (are_cells_adjacent(char_cell, cell)) {
return cell;
} else {
break;
};
};
};
print_message(fmt::asprintf("Illegal move \"{}\".", grid[player][char_cell])!, player);
return invalid;
};
// If the given player's command is valid, i.e. a grid character, return its
// position; otherwise return the `invalid` error type.
//
fn command_to_position(player: int, command: str) (int | invalid) = {
if (command != BLANK) {
for (let position = 0; position < CELLS; position += 1) {
if (command == grid[player][position]) {
return position;
};
};
};
print_message(fmt::asprintf("Invalid character \"{}\".", command)!, player);
return invalid;
};
// Forget the given player, who quitted.
//
fn forget_player(player: int) void = {
is_playing[player] = false;
print_grid(player, DEFAULT_INK);
};
// Play the turn of the given player.
//
fn play_turn(player: int) void = {
let blank_position: int = 0;
let character_position: int = 0;
if (is_playing[player]) {
for (true) {
for (true) {
const coord = grid_prompt_position_of(player);
set_cursor_coord(coord);
erase_line_to_end();
set_cursor_coord(coord);
const command = ascii::strupper(strings::trim(get_string("Move: ")))!;
defer free(command);
if (command == QUIT_COMMAND) {
forget_player(player);
return;
};
match (command_to_position(player, command)) {
case let position: int =>
character_position = position;
break;
case =>
void;
};
};
match (position_to_cell(player, character_position)) {
case let position: int =>
blank_position = position;
break;
case =>
void;
};
};
erase_message(player);
grid[player][blank_position] = grid[player][character_position];
grid[player][character_position] = BLANK;
};
};
// Play the turns of all players.
//
fn play_turns() void = {
for (let player = 0; player < players; player += 1) {
play_turn(player);
};
};
// Is someone playing?
//
fn is_someone_playing() bool = {
for (let player = 0; player < players; player += 1) {
if (is_playing[player]) {
return true;
};
};
return false;
};
fn has_an_empty_grid(player: int) bool = {
for (let i = 0; i < CELLS; i += 1) {
if (grid[player][i] != "") {
return false;
};
};
return true;
};
// Has someone won? If so, print a message for every winner and return `true`
// otherwise just return `false`.
//
fn has_someone_won() bool = {
let winners = 0;
for (let player = 0; player < players; player += 1) {
if (is_playing[player]) {
if (has_an_empty_grid(player)) {
winners += 1;
if (winners > 0) {
print_message(
fmt::asprintf("You're the winner{}!", if (winners > 1) ", too" else "")!,
player,
winners - 1);
};
};
};
};
return winners > 0;
};
// Init the game.
//
fn init_game() void = {
clear_screen();
players = number_of_players();
for (let player = 0; player < players; player += 1) {
is_playing[player] = true;
};
clear_screen();
print_title();
init_grids();
print_grids();
};
// Play the game.
//
fn play() void = {
init_game();
for (is_someone_playing()) {
play_turns();
print_grids();
if (has_someone_won()) {
break;
};
};
};
// Main {{{1
// =============================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
// Init the program, i.e. just once before the first game.
//
fn init_once() void = {
randomize();
// Init the pristine grid.
def FIRST_CHAR_CODE = 'A': int;
for (let cell = 0; cell < CELLS - 1; cell += 1) {
pristine_grid[cell] = strings::fromrunes([(FIRST_CHAR_CODE + cell): rune])!;
};
pristine_grid[CELLS - 1] = BLANK;
};
// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
fn enough() bool = {
set_cursor_coord(grid_prompt_position_of(FIRST_PLAYER));
return !yes("Another game? ");
};
export fn main() void = {
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. ");
for (true) {
play();
if (enough()) {
break;
};
};
fmt::println("So long…")!;
};
