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…")

}

Related pages

Basics off
Metaproject about the "Basics of…" projects.
Basics of 8th
Conversion of old BASIC programs to 8th in order to learn the basics of this language.
Basics of Ada
Conversion of old BASIC programs to Ada in order to learn the basics of this language.
Basics of Arturo
Conversion of old BASIC programs to Arturo in order to learn the basics of this language.
Basics of C#
Conversion of old BASIC programs to C# in order to learn the basics of this language.
Basics of C3
Conversion of old BASIC programs to C3 in order to learn the basics of this language.
Basics of Chapel
Conversion of old BASIC programs to Chapel in order to learn the basics of this language.
Basics of Clojure
Conversion of old BASIC programs to Clojure in order to learn the basics of this language.
Basics of Crystal
Conversion of old BASIC programs to Crystal in order to learn the basics of this language.
Basics of D
Conversion of old BASIC programs to D in order to learn the basics of this language.
Basics of Elixir
Conversion of old BASIC programs to Elixir in order to learn the basics of this language.
Basics of F#
Conversion of old BASIC programs to F# in order to learn the basics of this language.
Basics of Factor
Conversion of old BASIC programs to Factor in order to learn the basics of this language.
Basics of FreeBASIC
Conversion of old BASIC programs to FreeBASIC in order to learn the basics of this language.
Basics of Gleam
Conversion of old BASIC programs to Gleam in order to learn the basics of this language.
Basics of Go
Conversion of old BASIC programs to Go in order to learn the basics of this language.
Basics of Hare
Conversion of old BASIC programs to Hare in order to learn the basics of this language.
Basics of Haxe
Conversion of old BASIC programs to Haxe in order to learn the basics of this language.
Basics of Icon
Conversion of old BASIC programs to Icon in order to learn the basics of this language.
Basics of Io
Conversion of old BASIC programs to Io in order to learn the basics of this language.
Basics of Janet
Conversion of old BASIC programs to Janet in order to learn the basics of this language.
Basics of Julia
Conversion of old BASIC programs to Julia in order to learn the basics of this language.
Basics of Kotlin
Conversion of old BASIC programs to Kotlin in order to learn the basics of this language.
Basics of Lobster
Conversion of old BASIC programs to Lobster in order to learn the basics of this language.
Basics of Lua
Conversion of old BASIC programs to Lua in order to learn the basics of this language.
Basics of Nature
Conversion of old BASIC programs to Nature in order to learn the basics of this language.
Basics of Neat
Conversion of old BASIC programs to Neat in order to learn the basics of this language.
Basics of Neko
Conversion of old BASIC programs to Neko in order to learn the basics of this language.
Basics of Nelua
Conversion of old BASIC programs to Nelua in order to learn the basics of this language.
Basics of Nim
Conversion of old BASIC programs to Nim in order to learn the basics of this language.
Basics of Nit
Conversion of old BASIC programs to Nit in order to learn the basics of this language.
Basics of Oberon-07
Conversion of old BASIC programs to Oberon-07 in order to learn the basics of this language.
Basics of OCaml
Conversion of old BASIC programs to OCaml in order to learn the basics of this language.
Basics of Pike
Conversion of old BASIC programs to Pike in order to learn the basics of this language.
Basics of Pony
Conversion of old BASIC programs to Pony in order to learn the basics of this language.
Basics of Python
Conversion of old BASIC programs to Python in order to learn the basics of this language.
Basics of Racket
Conversion of old BASIC programs to Racket in order to learn the basics of this language.
Basics of Raku
Conversion of old BASIC programs to Raku in order to learn the basics of this language.
Basics of Retro
Conversion of old BASIC programs to Retro in order to learn the basics of this language.
Basics of Rexx
Conversion of old BASIC programs to Rexx in order to learn the basics of this language.
Basics of Ring
Conversion of old BASIC programs to Ring in order to learn the basics of this language.
Basics of Rust
Conversion of old BASIC programs to Rust in order to learn the basics of this language.
Basics of Scala
Conversion of old BASIC programs to Scala in order to learn the basics of this language.
Basics of Scheme
Conversion of old BASIC programs to Scheme in order to learn the basics of this language.
Basics of Styx
Conversion of old BASIC programs to Styx in order to learn the basics of this language.
Basics of Swift
Conversion of old BASIC programs to Swift in order to learn the basics of this language.
Basics of V
Conversion of old BASIC programs to V in order to learn the basics of this language.
Basics of Vala
Conversion of old BASIC programs to Vala in order to learn the basics of this language.
Basics of Zig
Conversion of old BASIC programs to Zig in order to learn the basics of this language.

External related links