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