cat

The UNIX cat coreutil.

// cat.bs

var code = 0

for i in 1..len(os.args) {
    const path = os.args[i]
    const contents = io.readfile(path)
    if !contents {
        io.eprintln("Error: could not read file '{path}'")
        code = 1
        continue
    }

    io.print(contents)
}

os.exit(code)
$ bs cat.bs cat.bs # Poor man's quine
var code = 0

for i in 1..len(os.args) {
    const path = os.args[i]
    const contents = io.readfile(path)
    if !contents {
        io.eprintln("Error: could not read file '{path}'")
        code = 1
        continue
    }

    io.print(contents)
}

os.exit(code)

ls

The UNIX ls coreutil.

// ls.bs

fn compare(a, b) {
    if a.suffix("/") && !b.suffix("/") {
        return true
    }

    if !a.suffix("/") && b.suffix("/") {
        return false
    }

    return a.compare(b) < 0
}

io.readdir(if len(os.args) >= 2 then os.args[1] else ".")
    .map(fn (e) -> e.name() $ if e.isdir() then "/" else "")
    .sort(compare)
    .map(io.println)
$ bs ls.bs
README.md
ls.bs

grep

The UNIX grep coreutil.

// grep.bs

fn grep(f, path, pattern) {
    var row = 0
    while !f.eof() {
        const line = f.readln()

        const col = line.find(pattern)
        if col {
            io.println("{path}:{row + 1}:{col + 1}: {line}")
        }

        row += 1
    }
}

if len(os.args) < 2 {
    io.eprintln("Usage: {os.args[0]} <pattern> [file...]")
    io.eprintln("Error: pattern not provided")
    os.exit(1)
}

const pattern = Regex(os.args[1])
if !pattern {
    io.eprintln("Error: invalid pattern")
    os.exit(1)
}

if len(os.args) == 2 {
    return grep(io.stdin, "<stdin>", pattern)
}

var code = 0

for i in 2..len(os.args) {
    const path = os.args[i]

    const f = io.Reader(path)
    if !f {
        io.println("Error: could not open file '{path}'")
        code = 1
        continue
    }

    grep(f, path, pattern)
    f.close()
}

