Basics of Odin
Description of the page content
Conversion of old BASIC programs to Odin in order to learn the basics of this language.
Tags:
3D Plot
/*
3D Plot
Original version in BASIC:
Creative Computing (Morristown, New Jersey, USA), ca. 1980.
This version in Odin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-02/03, 2023-08/09, 2023-12.
Last modified: 20241229T1712+0100.
*/
package three_d_plot
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math"
SPACE :: ' '
DOT :: '*'
WIDTH :: 56
// Display the credits and wait for a keypress.
//
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
read.a_prompted_string("Press Enter to start the program. ")
}
a :: proc(z : f64) -> f64 {
return 30 * math.exp(-z * z / 100)
}
draw :: proc() {
l := 0
z := 0
y1 := 0
line : [WIDTH]rune
for x := -30.0; x <= 30.0; x += 1.5 {
for pos in 0 ..< WIDTH {
line[pos] = SPACE
}
l = 0
y1 = 5 * int(math.sqrt(900 - x * x) / 5)
for y := y1; y >= -y1; y += -5 {
z = int(25 + a(math.sqrt(x * x + f64(y * y))) - 0.7 * f64(y))
if z > l {
l = z
line[z] = DOT
}
} // y loop
for pos in 0 ..< WIDTH {
fmt.print(line[pos])
}
fmt.println("")
} // x loop
}
main :: proc() {
term.clear_screen()
print_credits()
term.clear_screen()
draw()
}
Bagels
/*
Bagels
Original version in BASIC:
D. Resek, P. Rowe, 1978.
Creative Computing (Morristown, New Jersey, USA), 1978.
This version in Odin:
Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-04, 2023-09/10, 2023-12, 2024-12, 2025-02.
Last modified: 20250407T1920+0200.
*/
package bagels
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:slice"
import "core:strings"
import "core:unicode"
press_enter :: proc(prompt: string) {
s, _ := read.a_prompted_string(prompt)
delete(s)
}
// Clear the screen, display the credits and wait for a keypress.
//
print_credits :: proc() {
term.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 Odin:")
fmt.println(" Copyright (c) 2023, 2024, 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.
//
print_instructions :: proc() {
term.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")
press_enter("\nPress Enter to start. ")
}
DIGITS :: 3
random_digit : [DIGITS]int
// Return three random digits.
//
random :: proc() -> []int {
for i in 0 ..< DIGITS {
digit_loop: for {
random_digit[i] = rand.int_max(10)
for j in 0 ..< i {
if i != j && random_digit[i] == random_digit[j] do continue digit_loop
}
break
}
}
return random_digit[:]
}
// Return `true` if any of the given numbers is repeated; otherwise return
// `false`.
//
is_any_repeated :: proc(num : []int) -> bool {
for n0, i0 in num[:] {
for n1 in num[i0 + 1 : len(num)] {
if n0 == n1 do return true
}
}
return false
}
// Print the given prompt and update the array whose address is given with a
// three-digit number from the user.
//
get_input :: proc(prompt : string, user_digit : ^[DIGITS]int) {
// XXX TODO Create a local array, appending every digit after checking the contents,
// making `is_any_repeated` unnecessary.
ASCII_0 :: 48
get_loop: for {
input, ok := read.a_prompted_string(prompt)
defer delete(input)
if !ok do input = ""
if len(input) != DIGITS {
fmt.printfln("Remember it's a %v-digit number.", DIGITS)
continue get_loop
}
for digit, pos in input {
if unicode.is_digit(digit) {
user_digit^[pos] = int(digit) - 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
}
}
// Return `true` if the given string is "yes" or a synonym.
//
is_yes :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"ok", "y", "yeah", "yes"}, lowercase_s)
}
// Return `true` if the given string is "no" or a synonym.
//
is_no :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"n", "no", "nope"}, lowercase_s)
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
yes :: proc(prompt : string) -> bool {
for {
answer, ok := read.a_prompted_string(prompt)
defer delete(answer)
if !ok do answer = ""
if is_yes(answer) do return true
if is_no(answer) do return false
}
}
// Init and run the game loop.
//
play :: proc() {
TRIES :: 20
score := 0
fermi : int // counter
pico : int // counter
user_number : [DIGITS]int
for {
term.clear_screen()
computer_number := random()
fmt.println("O.K. I have a number in mind.")
for guess in 1 ..= TRIES {
// fmt.printfln("My number: %v", computer_number) // XXX TMP
get_input(fmt.tprintf("Guess #%2v: ", guess), &user_number)
fermi = 0
pico = 0
for i in 0 ..< DIGITS {
for j in 0 ..< DIGITS {
if user_number[i] == computer_number[j] {
if i == j {
fermi += 1
} else {
pico += 1
}
}
}
}
picos := strings.repeat("PICO ", pico)
defer delete(picos)
fermis := strings.repeat("FERMI ", fermi)
defer delete(fermis)
fmt.print(picos)
fmt.print(fermis)
if pico + fermi == 0 do fmt.print("BAGELS")
fmt.println()
if fermi == DIGITS do break
}
if fermi == DIGITS {
fmt.println("You got it!!!")
score += 1
} else {
fmt.println("Oh well.")
fmt.printf("That's %v guesses. My number was ", TRIES)
for i in 0 ..< DIGITS do fmt.print(computer_number[i])
fmt.println(".")
}
if !yes("Play again? ") do break
}
if score != 0 {
fmt.printfln("A %v-point bagels, buff!!", score)
}
fmt.println("Hope you had fun. Bye.")
}
main :: proc() {
print_credits()
print_instructions()
play()
}
Bug
/*
Bug
Original version in BASIC:
Brian Leibowitz, 1978.
Creative Computing (Morristown, New Jersey, USA), 1978.
This version in Odin:
Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-03, 2023-09, 2023-10, 2023-12, 2024-05, 2024-07, 2024-12, 2025-02.
Last modified: 20250227T1849+0100.
*/
package bug
import "../lib/anodino/src/read"
import "../lib/anodino/src/str"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:strings"
// Bug type.
//
Bug :: struct {
body : bool,
neck : bool,
head : bool,
feelers : int,
feeler_type : rune,
tail : bool,
legs : int,
}
// Player type.
//
Player :: struct {
pronoun : string,
possessive : string,
bug : Bug,
}
// Players.
//
computer, human : Player
// Bug body parts.
//
Part :: enum {body = 1, neck, head, feeler, tail, leg}
// Bug body attributes.
//
BODY_HEIGHT :: 2
FEELER_LENGTH :: 4
LEG_LENGTH :: 2
MAX_FEELERS :: 2
MAX_LEGS :: 6
NECK_LENGTH :: 2
// Move the cursor to the previous row, without changing the column position,
// and erase its line.
//
erase_previous_line :: proc() {
term.move_cursor_up()
term.erase_line()
}
// Clear the screen, display the credits and wait for a keypress.
//
print_credits :: proc() {
term.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 Odin:")
fmt.println(" Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
s, _ := read.a_prompted_string("Press Enter to read the instructions. ")
delete(s)
}
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:
`
// Print a table with the bug parts' description.
//
print_parts_table :: proc() {
COLUMNS :: 3
COLUMN_WIDTH :: 8
COLUMN_SEPARATION :: 2
// Headers
header := []string{"Number", "Part", "Quantity"}
for i in 0 ..< COLUMNS {
justified_header := strings.left_justify(header[i], COLUMN_WIDTH + COLUMN_SEPARATION, " ")
defer delete(justified_header)
fmt.print(justified_header)
}
fmt.println()
// Rulers
ruler := strings.repeat("-", COLUMN_WIDTH)
defer delete(ruler)
padding := strings.repeat(" ", COLUMN_SEPARATION)
defer delete(padding)
for i in 0 ..< COLUMNS {
fmt.print(ruler, i == COLUMNS - 1 ? "" : padding, sep = "")
}
fmt.println()
// Data
part_quantity : [len(Part) + 1]int
part_quantity[Part.body] = 1
part_quantity[Part.neck] = 1
part_quantity[Part.head] = 1
part_quantity[Part.feeler] = 2
part_quantity[Part.tail] = 1
part_quantity[Part.leg] = 6
for part in Part {
part_number := fmt.tprint(int(part))
justified_part_number := strings.left_justify(part_number, COLUMN_WIDTH + COLUMN_SEPARATION, " ")
defer delete(justified_part_number)
part_name := fmt.tprint(part)
capital_part_name := str.to_capital(part_name)
defer delete(capital_part_name)
justified_part_name := strings.left_justify(capital_part_name, COLUMN_WIDTH + COLUMN_SEPARATION, " ")
defer delete(justified_part_name)
fmt.println(justified_part_number, justified_part_name, part_quantity[part], sep = "")
}
}
// Clear the screen, print the instructions and wait for a keypress.
//
print_instructions :: proc() {
term.clear_screen()
fmt.println("Bug")
fmt.println(INSTRUCTIONS)
print_parts_table()
s, _ := read.a_prompted_string("\nPress Enter to start. ")
delete(s)
}
// Print a bug head.
//
print_head :: proc() {
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.
//
print_bug :: proc(bug : Bug) {
if bug.feelers > 0 {
for _ in 0 ..< FEELER_LENGTH {
fmt.print(" ")
for _ in 0 ..< bug.feelers {
fmt.print(" ", bug.feeler_type)
}
fmt.println()
}
}
if bug.head {
print_head()
}
if bug.neck {
for _ in 0 ..< NECK_LENGTH {
fmt.println(" N N")
}
}
if bug.body {
fmt.println(" BBBBBBBBBBBB")
for _ in 0 ..< BODY_HEIGHT {
fmt.println(" B B")
}
if bug.tail {
fmt.println("TTTTTB B")
}
fmt.println(" BBBBBBBBBBBB")
}
if bug.legs > 0 {
for _ in 0 ..< LEG_LENGTH {
fmt.print(" ")
for _ in 0 ..< bug.legs {
fmt.print(" L")
}
fmt.println()
}
}
}
// Return `true` if the given bug is finished; otherwise return `false`.
//
finished :: proc(bug : Bug) -> bool {
return bug.feelers == MAX_FEELERS && bug.tail && bug.legs == MAX_LEGS
}
// Return a random number from 1 to 6 (inclusive).
//
dice :: proc() -> int {
return rand.int_max(6) + 1
}
// Array to convert a number to its equilavent text.
//
as_text := []string{
"no",
"a",
"two",
"three",
"four",
"five",
"six" } // MAX_LEGS
// Return a string containing the given number and noun in their proper form.
//
plural :: proc(number : int, noun : string) -> string {
return fmt.tprint(as_text[number], " ", noun, (number > 1) ? "s" : "", sep = "")
}
// Add the given part to the given player's bug.
//
add_part :: proc(part : Part, player : ^Player) -> bool {
changed : bool = false
#partial switch (part) {
case 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 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 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 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 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 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
}
// Ask the user to press the Enter key, wait for the input, then erase the
// prompt text.
//
prompt :: proc() {
s, _ := read.a_prompted_string("Press Enter to roll the dice. ")
delete(s)
erase_previous_line()
}
// Play one turn for the given player, rolling the dice and updating his bug.
//
turn :: proc(player : ^Player) {
prompt()
number : int = dice()
part := Part(number)
pronoun := str.to_capital(player^.pronoun)
defer delete(pronoun)
fmt.printf("%s rolled a %i (%s)", pronoun, number, part)
if add_part(part, player) {
fmt.println()
print_bug(player^.bug)
}
fmt.println()
}
// Print a message about the winner.
//
print_winner :: proc() {
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.
//
game_over :: proc() -> bool {
return finished(human.bug) || finished(computer.bug)
}
// Execute the game loop.
//
play :: proc() {
term.clear_screen()
for !game_over() {
turn(&human)
turn(&computer)
}
print_winner()
}
// Init the players' data before a new game.
//
init :: proc() {
human.pronoun = "you"
human.possessive = "Your"
human.bug.feeler_type = 'A'
computer.pronoun = "I"
computer.possessive = "My"
computer.bug.feeler_type = 'F'
}
main :: proc() {
init()
print_credits()
print_instructions()
play()
fmt.println("I hope you enjoyed the game, play it again soon!!")
}
Bunny
/*
Bunny
Original version in BASIC:
Creative Computing (Morristown, New Jersey, USA), 1978.
This version in Odin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-03, 2023-08/09, 2023-11, 2023-12.
Last modified: 20250101T2036+0100.
*/
package bunny
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
// Print the credits and wait for a keypress.
//
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
read.a_prompted_string("Press Enter to start the program. ")
}
WIDTH :: 53
line : [WIDTH]u8 // buffer
// Clear the line buffer with spaces.
//
clear_line :: proc() {
for column in 0 ..< len(line) {
line[column] = ' '
}
}
letter : []u8 = {'B', 'U', 'N', 'N', 'Y'}
letters : i8 = i8(len(letter))
EOL :: -1 // end of line identifier
data : []i8 = {
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 }
data_len := len(data)
data_index := 0 // data pointer
datum :: proc() -> i8 {
data_index += 1
return data[data_index - 1]
}
// Draw the graphic out of `data` and `letter`.
//
draw :: proc() {
clear_line()
for data_index < data_len {
first_column := datum()
if first_column == EOL {
fmt.printfln("%s", line)
clear_line()
} else {
last_column := datum()
for column in first_column ..= last_column {
line[column] = letter[column % letters]
}
}
}
}
main :: proc() {
term.clear_screen()
print_credits()
term.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
Version in Oberon-07:
Copyright (c) 2022, 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
This version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2023-12-15, 2025-02-27.
Last modified 20250227T1852+0100.
*/
package chase
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:slice"
import "core:strings"
// Globals {{{1
// =============================================================
// Colors
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
TITLE_INK :: basic.BRIGHT_RED
// Arena
ARENA_WIDTH :: 20
ARENA_HEIGHT :: 10
ARENA_LAST_X :: ARENA_WIDTH - 1
ARENA_LAST_Y :: ARENA_HEIGHT - 1
ARENA_ROW :: 3
arena : [ARENA_HEIGHT][ARENA_WIDTH]string
EMPTY :: " "
FENCE :: "X"
MACHINE :: "m"
HUMAN :: "@"
FENCES :: 15 // inner obstacles, not the border
// The end
End :: enum {
Not_Yet,
Quit,
Electrified,
Killed,
Victory,
}
the_end : End
// Machines
MACHINES :: 5
MACHINES_DRAG :: 2 // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
machine_x : [MACHINES]int
machine_y : [MACHINES]int
operative : [MACHINES]bool
destroyed_machines : int // counter
// Human
human_x : int
human_y : int
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
input_string :: proc(prompt : string = "") -> (string, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string()
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
s, _ := input_string(prompt)
delete(s)
}
// Return `true` if the given string is "yes" or a synonym.
//
is_yes :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"ok", "y", "yeah", "yes"}, lowercase_s)
}
// Return `true` if the given string is "no" or a synonym.
//
is_no :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"n", "no", "nope"}, lowercase_s)
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
yes :: proc(prompt : string) -> bool {
for {
answer, _ := input_string(prompt)
defer delete(answer)
if is_yes(answer) do return true
if is_no(answer) do return false
}
}
// Title, credits and instructions {{{1
// =============================================================
TITLE :: "Chase"
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println(TITLE)
basic.color(DEFAULT_INK)
}
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair")
}
// Print the game instructions and wait for a key.
//
print_instructions :: proc() {
print_title()
basic.color(INSTRUCTIONS_INK)
defer basic.color(DEFAULT_INK)
fmt.printfln("\nYou (%v) are in a high voltage maze with %v", HUMAN, MACHINES)
fmt.printfln("security machines (%v) trying to kill you.", MACHINE)
fmt.printfln("You must maneuver them into the maze (%v) 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.")
}
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
print_arena :: proc() {
term.set_cursor_position(ARENA_ROW, 1)
for y in 0 ..= ARENA_LAST_Y {
for x in 0 ..= ARENA_LAST_X {
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`.
//
is_border :: proc(y, x: int) -> bool {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X)
}
// Return a random integer in the given inclusive range.
//
random_int_in_inclusive_range :: proc(min, max : int) -> int {
return rand.int_max(max - min + 1) + min
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
place :: proc(s: string) -> (y, x : int) {
for {
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 do break
}
arena[y][x] = s
return y, x
}
// Inhabit the arena with the machines, the inner fences and the human.
//
inhabit_arena :: proc() {
for m in 0 ..< MACHINES {
machine_y[m], machine_x[m] = place(MACHINE)
operative[m] = true
}
for _ in 0 ..< FENCES do place(FENCE)
human_y, human_x = place(HUMAN)
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
clean_arena :: proc() {
for y in 0 ..= ARENA_LAST_Y {
for x in 0 ..= ARENA_LAST_X {
arena[y][x] = is_border(y, x) ? FENCE : EMPTY
}
}
}
// Game {{{1
// =============================================================
// Init the game.
//
init_game :: proc() {
clean_arena()
inhabit_arena()
destroyed_machines = 0
the_end = .Not_Yet
}
// Move the given machine.
//
move_machine :: proc(m: int) {
maybe : int
arena[machine_y[m]][machine_x[m]] = EMPTY
maybe = rand.int_max(2)
if machine_y[m] > human_y {
machine_y[m] -= maybe
} else if machine_y[m] < human_y {
machine_y[m] += maybe
}
maybe = rand.int_max(2)
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 = .Victory
}
} else if arena[machine_y[m]][machine_x[m]] == HUMAN {
the_end = .Killed
}
}
// Maybe move the given operative machine.
//
maybe_move_machine :: proc(m: int) {
if rand.int_max(MACHINES_DRAG) == 0 do move_machine(m)
}
// Move the operative machines.
//
move_machines :: proc() {
for m in 0 ..< MACHINES {
if operative[m] do maybe_move_machine(m)
}
}
// Read a user command; update `the_end` accordingly and return the direction
// increments.
//
get_move :: proc() -> (y_inc, x_inc : int) {
y_inc = 0
x_inc = 0
fmt.println()
term.erase_line_right()
raw_command, _ := input_string("Command: ")
defer delete(raw_command)
command := strings.to_lower(raw_command)
defer delete(command)
switch command {
case "q" : the_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
}
return y_inc, x_inc
}
play :: proc() {
y_inc, x_inc : int
for { // game loop
term.clear_screen()
print_title()
init_game()
for { // action loop
print_arena()
y_inc, x_inc = get_move()
if the_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 = .Electrified
} else if arena[human_y + y_inc][human_x + x_inc] == MACHINE {
the_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 != .Not_Yet do break
}// action loop
#partial switch the_end {
case .Quit :
fmt.println("\nSorry to see you quit.")
case .Electrified :
fmt.println("\nZap! You touched the fence!")
case .Killed :
fmt.println("\nYou have been killed by a lucky machine.")
case .Victory :
fmt.println("\nYou are lucky, you destroyed all machines.")
}
if ! yes("\nDo you want to play again? ") do break
} // game loop
fmt.println("\nHope you don't feel fenced in.")
fmt.println("Try again sometime.")
}
// Main {{{1
// =============================================================
main :: proc() {
basic.randomize()
basic.color(DEFAULT_INK)
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
term.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 Odin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2023-03-01.
Last modified: 20241226T2046+0100.
*/
package diamond
import "core:fmt"
LINES :: 17
main :: proc() {
for i in 1 ..= LINES / 2 + 1 {
for _ in 1 ..= (LINES + 1) / 2 - i + 1 {
fmt.print(" ")
}
for _ in 1 ..= i * 2 - 1 {
fmt.print("*")
}
fmt.println()
}
for i in 1 ..= LINES / 2 {
for _ in 1 ..= i + 1 {
fmt.print(" ")
}
for _ in 1 ..= ((LINES + 1) / 2 - i) * 2 - 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 Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-09, 2023-10, 2025-02.
Last modified: 20250227T1855+0100.
Acknowledgment:
The following Python port was used as a reference of the original
variables: <https://github.com/jquast/hamurabi.py>.
*/
package hammurabi
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:os"
ACRES_A_BUSHEL_CAN_SEED :: 2 // yearly
ACRES_A_PERSON_CAN_SEED :: 10 // yearly
ACRES_PER_PERSON :: 10 // to calculate the initial acres of the city
BUSHELS_TO_FEED_A_PERSON :: 20 // yearly
IRRITATION_LEVELS :: 5 // after the switch in `show_irritation`
IRRITATION_STEP : int : MAX_IRRITATION / IRRITATION_LEVELS
MAX_HARVESTED_BUSHELS_PER_ACRE :: MIN_HARVESTED_BUSHELS_PER_ACRE + RANGE_OF_HARVESTED_BUSHELS_PER_ACRE - 1
MIN_HARVESTED_BUSHELS_PER_ACRE :: 17
MAX_IRRITATION :: 16
PLAGUE_CHANCE :: 0.15 // 15% yearly
RANGE_OF_HARVESTED_BUSHELS_PER_ACRE :: 10
YEARS :: 10 // goverment period
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
RESULT_INK :: basic.BRIGHT_CYAN
SPEECH_INK :: basic.PINK
TITLE_INK :: basic.BRIGHT_WHITE
WARNING_INK :: basic.BRIGHT_RED
Result :: enum {
Very_Good,
Not_Too_Bad,
Bad,
Very_Bad,
}
acres : int
bushels_eaten_by_rats : int
bushels_harvested : int
bushels_harvested_per_acre : int
bushels_in_store : int
bushels_to_feed_with : int
dead : int
infants : int
irritation : int // counter (0 ..= 99)
population : int
starved_people_percentage : int
total_dead : int
// Return a random number from 1 to 5 (inclusive).
//
random_1_to_5 :: proc() -> int {
return rand.int_max(5) + 1
}
// Return the proper wording for `n` persons, using the given or default words
// for singular and plural forms.
//
persons :: proc(
n: int,
singular := "person",
plural := "people"
) -> string {
switch n {
case 0 : return "nobody"
case 1 : return fmt.tprint("one", singular)
case : return fmt.tprint(n, plural)
}
}
instructions :: proc() -> string {
return fmt.tprintf(
`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 %v and %v 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 %v-year term of office.`,
MIN_HARVESTED_BUSHELS_PER_ACRE, MAX_HARVESTED_BUSHELS_PER_ACRE, YEARS)
}
print_instructions :: proc() {
basic.color(INSTRUCTIONS_INK)
fmt.println(instructions())
basic.color(DEFAULT_INK)
}
pause :: proc(prompt : string = "> ") {
basic.color(INPUT_INK)
fmt.print(prompt)
s, _ := read.a_string()
delete(s)
basic.color(DEFAULT_INK)
}
// Print the given prompt in the correspondent color, wait until the user
// enters a string and return it parsed as an `int`; return also an ok flag
// which is `false` if no appropriate value could be found, or if the input
// string contained more than just the number. Restore the default color.
//
input :: proc(prompt : string) -> (int, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.an_int()
}
credits :=
`Hammurabi
Original program:
Written in FOCAL on a DEP PDP-8 by Rick Merrill, 1969.
BASIC port:
Ported from FOCAL and modified for Edusystem 70 by David Ahl, c. 1973.
Modified for 8K Microsoft BASIC by Peter Turnbull, c. 1978.
This improved remake in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair`
print_credits :: proc() {
basic.color(TITLE_INK)
fmt.println(credits)
basic.color(DEFAULT_INK)
}
ordinal_suffix :: proc(n : int) -> string {
switch n {
case 1 : return "st"
case 2 : return "nd"
case 3 : return "rd"
case : return "th"
}
}
// Return the description of the given year as the previous one.
//
previous :: proc(year : int) -> string {
if year == 0 {
return "the previous year"
} else {
return fmt.tprintf("your %v%v year", year, ordinal_suffix(year))
}
}
print_annual_report :: proc(year : int) {
term.clear_screen()
basic.color(SPEECH_INK)
fmt.println("Hammurabi, I beg to report to you.")
basic.color(DEFAULT_INK)
fmt.printf(
"\nIn %v, %v starved and %v %v born.\n",
previous(year),
persons(dead),
persons(infants, "infant", "infants"),
infants > 1 ? "were" : "was"
)
population += infants
if year > 0 && rand.float64() <= PLAGUE_CHANCE {
population = int(population / 2)
basic.color(WARNING_INK)
fmt.println("A horrible plague struck! Half the people died.")
basic.color(DEFAULT_INK)
}
fmt.printfln("The population is %v.", population)
fmt.println("The city owns", acres, "acres.")
fmt.printf(
"You harvested %v bushels (%v 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 =
int(RANGE_OF_HARVESTED_BUSHELS_PER_ACRE * rand.float64()) +
MIN_HARVESTED_BUSHELS_PER_ACRE
fmt.println("Land is trading at", bushels_harvested_per_acre, "bushels per acre.\n")
}
say_bye :: proc() {
basic.color(DEFAULT_INK)
fmt.println("\nSo long for now.\n")
}
quit_game :: proc() {
say_bye()
os.exit(0)
}
relinquish :: proc() {
basic.color(SPEECH_INK)
fmt.println("\nHammurabi, I am deeply irritated and cannot serve you anymore.")
fmt.println("Please, get yourself another steward!")
basic.color(DEFAULT_INK)
quit_game()
}
increase_irritation :: proc() {
irritation += 1 + rand.int_max(IRRITATION_STEP)
if irritation >= MAX_IRRITATION do relinquish() // this never returns
}
print_irritated :: proc(adverb : string) {
fmt.printfln("The steward seems %v irritated.", adverb)
}
show_irritation :: proc() {
switch {
case irritation < IRRITATION_STEP :
case irritation < IRRITATION_STEP * 2 : print_irritated("slightly")
case irritation < IRRITATION_STEP * 3 : print_irritated("quite")
case irritation < IRRITATION_STEP * 4 : print_irritated("very")
case : print_irritated("profoundly")
}
}
// Print a message begging to repeat an ununderstandable input.
//
beg_repeat :: proc() {
increase_irritation() // this may never return
basic.color(SPEECH_INK)
fmt.println("I beg your pardon? I did not understand your order.")
basic.color(DEFAULT_INK)
show_irritation()
}
// Print a message begging to repeat a wrong input, because there's only `n`
// items of `name`.
//
beg_think_again :: proc(n : int, name : string) {
increase_irritation() // this may never return
basic.color(SPEECH_INK)
fmt.printfln("I beg your pardon? You have only %v %s. Now then…", n, name)
basic.color(DEFAULT_INK)
show_irritation()
}
// Buy or sell land.
//
trade :: proc() {
acres_to_buy : int
acres_to_sell : int
ok : bool
for {
acres_to_buy, ok = input("How many acres do you wish to buy? (0 to sell): ")
if ! ok || acres_to_buy < 0 {
beg_repeat() // this may never return
continue
}
if bushels_harvested_per_acre * acres_to_buy <= bushels_in_store do break
beg_think_again(bushels_in_store, "bushels of grain")
}
if acres_to_buy != 0 {
fmt.printfln("You buy %v acres.", acres_to_buy)
acres += acres_to_buy
bushels_in_store -= bushels_harvested_per_acre * acres_to_buy
fmt.printfln("You now have %v acres and %v bushels.", acres, bushels_in_store)
} else {
for {
acres_to_sell, ok = input("How many acres do you wish to sell?: ")
if ! ok || acres_to_sell < 0 {
beg_repeat() // this may never return
continue
}
if acres_to_sell < acres do break
beg_think_again(acres, "acres")
}
if acres_to_sell > 0 {
fmt.printfln("You sell %v acres.", acres_to_sell)
acres -= acres_to_sell
bushels_in_store += bushels_harvested_per_acre * acres_to_sell
fmt.printfln("You now have %v acres and %v bushels.", acres, bushels_in_store)
}
}
}
// Feed the people.
//
feed :: proc() {
ok : bool
for {
bushels_to_feed_with, ok = input("How many bushels do you wish to feed your people with?: ")
if ! ok || 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 do break
beg_think_again(bushels_in_store, "bushels of grain")
}
fmt.printfln("You feed your people with %v bushels.", bushels_to_feed_with)
bushels_in_store -= bushels_to_feed_with
fmt.printfln("You now have %v bushels.", bushels_in_store)
}
// Seed the land.
//
seed :: proc() {
acres_to_seed : int
ok : bool
for {
acres_to_seed, ok = input("How many acres do you wish to seed?: ")
if ! ok || acres_to_seed < 0 {
beg_repeat() // this may never return
continue
}
if acres_to_seed == 0 do break
// Trying to seed more acres than you own?
if acres_to_seed > acres {
beg_think_again(acres, "acres")
continue
}
// Enough grain for seed?
if int(acres_to_seed / ACRES_A_BUSHEL_CAN_SEED) > bushels_in_store {
beg_think_again(
bushels_in_store,
fmt.tprintf(
"bushels of grain,\nand one bushel can seed %v acres",
ACRES_A_BUSHEL_CAN_SEED
)
)
continue
}
// Enough people to tend the crops?
if acres_to_seed <= ACRES_A_PERSON_CAN_SEED * population do break
beg_think_again(
population,
fmt.tprintf(
"people to tend the fields,\nand one person can seed %v acres",
ACRES_A_PERSON_CAN_SEED
)
)
}
bushels_used_for_seeding := int(acres_to_seed / ACRES_A_BUSHEL_CAN_SEED)
fmt.printfln("You seed %v acres using %v bushels.", acres_to_seed, bushels_used_for_seeding)
bushels_in_store -= bushels_used_for_seeding
fmt.printfln("You now have %v 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
}
is_even :: proc(n : int) -> bool {
return n %% 2 == 0
}
check_rats :: proc() {
rat_chance := random_1_to_5()
bushels_eaten_by_rats = is_even(rat_chance) ? int(bushels_in_store / rat_chance) : 0
bushels_in_store -= bushels_eaten_by_rats
}
// Set the variables to their values in the first year.
//
init :: proc() {
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
}
print_result :: proc(result : Result) {
basic.color(RESULT_INK)
switch result {
case .Very_Good :
fmt.println("A fantastic performance! Charlemagne, Disraeli and Jefferson combined could")
fmt.println("not have done better!")
case .Not_Too_Bad :
fmt.println("Your performance could have been somewat better, but really wasn't too bad at")
fmt.printf(
"all. %v people would dearly like to see you assassinated, but we all have our\n",
int(f64(population) * .8 * rand.float64()))
fmt.println("trivial problems.")
case .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 .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!!!")
}
basic.color(DEFAULT_INK)
}
print_final_report :: proc() {
term.clear_screen()
if starved_people_percentage > 0 {
fmt.printf(
"In your %v-year term of office, %v percent of the\n",
YEARS,
starved_people_percentage
)
fmt.printf(
"population starved per year on the average, i.e., a total of %v people died!\n\n",
total_dead
)
}
acres_per_person := acres / population
fmt.printf(
"You started with %v acres per person and ended with %v.\n\n",
ACRES_PER_PERSON,
acres_per_person
)
switch {
case starved_people_percentage > 33, acres_per_person < 07 : print_result(.Very_Bad)
case starved_people_percentage > 10, acres_per_person < 09 : print_result(.Bad)
case starved_people_percentage > 03, acres_per_person < 10 : print_result(.Not_Too_Bad)
case : print_result(.Very_Good)
}
}
check_starvation :: proc(year : int) {
// How many people has been fed?
fed_people := int(bushels_to_feed_with / BUSHELS_TO_FEED_A_PERSON)
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 > int(.45 * f64(population)) {
basic.color(WARNING_INK)
fmt.println("\nYou starved", dead, "people in one year!!!\n")
basic.color(DEFAULT_INK)
print_result(.Very_Bad)
quit_game()
}
}
}
govern :: proc() {
init()
print_annual_report(0)
for year in 1 ..= YEARS {
trade()
feed()
seed()
check_rats()
// Let's have some babies
infants = int(random_1_to_5() * (20 * acres + bushels_in_store) / population / 100 + 1)
check_starvation(year)
pause("\nPress the Enter key to read the annual report. ")
print_annual_report(year)
}
}
main :: proc() {
basic.randomize()
term.clear_screen()
print_credits()
pause("\nPress the Enter key to read the instructions. ")
term.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 Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-11, 2023-12, 2025-02.
Last modified: 20250407T1920+0200.
*/
package high_noon
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:slice"
import "core:strings"
// Global variables and constants {{{1
// =============================================================
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
TITLE_INK :: basic.BRIGHT_RED
INITIAL_DISTANCE :: 100
INITIAL_BULLETS :: 4
MAX_WATERING_TROUGHS :: 3
distance : int // distance between both gunners, in paces
player_bullets : int
opponent_bullets : int
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters an integer.
//
input_int :: proc(prompt : string = "") -> int {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.an_int_or_0()
}
// Print the given prompt and wait until the user enters a string.
//
input_string :: proc(prompt : string = "") -> (string, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string()
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
s, _ := input_string(prompt)
delete(s)
}
// Return `true` if the given string is "yes" or a synonym.
//
is_yes :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"ok", "y", "yeah", "yes"}, lowercase_s)
}
// Return `true` if the given string is "no" or a synonym.
//
is_no :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"n", "no", "nope"}, lowercase_s)
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
yes :: proc(prompt : string) -> bool {
for {
answer, _ := input_string(prompt)
defer delete(answer)
if is_yes(answer) do return true
if is_no(answer) do return false
}
}
// Title, instructions and credits {{{1
// =============================================================
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println("High Noon")
basic.color(DEFAULT_INK)
}
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair")
}
print_instructions :: proc() {
print_title()
basic.color(INSTRUCTIONS_INK)
defer basic.color(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 %v 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
// =============================================================
plural_suffix :: proc(n : int) -> string {
switch n {
case 1 : return ""
case : return "s"
}
}
print_shells_left :: proc() {
if player_bullets == opponent_bullets {
fmt.printfln("Both of you have %v bullets.", player_bullets)
} else {
fmt.printf(
"You now have %v bullet%s to Black Bart's %v bullet%s.\n",
player_bullets,
plural_suffix(player_bullets),
opponent_bullets,
plural_suffix(opponent_bullets))
}
}
print_check :: proc() {
fmt.println("******************************************************")
fmt.println("* *")
fmt.println("* BANK OF DODGE CITY *")
fmt.println("* CASHIER'S RECEIT *")
fmt.println("* *")
fmt.printfln("* CHECK NO. %04v AUGUST %vTH, 1889 *",
rand.int_max(1000),
10 + rand.int_max(10))
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("******************************************************")
}
get_reward :: proc() {
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.")
}
move_the_opponent :: proc() {
paces := 2 + rand.int_max(8)
fmt.printfln("Black Bart moves %v 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.
//
maybe_move_the_opponent :: proc(silent := false) -> bool {
if rand.int_max(2) == 0 { // 50% chances
move_the_opponent()
return true
} else {
if ! silent {
fmt.println("Black Bart stands still.")
}
return false
}
}
the_opponent_moves :: maybe_move_the_opponent
missed_shot :: proc() -> bool {
return rand.float64() * 10 <= f64(distance / 10)
}
// Handle the opponent's shot and return a flag with the result: if the
// opponent kills the player, return `true`; otherwise return `false`.
//
the_opponent_fires_and_kills :: proc(strategy: string) -> 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.")
}
} else {
if 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`.
//
the_opponent_kills_or_runs :: proc(strategy: string) -> bool {
if distance >= 10 || player_bullets == 0 {
if the_opponent_moves(silent = true) do return false
}
if opponent_bullets > 0 {
return the_opponent_fires_and_kills(strategy)
} else {
if player_bullets > 0 {
if rand.int_max(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
}
play :: proc() {
distance = INITIAL_DISTANCE
watering_troughs := 0
player_bullets = INITIAL_BULLETS
opponent_bullets = INITIAL_BULLETS
showdown: for {
fmt.printfln("You are now %v paces apart from Black Bart.", distance)
print_shells_left()
basic.color(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")
basic.color(DEFAULT_INK)
raw_strategy, _ := input_string("What is your strategy? ")
defer delete(raw_strategy)
strategy := strings.to_lower(raw_strategy)
defer delete(strategy)
switch strategy {
case "a" : // advance
for {
paces := input_int("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.")
}
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. ")
term.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?")
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 rand.int_max(opponent_bullets + 2) == 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.")
}
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(strategy) do 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
// =============================================================
main :: proc() {
basic.randomize()
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
term.clear_screen()
print_instructions()
press_enter("\nPress the Enter key to start. ")
term.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 Odin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-03, 2023-04, 2023-09, 2023-12.
Last modified: 20250101T2036+0100.
*/
package vintage_basic_math
import "../lib/anodino/src/read"
import "core:fmt"
import "core:math"
main :: proc() {
n : f64
ok := false
for ; ! ok; n, ok = read.a_prompted_f64("Enter a number: ") { }
fmt.printfln("ABS(%v) -> math.abs(%v) -> %v", n, n, math.abs(n))
fmt.printfln("ATN(%v) -> math.atan(%v) -> %v", n, n, math.atan(n))
fmt.printfln("COS(%v) -> math.cos_f64(%v) -> %v", n, n, math.cos_f64(n))
fmt.printfln("EXP(%v) -> math.exp(%v) -> %v", n, n, math.exp(n))
fmt.printfln("INT(%v) -> int(%v) -> %v", n, n, int(n))
fmt.printfln("LOG(%v) -> math.log_f64(%v, %v) -> %v", n, n, math.E, math.log_f64(n, math.E))
fmt.printfln("SGN(%v) -> math.sign_f64(%v) -> %v", n, n, math.sign_f64(n))
fmt.printfln("SQR(%v) -> math.sqrt_f64(%v) -> %v", n, n, math.sqrt_f64(n))
fmt.printfln("TAN(%v) -> math.tan_f64(%v) -> %v", n, n, math.tan_f64(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 Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-03, 2023-09, 2023-12, 2025-02.
Last modified: 20250731T1954+0200.
*/
package mugwump
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math"
import "core:math/rand"
import "core:slice"
import "core:strings"
GRID_SIZE :: 10
TURNS :: 10
MUGWUMPS :: 4
Mugwump :: struct {
x : int,
y : int,
hidden : bool,
}
mugwump : [MUGWUMPS]Mugwump
found : int // counter
// Wait until the user enters a string and return it.
//
get_string :: proc(prompt : string) -> string {
fmt.print(prompt)
return read.a_string() or_else ""
}
// Print the given prompt, wait until the user enters a valid integer and
// return it.
//
get_number :: proc(prompt : string) -> (number : int) {
ok := false
for ; !ok; number, ok = read.a_prompted_int(prompt) {}
return number
}
// Print the given prompt and wait until the user enters an empty string.
//
press_enter :: proc(prompt : string) {
for {
s := get_string(prompt)
if len(s) == 0 do break
defer delete(s)
}
}
// Return `true` if the given string is "yes" or a synonym.
//
is_yes :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"ok", "y", "yeah", "yes"}, lowercase_s)
}
// Return `true` if the given string is "no" or a synonym.
//
is_no :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"n", "no", "nope"}, lowercase_s)
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
yes :: proc(prompt : string) -> bool {
for {
answer := get_string(prompt)
defer delete(answer)
if is_yes(answer) do return true
if is_no(answer) do return false
}
}
// Clear the screen, print the credits and ask the user to press enter.
//
print_credits :: proc() {
term.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 Odin:")
fmt.println(" Copyright (c) 2023, 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 ask the user to press enter.
//
print_instructions :: proc() {
term.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 %v tries. After each try, you will see", TURNS)
fmt.println("how far you are from each mugwump.\n")
press_enter("Press Enter to start. ")
}
// Init the mugwumps' positions, `hidden` flags and count.
//
hide_mugwumps :: proc() {
for m in 0 ..< MUGWUMPS {
mugwump[m].x = rand.int_max(GRID_SIZE)
mugwump[m].y = rand.int_max(GRID_SIZE)
mugwump[m].hidden = true
}
found = 0 // counter
}
// Print the given prompt, wait until the user enters a valid coord and return
// it.
//
get_coord :: proc(prompt : string) -> (coord : int) {
for {
coord = get_number(prompt)
if coord < 0 || coord >= GRID_SIZE {
fmt.printfln("Invalid value %v: not in range [0, %v].", coord, GRID_SIZE - 1)
} else {
break
}
}
return
}
// Return `true` if the given mugwump is hidden in the given coords.
//
is_here :: proc(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.
//
distance :: proc(m : int, x : int, y : int) -> int {
return int(math.sqrt( \
math.pow_f64(f64(mugwump[m].x - x), 2) \
+ math.pow_f64(f64(mugwump[m].y - y), 2)))
}
// Return a plural ending (default: "s") if the given number is greater than 1;
// otherwise return a singular ending (default: an empty string).
//
plural :: proc(n : int, plural := "s", singular := "") -> string {
return n > 1 ? plural : singular
}
// Run the game.
//
play :: proc() {
x, y : int
turn : int // counter
for { // game
term.clear_screen()
hide_mugwumps()
turns_loop: for turn = 1; turn <= TURNS; turn += 1 {
fmt.printfln("Turn number %v\n", turn)
fmt.printfln("What is your guess (in range [0, %v])?", 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 (%v, %v).", x, y)
for m in 0 ..< MUGWUMPS {
if is_here(m, x, y) {
mugwump[m].hidden = false
found += 1
fmt.printfln("You have found mugwump %v!", m)
if found == MUGWUMPS do break turns_loop
}
}
for m in 0 ..< MUGWUMPS {
if mugwump[m].hidden {
fmt.printfln("You are %v units from mugwump %v.", distance(m, x, y) , m)
}
}
fmt.println()
} // turns
if found == MUGWUMPS {
fmt.printfln("\nYou got them all in %v turn%s!\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 %v tr%s.\n", TURNS, plural(TURNS, "ies", "y"))
fmt.println("Here is where they're hiding:")
for m in 0 ..< MUGWUMPS {
if mugwump[m].hidden {
fmt.printfln("Mugwump %v is at (%v, %v).", m, mugwump[m].x, mugwump[m].y)
}
}
}
fmt.println()
if !yes("Do you want to play again? ") do break
} // game
}
main :: proc() {
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 Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-10-01/02, 2025-02.
Last modified: 20250226T2325+0100.
*/
package name
import "../lib/anodino/src/read"
import "core:fmt"
main :: proc() {
fmt.print("What is your name? ")
name, _ := read.a_string()
defer delete(name)
number : f64
for ok := false; !ok; {
fmt.print("Enter a number: ")
number, ok = read.an_f64()
if !ok do fmt.println("Number expected.")
}
for _ in 0 ..< int(number) do fmt.printfln("Hello, %v!", 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 Odin:
Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2023-12-14, 2024-07-11, 2024-12-12/13, 2025-02-27.
Last modified: 20250731T1954+0200.
*/
package poetry
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:time"
// Globals {{{1
// =============================================================
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
TITLE_INK :: basic.BRIGHT_RED
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
input_string :: proc(prompt : string = "") -> (string, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string()
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
s, _ := input_string(prompt)
delete(s)
}
// Title and credits {{{1
// =============================================================
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println("Poetry")
basic.color(DEFAULT_INK)
}
// Print the credits at the current cursor position.
//
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair")
}
// Main {{{1
// =============================================================
// Wait the given number of nanoseconds.
//
nanoseconds :: proc(ns : i64) {
for end := time.now()._nsec + ns; time.now()._nsec < end; { }
}
// Is the given integer even?
//
is_even :: proc(n : int) -> bool {
return n %% 2 == 0
}
play :: proc() {
basic.randomize()
MAX_PHRASES_AND_VERSES :: 20
// counters:
action := 0
phrase := 0
phrases_and_verses := 0
verse_chunks := 0
verse: for {
manage_the_verse_continuation := true
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 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 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 do fmt.print("SIGN OF PARTING")
}
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 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
}
}
if manage_the_verse_continuation {
nanoseconds(250_000_000) // 250 ms
if maybe_add_comma && ! (verse_chunks == 0 || rand.float64() > 0.19) {
fmt.print(",")
verse_chunks = 2
}
if rand.float64() > 0.65 {
fmt.println()
verse_chunks = 0
} else {
fmt.print(" ")
verse_chunks += 1
}
}
action += 1
phrase = rand.int_max(5)
phrases_and_verses += 1
if ! (verse_chunks > 0 || is_even(action)) {
fmt.print(" ")
}
} // verse loop
}
main :: proc() {
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to start. ")
term.clear_screen()
play()
}
Russian Roulette
/*
Russian Roulette
Original version in BASIC:
Creative Computing (Morristown, New Jersey, USA), ca. 1980.
This version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-02/03, 2023-08/0, 2023-12, 2025-02.
Last modified: 20250407T1920+0200.
*/
package russian_roulette
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
press_enter_to_start :: proc() {
s, _ := read.a_prompted_string("Press Enter to start. ")
delete(s)
}
print_credits :: proc() {
term.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 Odin:")
fmt.println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
press_enter_to_start()
}
print_instructions :: proc() {
term.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")
}
play :: proc() {
times := 0
for { // game loop
print_instructions()
times = 0
play_loop: for {
command := read.a_prompted_string("> ") or_else ""
defer delete(command)
switch command {
case "f" : // fire
if rand.int31_max(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
}
} // play loop
press_enter_to_start()
} // game loop
}
main :: proc() {
print_credits()
play()
fmt.println("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 Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-11, 2023-12, 2025-02.
Last modified: 20250227T0037+0100.
*/
package seance
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:strings"
import "core:time"
MAX_SCORE :: 50
MAX_MESSAGE_LENGTH :: 6
MIN_MESSAGE_LENGTH :: 3
BASE_CHARACTER :: int('@')
PLANCHETTE :: '*'
FIRST_LETTER_NUMBER :: 1
LAST_LETTER_NUMBER :: 26
BOARD_INK :: basic.BRIGHT_CYAN
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
MISTAKE_EFFECT_INK :: basic.BRIGHT_RED
PLANCHETTE_INK :: basic.YELLOW
TITLE_INK :: basic.BRIGHT_RED
INPUT_X :: BOARD_X
INPUT_Y :: BOARD_Y + BOARD_BOTTOM_Y + 4
MESSAGES_Y :: INPUT_Y
MISTAKE_EFFECT_PAUSE :: 3 // seconds
BOARD_ACTUAL_WIDTH :: BOARD_WIDTH + 2 * BOARD_PAD // screen columns
BOARD_BOTTOM_Y :: BOARD_HEIGHT + 1 // relative to the board
BOARD_HEIGHT :: 5 // characters displayed on the left and right borders
BOARD_PAD :: 1 // blank characters separating the board from its left and right borders
BOARD_WIDTH :: 8 // characters displayed on the top and bottom borders
BOARD_X :: 29 // screen column
BOARD_Y :: 5 // screen line
// Print the given prompt and wait until the user enters a string.
//
input :: proc(prompt : string = "") -> (string, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string()
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
s, _ := input(prompt)
delete(s)
}
// Return the x coordinate to print the given text centered on the board.
//
board_centered_x :: proc(text : string) -> int {
return BOARD_X + (BOARD_ACTUAL_WIDTH - len(text)) / 2
}
// Print the given text on the given row, centered on the board.
//
print_board_centered :: proc(text : string, y : int) {
term.set_cursor_position(y, board_centered_x(text))
fmt.println(text)
}
TITLE :: "Seance"
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println(TITLE)
basic.color(DEFAULT_INK)
}
// Print the title on the given row, centered on the board.
//
print_board_centered_title :: proc(y : int) {
basic.color(TITLE_INK)
print_board_centered(TITLE, y)
basic.color(DEFAULT_INK)
}
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
}
print_instructions :: proc() {
print_title()
basic.color(INSTRUCTIONS_INK)
defer basic.color(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.")
}
// Print the given letter at the given board coordinates.
//
print_character :: proc(y, x : int, a : rune) {
term.set_cursor_position(y + BOARD_Y, x + BOARD_X)
fmt.print(a)
}
print_board :: proc() {
basic.color(BOARD_INK)
defer basic.color(DEFAULT_INK)
for i in 1 ..= BOARD_WIDTH {
print_character(0, i + 1, rune(BASE_CHARACTER + i)) // top border
print_character(BOARD_BOTTOM_Y, i + 1, rune(BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1)) // bottom border
}
for i in 1 ..= BOARD_HEIGHT {
print_character(i , 0, rune(BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1)) // left border
print_character(i , 3 + BOARD_WIDTH, rune(BASE_CHARACTER + BOARD_WIDTH + i)) // right border
}
fmt.println()
}
// Return a random integer in the given inclusive range.
//
random_in_inclusive_range :: proc(min, max : int) -> int {
return rand.int_max(max - min + 1) + min
}
// Erase the given line to the right of the given column.
//
erase_line_right_from :: proc(line, column : int) {
term.set_cursor_position(line, column)
term.erase_line_right()
}
// Wait the given number of seconds.
//
wait_seconds :: proc(seconds : int) {
time.sleep(time.Duration(seconds) * time.Second)
}
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
print_mistake_effect :: proc(effect : string) {
x := board_centered_x(effect)
term.hide_cursor()
term.set_cursor_position(MESSAGES_Y, x)
basic.color(MISTAKE_EFFECT_INK)
fmt.println(effect)
basic.color(DEFAULT_INK)
wait_seconds(MISTAKE_EFFECT_PAUSE)
erase_line_right_from(MESSAGES_Y, x)
term.show_cursor()
}
// Return a new message of the given length, after marking its letters on the
// board.
//
message :: proc(length : int) -> string {
y, x : int
message := ""
term.hide_cursor()
for _ in 1 ..= length {
letter_number := random_in_inclusive_range(FIRST_LETTER_NUMBER, LAST_LETTER_NUMBER)
letter := rune(BASE_CHARACTER + letter_number)
message = fmt.tprintf("%v%v", message, letter) // add letter to message
switch {
case letter_number <= BOARD_WIDTH :
// top border
y = 1
x = letter_number + 1
case letter_number <= BOARD_WIDTH + BOARD_HEIGHT :
// right border
y = letter_number - BOARD_WIDTH
x = 2 + BOARD_WIDTH
case 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
case :
// left border
y = 1 + LAST_LETTER_NUMBER - letter_number
x = 1
}
basic.color(PLANCHETTE_INK)
print_character(y, x, PLANCHETTE)
basic.color(DEFAULT_INK)
wait_seconds(1)
print_character(y, x, ' ')
}
term.show_cursor()
return message
}
// Accept a string from the user, erase it from the screen and return it.
//
message_understood :: proc() -> string {
term.set_cursor_position(INPUT_Y, INPUT_X)
defer erase_line_right_from(INPUT_Y, INPUT_X)
s, _ := input("? ")
defer delete(s)
return strings.to_upper(s)
}
play :: proc() {
score := 0
mistakes := 0
print_board_centered_title(1)
print_board()
for {
message_length := random_in_inclusive_range(MIN_MESSAGE_LENGTH, MAX_MESSAGE_LENGTH)
msg := message(message_length)
defer delete(msg)
msg_understood := message_understood()
defer delete(msg_understood)
if msg != msg_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
}
} 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 :: proc() {
basic.randomize()
basic.color(DEFAULT_INK)
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
term.clear_screen()
print_instructions()
press_enter("\nPress the Enter key to start. ")
term.clear_screen()
play()
fmt.println("\n")
}
Sine Wave
/*
Sine Wave
Original version in BASIC:
Creative Computing (Morristown, New Jersey, USA), ca. 1980.
This version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-02/03, 2023-08/09, 2023-11, 2023-12, 2025-02.
Last modified: 20250226T2323+0100.
*/
package sine_wave
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math"
import "core:strings"
word : [2]string = {"", ""}
// Ask the user to enter two words and store them.
//
get_words :: proc() {
order : [2]string = {"first", "second"}
term.clear_screen()
for n in 0 ..= 1 {
for word[n] == "" {
word[n] = read.a_prompted_string(fmt.tprintf("Enter the %v word: ", order[n])) or_else ""
}
}
}
// Display the credits and wait for a keypress.
//
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
read.a_prompted_string("Press Enter to start the program. ")
}
draw :: proc() {
even := false
for angle := 0.0; angle <= 40.0; angle += 0.25 {
margin := strings.repeat(" ", int(26 + 25 * math.sin(angle)))
defer delete(margin)
fmt.print(margin)
fmt.println(word[int(even)])
even = ! even
}
}
delete_words :: proc() {
delete(word[0])
delete(word[1])
}
main :: proc() {
term.clear_screen()
print_credits()
get_words()
term.clear_screen()
draw()
delete_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 Odin:
Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-09/10, 2024-04, 2024-05, 2024-12, 2025-04.
Last modified: 20250407T2030+0200.
*/
package slots
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:time"
image := []string{" BAR ", " BELL ", "ORANGE", "LEMON ", " PLUM ", "CHERRY"}
BAR :: 0 // position of "BAR" in `image`.
color := []int{
term.FOREGROUND + term.WHITE,
term.FOREGROUND + term.CYAN,
term.FOREGROUND + term.YELLOW,
term.FOREGROUND + term.BRIGHT + term.YELLOW,
term.FOREGROUND + term.BRIGHT + term.WHITE,
term.FOREGROUND + term.BRIGHT + term.RED }
MAX_BET :: 100
MIN_BET :: 1
press_enter :: proc(prompt : string) {
fmt.print(prompt)
s, _ := read.a_string()
delete(s)
}
print_credits :: proc() {
term.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 Odin:")
fmt.println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
press_enter("Press Enter for instructions.")
}
print_instructions :: proc() {
term.clear_screen()
fmt.println("You are in the H&M casino, in front of one of our")
fmt.printf(
"one-arm bandits. Bet from %v to %v USD (or 0 to quit).\n\n",
MIN_BET, MAX_BET)
press_enter("Press Enter to start.")
}
won :: proc(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***")
}
fmt.println("You won!")
return (prize + 1) * bet
}
show_standings :: proc(usd : int) {
fmt.printfln("Your standings are %v USD.", usd)
}
print_reels :: proc(reel : ^[]int) {
term.move_cursor_home()
for r in reel {
term.set_color(color[r])
fmt.printf("[%v] ", image[r])
}
term.set_attribute(term.NORMAL)
fmt.println("")
}
init_reels :: proc(reel : ^[]int) {
images := len(image)
for _, i in reel {
reel[i] = int(rand.int31_max(i32(images)))
}
}
spin_reels :: proc(reel : ^[]int) {
DURATION_IN_SECONDS :: 2
term.hide_cursor()
first_second := time.to_unix_seconds(time.now())
for (time.to_unix_seconds(time.now()) - first_second) < DURATION_IN_SECONDS {
init_reels(reel)
print_reels(reel)
}
term.show_cursor()
}
// Return the number of equals and bars in the given `reel` array.
//
prize :: proc(reel : ^[]int) -> (equals : int, bars : int) {
for i in 0 ..< len(image) {
count := 0;
for r in 0 ..< len(reel) {
count += int(reel[r] == i)
}
equals = max(equals, count)
}
for r in 0 ..< len(reel) {
bars += int(reel[r] == BAR)
}
return equals, bars
}
play :: proc() {
standings := 0
bet := 0
reel := []int{0, 0, 0}
equals := 0
bars := 0
init_reels(&reel)
play_loop: for {
bet_loop: for {
term.clear_screen()
print_reels(&reel)
fmt.print("Your bet (or 0 to quit): ")
bet = read.an_int_or_0()
switch {
case bet > MAX_BET :
fmt.printfln("House limits are %v USD.", MAX_BET)
press_enter("Press Enter to try again.")
case bet < MIN_BET :
fmt.println("Type \"q\" to confirm you want to quit.")
confirm, _ := read.a_string()
defer delete(confirm)
if confirm == "q" {
break play_loop
}
case :
break bet_loop
} // bet check
} // bet loop
term.clear_screen()
spin_reels(&reel)
equals, bars = prize(&reel)
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)
switch {
case standings < 0 :
fmt.println("Pay up! Please leave your money on the terminal.")
case standings == 0 :
fmt.println("Hey, you broke even.")
case standings > 0 :
fmt.println("Collect your winnings from the H&M cashier.")
}
}
main :: proc() {
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 Odin:
Copyright (c) 2023, 2024, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-10, 2024-12, 2025-02.
Last modified: 20250226T2357+0100.
*/
package name
import "../lib/anodino/src/read"
import "core:fmt"
import "core:strings"
main :: proc() {
fmt.print("What is your name? ")
name, _ := read.a_string()
defer delete(name)
fmt.printfln("Hello, %v.", name)
number : int
quit := false
for ! quit {
for ok := false; !ok; {
fmt.print("How many stars do you want? ")
number, ok = read.an_int()
if !ok do fmt.println("Integer expected.")
}
stars := strings.repeat("*", number)
defer delete(stars)
fmt.println(stars)
fmt.print("Do you want more stars? ")
answer, _ := read.a_string()
defer delete(answer)
lowercase_answer := strings.to_lower(answer)
defer delete(lowercase_answer)
switch answer {
case "ok", "yeah", "yes", "y" :
case : quit = true
}
}
}
Strings
/*
Strings
Original version in BASIC:
Example included in Vintage BASIC 1.0.3.
http://www.vintage-basic.net
This version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-04, 2023-09, 2023-12, 2025-04.
Last modified: 20250424T1230+0200.
*/
package vintage_basic_strings
import "../lib/anodino/src/read"
import "core:fmt"
import "core:strconv"
import "core:strings"
import "core:unicode/utf8"
get_string :: proc(prompt : string) -> string {
fmt.print(prompt)
return read.a_string() or_else ""
}
get_number :: proc(prompt : string) -> (number : int) {
ok := false
for ; !ok; number, ok = strconv.parse_int(get_string(prompt)) {}
return
}
main :: proc() {
s := get_string("Enter a string: ")
n := get_number("Enter an integer: ")
fmt.println()
fmt.printf("ASC(\"%s\") --> ", s)
fmt.printfln("int(utf8.string_to_runes(\"%s\")[0]) --> %v", s, int(utf8.string_to_runes(s)[0]))
fmt.printf("CHR$(%v) --> ", n)
fmt.printfln("rune(%v) --> '%v'", n, rune(n))
fmt.printf("LEFT$(\"%s\", %v) --> ", s, n)
fmt.printfln("strings.cut(\"%s\", %v, %v) --> \"%s\"", s, 0, n, strings.cut(s, 0, n))
fmt.printf("MID$(\"%s\", %v) --> ", s, n)
// XXX FIXME When n is greater than string length, the result is not that of the BASIC original:
fmt.printfln("strings.cut(\"%[0]s\", %[1]v - 1, len(\"%[0]s\") - %[1]v - 1) --> \"%[2]s\"", s, n, strings.cut(s, n - 1, len(s) - 1))
fmt.printf("MID$(\"%s\", %v, 3) --> ", s, n)
fmt.printfln("strings.cut(\"%s\", %v - 1, 3) --> \"%s\"", s, n, strings.cut(s, n - 1, 3))
fmt.printf("RIGHT$(\"%s\", %v) --> ", s, n)
fmt.printfln("strings.cut(\"%[0]s\", len(\"%[0]s\")-%[1]v, %[1]v) --> \"%[2]s\"", s, n, strings.cut(s, len(s) - n, n))
fmt.printf("LEN(\"%s\") --> ", s)
fmt.printfln("len(\"%s\") --> %v", s, len(s))
fmt.printf("VAL(\"%s\") --> ", s)
fmt.printfln("strconv.parse_int(\"%s\") or_else 0 --> %v", s, strconv.parse_int(s) or_else 0)
fmt.printf("STR$(%v) --> ", n)
fmt.printfln("fmt.tprint(%v) --> \"%s\"", n, fmt.tprint(n))
fmt.printf("SPC(%v) --> ", n)
fmt.printfln("strings.repeat(\" \", %v) --> \"%s\"", n, strings.repeat(" ", n))
}
Xchange
/*
Xchange
Original version in BASIC:
Written by Thomas C. McIntire, 1979.
Published in "The A to Z Book of Computer Games", 1979.
https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
https://github.com/chaosotter/basic-games
This improved remake in Odin:
Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-12, 2024-12, 2025-02.
Last modified: 20250518T1741+0200.
*/
package xchange
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:slice"
import "core:strings"
import "core:unicode/utf8"
// Globals {{{1
// =============================================================
BOARD_INK :: basic.BRIGHT_CYAN
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
TITLE_INK :: basic.BRIGHT_RED
BLANK :: "*"
GRID_HEIGHT :: 3 // cell rows
GRID_WIDTH :: 3 // cell columns
CELLS :: GRID_WIDTH * GRID_HEIGHT
pristine_grid := [CELLS]string{}
GRIDS_ROW :: 3 // screen row where the grids are printed
GRIDS_COLUMN :: 5 // screen column where the left grid is printed
CELLS_GAP :: 2 // distance between the grid cells, in screen rows or columns
GRIDS_GAP :: 16 // screen columns between equivalent cells of the grids
MAX_PLAYERS :: 4
grid := [MAX_PLAYERS][CELLS]string{}
is_playing := [MAX_PLAYERS]bool{}
players : int
QUIT_COMMAND :: "X"
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters an integer.
//
input_int :: proc(prompt : string = "") -> int {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.an_int_or_0()
}
// Print the given prompt and wait until the user enters a string.
//
input_string :: proc(prompt : string = "") -> string {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string() or_else ""
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
input_string(prompt)
}
// Return `true` if the given string is "yes" or a synonym.
//
is_yes :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"ok", "y", "yeah", "yes"}, lowercase_s)
}
// Return `true` if the given string is "no" or a synonym.
//
is_no :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"n", "no", "nope"}, lowercase_s)
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
yes :: proc(prompt : string) -> bool {
for {
answer := input_string(prompt)
if is_yes(answer) do return true
if is_no(answer) do return false
}
}
// Title, instructions and credits {{{1
// =============================================================
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println("Xchange")
basic.color(DEFAULT_INK)
}
// Print the credits at the current cursor position.
//
print_credits :: proc() {
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 Odin:")
fmt.println(" Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair")
}
// Print the instructions at the current cursor position.
//
print_instructions :: proc() {
print_title()
basic.color(INSTRUCTIONS_INK)
defer basic.color(DEFAULT_INK)
fmt.println("\nOne or two may play. If two, you take turns. A grid looks like this:\n")
basic.color(BOARD_INK)
fmt.println(" F G D")
fmt.printfln(" A H %s", BLANK)
fmt.println(" E B C\n")
basic.color(INSTRUCTIONS_INK)
fmt.println("But it should look like this:\n")
basic.color(BOARD_INK)
fmt.println(" A B C")
fmt.println(" D E F")
fmt.printfln(" G H %s\n", BLANK)
basic.color(INSTRUCTIONS_INK)
fmt.printfln("You may exchange any one letter with the '%s', 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("'%s' to give up.\n", QUIT_COMMAND)
fmt.println("Here we go...")
}
// Grids {{{1
// =============================================================
// Print the given player's grid title.
//
print_grid_title :: proc(player : int) {
term.set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP))
fmt.printf("Player %v", player + 1)
}
// Return the cursor position of the given player's grid cell.
//
cell_position :: proc(player, cell : int) -> (row, column : int) {
grid_row : int = cell / GRID_HEIGHT
grid_column : int = cell % GRID_WIDTH
title_margin := players > 1 ? 2 : 0
return \
GRIDS_ROW + title_margin + grid_row,
GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)
}
// Return the cursor position of the given player's grid prompt.
//
grid_prompt_position :: proc(player : int) -> (row, column : int) {
grid_row : int = CELLS / GRID_HEIGHT
grid_column : int = CELLS % GRID_WIDTH
title_margin := players > 1 ? 2 : 0
return \
GRIDS_ROW + title_margin + grid_row + 1,
GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)
}
// Print the given player's grid, in the given or default color.
//
print_grid :: proc(player : int, color := BOARD_INK) {
if players > 1 do print_grid_title(player)
basic.color(color)
defer basic.color(DEFAULT_INK)
for cell in 0 ..< CELLS {
term.set_cursor_position(cell_position(player, cell))
fmt.print(grid[player][cell])
}
}
// Print the current players' grids.
//
print_grids :: proc() {
for player in 0 ..< players {
if is_playing[player] do print_grid(player)
}
fmt.println()
term.erase_screen_down()
}
// Scramble the grid of the given player.
//
scramble_grid :: proc(player : int) {
for cell in 0 ..< CELLS {
random_cell := rand.int_max(CELLS)
// Exchange the contents of the current cell with that of the random one.
grid[player][cell], grid[player][random_cell] =
grid[player][random_cell], grid[player][cell]
}
}
// Init the grids.
//
init_grids :: proc() {
grid[0] = pristine_grid
scramble_grid(0)
for player in 0 + 1 ..< players {
grid[player] = grid[0]
}
}
// Messages {{{1
// =============================================================
// Return a message prefix for the given player.
//
player_prefix :: proc(player : int) -> string {
return players > 1 ? fmt.tprintf("Player %i: ", player + 1) : ""
}
// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
message_position :: proc(player : int, row_inc := 0) -> (row, column : int) {
prompt_row, _ := grid_prompt_position(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.
//
print_message :: proc(message : string, player : int, row_inc := 0) {
term.set_cursor_position(message_position(player, row_inc))
fmt.printf("%s%s", player_prefix(player), message)
term.erase_line_right()
fmt.println()
}
// Erase the last message about the given player.
//
erase_message :: proc(player : int) {
term.set_cursor_position(message_position(player))
term.erase_line_right()
}
// Game loop {{{1
// =============================================================
// Return a message with the players range.
//
players_range_message :: proc() -> string {
if MAX_PLAYERS == 2 {
return "1 or 2"
} else {
return fmt.tprintf("from 1 to %v", MAX_PLAYERS)
}
}
// Return the number of players, asking the user if needed.
//
number_of_players :: proc() -> int {
players := 0
print_title()
fmt.println()
if MAX_PLAYERS == 1 {
players = 1
} else {
for players < 1 || players > MAX_PLAYERS {
players = input_int(fmt.tprintf("Number of players (%s): ", players_range_message()))
}
}
return players
}
// Is the given cell the first one on a grid row?
//
is_first_cell_of_a_grid_row :: proc(cell : int) -> bool {
return cell % GRID_WIDTH == 0
}
// Is the given cell the last one on a grid row?
//
is_last_cell_of_a_grid_row :: proc(cell : int) -> bool {
return (cell + 1) % GRID_WIDTH == 0
}
// Are the given cells adjacent?
//
are_cells_adjacent :: proc(cell_1, cell_2 : int) -> bool {
switch {
case cell_2 == cell_1 + 1 && ! is_first_cell_of_a_grid_row(cell_2) :
return true
case cell_2 == cell_1 + GRID_WIDTH :
return true
case cell_2 == cell_1 - 1 && ! is_last_cell_of_a_grid_row(cell_2) :
return true
case cell_2 == cell_1 - GRID_WIDTH :
return true
}
return false
}
// Is the given player's character cell a valid move, i.e. is it adjacent to
// the blank cell? If so, return the blank cell of the grid and `true`;
// otherwise return a fake cell and `false`.
//
is_legal_move :: proc(player, char_cell : int) -> (blank_cell : int, ok : bool) {
NOWHERE :: -1
for cell_content, cell_number in grid[player] {
if cell_content == BLANK {
if are_cells_adjacent(char_cell, cell_number) {
return cell_number, true
} else {
break
}
}
}
print_message(fmt.tprintf("Illegal move \"%s\".", grid[player][char_cell]), player)
return NOWHERE, false
}
// Is the given player's command valid, i.e. a grid character? If so, return
// its position in the grid and `true`; otherwise print an error message and
// return a fake position and `false`.
//
is_valid_command :: proc(player : int, command : string) -> (position : int, ok : bool) {
NOWHERE :: -1
if command != BLANK {
for cell_content, position in grid[player] {
if cell_content == command do return position, true
}
}
print_message(fmt.tprintf("Invalid character \"%s\".", command), player)
return NOWHERE, false
}
// Forget the given player, who quitted.
//
forget_player :: proc(player : int) {
is_playing[player] = false
print_grid(player, DEFAULT_INK)
}
// Play the turn of the given player.
//
play_turn :: proc(player : int) {
blank_position : int
character_position : int
if is_playing[player] {
for {
ok : bool
command : string
for ok = false ; ! ok; character_position, ok = is_valid_command(player, command) {
row, column := grid_prompt_position(player)
term.set_cursor_position(row, column)
term.erase_line_right()
term.set_cursor_position(row, column)
raw_command := input_string("Move: ")
defer delete(raw_command)
command = strings.to_upper(strings.trim_space(raw_command))
defer delete(command)
if command == QUIT_COMMAND {
forget_player(player)
return
}
}
blank_position, ok = is_legal_move(player, character_position)
if ok do break
}
erase_message(player)
grid[player][blank_position] = grid[player][character_position]
grid[player][character_position] = BLANK
}
}
// Play the turns of all players.
//
play_turns :: proc() {
for player in 0 ..< players do play_turn(player)
}
// Is someone playing?
//
is_someone_playing :: proc() -> bool {
for player in 0 ..< players {
if is_playing[player] do return true
}
return false
}
// Has someone won? If so, print a message for every winner and return `true`;
// otherwise just return `false`.
//
has_someone_won :: proc() -> bool {
winners := 0
for player in 0 ..< players {
if is_playing[player] {
if grid[player] == pristine_grid {
winners += 1
if winners > 0 {
print_message(
fmt.tprintf("You're the winner%s!", winners > 1 ? ", too" : ""),
player,
row_inc = winners - 1)
}
}
}
}
return winners > 0
}
// Init the game.
//
init_game :: proc() {
term.clear_screen()
players = number_of_players()
for player in 0 ..< players {
is_playing[player] = true
}
term.clear_screen()
print_title()
init_grids()
print_grids()
}
// Play the game.
//
play :: proc() {
init_game()
for is_someone_playing() {
play_turns()
print_grids()
if has_someone_won() do break
}
}
// Main {{{1
// =============================================================
// Init the program, i.e. just once before the first game.
//
init_once :: proc() {
basic.randomize()
// Init the pristine grid.
FIRST_CHAR_CODE :: int('A')
for cell in 0 ..< CELLS - 1 {
pristine_grid[cell] = utf8.runes_to_string([]rune{rune(FIRST_CHAR_CODE + cell)})
}
pristine_grid[CELLS - 1] = BLANK
}
// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
enough :: proc() -> bool {
term.set_cursor_position(grid_prompt_position(player = 0))
return ! yes("Another game? ")
}
main :: proc() {
init_once()
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
term.clear_screen()
print_instructions()
press_enter("\nPress the Enter key to start. ")
for {
play()
if enough() do break
}
fmt.println("So long…")
}
