Chapter IV: Role-Playing Game

In this chapter we’ll create a simplified role-playing game and leverage multiple structures via struct. Let’s start with place properties such as name and links for connecting multiple place instances between themselves. Each place will have one previous place and two places, one on the left side and one on the right side.

               ___ <left>
              /
<previous> ---
              \___ <right>

To create a link between two place structures we need references to them and use such references as values. A symbol & (ampersand) creates a reference for a computer memory where our place struct is stored. Reference is a voidptr type.

struct Place {
    name string
    left &Place
    right &Place
    previous &Place
}

Each reference value for a struct will start as a nil which is a pointer that stores a value 0 and for that there is a checker in V - isnil(v voidptr).

Let’s try to create and connect these places with code:

                 ___ Pile of old leaves (left)
                /
Tree (start) ---
                \___ Shrubbery (right) ---
                                          \___ Bear behind Shrubbery (right)

Each of the place nodes can either have nil as a previous/next node or an initialized different place. Set the reference again by using & character before the symbol you want to reference, in this case an initialized place struct.

fn main() {
    mut tree := Place{name: "Tree"}
    mut pile := Place{name: "Pile of old leaves"}
    mut shrub := Place{name: "Shrubbery"}
    mut bear := Place{name: "Bear behind Shrubbery"}

    // connect tree node with its children
    tree.left = &pile
    tree.right = &shrub
    pile.previous = &tree
    shrub.previous = &tree

    // forward-connect shrub node only
    // because it already has 'previous' set
    shrub.right = &bear
    bear.previous = &shrub

    println(&tree)
    println(tree)

    println(&pile)
    println(pile)

    println(&shrub)
    println(shrub)

    println(&bear)
    println(bear)
}

Running this piece of code won’t yet work due to struct fields being immutable. To mark them as mutable use mut keyword with a colon (:) suffix. mut: will mark all fields after it as mutable unless it encounters some other keyword changing the struct field properties.

struct Place {
    name string
mut:
    left &Place
    right &Place
    previous &Place
}

Once that is fixed, we get an output like below (the memory addresses will be different). Notice the connections between each of the nodes such as Tree memory address (0x7ffe01a7eac0 in this case) being equal Shrubbery previous node’s address.

0x7ffe01a7eac0
{
    name: Tree
    left: 0x7ffe01a7ea90
    right: 0x7ffe01a7ea60
    previous: (nil)
}
0x7ffe01a7ea90
{
    name: Pile of old leaves
    left: (nil)
    right: (nil)
    previous: 0x7ffe01a7eac0
}
0x7ffe01a7ea60
{
    name: Shrubbery
    left: (nil)
    right: 0x7ffe01a7ea30
    previous: 0x7ffe01a7eac0
}
0x7ffe01a7ea30
{
    name: Bear behind Shrubbery
    left: (nil)
    right: (nil)
    previous: 0x7ffe01a7ea60
}

The places are ready, let’s create a traveler who will navigate through them. Our traveler will for now contain a single property - location - which is a reference for an already existing place. This always requires a value and must not be nil. We can achieve such behavior by initializing the struct with a value and assigning only valid values to that property.

struct Traveler {
mut:
    location &Place
}

fn main() {
    ...
    traveler := Traveler{location: &tree}
    ...
    println(traveler.location.name)
}

While assigning invalid values e.g. an incorrectly typed (or raw) value is not an error right now and we can happily do traveler.location = 0, it will make the program access a part of memory which it should not be allowed to do, therefore will result in a Segmentation fault (core dumped).

By using reference for a place and its references for the neighborhood creating a function that will move the traveler is a piece of a cake. We just need to watch out for nil with a short check that both accepts and returns references for places.

fn nonnil_or_stay(old_place &Place, new_place &Place) &Place {
    if isnil(new_place) {
       println("Nothing is there.")
       return old_place
    } else {
       return new_place
    }
}

With this block we can repeat the check for multiple directions we would like to travel - back, left or right - which helps with directly assigning them to traveler’s location property since it should return either its old value or the new one for the desired direction if not nil.

The function moving our traveler will require a mutable instance of traveler struct and an immutable string for the direction. Mutability can again be achieved by mut keyword.

fn move(trav mut Traveler, direction string)

First pull some values out of the traveler instance directly and even from its references into variables for easier access and less repetitive code.

place := trav.location
back := place.previous
left := place.left
right := place.right

Then complete the function by choosing the right location for traveler property via the place checking function and stored place references,

fn move(trav mut Traveler, direction string) {
    place := trav.location
    back := place.previous
    left := place.left
    right := place.right

    println("Old location $trav.location.name")
    println("Trying to move $direction")

    if direction == "back" {
        trav.location = nonnil_or_stay(place, back)
    } else if direction == "left" {
        trav.location = nonnil_or_stay(place, left)
    } else if direction == "right" {
        trav.location = nonnil_or_stay(place, right)
    } else {
        println("$direction is not valid, use back, left or right")
    }
    println("New location $trav.location.name")
}

and replace the place properties’ values printing with move functions.

fn main() {
    ...
    // forward-connect shrub node only because it already has 'previous' set
    shrub.right = &bear
    bear.previous = &shrub

    move(mut traveler, "back")
    move(mut traveler, "left")
    move(mut traveler, "back")
    move(mut traveler, "right")
    move(mut traveler, "back")
}

Here is a sample output of how it should look like.

Old location Tree
Trying to move back
Nothing is there.
New location Tree
Old location Tree
Trying to move left
New location Pile of old leaves
Old location Pile of old leaves
Trying to move back
New location Tree
Old location Tree
Trying to move right
New location Shrubbery
Old location Shrubbery
Trying to move back
New location Tree

Any function you create with fn keyword can also contain a special syntax before its name - a receiver - which is in simple terms just a specification for the compiler to allow calling that function via dot-lookup (using . for fetching an attribute of some entity).

Using (variable mutability Struct) syntax let’s make the function for moving accessible directly from the traveler just by moving a function argument slightly to the left in the function declaration. Also, don’t forget to make the traveler mutable.

fn (trav mut Traveler) move(direction string) {
    ...
}

fn main() {
    ...
    mut traveler := Traveler{location: &tree}
    ...
    traveler.move("back")
    traveler.move("left")
    traveler.move("back")
    traveler.move("right")
    traveler.move("back")
}