os.exit(code)
$ bs grep.bs '\.[A-z]+\(' grep.bs
grep.bs:3:13:     while !f.eof() {
grep.bs:4:23:         const line = f.readln()
grep.bs:6:25:         const col = line.find(pattern)
grep.bs:8:15:             io.println("{path}:{row + 1}:{col + 1}: {line}")
grep.bs:16:7:     io.eprintln("Usage: {os.args[0]} <pattern> [file...]")
grep.bs:17:7:     io.eprintln("Error: pattern not provided")
grep.bs:18:7:     os.exit(1)
grep.bs:23:7:     io.eprintln("Error: invalid pattern")
grep.bs:24:7:     os.exit(1)
grep.bs:33:12: for i in 2..len(os.args) {
grep.bs:36:17:     const f = io.Reader(path)
grep.bs:38:11:         io.println("Error: could not open file '{path}'")
grep.bs:44:6:     f.close()
grep.bs:47:3: os.exit(code)

$ cat grep.bs | bs grep.bs '\.[A-z]+\(' # DON'T CAT INTO GREP!!!
<stdin>:3:13:     while !f.eof() {
<stdin>:4:23:         const line = f.readln()
<stdin>:6:25:         const col = line.find(pattern)
<stdin>:8:15:             io.println("{path}:{row + 1}:{col + 1}: {line}")
<stdin>:16:7:     io.eprintln("Usage: {os.args[0]} <pattern> [file...]")
<stdin>:17:7:     io.eprintln("Error: pattern not provided")
<stdin>:18:7:     os.exit(1)
<stdin>:23:7:     io.eprintln("Error: invalid pattern")
<stdin>:24:7:     os.exit(1)
<stdin>:33:12: for i in 2..len(os.args) {
<stdin>:36:17:     const f = io.Reader(path)
<stdin>:38:11:         io.println("Error: could not open file '{path}'")
<stdin>:44:6:     f.close()
<stdin>:47:3: os.exit(code)

Shell

A simple UNIX shell.

// shell.bs

const home = os.getenv("HOME")

// Return a pretty current working directory
fn pwd() {
    const cwd = os.getcwd()
    if cwd.prefix(home) {
        return "~" $ cwd.slice(len(home))
    }

    return cwd
}

// Rawdogging shell lexing with regex any%
const delim = Regex("[ \n\t]+")

// Previous working directory
var previous = nil

while !io.stdin.eof() {
    const args = io.input("{pwd()} $ ").split(delim)
    if len(args) == 0 {
        continue
    }

    const cmd = args[0]
    match cmd {
        // Builtin 'exit'
        // Usage:
        //   exit         -> Exits with 0
        //   exit <CODE>  -> Exits with CODE
        "exit" -> {
            if len(args) > 2 {
                io.eprintln("Error: too many arguments to command '{cmd}'")
                continue
            }

            const code = if len(args) == 2 then args[1].tonumber() else 0
            if !code {
                io.eprintln("Error: invalid exit code '{args[1]}'")
                continue
            }

            os.exit(code)
        }

        // Builtin 'cd'
        // Usage:
        //   cd        -> Go to HOME
        //   cd <DIR>  -> Go to DIR
        //   cd -      -> Go to previous location
        "cd" -> {
            if len(args) > 2 {
                io.eprintln("Error: too many arguments to command '{cmd}'")
                continue
            }

            var path = if len(args) == 2 then args[1] else "~"
            if path == "-" {
                if !previous {
                    io.eprintln("Error: no previous directory to go into")
                    continue
                }

                path = previous
            }

            if path.prefix("~") {
                path = home $ path.slice(1)
            }

            const current = os.getcwd()
            if !os.setcwd(path) {
                io.eprintln("Error: the directory '{path}' does not exist")
            }

            previous = current
        }
    } else {
        const p = os.Process(args)
        if !p {
            io.eprintln("Error: unknown command '{cmd}'")
            continue
        }
        p.wait()
    }
}

// In case of CTRL-d
io.println()
$ bs shell.bs
~/Git/bs/examples/shell $ ls -a
.  ..  README.md  shell.bs

Use rlwrap in order to get readline bindings and filename autocompletion.

$ rlwrap -c bs shell.bs # Like so

Rule110

An implementation of Rule110 for proof of Turing Completeness.

// rule110.bs

const board = [].resize(30).fill(0)
board[len(board) - 2] = 1

for i in 0..len(board) - 2 {
    for _, j in board {
        io.print(if j != 0 then "*" else " ")
    }
    io.println()

    var pattern = (board[0] << 1) | board[1]
    for j in 1..len(board) - 1 {
        pattern = ((pattern << 1) & 7) | board[j + 1]
        board[j] = (110 >> pattern) & 1
    }
}
$ bs rule110.bs
                            *
                           **
                          ***
                         ** *
                        *****
                       **   *
                      ***  **
                     ** * ***
                    ******* *
                   **     ***
                  ***    ** *
                 ** *   *****
                *****  **   *
               **   * ***  **
              ***  **** * ***
             ** * **  ***** *
            ******** **   ***
           **      ****  ** *
          ***     **  * *****
         ** *    *** ****   *
        *****   ** ***  *  **
       **   *  ***** * ** ***
      ***  ** **   ******** *
     ** * ******  **      ***
    *******    * ***     ** *
   **     *   **** *    *****
  ***    **  **  ***   **   *
 ** *   *** *** ** *  ***  **

Game Of Life

An implementation of Conway's Game Of Life

// GameOfLife.bs

class GameOfLife {
    init(width, height) {
        this.width = width
        this.height = height

        this.board = []
        this.buffer = []

        this.board.resize(width * height)
        this.buffer.resize(width * height)
    }

    get(x, y) {
        x %= this.width
        y %= this.height
        return this.board[y * this.width + x]
    }

    set(x, y, v) {
        x %= this.width
        y %= this.height
        this.board[y * this.width + x] = v
    }

    step() {
        fn set(x, y, v) {
            x %= this.width
            y %= this.height
            this.buffer[y * this.width + x] = v
        }

        fn nbors(x, y) {
            var count = 0
            for dy in -1..2 {
                for dx in -1..2 {
                    if dx == 0 && dy == 0 {
                        continue
                    }

                    if this.get(x + dx, y + dy) {
                        count += 1
                    }
                }
            }
            return count
        }

        for y in 0..this.height {
            for x in 0..this.width {
                const n = nbors(x, y)
                if n == 2 {
                    set(x, y, this.get(x, y))
                } else {
                    set(x, y, n == 3)
                }
            }
        }

        // Swap buffers
        const t = this.board
        this.board = this.buffer
        this.buffer = t
    }

    // (X, Y) is the center of the glider
    glider(x, y) {
        this.set(x + 0, y - 1, true)
        this.set(x + 1, y + 0, true)
        this.set(x - 1, y + 1, true)
        this.set(x + 0, y + 1, true)
        this.set(x + 1, y + 1, true)
    }
}

return GameOfLife

Console

// game_of_life_tui.bs

const GameOfLife = import("GameOfLife")

const INTERVAL = 0.1

class GameOfLifeTUI < GameOfLife {
    init(width, height) {
        super.init(width, height)
    }

    show() {
        for y in 0..this.height {
            for x in 0..this.width {
                io.print(if this.get(x, y) then "#" else ".")
            }
            io.println()
        }
    }
}

const gol = GameOfLifeTUI(20, 10)
gol.glider(1, 1)

while true {
    gol.show()
    gol.step()
    io.print("\e[{gol.height}A\e[{gol.width}D")
    os.sleep(INTERVAL)
}

Run the program.

$ bs game_of_life_tui.bs

Raylib

// raylib.c

#include <math.h>

#include <bs/object.h>
#include <raylib.h>

static Bs_Value rl_init_window(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 3);
    bs_arg_check_whole_number(bs, args, 0);
    bs_arg_check_whole_number(bs, args, 1);
    bs_arg_check_object_type(bs, args, 2, BS_OBJECT_STR);

    const int width = args[0].as.number;
    const int height = args[1].as.number;
    const Bs_Str *title = (const Bs_Str *)args[2].as.object;

    // BS strings are null terminated by default for FFI convenience
    InitWindow(width, height, title->data);
    return bs_value_nil;
}

static Bs_Value rl_close_window(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    CloseWindow();
    return bs_value_nil;
}

static Bs_Value rl_window_should_close(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_bool(WindowShouldClose());
}

static Bs_Value rl_begin_drawing(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    BeginDrawing();
    return bs_value_nil;
}

static Bs_Value rl_end_drawing(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    EndDrawing();
    return bs_value_nil;
}

static Bs_Value rl_clear_background(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    ClearBackground(GetColor(args[0].as.number));
    return bs_value_nil;
}

static Bs_Value rl_set_exit_key(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    SetExitKey(args[0].as.number);
    return bs_value_nil;
}

static Bs_Value rl_set_config_flags(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    SetConfigFlags(args[0].as.number);
    return bs_value_nil;
}

static Bs_Value rl_draw_line(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 5);
    bs_arg_check_value_type(bs, args, 0, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 2, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 3, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 4, BS_VALUE_NUM);

    const int startPosX = round(args[0].as.number);
    const int startPosY = round(args[1].as.number);
    const int endPosX = round(args[2].as.number);
    const int endPosY = round(args[3].as.number);
    const Color color = GetColor(args[4].as.number);

    DrawLine(startPosX, startPosY, endPosX, endPosY, color);
    return bs_value_nil;
}

static Bs_Value rl_draw_text(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 5);
    bs_arg_check_object_type(bs, args, 0, BS_OBJECT_STR);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 2, BS_VALUE_NUM);
    bs_arg_check_whole_number(bs, args, 3);
    bs_arg_check_whole_number(bs, args, 4);

    const Bs_Str *text = (const Bs_Str *)args[0].as.object;
    const int x = round(args[1].as.number);
    const int y = round(args[2].as.number);
    const int size = args[3].as.number;
    const Color color = GetColor(args[4].as.number);

    DrawText(text->data, x, y, size, color);
    return bs_value_nil;
}

