Skip to content
FutureBlitz

    Install

    The tutorial, part 1 of 3

    A small game

    A dungeon crawl in the terminal, in three steps: fighters, the game loop, and a package of files.

    This part builds a small game that plays itself in the terminal: a hero goes down through the rooms of a dungeon, fighting what’s in each, until the hero reaches the bottom or falls. It uses Classes, a seeded random number generator, a loop, and then a package of a few files. Each step’s program is whole, so you can paste it into main.fb and fb run it.

    Step 1: fighters

    A fighter is a Class: it has a name, hit points, and how hard it hits, and it’s shared, so a fighter that’s hit stays hit. Random(7) is std.math’s generator, seeded, so every run rolls the same numbers, which makes a game easy to test.

    Import std.core
    Import std.math
    
    Class Fighter
        Field name: String
        Field hp: Int
        Field attack: Int
    
        New(name: String, hp: Int, attack: Int)
            Self.name = name
            Self.hp = hp
            Self.attack = attack
        End New
    
        Property Alive: Bool
            Get => hp > 0
        End Property
    
        Method Hit(target: Fighter, dice: Random)
            Local damage = dice.Int(1, attack)
            target.hp -= damage
            Print("{name} hits {target.name} for {damage}")
        End Method
    End Class
    
    Local dice = Random(7)
    Local hero = Fighter("Hero", 30, 8)
    Local slime = Fighter("Slime", 12, 4)
    hero.Hit(slime, dice)
    slime.Hit(hero, dice)
    Print("{hero.name} has {hero.hp} hp, the {slime.name} {slime.hp}")
    Output
    Hero hits Slime for 8
    Slime hits Hero for 1
    Hero has 29 hp, the Slime 4

    dice.Int(1, attack) rolls from 1 to attack, both included. Alive is a Property: it reads like a Field and works the answer out each time.

    Step 2: the game loop

    Each room’s monsters come from a Function of its depth. The loop goes room by room; in each, the hero and the monsters trade blows until one side is down. Between rooms, the hero drinks a potion when there’s one left and it’s needed.

    Import std.core
    Import std.math
    Import std.collections
    
    Class Fighter
        Field name: String
        Field hp: Int
        Field attack: Int
    
        New(name: String, hp: Int, attack: Int)
            Self.name = name
            Self.hp = hp
            Self.attack = attack
        End New
    
        Property Alive: Bool
            Get => hp > 0
        End Property
    
        Method Hit(target: Fighter, dice: Random)
            target.hp -= dice.Int(1, attack)
        End Method
    End Class
    
    Function Monsters(depth: Int): Fighter[]
        Select depth
            Case 1
                Return [Fighter("rat", 5, 2), Fighter("rat", 5, 2)]
            Case 2
                Return [Fighter("goblin", 10, 4)]
            Case 3
                Return [Fighter("skeleton", 14, 5), Fighter("bat", 4, 3)]
            Default
                Return [Fighter("ogre", 30, 8)]
        End Select
    End Function
    
    Function Fight(hero: Fighter, monsters: Fighter[], dice: Random)
        While hero.Alive && monsters.Any(Function(m: Fighter) => m.Alive)
            Local target = monsters.Find(Function(m: Fighter) => m.Alive)
            If target != Null Then hero.Hit(target, dice)
            For m In monsters
                If m.Alive Then m.Hit(hero, dice)
            End For
        End While
    End Function
    
    Local dice = Random(2026)
    Local hero = Fighter("Hero", 40, 9)
    Local potions = 2
    
    For depth = 1 To 4
        Local monsters = Monsters(depth)
        Local names = ""
        For m In monsters
            names = names == "" ? m.name : "{names}, {m.name}"
        End For
        Print("Room {depth}: {names}")
        Fight(hero, monsters, dice)
        If !hero.Alive
            Print("The hero falls in room {depth}.")
            Break
        End If
        Print("  cleared, {hero.hp} hp left")
        If hero.hp < 20 && potions > 0
            potions -= 1
            hero.hp += 15
            Print("  a potion: {hero.hp} hp, {potions} left")
        End If
    End For
    
    If hero.Alive Then Print("The hero reaches the bottom with {hero.hp} hp.")
    Output
    Room 1: rat, rat
      cleared, 34 hp left
    Room 2: goblin
      cleared, 32 hp left
    Room 3: skeleton, bat
      cleared, 5 hp left
      a potion: 20 hp, 1 left
    Room 4: ogre
    The hero falls in room 4.

    monsters.Any and monsters.Find are std.collections’ Methods on arrays, which take a lambda. Find gives a Fighter?, Null when no monster is alive, so the code checks it before hitting. Change the seed, or the hero’s numbers, and the story changes.

    Step 3: a package

    As a game grows, it goes into files. fb new crawl makes a package; here the fighters go in fighter.fb, the dungeon in dungeon.fb, and main.fb keeps the loop. What another file uses is marked Public, and Import crawl.fighter brings it in.

    project.toml
    [project]
    name = "crawl"
    version = "0.1.0"
    fighter.fb
    Import std.core
    Import std.math
    
    Public Class Fighter
        Public Field name: String
        Public Field hp: Int
        Field attack: Int
    
        Public New(name: String, hp: Int, attack: Int)
            Self.name = name
            Self.hp = hp
            Self.attack = attack
        End New
    
        Public Property Alive: Bool
            Get => hp > 0
        End Property
    
        Public Method Hit(target: Fighter, dice: Random)
            target.hp -= dice.Int(1, attack)
        End Method
    End Class
    dungeon.fb
    Import std.core
    Import std.math
    Import std.collections
    Import crawl.fighter
    
    Public Function Monsters(depth: Int): Fighter[]
        Local count = depth < 3 ? 2 : 1
        Local monsters: Fighter[] = []
        For i = 1 To count
            monsters.Add(Fighter(depth < 3 ? "rat" : "ogre", depth * 6, depth + 2))
        End For
        Return monsters
    End Function
    
    Public Function Fight(hero: Fighter, monsters: Fighter[], dice: Random): Bool
        While hero.Alive && monsters.Any(Function(m: Fighter) => m.Alive)
            Local target = monsters.Find(Function(m: Fighter) => m.Alive)
            If target != Null Then hero.Hit(target, dice)
            For m In monsters
                If m.Alive Then m.Hit(hero, dice)
            End For
        End While
        Return hero.Alive
    End Function
    main.fb
    Import std.core
    Import std.math
    Import crawl.fighter
    Import crawl.dungeon
    
    Local dice = Random(99)
    Local hero = Fighter("Hero", 45, 9)
    
    For depth = 1 To 4
        If !Fight(hero, Monsters(depth), dice)
            Print("Fallen in room {depth}.")
            Break
        End If
        Print("Room {depth} cleared: {hero.hp} hp")
    End For
    Output
    Room 1 cleared: 40 hp
    Room 2 cleared: 12 hp
    Room 3 cleared: 6 hp
    Fallen in room 4.

    fb run in the folder builds the three files into one executable. attack isn’t Public, so only fighter.fb sees it: main.fb couldn’t change how hard the hero hits even by mistake.

    Next, the same language as a command-line tool.

    Report an issue with this page or with FutureBlitz on GitHub.