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}")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.")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]
name = "crawl"
version = "0.1.0"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 ClassImport 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 FunctionImport 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 ForRoom 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.