static Bs_Value rl_draw_rectangle(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 5);
    bs_arg_check_value_type(bs, args, 0, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 2, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 3, BS_VALUE_NUM);
    bs_arg_check_whole_number(bs, args, 4);

    const int posX = round(args[0].as.number);
    const int posY = round(args[1].as.number);
    const int width = round(args[2].as.number);
    const int height = round(args[3].as.number);
    const Color color = GetColor(args[4].as.number);

    DrawRectangle(posX, posY, width, height, color);
    return bs_value_nil;
}

static Bs_Value rl_get_screen_width(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(GetScreenWidth());
}

static Bs_Value rl_get_screen_height(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(GetScreenHeight());
}

static Bs_Value rl_get_frame_time(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(GetFrameTime());
}

static Bs_Value rl_get_mouse_x(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(GetMouseX());
}

static Bs_Value rl_get_mouse_y(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(GetMouseY());
}

static Bs_Value rl_is_mouse_button_released(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    return bs_value_bool(IsMouseButtonReleased(args[0].as.number));
}

static Bs_Value rl_is_key_pressed(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    return bs_value_bool(IsKeyPressed(args[0].as.number));
}

static Bs_Value rl_measure_text(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 2);
    bs_arg_check_object_type(bs, args, 0, BS_OBJECT_STR);
    bs_arg_check_whole_number(bs, args, 1);

    const Bs_Str *text = (const Bs_Str *)args[0].as.object;
    const int size = args[1].as.number;

    return bs_value_num(MeasureText(text->data, size));
}

BS_LIBRARY_INIT void bs_library_init(Bs *bs, Bs_C_Lib *library) {
    static const Bs_FFI ffi[] = {
        {"init_window", rl_init_window},
        {"close_window", rl_close_window},
        {"window_should_close", rl_window_should_close},
        {"begin_drawing", rl_begin_drawing},
        {"end_drawing", rl_end_drawing},
        {"clear_background", rl_clear_background},
        {"set_exit_key", rl_set_exit_key},
        {"set_config_flags", rl_set_config_flags},
        {"draw_line", rl_draw_line},
        {"draw_text", rl_draw_text},
        {"draw_rectangle", rl_draw_rectangle},
        {"get_screen_width", rl_get_screen_width},
        {"get_screen_height", rl_get_screen_height},
        {"get_frame_time", rl_get_frame_time},
        {"get_mouse_x", rl_get_mouse_x},
        {"get_mouse_y", rl_get_mouse_y},
        {"is_mouse_button_released", rl_is_mouse_button_released},
        {"is_key_pressed", rl_is_key_pressed},
        {"measure_text", rl_measure_text},
    };
    bs_c_lib_ffi(bs, library, ffi, bs_c_array_size(ffi));

    bs_c_lib_set(bs, library, Bs_Sv_Static("MOUSE_BUTTON_LEFT"), bs_value_num(MOUSE_BUTTON_LEFT));

    bs_c_lib_set(
        bs, library, Bs_Sv_Static("FLAG_WINDOW_RESIZABLE"), bs_value_num(FLAG_WINDOW_RESIZABLE));
}

Compile the raylib module.

$ cc -o raylib.so -fPIC -shared raylib.c -lbs -lraylib    # On Linux
$ cc -o raylib.dylib -fPIC -shared raylib.c -lbs -lraylib # On macOS
$ cl /LD /Fe:raylib.dll raylib.c bs.lib raylib.lib        # On Windows

Make sure to provide the compiler and linker flags as required.

// game_of_life_raylib.bs

const rl = import("raylib")
const GameOfLife = import("GameOfLife")

const GRID = 0x5A524CFF
const BACKGROUND = 0x282828FF
const FOREGROUND = 0x89B482FF

const INTERVAL = 0.1
const FONT_SIZE = 30

var width = 800
var height = 600

var cell_size = 0
var padding_x = 0
var padding_y = 0

class GameOfLifeRaylib < GameOfLife {
    init(width, height) {
        super.init(width, height)
        this.clock = 0
        this.paused = false
    }

    show() {
        width = rl.get_screen_width()
        height = rl.get_screen_height() - FONT_SIZE * 1.2

        const cw = width / this.width
        const ch = height / this.height
        cell_size = cw.min(ch)

        padding_x = (width - this.width * cell_size) / 2
        padding_y = (height - this.height * cell_size) / 2

        rl.draw_rectangle(
            padding_x,
            padding_y,
            this.width * cell_size,
            this.height * cell_size,
            BACKGROUND)

        {
            const label = "Click to toggle cell, Space to play/pause"
            rl.draw_text(
                label,
                (width - rl.measure_text(label, FONT_SIZE)) / 2,
                this.height * cell_size + FONT_SIZE * 0.1,
                FONT_SIZE,
                FOREGROUND)
        }

        for i in 0..=this.width {
            const x = padding_x + i * cell_size
            rl.draw_line(x, 0, x, this.height * cell_size, GRID)
        }

        for i in 0..=this.height {
            const y = padding_y + i * cell_size
            rl.draw_line(padding_x, y, padding_x + this.width * cell_size, y, GRID)
        }

        for y in 0..this.height {
            for x in 0..this.width {
                if this.get(x, y) {
                    rl.draw_rectangle(
                        padding_x + x * cell_size,
                        padding_y + y * cell_size,
                        cell_size,
                        cell_size,
                        FOREGROUND)
                }
            }
        }
    }
}

rl.init_window(width, height, "Game Of Life")
rl.set_exit_key(ascii.code("Q"))
rl.set_config_flags(rl.FLAG_WINDOW_RESIZABLE)

const gol = GameOfLifeRaylib(20, 20)
gol.glider(2, 2)

while !rl.window_should_close() {
    rl.begin_drawing()
    rl.clear_background(0x181818FF)

    gol.show()

    if !gol.paused {
        gol.clock += rl.get_frame_time()
        if gol.clock >= INTERVAL {
            gol.clock = 0
            gol.step()
        }
    }

    if rl.is_key_pressed(ascii.code(" ")) {
        gol.paused = !gol.paused
    }

    if rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT) {
        const x = ((rl.get_mouse_x() - padding_x) / cell_size).floor()
        const y = ((rl.get_mouse_y() - padding_y) / cell_size).floor()
        gol.set(x, y, !gol.get(x, y))
    }

    rl.end_drawing()
}

rl.close_window()

Run the program.

$ bs game_of_life_raylib.bs

CLI Task Management APP

// tasks.bs

const TASKS_PATH = os.getenv("HOME") $ "/.tasks"

class Tasks {
    init(path) {
        this.path = path
        this.tasks = (io.readfile(this.path) || "").split("\n")
    }

    add(title) {
        this.tasks.push(title)
        this.save()
        io.println("Added: {title}")
    }

    verify(index) {
        const total = len(this.tasks)
        if index >= total {
            io.eprintln("Error: invalid index {index} (total is {total})")
            os.exit(1)
        }
    }

    done(index) {
        this.verify(index)
        const title = delete(this.tasks[index])
        this.save()
        io.println("Done {index}: {title}")
    }

    edit(index, title) {
        this.verify(index)
        this.tasks[index] = title
        this.save()
        io.println("Edit {index}: {title}")
    }

    list(query) {
        for i, t in this.tasks {
            if query && !t.find(query) {
                continue
            }

            io.println("{i}: {t}")
        }
    }

    save() {
        const f = io.Writer(this.path)
        if !f {
            io.eprintln("Error: could not save tasks to '{this.path}'")
            os.exit(1)
        }

        f.write(this.tasks.join("\n"))
        f.close()
    }
}

fn usage(f) {
    f.writeln("Usage: tasks <command> [args...]")
    f.writeln("Commands:")
    f.writeln("    help                    Show this message and exit")
    f.writeln("    add  <title>            Add a task")
    f.writeln("    done <index>            Mark task as done")
    f.writeln("    edit <index> <title>    Edit a task")
    f.writeln("    list [query]            List tasks, with optional query")
}

if len(os.args) < 2 {
    return Tasks(TASKS_PATH).list(nil)
}

const command = os.args[1]
match command {
    "help" -> usage(io.stdout)

    "add" -> {
        if len(os.args) < 3 {
            io.eprintln("Error: task title not provided")
            io.eprintln("Usage: tasks add <title>")
            os.exit(1)
        }

        Tasks(TASKS_PATH).add(os.args[2])
    }

    "done" -> {
        if len(os.args) < 3 {
            io.eprintln("Error: task index not provided")
            io.eprintln("Usage: tasks done <index>")
            os.exit(1)
        }

        const index = os.args[2].tonumber()
        if !index {
            io.eprintln("Error: invalid index '{os.args[2]}'")
            os.exit(1)
        }

        Tasks(TASKS_PATH).done(index)
    }

    "edit" -> {
        if len(os.args) < 3 {
            io.eprintln("Error: task index not provided")
            io.eprintln("Usage: tasks edit <index> <title>")
            os.exit(1)
        }

        const index = os.args[2].tonumber()
        if !index {
            io.eprintln("Error: invalid index '{os.args[2]}'")
            os.exit(1)
        }

        if len(os.args) < 4 {
            io.eprintln("Error: task title not provided")
            io.eprintln("Usage: tasks edit <index> <title>")
            os.exit(1)
        }

        Tasks(TASKS_PATH).edit(index, os.args[3])
    }

    "list" -> {
        var query = nil
        if len(os.args) > 2 {
            query = Regex(os.args[2])
            if !query {
                io.eprintln("Error: invalid query '{os.args[2]}'")
                os.exit(1)
            }
        }

        Tasks(TASKS_PATH).list(query)
    }
} else {
    io.eprintln("Error: invalid command '{command}'")
    usage(io.stderr)
    os.exit(1)
}
$ bs tasks.bs add "Finish reading book"
Added: Finish reading book

$ bs tasks.bs add "Submit project report"
Added: Submit project report

$ bs tasks.bs add "Submit homework"
Added: Submit homework

$ bs tasks.bs list
[0] Finish reading book
[1] Submit project report
[2] Submit homework

$ bs tasks.bs list 'Submit.*report'
[1] Submit project report

$ bs tasks.bs done 0
Done: Finish reading book

$ bs tasks.bs list
[0] Submit project report
[1] Submit homework

Flappy Bird

Flappy Bird using Raylib.

// raylib.c

#include <bs/object.h>
#include <raylib.h>

static Bs_Value rl_init_window(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 3);
    bs_arg_check_whole_number(bs, args, 0);
    bs_arg_check_whole_number(bs, args, 1);
    bs_arg_check_object_type(bs, args, 2, BS_OBJECT_STR);

    const int width = args[0].as.number;
    const int height = args[1].as.number;
    const Bs_Str *title = (const Bs_Str *)args[2].as.object;

    InitWindow(width, height, title->data);
    return bs_value_nil;
}

static Bs_Value rl_init_audio_device(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    InitAudioDevice();
    return bs_value_nil;
}

static Bs_Value rl_set_target_fps(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    SetTargetFPS(args[0].as.number);
    return bs_value_nil;
}

static Bs_Value rl_close_window(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    CloseWindow();
    return bs_value_nil;
}

static Bs_Value rl_close_audio_device(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    CloseAudioDevice();
    return bs_value_nil;
}

static Bs_Value rl_window_should_close(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_bool(WindowShouldClose());
}

static Bs_Value rl_begin_drawing(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    BeginDrawing();
    return bs_value_nil;
}

static Bs_Value rl_end_drawing(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    EndDrawing();
    return bs_value_nil;
}

static Bs_Value rl_clear_background(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    ClearBackground(GetColor(args[0].as.number));
    return bs_value_nil;
}

static Bs_Value rl_draw_rectangle(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 5);
    bs_arg_check_value_type(bs, args, 0, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 2, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 3, BS_VALUE_NUM);
    bs_arg_check_whole_number(bs, args, 4);

    const int x = args[0].as.number;
    const int y = args[1].as.number;
    const int width = args[2].as.number;
    const int height = args[3].as.number;
    const Color color = GetColor(args[4].as.number);

    DrawRectangleLines(x, y, width, height, color);
    return bs_value_nil;
}

static Bs_Value rl_is_key_pressed(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    return bs_value_bool(IsKeyPressed(args[0].as.number));
}

static Bs_Value rl_get_frame_time(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(GetFrameTime());
}

static Bs_Value rl_texture_init(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_object_type(bs, args, 0, BS_OBJECT_STR);

    const Bs_Str *path = (const Bs_Str *)args[0].as.object;

    Texture texture = LoadTexture(path->data);
    if (!IsTextureValid(texture)) {
        return bs_value_nil;
    }

    bs_this_c_instance_data_as(args, Texture) = texture;
    return args[-1];
}

static Bs_Value rl_texture_draw(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 5);
    bs_arg_check_value_type(bs, args, 0, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 2, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 3, BS_VALUE_NUM);
    bs_arg_check_whole_number(bs, args, 4);

    const float scale = args[3].as.number;

    const Texture texture = bs_this_c_instance_data_as(args, Texture);
    const Rectangle src = {0, 0, texture.width, texture.height};
    const Rectangle dst = {
        args[0].as.number + (texture.width * scale) / 2.0,
        args[1].as.number + (texture.height * scale) / 2.0,
        texture.width * scale,
        texture.height * scale,
    };

    const Vector2 origin = {texture.width * scale / 2.0, texture.height * scale / 2.0};
    const float rotation = args[2].as.number;
    const Color tint = GetColor(args[4].as.number);

    DrawTexturePro(texture, src, dst, origin, rotation, tint);
    return bs_value_nil;
}

static Bs_Value rl_texture_width(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(bs_this_c_instance_data_as(args, Texture).width);
}

static Bs_Value rl_texture_height(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_num(bs_this_c_instance_data_as(args, Texture).height);
}

static Bs_Value rl_texture_unload(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    UnloadTexture(bs_this_c_instance_data_as(args, Texture));
    return bs_value_nil;
}

static Bs_Value rl_sound_init(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_object_type(bs, args, 0, BS_OBJECT_STR);

    const Bs_Str *path = (const Bs_Str *)args[0].as.object;

    Sound sound = LoadSound(path->data);
    if (!IsSoundValid(sound)) {
        return bs_value_nil;
    }

    bs_this_c_instance_data_as(args, Sound) = sound;
    return args[-1];
}

static Bs_Value rl_sound_play(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    PlaySound(bs_this_c_instance_data_as(args, Sound));
    return bs_value_nil;
}

static Bs_Value rl_sound_unload(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    UnloadSound(bs_this_c_instance_data_as(args, Sound));
    return bs_value_nil;
}

static Bs_Value rl_music_init(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_object_type(bs, args, 0, BS_OBJECT_STR);

    const Bs_Str *path = (const Bs_Str *)args[0].as.object;

    Music music = LoadMusicStream(path->data);
    if (!IsMusicValid(music)) {
        return bs_value_nil;
    }

    // An official binding would never do this, but do I care?
    music.looping = true;
    PlayMusicStream(music);
    SetMusicVolume(music, 0.2);

    bs_this_c_instance_data_as(args, Music) = music;
    return args[-1];
}

static Bs_Value rl_music_toggle(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    Music music = bs_this_c_instance_data_as(args, Music);
    if (IsMusicStreamPlaying(music)) {
        StopMusicStream(music);
    } else {
        PlayMusicStream(music);
    }
    return bs_value_nil;
}

static Bs_Value rl_music_update(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    UpdateMusicStream(bs_this_c_instance_data_as(args, Music));
    return bs_value_nil;
}

static Bs_Value rl_music_unload(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    UnloadMusicStream(bs_this_c_instance_data_as(args, Music));
    return bs_value_nil;
}

BS_LIBRARY_INIT void bs_library_init(Bs *bs, Bs_C_Lib *library) {
    static const Bs_FFI ffi[] = {
        {"init_window", rl_init_window},
        {"init_audio_device", rl_init_audio_device},
        {"set_target_fps", rl_set_target_fps},
        {"close_window", rl_close_window},
        {"close_audio_device", rl_close_audio_device},
        {"window_should_close", rl_window_should_close},
        {"begin_drawing", rl_begin_drawing},
        {"end_drawing", rl_end_drawing},
        {"clear_background", rl_clear_background},
        {"draw_rectangle", rl_draw_rectangle},
        {"is_key_pressed", rl_is_key_pressed},
        {"get_frame_time", rl_get_frame_time},
    };
    bs_c_lib_ffi(bs, library, ffi, bs_c_array_size(ffi));

    Bs_C_Class *texture_class =
        bs_c_class_new(bs, Bs_Sv_Static("Texture"), sizeof(Texture), rl_texture_init);

    texture_class->can_fail = true;
    bs_c_class_add(bs, texture_class, Bs_Sv_Static("draw"), rl_texture_draw);
    bs_c_class_add(bs, texture_class, Bs_Sv_Static("width"), rl_texture_width);
    bs_c_class_add(bs, texture_class, Bs_Sv_Static("height"), rl_texture_height);
    bs_c_class_add(bs, texture_class, Bs_Sv_Static("unload"), rl_texture_unload);
    bs_c_lib_set(bs, library, texture_class->name, bs_value_object(texture_class));

    Bs_C_Class *sound_class =
        bs_c_class_new(bs, Bs_Sv_Static("Sound"), sizeof(Sound), rl_sound_init);

    sound_class->can_fail = true;
    bs_c_class_add(bs, sound_class, Bs_Sv_Static("play"), rl_sound_play);
    bs_c_class_add(bs, sound_class, Bs_Sv_Static("unload"), rl_sound_unload);
    bs_c_lib_set(bs, library, sound_class->name, bs_value_object(sound_class));

    Bs_C_Class *music_class =
        bs_c_class_new(bs, Bs_Sv_Static("Music"), sizeof(Music), rl_music_init);

    music_class->can_fail = true;
    bs_c_class_add(bs, music_class, Bs_Sv_Static("toggle"), rl_music_toggle);
    bs_c_class_add(bs, music_class, Bs_Sv_Static("update"), rl_music_update);
    bs_c_class_add(bs, music_class, Bs_Sv_Static("unload"), rl_music_unload);
    bs_c_lib_set(bs, library, music_class->name, bs_value_object(music_class));
}

Compile the raylib module.

$ cc -o raylib.so -fPIC -shared raylib.c -lbs -lraylib    # On Linux
$ cc -o raylib.dylib -fPIC -shared raylib.c -lbs -lraylib # On macOS
$ cl /LD /Fe:raylib.dll raylib.c bs.lib raylib.lib        # On Windows

Make sure to provide the compiler and linker flags as required.

// flappy_bird.bs

const rl = import("raylib")
const game = {}

const WIDTH = 800
const HEIGHT = 600

const GRAVITY = 0.5
const IMPULSE = -7

const BIRD_TILT = 20
const BIRD_STARTOFF_SPEED = 7
const BIRD_ANIMATION_SPEED = 0.2

const BIRD_MIN_X = 100
const BIRD_MIN_Y = 50

const PIPE_GAP = 120
const PIPE_SPEED = -4
const PIPE_SPAWN_DELAY = 1.5
const PIPE_SPAWN_RANGE = HEIGHT / 5

const BACKGROUND_SCROLL_SPEED = 0.5

const MESSAGE_PADDING = 30
const OVER_MESSAGE_SCALE = 1.2
const TITLE_MESSAGE_SCALE = 1.2
const CONTINUE_MESSAGE_SCALE = 0.7

const BACKGROUND_MUSIC_DELAY = 1.2

const DEBUG_COLOR = 0xFF0000FF
const DEBUG_HITBOX = false

fn exit(code) {
    game.assets.unload()
    rl.close_audio_device()
    rl.close_window()
    os.exit(code)
}

class Assets {
    init() {
        this.sounds = {}
        this.textures = {}
    }

    sound(path, loader) {
        path = "assets/sounds/" $ path
        if path in this.sounds {
            return this.sounds[path]
        }

        const sound = loader(path)
        if !sound {
            io.eprintln("Error: could not load sound '{path}'")
            exit(1)
        }

        this.sounds[path] = sound
        return sound
    }

    texture(path) {
        path = "assets/images/" $ path
        if path in this.textures {
            return this.textures[path]
        }

        const texture = rl.Texture(path)
        if !texture {
            io.eprintln("Error: could not load image '{path}'")
            exit(1)
        }

        this.textures[path] = texture
        return texture
    }

    unload() {
        for _, sound in this.sounds {
            sound.unload()
        }

        for _, texture in this.textures {
            texture.unload()
        }
    }
}

class Hitbox {
    init(x, y, width, height) {
        this.x = x
        this.y = y
        this.width = width
        this.height = height
    }

    move(x, y) {
        this.x = x
        this.y = y
    }

    debug() {
        if DEBUG_HITBOX {
            rl.draw_rectangle(this.x, this.y, this.width, this.height, DEBUG_COLOR)
        }
    }

    collides(that) {
        return this.x < that.x + that.width && this.x + this.width > that.x &&
            this.y < that.y + that.height && this.y + this.height > that.y
    }
}

class Bird {
    init() {
        this.dy = 0
        this.current = 0
        this.textures = [
            game.assets.texture("bird0.png"),
            game.assets.texture("bird1.png"),
            game.assets.texture("bird2.png"),
        ]

        const w = this.textures[0].width() * game.scale
        const h = this.textures[0].height() * game.scale
        this.hitbox = Hitbox((WIDTH - w) / 2, HEIGHT / 3, w, h)
    }

    update() {
        if game.started {
            if !game.over && this.hitbox.x > BIRD_MIN_X {
                this.hitbox.x -= BIRD_STARTOFF_SPEED
                this.dy -= 0.3 * GRAVITY
            }

            const needs_dy = !game.over || this.hitbox.y + this.hitbox.height * game.scale < game.base.y
            if needs_dy {
                this.dy += GRAVITY
            }

            if !game.over && rl.is_key_pressed(32) {
                this.dy = IMPULSE
            }

            if needs_dy {
                this.hitbox.y += this.dy
                if this.hitbox.y < BIRD_MIN_Y {
                    this.hitbox.y = BIRD_MIN_Y
                }
            }

            if this.hitbox.y + this.hitbox.height >= game.base.y {
                game.die()
            }
        }

        if !game.over {
            this.current += rl.get_frame_time()
            if this.current >= BIRD_ANIMATION_SPEED * len(this.textures) {
                this.current %= BIRD_ANIMATION_SPEED * len(this.textures)
            }
        }

        this.textures[(this.current / BIRD_ANIMATION_SPEED).floor()].draw(
            this.hitbox.x,
            this.hitbox.y,
            this.dy.sign() * BIRD_TILT,
            game.scale,
            0xFFFFFFFF)

        this.hitbox.debug()
    }
}

class Background {
    init() {
        this.x = 0
        this.texture = game.assets.texture("background.png")
        this.width = this.texture.width()
        game.scale = HEIGHT / this.texture.height()
    }

    update() {
        if !game.over {
            this.x -= BACKGROUND_SCROLL_SPEED
            if this.x < -this.width {
                this.x = 0
            }
        }

        var i = 0
        while this.x + this.width * i < WIDTH {
            this.texture.draw(this.x + this.width * i, 0, 0, game.scale, 0xFFFFFFFF)
            i += 1
        }
    }
}

class Base {
    init() {
        this.x = 0
        this.texture = game.assets.texture("base.png")
        this.width = this.texture.width()
        this.y = HEIGHT - this.texture.height() * game.scale * 0.9
    }

    update() {
        if !game.over {
            this.x += PIPE_SPEED
            if this.x < -this.width {
                this.x = 0
            }
        }

        var i = 0
        while this.x + this.width * i < WIDTH {
            this.texture.draw(
                this.x + this.width * i,
                this.y,
                0,
                game.scale,
                0xFFFFFFFF)

            i += 1
        }
    }
}

class Pipe {
    init(y) {
        this.dx = PIPE_SPEED
        this.texture = game.assets.texture("pipe.png")
        this.hitbox = Hitbox(
            WIDTH,
            y,
            this.texture.width() * game.scale,
            this.texture.height() * game.scale)

        this.scored = false
    }

    top() {
        return Hitbox(
            this.hitbox.x,
            this.hitbox.y - this.hitbox.height / 2,
            this.hitbox.width,
            this.hitbox.height)
    }

    bottom() {
        return Hitbox(
            this.hitbox.x,
            this.hitbox.y + this.hitbox.height / 2 + PIPE_GAP,
            this.hitbox.width,
            this.hitbox.height)
    }

    update() {
        if !game.over {
            this.hitbox.x += this.dx
            if this.hitbox.x + this.hitbox.width < BIRD_MIN_X && !this.scored {
                game.point()
                this.scored = true
            }
        }

        const top = this.top()
        this.texture.draw(top.x, top.y, 180, game.scale, 0xFFFFFFFF)

        const bottom = this.bottom()
        this.texture.draw(bottom.x, bottom.y, 0, game.scale, 0xFFFFFFFF)

        top.debug()
        bottom.debug()
    }
}

class Pipes {
    init() {
        this.items = []
        this.clock = 0
    }

    update() {
        if !game.started {
            return true
        }

        if !game.over {
            this.clock += rl.get_frame_time()
            if this.clock >= PIPE_SPAWN_DELAY {
                this.items.push(Pipe(game.rng.number(-PIPE_SPAWN_RANGE, PIPE_SPAWN_RANGE)))
                this.clock %= PIPE_SPAWN_DELAY
            }

            this.items = this.items.filter(fn (p) -> p.hitbox.x >= -p.hitbox.width)
        }

        for _, pipe in this.items {
            pipe.update()
            if !game.over &&
                (pipe.top().collides(game.bird.hitbox) ||
                 pipe.bottom().collides(game.bird.hitbox)) {
                return false
            }
        }

        return true
    }
}

class Score {
    init() {
        this.best = nil
        this.current = 0
        this.textures = [
            game.assets.texture("0.png"),
            game.assets.texture("1.png"),
            game.assets.texture("2.png"),
            game.assets.texture("3.png"),
            game.assets.texture("4.png"),
            game.assets.texture("5.png"),
            game.assets.texture("6.png"),
            game.assets.texture("7.png"),
            game.assets.texture("8.png"),
            game.assets.texture("9.png"),
        ]
    }

    reset() {
        this.current = 0
    }

    display() {
        if game.started {
            fn show(n, y, scale, tint) {
                const digits = []
                if n < 10 {
                    digits.push(n)
                } else {
                    while n != 0 {
                        digits.push(n % 10)
                        n = (n / 10).floor()
                    }
                }
                digits.reverse()

                scale *= game.scale
                const width = digits.reduce(fn (a, b) -> a + this.textures[b].width() * scale, 0)
                var x = (WIDTH - width) / 2
                for _, n in digits {
                    this.textures[n].draw(x, y, 0, scale, tint)
                    x += this.textures[n].width() * scale
                }
            }

            show(this.current, HEIGHT / 3.6, 1.0, 0xFFFFFFFF)
            if this.best {
                show(this.best, HEIGHT / 2.8, 0.7, 0xDDDDDDFF)
            }
        }
    }
}

rl.init_window(WIDTH, HEIGHT, "Hello from BS!")
rl.init_audio_device()
rl.set_target_fps(60)

game.assets = Assets()
game.started = false
game.over = false
game.rng = math.Random()

fn game.die() {
    if game.over {
        return
    }

    game.over = true
    if !game.score.best || game.score.current > game.score.best {
        game.score.best = game.score.current
    }

    game.die_sound.play()
    game.background_music_delay = BACKGROUND_MUSIC_DELAY
}

fn game.point() {
    game.score.current += 1
    game.point_sound.play()
}

fn game.start() {
    game.start_sound.play()
    game.background_music.toggle()
}

game.background = Background()
game.base = Base()
game.bird = Bird()
game.pipes = Pipes()
game.score = Score()

game.over_message = game.assets.texture("gameover.png")
game.title_message = game.assets.texture("title.png")
game.continue_message = game.assets.texture("continue.png")

game.die_sound = game.assets.sound("die.wav", rl.Sound)
game.point_sound = game.assets.sound("point.wav", rl.Sound)
game.start_sound = game.assets.sound("start.wav", rl.Sound)
game.background_music = game.assets.sound("background.mp3", rl.Music)

game.background_music_delay = 0

while !rl.window_should_close() {
    rl.begin_drawing()
    game.background.update()

    if game.background_music_delay > 0 {
        game.background_music_delay -= rl.get_frame_time()
        if game.background_music_delay <= 0 {
            game.background_music.toggle()
        }
    }

    game.background_music.update()

    if !game.pipes.update() {
        game.die()
    }

    game.base.update()
    game.bird.update()

    if !game.started {
        const x = (WIDTH - game.title_message.width() * TITLE_MESSAGE_SCALE) / 2
        game.title_message.draw(x, HEIGHT / 7, 0, TITLE_MESSAGE_SCALE, 0xFFFFFFFF)

        const x = (WIDTH - game.continue_message.width() * CONTINUE_MESSAGE_SCALE) / 2
        game.continue_message.draw(
            x,
            HEIGHT / 3 + game.continue_message.height() * CONTINUE_MESSAGE_SCALE + MESSAGE_PADDING,
            0,
            CONTINUE_MESSAGE_SCALE,
            0xFFFFFFFF)

        if rl.is_key_pressed(32) {
            game.started = true
            game.bird.dy = IMPULSE
            game.start()
        }
    }

    if game.over {
        const x = (WIDTH - game.over_message.width() * OVER_MESSAGE_SCALE) / 2
        game.over_message.draw(x, HEIGHT / 7, 0, OVER_MESSAGE_SCALE, 0xFFFFFFFF)

        if game.background_music_delay <= 0 {
            const x = (WIDTH - game.continue_message.width() * CONTINUE_MESSAGE_SCALE) / 2
            game.continue_message.draw(
                x,
                HEIGHT / 3 + game.continue_message.height() * CONTINUE_MESSAGE_SCALE + MESSAGE_PADDING,
                0,
                CONTINUE_MESSAGE_SCALE,
                0xFFFFFFFF)

            if rl.is_key_pressed(32) {
                game.over = false
                game.score.reset()

                game.bird = Bird()
                game.pipes = Pipes()

                game.bird.dy = IMPULSE
                game.start()
            }
        }
    }

    game.score.display()
    rl.end_drawing()
}

exit(0)

Download and extract the assets

Run the program.

$ bs flappy_bird.bs

Asset Sources (Reference)