0% found this document useful (0 votes)
420 views856 pages

Rust Tut Roguelike Notsedovic PDF

Rust game programming. Roguelike

Uploaded by

lerina
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
420 views856 pages

Rust Tut Roguelike Notsedovic PDF

Rust game programming. Roguelike

Uploaded by

lerina
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 856

Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Introduction

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Every year, the �ne fellows over at r/roguelikedev run a Tutorial Tuesday series -
encouraging new programmers to join the ranks of roguelike developers. Most
languages end up being represented, and this year (2019) I decided that I'd use it as
an excuse to learn Rust. I didn't really want to use libtcod , the default engine - so I
created my own, RLTK. My initial entry into the series isn't very good, but I learned a
lot from it - you can �nd it here, if you are curious.

The series always points people towards an excellent series of tutorials, using Python
and libtcod . You can �nd it here. Section 1 of this tutorial mirrors the structure of
this tutorial - and tries to take you from zero (how do I open a console to say Hello Rust)
to hero (equipping items to �ght foes in a multi-level dungeon). I'm hoping to continue to
extend the series.

I also really wanted to use an Entity Component System. Rust has an excellent one
called Specs, so I went with it. I've used ECS-based setups in previous games, so it felt
natural to me to use it. It's also a cause of continual confusion on the subreddit, so
hopefully this tutorial can shine some light on its bene�ts and why you might want to
use one.

I've had a blast writing this - and hope to continue writing. Please feel free to contact
me (I'm @herberticus on Twitter) if you have any questions, ideas for improvements,
or things you'd like me to add. Also, sorry about all the Patreon spam - hopefully
someone will �nd this su�ciently useful to feel like throwing a co�ee or two my way.
:-)

Copyright (C) 2019, Herbert Wolverson.

1 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Chapter 1 : Hello Rust

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

This tutorial is primarily about learning to make roguelikes (and by extension other
games), but it should also help you get used to Rust and RLTK - The Roguelike Tool Kit
we'll be using to provide input/output. Even if you don't want to use Rust, my hope is
that you can bene�t from the structure, ideas and general game development advice.

Why Rust?
Rust �rst appeared in 2010, but has only relatively recently hit "stable" status - that is,
code you write is pretty unlikely to stop working when the language changes now.
Development is very much ongoing, with whole new sections of the language (such as
the asynchronous system) still appearing/stabilizing. This tutorial will stay away from
the bleeding edge of development - it should be stable.

Rust was designed to be a "better systems language" - that is, low-level like C++ , but
with far fewer opportunities to shoot yourself in the foot, a focus on avoiding the
many "gotchas" that make C++ development di�cult, and a massive focus on memory
and thread safety: it's designed to be really di�cult to write a program that corrupts
its memory, or su�ers from race conditions (it's not impossible, but you have to try!).
It is rapidly gaining traction, with everyone from Mozilla to Microsoft showing interest
- and an ever expanding number of tools being written in it.

Rust is also designed to have a better ecosystem than C++. Cargo provides a
complete package manager (so do vcpkg , conan , etc. in C++ land, but cargo is well-
integrated), a complete build system (similar to cmake , make , meson , etc. - but
standardized). It doesn't run on as many platforms as C or C++, but the list is ever-
growing.

2 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I tried Rust (after urging from friends), and found that while it doesn't replace C++ in
my daily toolbox - there are times that it really helped get a project out of the door.
It's syntax takes a bit of getting used to, but it really does drop in nicely to existing
infrastructure.

Learning Rust
If you've used other programming languages, then there's a lot of help available!

The Rust Programming Language Book provides an excellent top-down


introduction to the language.
Learn Rust by Example is closer to my preferred way of learning (I'm already
experienced in a number of languages), providing common usage examples for
most of the topics you are likely to encounter.
24 Days of Rust provides a somewhat web-focused 24-day course on learning
Rust.
Rust's Ownership Model for JavaScript Developers should be helpful if you are
coming from JS or another very-high-level language.

If you �nd that you need something that isn't in there, it's quite likely that someone
has written a crate ("package" in every other language, but cargo deals with crates...)
to help. Once you have a working environment, you can type
cargo search <my term> to look for crates that help. You can also head to crates.io
to see a full list of crates that are on o�er in Cargo - complete with documentation
and examples.

If you are completely new to programming, then a piece of bad news: Rust is a
relatively young language, so there isn't a lot of "learn programming from scratch with
Rust" material out there - yet. You may �nd it easier to start with a higher-level
language, and then move "down" (closer to the metal, as it were) to Rust. The
tutorials/guides linked above should get you started if you decide to take the plunge,
however.

Getting Rust

3 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

On most platforms, rustup is enough to get you a working Rust toolchain. On


Windows, it's an easy download - and you get a working Rust environment when it is
done. On Unix-derived systems (such as Linux, and OS X) it provides some command-
line instructions to install the environment.

Once it is installed, verify that it is working by typing cargo --version on your


command line. You should see something like
cargo 1.36.0 (c4fcfb725 2019-05-15) (the version will change over time).

Getting comfortable with a development


environment
You want to make a directory/folder for your development work (I personally use
users/herbert/dev/rust - but that's a personal choice. It really can be anywhere you
like!). You'll also want a text editor. I'm a fan of Visual Studio Code, but you can use
whatever you are comfortable with. If you do use Visual Studio Code, I recommend
the following extensions:

Better TOML : makes reading toml �les nice; Rust uses them a lot
C/C++ : uses the C++ debugger system to debug Rust code
Rust (rls) : not the fastest, but thorough syntax highlighting and error
checking as you go.

Once you've picked your environment, open up an editor and navigate to your new
folder (in VS Code, File -> Open Folder and choose the folder).

Creating a project
Now that you are in your chosen folder, you want to open a terminal/console window
there. In VS Code, this is Terminal -> New Terminal . Otherwise, open a command
line as normal and cd to your folder.

Rust has a built-in package manager called cargo . Cargo can make project templates
for you! So to create your new project, type cargo init hellorust . After a moment,

4 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

a new folder has appeared in your project - titled hellorust . It will contain the
following �les and directories:

src\main.rs
Cargo.toml
.gitignore

These are:

The .gitignore is handy if you are using git - it stops you from accidentally
putting �les into the git repository that don't need to be there. If you aren't using
git, you can ignore it.
src\main.rs is a simple Rust "hello world" program source.
Cargo.toml de�nes your project, and how it should be built.

Quick Rust Introduction - The Anatomy of Hello World

The auto-generated main.rs �le looks like this:

fn main() {
println!("Hello, world!");
}

If you've used other programming languages, this should look somewhat familiar - but
the syntax/keywords are probably di�erent. Rust started out as a mashup between
ML and C, with the intent to create a �exible "systems" language (meaning: you can
write bare-metal code for your CPU without needing a virtual machine like Java or C#
do). Along the way, it inherited a lot of syntax from the two languages. I found the
syntax looked awful for the �rst week of using it, and came quite naturally after that.
Just like a human language, it takes a while for your brain to key into the syntax and
layout.

So what does this all mean?

1. fn is Rust's keyword for function. In JavaScript or Java, this would read


function main() . In C, it would read void main() (even though main is meant
to return an int in C). In C#, it would be static void Main(...) .
2. main is the name of the function. In this case, the name is a special case: the
operating system needs to know what to run �rst when it loads a program into

5 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

memory - and Rust will do the extra work to mark main as the �rst function. You
generally need a main function if you want your program to do anything, unless
you are making a library (a collection of functions for other programs to use).
3. The () is the function arguments or parameters. In this case, there aren't any -
so we just use empty opening and closing parentheses.
4. The { indicates the start of a block. In this case, the block is the body of the
function. Everything within the { and } is the content of the function:
instructions for it to run, in turn. Blocks also denote scope - so anything you
declare inside the function has its access limited to that function. In other words,
if you make a variable inside a function called cheese - it won't be visible from
inside a function called mouse (and vice versa). There are ways around this, and
we'll cover them as we build our game.
5. println! is a macro. You can tell Rust macros because they have an ! after
their name. You can learn all about macros here; for now, you just need to know
that they are special functions that are parsed into other code during compilation.
Printing to the screen can be quite complicated - you might want to say more
than "hello world" - and the println! macro covers a lot of formatting cases. (If
you are familiar with C++, it's equivalent to std::fmt . Most languages have their
own string formatting system, since programmers tend to have to output a lot of
text!)
6. The �nal } closes the block started in 4 .

Go ahead and type cargo run . After some compilation, if everything is working you
will be greeted with "Hello World" on your terminal.

Useful cargo commands

Cargo is quite the tool! You can learn a bit about it from the Learn Rust book, and
everything about it from The Cargo Book if you are interested.

You'll be interacting with cargo a lot while you work in Rust. If you initialize your
program with cargo init , your program is a cargo crate. Compilation, testing,
running, updating - Cargo can help you with all of it. It even sets up git for you by
default.

You may �nd the following cargo features handy:

cargo init creates a new project. That's what you used to make the hello

6 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

world program. If you really don't want to be using git , you can type
cargo init --vcs none (projectname) .
cargo build downloads all dependencies for a project and compiles them, and
then compiles your program. It doesn't actually run your program - but this is a
good way to quickly �nd compiler errors.
cargo update will fetch new versions of the crates you listed in your
cargo.toml �le (see below).
cargo clean can be used to delete all of the intermediate work �les for your
project, freeing up a bunch of disk space. They will automatically download and
recompile the next time you run/build your project. Occasionally, a cargo clean
can help when things aren't working properly - particularly IDE integration.
cargo verify-project will tell you if your Cargo settings are correct.
cargo install can be used to install programs via Cargo. This is helpful for
installing tools that you need.

Cargo also supports extensions - that is, plugins that make it do even more. There are
some that you may �nd particularly useful:

Cargo can reformat all your source code to look like standard Rust from the Rust
manuals. You need to type rustup component add rustfmt once to install the
tool. After that's done, you can type cargo fmt to format your code at any time.
If you'd like to work with the mdbook format - used for this book! - cargo can
help with that, too. Just once, you need to run cargo install mdbook to add the
tools to your system. After that, mdbook build will build a book project,
mdbook init will make a new one, and mdbook serve will give you a local
webserver to view your work! You can learn all about mdbook on their
documentation page.
Cargo can also integrate with a "linter" - called Clippy . Clippy is a little pedantic
(just like his Microsoft O�ce namesake!). Just the once, run
rustup component add clippy . You can now type cargo clippy at any time to
see suggestions for what may be wrong with your code!

Making a new project

Lets modify the newly created "hello world" project to make use of RLTK - the
Roguelike Toolkit.

7 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Setup Cargo.toml
The auto-generated Cargo �le will look like this:

[package]
name = "helloworld"
version = "0.1.0"
authors = ["Your name if it knows it"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo


/reference/manifest.html

[dependencies]

Go ahead and make sure that your name is correct! Next, we're going to ask Cargo to
use RLTK - the Roguelike toolkit library. Rust makes this very easy. Adjust the
dependencies section to look like this:

[dependencies]
rltk = { git = "https://github.com/thebracket/rltk_rs" }

We're telling it that the package is named rltk and giving it a Github location to pull
from.

It's a good idea to occasionally run cargo update - this will update the libraries used
by your program.

Hello Rust - RLTK Style!


Go ahead and replace the contents of src\main.rs with:

8 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

rltk::add_wasm_support!();
use rltk::{Rltk, GameState, Console};

struct State {}
impl GameState for State {
fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();
ctx.print(1, 1, "Hello Rust World");
}
}

fn main() {
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
"resources");
let gs = State{ };
rltk::main_loop(context, gs);
}

Now create a new folder called resources . RLTK needs a few �les to run, and this is
where we put them. Download resources.zip, and unzip it into this folder. Be careful
to have resources/backing.fs (etc.) and not resources/resources/backing.fs .

Save, and go back to the terminal. Type cargo run , and you will be greeted with a
console window showing Hello Rust .

9 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you're new to Rust, you are probably wondering what exactly the Hello Rust code
does, and why it is there - so we'll take a moment to go through it.

1. The line rltk::add_wasm_support!(); is optional. It calls a macro (helper code)


in RLTK that detects if you are compiling to web assembly, and if you are it adds
some shims to make things work. It's worth putting this in there, in case you
decide to publish your project on the web (this is new as of RLTK_RS version
0.3.0).
2. The �rst line is equivalent to C++'s #include or C#'s using . It simply tells the
compiler that we are going to require Rltk , GameState , and Console types
from the namespace rltk . You used to need an additional extern crate line
here, but the most recent version of Rust can now �gure it out for you.
3. With struct State{} , we are creating a new structure . Structures are like
Records in Pascal, or Classes in many other languages: you can store a bunch of
data inside them, and you can also attach "methods" (functions) to them. In this
case, we don't actually need any data - we just need a place to attach code. If
you'd like to learn more about Structs, this is the Rust Book chapter on the topic
4. impl GameState for State is quite a mouthful! We're telling Rust that our

10 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

State structure implements the trait GameState . Traits are like interfaces or
base classes in other languages: they setup a structure for you to implement in
your own code, which can then interact with the library that provides them -
without that library having to know anything else about your code. In this case,
GameState is a trait provided by RLTK. RLTK requires that you have one - it uses
it to call into your program on each frame. You can learn about traits in this
chapter of the Rust book.
5. fn tick(&mut self, ctx : &mut Rltk) is a function de�nition. We're inside the
trait implementation scope, so we are implementing the function for the trait -
so it has to match the type required by the trait. Functions are a basic building
block of Rust, I recommend the Rust book chapter on the topic.
1. In this case, fn tick means "make a function, called tick" (it's called "tick"
because it "ticks" with each frame that is rendered; it's common in game
programming to refer to each iteration as a tick).
2. It doesn't end with an -> type , so it is equivalent to a void function in C -
it doesn't return any data once called. The parameters can also bene�t
from a little explanation.
3. &mut self means "this function requires access to the parent structure,
and may change it" (the mut is short for "mutable" - meaning it can change
variables inside the structure - "state"). You can also have functions in a
structure that just have &self - meaning, we can see the content of the
structure, but can't change it. If you omit the &self altogether, the
function can't see the structure at all - but can be called as if the structure
was a namespace (you see this a lot with functions called new - they make a
new copy of the structure for you).
4. ctx: &mut Rltk means "pass in a variable called ctx " ( ctx is an
abbreviation for "context"). The colon indicates that we're specifying what
type of variable it must be.
5. & means "pass a reference" - which is a pointer to an existing copy of the
variable. The variable isn't copied, you are working on the version that was
passed in; if you make a change, you are changing the original. The Rust
Book explains this better than I can.
6. mut once again indicates that this is a "mutable" reference: you are
allowed to make changes to the context.
7. Finally Rltk is the type of the variable you are receiving. In this case, it's a
struct de�ned inside the RLTK library that provides various things you
can do to the screen.
6. ctx.cls(); says "call the cls function provided by the variable ctx . cls is a

11 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

common abbreviation for "clear the screen" - we're telling our context that it
should clear the virtual terminal. It's a good idea to do this at the beginning of a
frame, unless you speci�cally don't want to.
7. ctx.print(1, 1, "Hello Rust World"); is asking the context to print "Hello
Rust World" at the location (1,1).
8. Now we get to fn main() . Every program has a main function: it tells the
operating system where to start the program.
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
9.
"resources");
is an example of calling a function from inside a struct - where that struct
doesn't take a "self" function. In other languages, this would be called a
constructor. We're calling the function init_simple8x8 (which is a helper
provided by RLTK to make a terminal using an 8 pixels by 8 pixels font). We ask
that the console dimensions be 80 characters wide, by 50 characters high. The
window title is "Hello Rust World". "resources" tells the program where to �nd
the resources folder we setup earlier.
10. let gs = State{ }; is an example of a variable assignment (see The Rust
Book). We're making a new variable called gs (short for "game state"), and
setting it to be a copy of the State struct we de�ned above.
11. rltk::main_loop(context, gs); calls into the rltk namespace, activating a
function called main_loop . It needs both the context and the GameState we
made earlier - so we pass those along. RLTK tries to take some of the complexity
of running a GUI/game application away, and provides this wrapper. The
function now takes over control of the program, and will call your tick function
(see above) every time the program "ticks" - that is, �nishes one cycle and moves
to the next. This can happen 60 or more times per second!

Hopefully that made some sense!

Playing with the tutorials


You'd probably like to play with the tutorial code without having to type it all in! The
good news is that it is up on GitHub for your perusal. You need to have git installed
(RustUp should have helped you with that). Choose where you would like to have the
tutorials, and open a terminal:

12 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

cd <path to tutorials>
git clone https://github.com/thebracket/rustrogueliketutorial .

After a while, this will download the complete tutorial (including the source code for
this book!). It is laid out as follows (this isn't complete!):

───book
├───chapter-01-hellorust
├───chapter-02-helloecs
├───chapter-03-walkmap
├───chapter-04-newmap
├───chapter-05-fov
├───resources
├───src

What's here?

The book folder contains the source code for this book. You can ignore it, unless
you feel like correcting my spelling!
Each chapter's example code is contained in chapter-xy-name folders; for
example, chapter-01-hellorust .
The src folder contains a simple script to remind you to change to a chapter
folder before running anything.
resources has the contents of the ZIP �le you downloaded for this example. All
the chapter folders are precon�gured to use this.
Cargo.toml is setup to include all of the tutorials as "workspace entries" - they
share dependencies, so it won't eat your whole drive re-downloading everything
each time you use it.

To run an example, open your terminal and:

cd <where you put the tutorials>


cd chapter-01-hellorust
cargo run

If you are using Visual Studio Code, you can instead use File -> Open Folder to open the
whole directory that you checked out. Using the inbuilt terminal, you can simply cd to
each example and cargo run it.

13 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Accessing Tutorial Source Code


You can get to the source code for all of the tutorials at https://github.com/thebracket
/rustrogueliketutorial.

Updating the Tutorial


I update this tutorial a lot - adding chapters, �xing issues, etc. You will periodically
want to open the tutorial directory, and type git pull . This tells git (the source
control manager) to go to the Github repository and look for what's new. It will then
download everything that has changed, and you once again have up-to-date tutorials.

Updating Your Project


You may �nd that rltk_rs or another package has updated, and you would like the
latest version. From your project's folder, you can type cargo update to update
everything. You can type cargo update --dryrun to see what it would like to update,
and not change anything (people update their crates a lot - so this can be a big list!).

Updating Rust Itself


I don't recommend running this from inside Visual Studio Code or another IDE, but
if you'd like to ensure that you have the most recent release of Rust (and associated
tools), you can type rustup self update . This updates the Rust update tools (I know
that sounds rather recursive). You can then type rustup update and install the latest
versions of all of the tools.

Getting Help

14 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

There's a number of ways to get help:

Feel free to contact me (I'm @herberticus on Twitter) if you have any questions,
ideas for improvements, or things you'd like me to add.
The �ne people on /r/rust are VERY helpful with Rust language issues.
The awesome people of /r/roguelikedev are VERY helpful when it comes to
Roguelike issues. Their Discord is pretty active, too.

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Chapter 2 - Entities and Components

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

This chapter will introduce the entire of an Entity Component System (ECS), which will
form the backbone of the rest of this tutorial. Rust has a very good ECS, called Specs -
and this tutorial will show you how to use it, and try to demonstrate some of the early
bene�ts of using it.

About Entities and Components


If you've worked on games before, you may well be used to an object oriented design
(this is very common, even in the original Python libtcod tutorial that inspired this
one). There's nothing really wrong with an object-oriented (OOP) design - but game
developers have moved away from it, mostly because it can become quite confusing
when you start to expand your game beyond your original design ideas.

15 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You've probably seen a "class hierarchy" such as this simpli�ed one:

BaseEntity
Monster
MeleeMob
OrcWarrior
ArcherMob
OrcArcher

You'd probably have something more complicated than that, but it works as an
illustration. BaseEntity would contain code/data required to appear on the map as
an entity, Monster indicates that it's a bad guy, MeleeMob would hold the logic for
�nding melee targets, closing in, and killing them. Likewise, ArcherMob would try to
maintain the optimal range and use their ranged weapon to �re from a safe distance.
The problem with a taxonomy like this is that it can be restrictive, and before you
know it - you are starting to write separate classes for more complicated
combinations. For example, what if we come up with an orc that can do both melee
and archery - and may become friendly if you've completed the Friends With The
Greenskins quest? You might well end up combining logic from all of them into one
special case class. It works - and plenty of games have published doing just that - but
what if there were an easier way?

Entity Component based design tries to eliminate the hierarchy, and instead
implement a set of "components" that describe what you want. An "entity" is a thing -
anything, really. An orc, a wolf, a potion, an Ethereal hard-drive formatting ghost -
whatever you want. It's also really simple: little more than an identi�cation number.
The magic comes from entities being able to have as many components as you want to
add. Components are just data, grouped by whatever properties you want to give an
entity.

For example, you could build the same set of mobs with components for: Position ,
Renderable , Hostile , MeleeAI , RangedAI , and some sort of CombatStats
component (to tell you about their weaponry, hit points, etc.). An Orc Warrior would
need a position so you know where they are, a renderable so you know how to draw
them. It's Hostile, so you mark it as such. Give it a MeleeAI and a set of game stats,
and you have everything you need to make it approach the player and try to hit them.
An Archer might be the same thing, but replacing MeleeAI with RangedAI. A hybrid
could keep all the components, but either have both AIs or an additional one if you
want custom behavior. If your orc becomes friendly, you could remove the Hostile
component - and add a Friendly one.

16 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

In other words: components are just like your inheritance tree, but instead of
inheriting traits you compose them by adding components until it does what you want.
This is often called "composition".

The "S" in ECS stands for "Systems". A System is a piece of code that gathers data from
the entity/components list and does something with it. It's actually quite similar to an
inheritance model, but in some ways it's "backwards". For example, drawing in an
OOP system is often: For each BaseEntity, call that entities Draw command. In an ECS
system, it would be Get all entities with a position and a renderable component, and use
that data to draw them.

For small games, an ECS often feels like it's adding a bit of extra typing to your code. It
is. You take the additional work up front, to make life easier later.

That's a lot to digest, so we'll look at a simple example of how an ECS can make your
life a bit easier.

Including Specs in the project


To start, we want to tell Cargo that we're going to use Specs. Open your Cargo.toml
�le, and change the dependencies section to look like this:

[dependencies]
rltk = { git = "https://github.com/thebracket/rltk_rs" }
specs = "0.15.0"
specs-derive = "0.4.0"

This is pretty straight-forward: we're telling Rust that we still want to use RLTK, and
we're also asking for specs (the version number is current at the time of writing; you
can check for new ones by typing cargo search specs ). We're also adding
specs-derive - which provides some helper code to reduce the amount of
boilerplate typing you have to do.

At the top of main.rs we add a few lines of code:

17 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{Console, GameState, Rltk, RGB, VirtualKeyCode};


use specs::prelude::*;
use std::cmp::{max, min};
#[macro_use]
extern crate specs_derive;

The use rltk:: is a short-hand; you can type rltk::Console every time you want a
console; this tells Rust that we'd like to just type Console instead. The
use specs::prelude::* line is there so we aren't continually typing
specs::prelude::World when we just want World .

The command #[macro_use] is a little scarier looking; it just means "the next crate
will contain macro code, please use it". This exists to avoid the C++ problem of
#define commands leaking everywhere and confusing you. Rust is all about being
explicit, to avoid confusing yourself later!

Finally, we call extern crate specs_derive . This crate contains a bunch of helpers to
reduce the amount of typing you need. You'll see its bene�ts shortly. Rust 2018
doesn't require that you use an extern crate for every crate you use - but if you are
including macros, you have to use one with #[macro_use] - you are telling Rust that
you explicitly want macros from that crate.

De�ning a position component


We're going to build a little demo that uses an ECS to put characters on the screen
and move them around. A basic part of this is to de�ne a position - so that entities
know where they are. We'll keep it simple: positions are just an X and Y coordinate on
the screen.

So, we de�ne a struct (these are like structs in C, records in Pascal, etc. - a group of
data stored together. See the Rust Book chapter on Structures):

struct Position {
x: i32,
y: i32,
}

18 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Very simple! A Position component has an x and y coordinate, as 32-bit integers.


Our Position structure is what is known as a POD - short for "plain old data". That is,
it is just data, and doesn't have any logic of its own. This is a common theme with
"pure" ECS (Entity Component System) components: they are just data, with no
associated logic. The logic will be implemented elsewhere. There are two reasons to
use this model: it keeps all of your code that does something in "systems" (that is, code
that runs across components and entities), and performance - it's very fast to keep all
of the positions next to each other in memory with no redirects.

At this point, you could use Position s, but there's very little to help you store them
or assign them to anyone - so we need to tell Specs that this is a component. Specs
provides a lot of options for this, but we want to keep it simple. The long-form (no
specs-derive help) would look like this:

struct Position {
x: i32,
y: i32,
}

impl Component for Position {


type Storage = VecStorage<Self>;
}

You will probably have a lot of components by the time your game is done - so that's a
lot of typing. Not only that, but it's lots of typing the same thing over and over - with
the potential to get confusing. Fortunately, specs-derive provides an easier way. You
can replace the previous code with:

#[derive(Component)]
struct Pos {
x: i32,
y: i32,
}

What does this do? #[derive(x)] is a macro that says "from my basic data, please
derive the boilerplate needed for x"; in this case, the x is a Component . The macro
generates the additional code for you, so you don't have to type it in for every
component. It makes it nice and easy to use components! The
#[macro_use] extern crate specs_derive; from earlier is making use of this; derive

19 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

macros are a special type of macro that implements additional functionality for a
structure on your behalf - saving lots of typing.

De�ning a renderable component


A second part of putting a character on the screen is what character should we draw,
and in what color? To handle this, we'll create a second component - Renderable . It
will contain a foreground, background, and glyph (such as @ ) to render. So we'll
create a second component structure:

#[derive(Component)]
struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
}

RGB comes from RLTK, and represents a color. That's why we have the
use rltk::{... RGB} statement - otherwise, we'd be typing rltk::RGB every time
there - saving keystrokes. Once again, this is a plain old data structure, and we are
using the derive macro to add the component storage information without having to
type it all out.

Worlds and Registration


So now we have two component types, but that's not very useful without somewhere
to put them! Specs requires that you register your components at start-up. What do
you register it with? A World !

A World is an ECS, provided by the Rust crate Specs . You can have more than one if
you want, but we won't go there yet. We'll extend our State structure to have a place
to store the world:

20 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

struct State {
ecs: World
}

And now in main , when we create the world - we'll put an ECS into it:

let mut gs = State {


ecs: World::new()
};

Notice that World::new() is another constructor - it's a method inside the World
type, but without a reference to self . So it doesn't work on existing World objects -
it can only make new ones. This is a pattern used everywhere in Rust, so it's a good
idea to be familiar with it. The Rust Book has a section on the topic.

The next thing to do is to tell the ECS about the components we have created. We do
this right after we create the world:

gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();

What this does is it tells our World to take a look at the types we are giving it, and do
some internal magic to create storage systems for each of them. Specs has made this
easy; so long as it implements Component , you can put anything you like in as a
component!

Creating entities
Now we've got a World that knows how to store Position and Renderable
components. Having these components simply exist doesn't help us, beyond providing
an indication of structure. In order to use them, they need to be attached to
something in the game. In the ECS world, that something is called an entity. Entities
are quite simple; they are little more than an identi�cation number, telling the ECS
that an entity exists. They can have any combination of components attached to them.
In this case, we're going to make an entity that knows where it is on the screen, and

21 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

knows how it should be represented on the screen.

We can create an entity with both a Renderable and a Position component like this:

gs.ecs
.create_entity()
.with(Position { x: 40, y: 25 })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.build();

What this does, is it tells our World ( ecs in gs - our game state) that we'd like a new
entity. That entity should have a position (we've picked the middle of the console), and
we'd like it to be renderable with an @ symbol in yellow on black. That's very simple;
we aren't even storing the entity (we could if we wanted to) - we're just telling the
world that it's there!

Notice that we are using an interesting layout: lots of functions that don't end in an ;
to separate out the end of the statement, but instead lots of . calls to another
function. This is called the builder pattern, and is very common in Rust. Combining
functions in this fashion is called method chaining (a method is a function inside a
structure). It works because each function returns a copy of itself - so each function
runs in turn, passing itself as the holder for the next method in the chain. So in this
example, we start with a create_entity call - which returns a new, empty, entity. On
that entity, we call with - which attaches a component to it. That in turn returns the
partially built entity - so we can call with again to add the Renderable component.
Finally, .build() takes the assembled entity and does the hard part - actually putting
together all of the disparate parts into the right parts of the ECS for you.

You could easily add a bunch more entities, if you want. Lets do just that:

22 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for i in 0..10 {
gs.ecs
.create_entity()
.with(Position { x: i * 7, y: 20 })
.with(Renderable {
glyph: rltk::to_cp437('☺'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.build();
}

This is the �rst time we've called a for loop in the tutorial! If you've used other
programming languages, the concept will be familiar: run the loop with i set to every
value from 0 to 9. Wait - 9, you say? Rust ranges are exclusive - they don't include the
very last number in the range! This is for familiarity with languages like C which
normally write for (i=0; i<10; ++i) . If you actually want to go all the way to the
end of the range (so 0 to 10), you would write the rather cryptic for i in 0..=10 .
The Rust Book provides a great primer for understanding control �ow in Rust.

You'll notice that we're putting them at di�erent positions (every 7 characters, 10
times), and we've changed the @ to an ☺ - a smiley face ( to_cp437 is a helper RLTK
provides to let you type/paste Unicode and get the equivalent member of the old
DOS/CP437 character set. You could replace the to_cp437('☺') with a 1 for the
same thing). You can �nd the glyphs available here.

Iterating entities - a generic render system


So we now have 11 entities, with di�ering render characteristics and positions. It
would be a great idea to do something with that data! In our tick function, we replace
the call to draw "Hello Rust" with the following:

23 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let positions = self.ecs.read_storage::<Position>();


let renderables = self.ecs.read_storage::<Renderable>();

for (pos, render) in (&positions, &renderables).join() {


ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}

What does this do? let positions = self.ecs.read_storage::<Position>(); asks


the ECS for read access to the container it is using to store Position components.
Likewise, we ask for read access to the Renderable storage. It only makes sense to
draw a character if it has both of these - you need a Position to know where to draw,
and Renderable to know what to draw! You can learn more about these stores in The
Specs Book. The important part is read_storage - we're asking for read-only access
to the structure used to store components of each type.

Fortunately, Specs has our back:

for (pos, render) in (&positions, &renderables).join() {

This line says join positions and renderables; like a database join, it only returns
entities that have both. It then uses Rust's "destructuring" to place each result (one
result per entity that has both components). So for each iteration of the for loop -
you get both components belonging to the same entity. That's enough to draw it!

The join function returns an iterator. The Rust Book has a great section on iterators.
In C++, iterators provide a begin , next and end function - and you can move
between elements in collections with them. Rust extends the same concept, only on
steroids: just about anything can be made into an iterator if you put your mind to it.
Iterators work very well with for loops - you can provide any iterator as the target in
for x in iterator loops. The 0..10 we discussed earlier really is a range - and
o�ers an iterator for Rust to navigate.

The other interesting thing here are the parentheses. In Rust, when you wrap
variables in brackets you are making a tuple. These are just a collection of variables,
grouped together - but without needing to go and make a structure just for this case.
You can access them individually via numeric access ( mytuple.0 , mytuple.1 , etc.) to
get to each �eld, or you can destructure them. (one, two) = (1, 2) sets the variable
one to 1 , and the variable two to 2 . That's what we're doing here: the join

24 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

iterator is returning tuples containing a Position and a Renderable component as


.0 and .1 . Since typing that is ugly and unclear, we destructure them into the named
variables pos and render . This can be confusing at �rst, so if you are struggling I
recommend Rust By Example's section on Tuples.

ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);

We're running this for every entity that has both a Position and a Renderable
component. The join method is passing us both, guaranteed to belong to the same
enitity. Any entities that have one or the other - but not both - simply won't be
included in the data returned to us.

ctx is the instance of RLTK passed to us when tick runs. It o�ers a function called
set , that sets a single terminal character to the glyph/colors of your choice. So we
pass it the data from pos (the Position component for that entity), and the
colors/glyph from render (the Renderable component for that entity).

With that in place, any entity that has both a Position and a Renderable will be
rendered to the screen! You could add as many as you like, and they will render.
Remove one component or the other, and they won't be rendered (for example, if an
item is picked up you might remove its Position component - and add another
indicating that it's in your backpack; more on that in later tutorials)

Rendering - complete code


If you've typed all of that in correctly, your main.rs now looks like this:

25 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{Console, GameState, Rltk, RGB};


use specs::prelude::*;
use std::cmp::{max, min};
#[macro_use]
extern crate specs_derive;

#[derive(Component)]
struct Position {
x: i32,
y: i32,
}

#[derive(Component)]
struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
}

struct State {
ecs: World
}

impl GameState for State {


fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();

for (pos, render) in (&positions, &renderables).join() {


ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}
}
}

fn main() {
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
"resources");
let mut gs = State {
ecs: World::new()
};
gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();

gs.ecs
.create_entity()

26 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

.with(Position { x: 40, y: 25 })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.build();

for i in 0..10 {
gs.ecs
.create_entity()
.with(Position { x: i * 7, y: 20 })
.with(Renderable {
glyph: rltk::to_cp437('☺'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.build();
}

rltk::main_loop(context, gs);
}

Running it (with cargo run ) will give you the following:

27 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

An example system - random movement


This example showed you how an ECS can get a disparate bag of entities to render.
Go ahead and play around with the entity creation - you can do a lot with this!
Unfortunately, it's pretty boring - nothing is moving! Lets rectify that a bit, and make a
shooting gallery type look.

First, we'll create a new component called LeftMover . Entities that have this
component are indicating that they really like going to the left. The component
de�nition is very simple; a component with no data like this is called a "tag
component". We'll put it up with our other component de�nitions:

#[derive(Component)]
struct LeftMover {}

Now we have to tell the ECS to use the type. With our other register calls, we add:

28 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

gs.ecs.register::<LeftMover>();

Now, lets only make the red smiley faces left movers. So their de�nition grows to:

for i in 0..10 {
gs.ecs
.create_entity()
.with(Position { x: i * 7, y: 20 })
.with(Renderable {
glyph: rltk::to_cp437('☺'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(LeftMover{})
.build();
}

Notice how we've added one line: .with(LeftMover{}) - that's all it takes to add one
more component to these entities (and not the yellow @ ).

Now to actually make them move. We're going to de�ne our �rst system. Systems are a
way to contain entity/component logic together, and have them run independently.
There's lots of complex �exibility available, but we're going to keep it simple. Here's
everything required for our LeftWalker system:

struct LeftWalker {}

impl<'a> System<'a> for LeftWalker {


type SystemData = (ReadStorage<'a, LeftMover>,
WriteStorage<'a, Position>);

fn run(&mut self, (lefty, mut pos) : Self::SystemData) {


for (_lefty,pos) in (&lefty, &mut pos).join() {
pos.x -= 1;
if pos.x < 0 { pos.x = 79; }
}
}
}

This isn't as nice/simple as I'd like, but it does make sense when you understand it.

29 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Lets go through it a piece at a time:

struct LeftWalker {} just de�nes an empty structure - somewhere to attach


the logic.
impl<'a> System<'a> for LeftWalker means we are implementing Specs'
System trait for our LeftWalker structure. The 'a are lifetime speci�ers: the
system is saying that the components it uses must exist long enough for the
system to run. For now, it's not worth worrying too much about it. If you are
interested, the Rust Book can clarify a bit.
type SystemData is de�ning a type to tell Specs what the system requires. In
this case, read access to LeftMover components, and write access (since it
updates them) to Position components. You can mix and match whatever you
need in here, as we'll see in later chapters.
fn run is the actual trait implementation, required by the impl System . It takes
itself, and the SystemData we de�ned.
The for loop is system shorthand for the same iteration we did in the rendering
system: it will run once for each entity that has both a LeftMover and a
Position . Note that we're putting an underscore before the LeftMover
variable name: we never actually use it, we just require that the entity has one.
The underscore tells Rust "we know we aren't using it, this isn't a bug!" and stops
it from warning us every time we compile.
The meat of the loop is very simple: we subtract one from the position
component, and if it is less than zero we scoot back to the right of the screen.

Notice that this is very similar to how we wrote the rendering code - but instead of
calling in to the ECS, the ECS system is calling into our function/system. It can be a
tough judgment call on which to use. If your system just needs data from the ECS,
then a system is the right place to put it. If it also needs access to other parts of your
program, it is probably better implemented on the outside - calling in.

Now that we've written our system, we need to be able to use it. We'll add a
run_systems function to our State :

30 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl State {
fn run_systems(&mut self) {
let mut lw = LeftWalker{};
lw.run_now(&self.ecs);
self.ecs.maintain();
}
}

This is relatively straightforward:

1. impl State means we would like to implement functionality for State .


2. fn run_systems(&mut self) means we are de�ning a function, and it needs
mutable (i.e. it is allowed to change things) access to self; this means it can access
the data in its instance of State with the self. keyword.
3. let mut lw = LeftWalker{} makes a new (changeable) instance of the
LeftWalker system.
4. lw.run_now(&self.ecs) tells the system to run, and tells it how to �nd the ECS.
5. self.ecs.maintain() tells Specs that if any changes were queued up by the
systems, they should apply to the world now.

Finally, we actually want to run our systems. In the tick function, we add:

self.run_systems();

The nice thing is that this will run all systems we register into our dispatcher; so as we
add more, we don't have to worry about calling them (or even calling them in the right
order). You still sometimes need more access than the dispatcher has; our renderer
isn't a system because it needs the Context from RLTK (we'll improve that in a future
chapter).

So your code now looks like this:

31 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{Console, GameState, Rltk, RGB};


use specs::prelude::*;
use std::cmp::{max, min};
#[macro_use]
extern crate specs_derive;

#[derive(Component)]
struct Position {
x: i32,
y: i32,
}

#[derive(Component)]
struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
}

#[derive(Component)]
struct LeftMover {}

struct State {
ecs: World,
}

impl GameState for State {


fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();

self.run_systems();

let positions = self.ecs.read_storage::<Position>();


let renderables = self.ecs.read_storage::<Renderable>();

for (pos, render) in (&positions, &renderables).join() {


ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}
}
}

struct LeftWalker {}

impl<'a> System<'a> for LeftWalker {


type SystemData = (ReadStorage<'a, LeftMover>,
WriteStorage<'a, Position>);

32 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn run(&mut self, (lefty, mut pos) : Self::SystemData) {


for (_lefty,pos) in (&lefty, &mut pos).join() {
pos.x -= 1;
if pos.x < 0 { pos.x = 79; }
}
}
}

impl State {
fn run_systems(&mut self) {
let mut lw = LeftWalker{};
lw.run_now(&self.ecs);
self.ecs.maintain();
}
}

fn main() {
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
"resources");
let mut gs = State {
ecs: World::new()
};
gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();
gs.ecs.register::<LeftMover>();

gs.ecs
.create_entity()
.with(Position { x: 40, y: 25 })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.build();

for i in 0..10 {
gs.ecs
.create_entity()
.with(Position { x: i * 7, y: 20 })
.with(Renderable {
glyph: rltk::to_cp437('☺'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})

33 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

.with(LeftMover{})
.build();
}

rltk::main_loop(context, gs);
}

If you run it (with cargo run ), the red smiley faces zoom to the left, while the @
watches.

Moving the player


Finally, lets make the @ move with keyboard controls. So we know which entity is the
player, we'll make a new tag component:

#[derive(Component, Debug)]
struct Player {}

We'll add it to registration:

gs.ecs.register::<Player>();

And we'll add it to the player's entity:

34 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

gs.ecs
.create_entity()
.with(Position { x: 40, y: 25 })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.build();

Now we implement a new function, try_move_player :

fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {


let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();

for (_player, pos) in (&mut players, &mut positions).join() {


pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));
}
}

Drawing on our previous experience, we can see that this gains write access to
Player and Position . It then joins the two, ensuring that it will only work on entities
that have both component types - in this case, just the player. It then adds delta_x to
x and delta_y to y - and does some checks to make sure that you haven't tried to
leave the screen.

We'll add a second function to read the keyboard information provided by RLTK:

35 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn player_input(gs: &mut State, ctx: &mut Rltk) {


// Player movement
match ctx.key {
None => {} // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
_ => {}
},
}
}

There's quite a bit of functionality here that we haven't seen before! The context is
providing information about a key - but the user may or may not be pressing one!
Rust provides a feature for this, called Option types. Option types have two possible
value: None (no data), or Some(x) - indicating that there is data here, held inside.

The context provides a key variable. It is an enumeration - that is, a variable that can
hold a value from a set of pre-de�ned values (in this case, keys on the keyboard). Rust
enumerations are really powerful, and can actually hold values as well - but we won't
use that yet.

So to get the data out of an Option , we need to unwrap it. There's a function called
unwrap - but if you call it when there isn't any data, your program will crash! So we'll
use Rust's match command to peek inside. Matching is one of Rust's strongest
bene�ts, and I highly recommend the Rust book chapter on it, or the Rust by Example
section if you prefer learning by examples.

So we call match ctx.key - and Rust expects us to provide a list of possibles matches.
In the case of ctx.key , there are only two possible values: Some or None . The
None => {} line says "match the case in which ctx.key has no data" - and runs an
empty block. Some(key) is the other option; there is some data - and we'll ask Rust to
give it to us as a variable named key (you can name it whatever you like).

We then match again, this time on the key. We have a line for each eventuality we
want to handle: VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs)
says that if key equals VirtualKeyCode::Left ( VirtualKeyCode is the name of the
enumeration type), we should call our try_move_player function with (-1, 0). We

36 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

repeat that for all four directions. The _ => {} is rather odd looking; _ means
anything else. So we're telling Rust that any other key code can be ignored here. Rust is
rather pedantic: if you don't specify every possible enumeration, it will give a compiler
error! By including the default, we don't have to type every possible keystroke.

This function takes the current game state and context, looks at the key variable in
the context, and calls the appropriate move command if the relevant movement key is
pressed. Lastly, we add it into tick :

player_input(self, ctx);

If you run your program (with cargo run ), you now have a keyboard controlled @
symbol, while the smiley faces zoom to the left!

The �nal code for chapter 2


The source code for this completed example may be found ready-to-run in
chapter-02-helloecs . It looks like this:

37 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{Console, GameState, Rltk, RGB, VirtualKeyCode};


use specs::prelude::*;
use std::cmp::{max, min};
#[macro_use]
extern crate specs_derive;

rltk::add_wasm_support!();

#[derive(Component)]
struct Position {
x: i32,
y: i32,
}

#[derive(Component)]
struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
}

#[derive(Component)]
struct LeftMover {}

#[derive(Component, Debug)]
struct Player {}

struct State {
ecs: World
}

fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {


let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();

for (_player, pos) in (&mut players, &mut positions).join() {


pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));
}
}

fn player_input(gs: &mut State, ctx: &mut Rltk) {


// Player movement
match ctx.key {
None => {} // Nothing happened
Some(key) => match key {

38 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),


VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
_ => {}
},
}
}

impl GameState for State {


fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();

player_input(self, ctx);
self.run_systems();

let positions = self.ecs.read_storage::<Position>();


let renderables = self.ecs.read_storage::<Renderable>();

for (pos, render) in (&positions, &renderables).join() {


ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}
}
}

struct LeftWalker {}

impl<'a> System<'a> for LeftWalker {


type SystemData = (ReadStorage<'a, LeftMover>,
WriteStorage<'a, Position>);

fn run(&mut self, (lefty, mut pos) : Self::SystemData) {


for (_lefty,pos) in (&lefty, &mut pos).join() {
pos.x -= 1;
if pos.x < 0 { pos.x = 79; }
}
}
}

impl State {
fn run_systems(&mut self) {
let mut lw = LeftWalker{};
lw.run_now(&self.ecs);
self.ecs.maintain();
}
}

39 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn main() {
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
"resources");
let mut gs = State {
ecs: World::new()
};
gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();
gs.ecs.register::<LeftMover>();
gs.ecs.register::<Player>();

gs.ecs
.create_entity()
.with(Position { x: 40, y: 25 })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.build();

for i in 0..10 {
gs.ecs
.create_entity()
.with(Position { x: i * 7, y: 20 })
.with(Renderable {
glyph: rltk::to_cp437('☺'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(LeftMover{})
.build();
}

rltk::main_loop(context, gs);
}

This chapter was a lot to digest, but provides a really solid base on which to build. The
great part is: you've now got further than many aspiring developers! You have entities
on the screen, and can move around with the keyboard.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

40 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Copyright (C) 2019, Herbert Wolverson.

Chapter 3 - Walking a Map

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

A Roguelike without a map to explore is a bit pointless, so in this chapter we'll put
together a basic map, draw it, and let your player walk around a bit. We're starting
with the code from chapter 2, but with the red smiley faces (and their leftward
tendencies) removed.

De�ning the map tiles


We'll start by allowing two tile types: walls and �oors. We can represent this with an
enum (to learn more about enumerations, The Rust Book has a large section on them):

#[derive(PartialEq, Copy, Clone)]


enum TileType {
Wall, Floor
}

Notice that we've included some derived features (more usage of derive macros, this
time built into Rust itself): Copy and Clone allow this to be used as a "value" type
(that is, it just passes around the value instead of pointers), and PartialEq allows us
to use == to see if two tile types match. If we didn't derive these features,
if tile_type == TileType::Wall would fail to compile!

41 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Building a simple map


Now we'll make a function that returns a vec (vector) of tiles, representing a simple
map. We'll use a vector sized to the whole map, which means we need a way to �gure
out which array index is at a given x/y position. So �rst, we make a new function
xy_idx :

pub fn xy_idx(x: i32, y: i32) -> usize {


(y as usize * 80) + x as usize
}

This is simple: it multiplies the y position by the map width (80), and adds x . This
guarantees one tile per location, and e�ciently maps it in memory for left-to-right
reading.

We're using a Rust function shorthand here. Notice that the function returns a usize
(equivalent to size_t in C/C++ - whatever the basic size type used for a platform is) -
and the function body lacks a ; at the end? Any function that ends with a statement
that lacks a semicolon treats that line as a return statement. So it's the same as
typing return (y as usize * 80) + x as usize . This comes from the Rust author's
other favorite language, ML - which uses the same shorthand. It's considered
"Rustacean" (canonical Rust; I always picture a Rust Monster with cute little claws and
shell) to use this style, so we've adopted it for the tutorial.

Then we write a constructor function to make a map:

42 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn new_map() -> Vec<TileType> {


let mut map = vec![TileType::Floor; 80*50];

// Make the boundaries walls


for x in 0..80 {
map[xy_idx(x, 0)] = TileType::Wall;
map[xy_idx(x, 49)] = TileType::Wall;
}
for y in 0..50 {
map[xy_idx(0, y)] = TileType::Wall;
map[xy_idx(79, y)] = TileType::Wall;
}

// Now we'll randomly splat a bunch of walls. It won't be pretty, but


it's a decent illustration.
// First, obtain the thread-local RNG:
let mut rng = rltk::RandomNumberGenerator::new();

for _i in 0..400 {
let x = rng.roll_dice(1, 79);
let y = rng.roll_dice(1, 49);
let idx = xy_idx(x, y);
if idx != xy_idx(40, 25) {
map[idx] = TileType::Wall;
}
}

map
}

There's a fair amount of syntax that we haven't encountered before here, so lets
break this down:

1. fn new_map() -> Vec<TileType> species a function named new_map . It doesn't


take any parameters, so it can be called from anywhere.
2. It returns a Vec . Vec is a Rust Vector (if you're familiar with C++, it's pretty much
exactly the same as a C++ std::vector ). A vector is like an array (see this Rust
by Example chapter), which lets you put a bunch of data into a list and access
each element. Unlike an array, a Vec doesn't have a size limit - and the size can
change while the program runs. So you can push (add) new items, and remove
them as you go. Rust by Example has a great chapter on Vectors; it's a good idea
to learn about them - they are used everywhere.

43 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

3. let mut map = vec![TileType::Floor; 80*50]; is a confusing looking


statement! Lets break it down:
1. let mut map is saying "make a new variable" ( let ), "let me change it" (
mut ) and call it "map".
2. vec! is a macro, another one build into the Rust standard library. The
exclamation mark is Rust's way of saying "this is a procedural macro" (as
opposed to a derive macro, like we've seen before). Procedural macros run
like a function - they de�ne a procedure, they just greatly reduce your
typing.
3. Macros take their parameters in square brackets.
4. The �rst parameter is the value for each element of the new vector. In this
case, we're setting every entry we create to be a Floor (from the
TileType enumeration).
5. The second parameter is how many tiles we should create. They will all be
set to the value we set above. In this case, our map is 80x50 tiles (4,000
tiles - but we'll let the compiler do the math for us!). So we need to make
4,000 tiles.
6. You could have replaced the vec! call with
for _i in 0..4000 { map.push(TileType::Floor); } . In fact, that's
pretty much what the macro did for you - but it's de�nitely less typing to
have the macro do it for you!
4. for x in 0..80 { is a for loop (see here), just like we used in the previous
example. In this case, we're iterating x from 0 to 79.
5. map[xy_idx(x, 0)] = TileType::Wall; �rst calls the xy_idx function we
de�ned above to get the vector index for x, 0 . It then indexes the vector, telling
it to set the vector entry at that position to be a wall. We do this again for x,49 .
6. We do the same thing, but looping y from 0..49 - and setting the vertical walls
on our map.
7. let mut rng = rltk::RandomNumberGenerator::new(); calls the
RandomNumberGenerator type in RLTK 's new function, and assigns it to a
variable called rng . We are asking RLTK to give us a new dice roller.
8. for _i in 0..400 { is the same as other for loops, but notice the _ before
i . We aren't actually looking at the value of i - we just want the loop to run
400 times. Rust will give you a warning if you have a variable you don't use;
adding the underscore pre�x tells Rust that it's ok, we meant to do that.
9. let x = rng.roll_dice(1, 79); calls the rng we grabbed in 7, and asks it for
a random number from 1 to 79. RLTK does not go with an exclusive range,

44 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

because it is trying to mirror the old D&D convention of dice being 1d20 or
similar. In this case, we should be glad that computers don't care about the
geometric di�culty of inventing a 79-sided dice! We also obtain a y value
between 1 and 49. We've rolled imaginary dice, and found a random location on
the map.
10. We set the variable idx (short for "index") to the vector index (via xy_idx we
de�ned earlier) for the coordinates we rolled.
11. if idx != xy_idx(40, 25) { checks that idx isn't the exact middle (we'll be
starting there, so we don't want to start inside a wall!).
12. If it isn't the middle, we set the randomly rolled location to be a wall.

It's pretty simple: it places walls around the outer edges of the map, and then adds
400 random walls anywhere that isn't the player's starting point.

Making the map visible to the world


Specs includes a concept of "resources" - shared data the whole ECS can use. So in
our main function, we add a randomly generated map to the world:

gs.ecs.insert(new_map());

The map is now available from anywhere the ECS can see! Now inside your code, you
can access the map with the rather unwieldy
let map = self.ecs.get_mut::<Vec<TileType>>(); ; it's available to systems in an
easier fashion. There's actually several ways to get the value of map, including
ecs.get , ecs.fetch . get_mut obtains a "mutable" (you can change it) reference to
the map - wrapped in an optional (in case the map isn't there). fetch skips the
Option type and gives you a map directly. You can learn more about this in the Specs
Book.

Draw the map


Now that we have a map available, we should put it on the screen! The complete code

45 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for the new draw_map function looks like this:

fn draw_map(map: &[TileType], ctx : &mut Rltk) {


let mut y = 0;
let mut x = 0;
for tile in map.iter() {
// Render a tile depending upon the tile type
match tile {
TileType::Floor => {
ctx.set(x, y, RGB::from_f32(0.5, 0.5, 0.5),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('.'));
}
TileType::Wall => {
ctx.set(x, y, RGB::from_f32(0.0, 1.0, 0.0),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('#'));
}
}

// Move the coordinates


x += 1;
if x > 79 {
x = 0;
y += 1;
}
}
}

This is mostly straightforward, and uses concepts we've already visited. In the
declaration, we pass the map as &[TileType] rather than &Vec<TileType> ; this
allows us to pass in "slices" (parts of) a map if we so choose. We won't do that yet, but
it may be useful later. It's also considered a more "rustic" (that is: idiomatic Rust) way
to do things, and the linter ( clippy ) warns about it. The Rust Book can teach you
about slices, if you are interested.

Otherwise, it takes advantage of the way we are storing our map - rows together, one
after the other. So it iterates through the entire map structure, adding 1 to the x
position for each tile. If it hits the map width, it zeroes x and adds one to y . This way
we aren't repeatedly reading all over the array - which can get slow. The actual
rendering is very simple: we match the tile type, and draw either a period or a hash
for walls/�oors.

We should also call the function! In our tick function, add:

46 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let map = self.ecs.fetch::<Vec<TileType>>();


draw_map(&map, ctx);

The fetch call is new (we mentioned it above). fetch requires that you promise that
you know that the resource you are requesting really does exist - and will crash if it
doesn't. It doesn't quite return a reference - it's a shred type, which acts like a
reference most of the time but occasionally needs a bit of coercing to be one. We'll
worry about that bridge when it comes time to cross it, but consider yourself warned!

Making walls solid


So now if you run the program ( cargo run ), you'll have a green and grey map with a
yellow @ who can move around. Unfortunately, you'll quickly notice that the player
can walk through walls! Fortunately, that's pretty easy to rectify.

To accomplish this, we modify the try_move_player to read the map and check that
the destination is open:

fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {


let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let map = ecs.fetch::<Vec<TileType>>();

for (_player, pos) in (&mut players, &mut positions).join() {


let destination_idx = xy_idx(pos.x + delta_x, pos.y + delta_y);
if map[destination_idx] != TileType::Wall {
pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));
}
}
}

The new parts are the let map = ... part, which uses fetch just the same way as
the main loop (this is the advantage of storing it in the ECS - you can get to it
everywhere without trying to coerce Rust into letting you use global variables!). We
calculate the cell index of the player's destination with
let destination_idx = xy_idx(pos.x + delta_x, pos.y + delta_y); - and if it isn't

47 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

a wall, we move as normal.

Run the program ( cargo run ) now, and you have a player in a map - and can move
around, properly obstructed by walls.

The full program now looks like this:

48 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{Console, GameState, Rltk, RGB, VirtualKeyCode};


use specs::prelude::*;
use std::cmp::{max, min};
#[macro_use]
extern crate specs_derive;

rltk::add_wasm_support!();

#[derive(Component)]
struct Position {
x: i32,
y: i32,
}

#[derive(Component)]
struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
}

#[derive(Component, Debug)]
struct Player {}

#[derive(PartialEq, Copy, Clone)]


enum TileType {
Wall, Floor
}

struct State {
ecs: World
}

pub fn xy_idx(x: i32, y: i32) -> usize {


(y as usize * 80) + x as usize
}

fn new_map() -> Vec<TileType> {


let mut map = vec![TileType::Floor; 80*50];

// Make the boundaries walls


for x in 0..80 {
map[xy_idx(x, 0)] = TileType::Wall;
map[xy_idx(x, 49)] = TileType::Wall;
}
for y in 0..50 {

49 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map[xy_idx(0, y)] = TileType::Wall;


map[xy_idx(79, y)] = TileType::Wall;
}

// Now we'll randomly splat a bunch of walls. It won't be pretty, but


it's a decent illustration.
// First, obtain the thread-local RNG:
let mut rng = rltk::RandomNumberGenerator::new();

for _i in 0..400 {
let x = rng.roll_dice(1, 79);
let y = rng.roll_dice(1, 49);
let idx = xy_idx(x, y);
if idx != xy_idx(40, 25) {
map[idx] = TileType::Wall;
}
}

map
}

fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {


let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let map = ecs.fetch::<Vec<TileType>>();

for (_player, pos) in (&mut players, &mut positions).join() {


let destination_idx = xy_idx(pos.x + delta_x, pos.y + delta_y);
if map[destination_idx] != TileType::Wall {
pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));
}
}
}

fn player_input(gs: &mut State, ctx: &mut Rltk) {


// Player movement
match ctx.key {
None => {} // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
_ => {}
},

50 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}

fn draw_map(map: &[TileType], ctx : &mut Rltk) {


let mut y = 0;
let mut x = 0;
for tile in map.iter() {
// Render a tile depending upon the tile type
match tile {
TileType::Floor => {
ctx.set(x, y, RGB::from_f32(0.5, 0.5, 0.5),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('.'));
}
TileType::Wall => {
ctx.set(x, y, RGB::from_f32(0.0, 1.0, 0.0),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('#'));
}
}

// Move the coordinates


x += 1;
if x > 79 {
x = 0;
y += 1;
}
}
}

impl GameState for State {


fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();

player_input(self, ctx);
self.run_systems();

let map = self.ecs.fetch::<Vec<TileType>>();


draw_map(&map, ctx);

let positions = self.ecs.read_storage::<Position>();


let renderables = self.ecs.read_storage::<Renderable>();

for (pos, render) in (&positions, &renderables).join() {


ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}
}
}

51 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl State {
fn run_systems(&mut self) {
self.ecs.maintain();
}
}

fn main() {
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
"resources");
let mut gs = State {
ecs: World::new()
};
gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();
gs.ecs.register::<Player>();

gs.ecs.insert(new_map());

gs.ecs
.create_entity()
.with(Position { x: 40, y: 25 })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.build();

rltk::main_loop(context, gs);
}

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Chapter 4 - A More Interesting Map

52 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In this chapter, we'll make a more interesting map. It will be room-based, and look a
bit like many of the earlier roguelikes such as Moria - but with less complexity. It will
also provide a great starting point for placing monsters!

Cleaning up
We're going to start by cleaning up our code a bit, and utilizing separate �les. As
projects gain in complexity/size, it's a good idea to start keeping them as a clean set of
�les/modules, so we can quickly �nd what we're looking for (and improve compilation
times, sometimes).

If you look at the source code for this chapter, you'll see that we've broken out a lot of
functionality into individual �les. When you make a new �le in Rust, it automatically
becomes a module. You then have to tell Rust to use these modules, so main.rs has
gained a few mod map and similar, followed by pub use map::* . This says "import the
module map, and then use - and make available to other modules - its public
contents".

We've also made a bunch of struct into pub struct , and added pub to their
members. If you don't do this, then the structure remains internal to that module only
- and you can't use it in other parts of the code. This is the same as putting a public:
C++ line in a class de�nition, and exporting the type in the header. Rust makes it a bit
cleaner, and no need to write things twice!

Making a more interesting map


We'll start by renaming new_map (now in map.rs ) to new_map_test . We'll stop using
it, but keep it around for a bit - it's a decent way to test our map code! We'll also use

53 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Rust's documentation tags to publish what this function does, in case we forget later:

/// Makes a map with solid boundaries and 400 randomly placed walls. No
guarantees that it won't
/// look awful.
pub fn new_map_test() -> Vec<TileType> {
...
}

In canonical Rust, if you pre�x a function with comments starting with /// , it makes it
into a function comment. Your IDE will then show you your comment text when you
hover the mouse over the function header, and you can use Cargo's documentation
features to make pretty documentation pages for the system you are writing. It's
mostly handy if you plan on sharing your code, or working with others - but it's nice to
have!

So now, in the spirit of the original libtcod tutorial, we'll start making a map. Our goal
is to randomly place rooms, and join them together with corridors.

Making a couple of rectangular rooms


We'll start with a new function:

pub fn new_map_rooms_and_corridors() -> Vec<TileType> {


let mut map = vec![TileType::Wall; 80*50];

map
}

This makes a solid 80x50 map, with walls on all tiles - you can't move! We've kept the
function signature, so changing the map we want to use in main.rs just requires
changing gs.ecs.insert(new_map_test()); to
gs.ecs.insert(new_map_rooms_and_corridors()); . Once again we're using the vec!
macro to make our life easier - see the previous chapter for a discussion of how that
works.

Since this algorithm makes heavy use of rectangles, and a Rect type - we'll start by

54 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

making one in rect.rs . We'll include some utility functions that will be useful later on
in this chapter:

pub struct Rect {


pub x1 : i32,
pub x2 : i32,
pub y1 : i32,
pub y2 : i32
}

impl Rect {
pub fn new(x:i32, y: i32, w:i32, h:i32) -> Rect {
Rect{x1:x, y1:y, x2:x+w, y2:y+h}
}

// Returns true if this overlaps with other


pub fn intersect(&self, other:&Rect) -> bool {
self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2
&& self.y2 >= other.y1
}

pub fn center(&self) -> (i32, i32) {


((self.x1 + self.x2)/2, (self.y1 + self.y2)/2)
}
}

There's nothing really new here, but lets break it down a bit:

1. We de�ne a struct called Rect . We added the pub tag to make it public - it's
available outside of this module (by putting it into a new �le, we automatically
created a code module; that's a built-in Rust way to compartmentalize your
code). Over in main.rs , we can add pub mod Rect to say "we use Rect , and
because we put a pub in front of it anything can get Rect from us as
super::rect::Rect . That's not very ergonomic to type, so a second line
use rect::Rect shortens that to super::Rect .
2. We make a new constructor, entitled new . It uses the return shorthand and
returns a rectangle based on the x , y , width and height we pass in.
3. We de�ne a member method, intersect . It has an &self , meaning it can see
into the Rect to which it is attached - but can't modify it (it's a "pure" function).
It returns a bool: true if the two rectangles overlap, false otherwise.
4. We de�ne center , also as a pure member method. It simply returns the

55 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

coordinates of the middle of the rectangle, as a tuple of x and y in val.0 and


val.1 .

We'll also make a new function to apply a room to a map:

fn apply_room_to_map(room : &Rect, map: &mut [TileType]) {


for y in room.y1 +1 ..= room.y2 {
for x in room.x1 + 1 ..= room.x2 {
map[xy_idx(x, y)] = TileType::Floor;
}
}
}

Notice that we are using for y in room.y1 +1 ..= room.y2 - that's an inclusive
range. We want to go all the way to the value of y2 , and not y2-1 ! Otherwise, it's
relatively straightforward: use two for loops to visit every tile inside the room's
rectangle, and set that tile to be a Floor .

With these two bits of code, we can create a new rectangle anywhere with
Rect::new(x, y, width, height) . We can add it to the map as �oors with
apply_room_to_map(rect, map) . That's enough to add a couple of test rooms. Our
map function now looks like this:

pub fn new_map_rooms_and_corridors() -> Vec<TileType> {


let mut map = vec![TileType::Wall; 80*50];

let room1 = Rect::new(20, 15, 10, 15);


let room2 = Rect::new(35, 15, 10, 15);

apply_room_to_map(&room1, &mut map);


apply_room_to_map(&room2, &mut map);

map
}

If you cargo run your project, you'll see that we now have two rooms - not linked
together.

56 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Making a corridor
Two disconnected rooms isn't much fun, so lets add a corridor between them. We're
going to need some comparison functions, so we have to tell Rust to import them (at
the top of map.rs ): use std::cmp::{max, min}; . min and max do what they say:
they return the minimum or maximum of two values. You could use if statements to
do the same thing, but some computers will optimize this into a simple (FAST) call for
you; we let Rust �gure that out!

Then we make two functions, for horizontal and vertical tunnels:

fn apply_horizontal_tunnel(map: &mut [TileType], x1:i32, x2:i32, y:i32) {


for x in min(x1,x2) ..= max(x1,x2) {
let idx = xy_idx(x, y);
if idx > 0 && idx < 80*50 {
map[idx as usize] = TileType::Floor;
}
}
}

fn apply_vertical_tunnel(map: &mut [TileType], y1:i32, y2:i32, x:i32) {


for y in min(y1,y2) ..= max(y1,y2) {
let idx = xy_idx(x, y);
if idx > 0 && idx < 80*50 {
map[idx as usize] = TileType::Floor;
}
}
}

Then we add a call, apply_horizontal_tunnel(&mut map, 25, 40, 23); to our map
making function, and voila! We have a tunnel between the two rooms! If you run (
cargo run ) the project, you can walk between the two rooms - and not into walls. So
our previous code is still working, but now it looks a bit more like a roguelike.

Making a simple dungeon


Now we can use that to make a random dungeon. We'll modify our function as
follows:

57 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new_map_rooms_and_corridors() -> Vec<TileType> {


let mut map = vec![TileType::Wall; 80*50];

let mut rooms : Vec<Rect> = Vec::new();


const MAX_ROOMS : i32 = 30;
const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;

let mut rng = RandomNumberGenerator::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, 80 - w - 1) - 1;
let y = rng.roll_dice(1, 50 - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
apply_room_to_map(&new_room, &mut map);
rooms.push(new_room);
}
}

map
}

There's quite a bit changed there:

We've added const constants for the maximum number of rooms to make, and
the minimum and maximum size of the rooms. This is the �rst time we've
encountered const : it just says "setup this value at the beginning, and it can
never change". It's the only easy way to have global variables in Rust; since they
can never change, they often don't even exist and get baked into the functions
where you use them. If they do exist, because they can't change there are no
concerns when multiple threads access them. It's often cleaner to setup a
named constant than to use a "magic number" - that is, a hard-coded value with
no real clue as to why you picked that value.
We acquire a RandomNumberGenerator from RLTK (which required that we add to
the use statement at the top of map.rs )

58 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We're randomly building a width and height.


We're then placing the room randomly so that x and y are greater than 0 and
less than the maximum map size minus one.
We iterate through existing rooms, rejecting the new room if it overlaps with one
we've already placed.
If its ok, we apply it to the room.
We're keeping rooms in a vector, although we aren't using it yet.

Running the project ( cargo run ) at this point will give you a selection of random
rooms, with no corridors between them.

Joining the rooms together


We now need to join the rooms together, with corridors. We'll add this to the if ok
section of the map generator:

if ok {
apply_room_to_map(&new_room, &mut map);

if !rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = rooms[rooms.len()-1].center();
if rng.range(0,1) == 1 {
apply_horizontal_tunnel(&mut map, prev_x, new_x, prev_y);
apply_vertical_tunnel(&mut map, prev_y, new_y, new_x);
} else {
apply_vertical_tunnel(&mut map, prev_y, new_y, prev_x);
apply_horizontal_tunnel(&mut map, prev_x, new_x, new_y);
}
}

rooms.push(new_room);
}

1. So what does this do? It starts by looking to see if the rooms list is empty. If it is,
then there is no previous room to join to - so we ignore it.
2. It gets the room's center, and stores it as new_x and new_y .
3. It gets the previous room in the vector's center, and stores it as prev_x and

59 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

prev_y .
4. It rolls a dice, and half the time it draws a horizontal and then vertical tunnel -
and half the time, the other way around.

Try cargo run now. It's really starting to look like a roguelike!

Placing the player


Currently, the player always starts in the center of the map - which with the new
generator, may not be a valid starting point! We could simply move the player to the
center of the �rst room, but it's likely that our generator will need to know where all
the rooms are - so we can put things in them - rather than just the player's location.
So we'll modify our new_map_rooms_and_corridors function to also return the room
list. So we change the method signature to:
pub fn new_map_rooms_and_corridors() -> (Vec<Rect>, Vec<TileType>) { , and
the return statement to (rooms, map)

Our main.rs �le also requires adjustments, to accept the new format. We change our
main function in main.rs to:

60 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn main() {
let context = Rltk::init_simple8x8(80, 50, "Hello Rust World",
"resources");
let mut gs = State {
ecs: World::new()
};
gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();
gs.ecs.register::<Player>();

let (rooms, map) = new_map_rooms_and_corridors();


gs.ecs.insert(map);
let (player_x, player_y) = rooms[0].center();

gs.ecs
.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.build();

rltk::main_loop(context, gs);
}

This is mostly the same, but we are receiving both the rooms list and the map from
new_map_rooms_and_corridors . We then place the player in the center of the �rst
room.

Wrapping Up - and supporting the numpad, and Vi


keys
Now you have a map that looks like a roguelike, places the player in the �rst room,
and lets you explore with the cursor keys. Not every keyboard has cursor keys that are
readily accessible (some laptops require interesting key combinations for them). Lots
of players like to steer with the numpad, but not every keyboard has one of those
either - so we also support the directional keys from the text editor vi . This makes

61 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

both hardcore UNIX users happy, and makes regular players happier.

We're not going to worry about diagonal movement yet. In player.rs , we change
player_input to look like this:

pub fn player_input(gs: &mut State, ctx: &mut Rltk) {


// Player movement
match ctx.key {
None => {} // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut
gs.ecs),
VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad6 => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Numpad8 => try_move_player(0, -1, &mut
gs.ecs),
VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
VirtualKeyCode::Numpad2 => try_move_player(0, 1, &mut gs.ecs),
VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
_ => {}
},
}
}

You should now get something like this when you cargo run your project:

62 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Chapter 5 - Field of View

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

We have a nicely drawn map, but it shows the whole dungeon! That reduces the
usefulness of exploration - if we already know where everything is, why bother

63 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

exploring? This chapter will add "�eld of view", and adjust rendering to show the parts
of the map we've already discovered. It will also refactor the map into its own
structure, rather than just a vector of tiles.

This chapter starts with the code from chapter 4.

Map refactor
We'll keep map-related functions and data together, to keep things clear as we make
an ever-more-complicated game. The bulk of this is creating a new Map structure, and
moving our helper functions to its implementation.

64 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate rltk;


use rltk::{ RGB, Rltk, Console, RandomNumberGenerator };
use super::{Rect};
use std::cmp::{max, min};

#[derive(PartialEq, Copy, Clone)]


pub enum TileType {
Wall, Floor
}

pub struct Map {


pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32
}

impl Map {
pub fn xy_idx(&self, x: i32, y: i32) -> usize {
(y as usize * self.width as usize) + x as usize
}

fn apply_room_to_map(&mut self, room : &Rect) {


for y in room.y1 +1 ..= room.y2 {
for x in room.x1 + 1 ..= room.x2 {
let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor;
}
}
}

fn apply_horizontal_tunnel(&mut self, x1:i32, x2:i32, y:i32) {


for x in min(x1,x2) ..= max(x1,x2) {
let idx = self.xy_idx(x, y);
if idx > 0 && idx < self.width as usize * self.height as usize
{
self.tiles[idx as usize] = TileType::Floor;
}
}
}

fn apply_vertical_tunnel(&mut self, y1:i32, y2:i32, x:i32) {


for y in min(y1,y2) ..= max(y1,y2) {
let idx = self.xy_idx(x, y);
if idx > 0 && idx < self.width as usize * self.height as usize

65 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
self.tiles[idx as usize] = TileType::Floor;
}
}
}

/// Makes a new map using the algorithm from


http://rogueliketutorials.com/tutorials/tcod/part-3/
/// This gives a handful of random rooms and corridors joining them
together.
pub fn new_map_rooms_and_corridors() -> Map {
let mut map = Map{
tiles : vec![TileType::Wall; 80*50],
rooms : Vec::new(),
width : 80,
height: 50
};

const MAX_ROOMS : i32 = 30;


const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;

let mut rng = RandomNumberGenerator::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, map.width - w - 1) - 1;
let y = rng.roll_dice(1, map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in map.rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
map.apply_room_to_map(&new_room);

if !map.rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = map.rooms[map.rooms.len()-
1].center();
if rng.range(0,1) == 1 {
map.apply_horizontal_tunnel(prev_x, new_x,
prev_y);
map.apply_vertical_tunnel(prev_y, new_y, new_x);
} else {

66 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map.apply_vertical_tunnel(prev_y, new_y, prev_x);


map.apply_horizontal_tunnel(prev_x, new_x, new_y);
}
}

map.rooms.push(new_room);
}
}

map
}
}

There's changes in main and player , too - see the example source for all the details.
This has cleaned up our code quite a bit - we can pass a Map around, instead of a
vector. If we want to teach Map to do more things - we have a place to do so.

The �eld-of-view component


Not just the player has limited visibility! Eventually, we'll want monsters to consider
what they can see, too. So, since its reusable code, we'll make a Viewshed
component. (I like the word viewshed; it comes from the cartography world - literally
"what can I see from here?" - and perfectly describes out problem). We'll give each
entity that has a Viewshed a list of tile indices they can see. In components.rs we add:

#[derive(Component)]
pub struct Viewshed {
pub visible_tiles : Vec<rltk::Point>,
pub range : i32
}

In main.rs , we tell the system about the new component:

gs.ecs.register::<Viewshed>();

Lastly, also in main.rs we'll give the Player a Viewshed component:

67 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

gs.ecs
.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.with(Viewshed{ visible_tiles : Vec::new(), range : 8 })
.build();

Player is getting quite complicated now - that's good, it shows what an ECS is good for!

A new system: generic viewsheds


We'll start by de�ning a system to take care of this for us. We want this to be generic,
so it works for anything that can bene�t from knowing what it can see. We create a
new �le, visibility_system.rs :

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Position};

pub struct VisibilitySystem {}

impl<'a> System<'a> for VisibilitySystem {


type SystemData = ( WriteStorage<'a, Viewshed>,
WriteStorage<'a, Position>);

fn run(&mut self, (mut viewshed, pos) : Self::SystemData) {


for (viewshed,pos) in (&mut viewshed, &pos).join() {
}
}
}

Now we have to adjust run_systems in main.rs to actually call the system:

68 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl State {
fn run_systems(&mut self) {
let mut vis = VisibilitySystem{};
vis.run_now(&self.ecs);
self.ecs.maintain();
}
}

We also have to tell main.rs to use the new module:

mod visibility_system;
use visibility_system::VisibilitySystem;

This doesn't actually do anything, yet - but we've added a system into the dispatcher,
and as soon as we �esh out the code to actually plot the visibility, it will apply to every
entity that has both a Viewshed and a Position component.

Asking RLTK for a Viewshed: Trait


Implementation
RLTK is written to not care about how you've chosen to lay out your map: I want it to
be useful for anyone, and not everyone does maps the way this tutorial does. To act
as a bridge between our map implementation and RLTK, it provides some traits for us
to support. For this example, we need BaseMap and Algorithm2D . Don't worry, they
are simple enough to implement.

In our map.rs �le, we add the following:

impl Algorithm2D for Map {


fn point2d_to_index(&self, pt: Point) -> i32 {
(pt.y * self.width) + pt.x
}

fn index_to_point2d(&self, idx:i32) -> Point {


Point{ x: idx % self.width, y: idx / self.width }
}
}

69 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This tells Rust that we are implementing Algorithm2D from RLTK (we also need to
adjust the use statement to
use rltk::{ RGB, Rltk, Console, RandomNumberGenerator, BaseMap,
Algorithm2D, Point };
). point2d_to_index is pretty much the same as the xy_idx function we've been
using: it returns the array index to which an x/y position points. index_to_point2d
does the same thing backwards: given an index, it returns the x/y coordinates it
references.

We also need to support BaseMap . We don't need all of it yet, so we're going to stub
parts of it out. In map.rs :

impl BaseMap for Map {


fn is_opaque(&self, idx:i32) -> bool {
self.tiles[idx as usize] == TileType::Wall
}

fn get_available_exits(&self, _idx:i32) -> Vec<(i32, f32)> {


Vec::new()
}

fn get_pathing_distance(&self, idx1:i32, idx2:i32) -> f32 {


let p1 = Point::new(idx1 % self.width, idx1 / self.width);
let p2 = Point::new(idx2 % self.width, idx2 / self.width);
rltk::DistanceAlg::Pythagoras.distance2d(p1, p2)
}
}

is_opaque simply returns true if the tile is a wall, and false otherwise. This will have
to be expanded if/when we add more types of tile, but works for now. We're not
touching get_available_exits yet - so we just return an empty list (this will be useful
in later chapters). get_pathing_distance is a simple distance calculation - so we
extract point locations from the two points and return a simple Pythagoras distance.
Again, this will be useful later.

Asking RLTK for a Viewshed: The System


So going back to visibility_system.rs , we now have what we need to request a
viewshed from RLTK. We extend our visibility_system.rs �le to look like this:

70 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Position, Map};
extern crate rltk;
use rltk::{field_of_view, Point};

pub struct VisibilitySystem {}

impl<'a> System<'a> for VisibilitySystem {


type SystemData = ( ReadExpect<'a, Map>,
WriteStorage<'a, Viewshed>,
WriteStorage<'a, Position>);

fn run(&mut self, data : Self::SystemData) {


let (map, mut viewshed, pos) = data;

for (viewshed,pos) in (&mut viewshed, &pos).join() {


viewshed.visible_tiles.clear();
viewshed.visible_tiles = field_of_view(Point::new(pos.x,
pos.y), viewshed.range, &*map);
}
}
}

There's quite a bit here, and the viewshed is actually the simplest part:

We've added a ReadExpect<'a, Map> - meaning that the system should be


passed our Map for use. We used ReadExpect , because not having a map is a
failure.
In the loop, we �rst clear the list of visible tiles.
Then we call RLTK's field_of_view function, providing the starting point (the
location of the entity, from pos ), the range (from the viewshed), and a slightly
convoluted "dereference, then get a reference" to unwrap Map from the ECS.

This will now run every frame (which is overkill, more on that later) - and store a list of
visible tiles.

Rendering visibility - badly!


As a �rst try, we'll change our draw_map function to retrieve the map, and the player's
viewshed. It will only draw tiles present in the viewshed:

71 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn draw_map(ecs: &World, ctx : &mut Rltk) {


let mut viewsheds = ecs.write_storage::<Viewshed>();
let mut players = ecs.write_storage::<Player>();
let map = ecs.fetch::<Map>();

for (_player, viewshed) in (&mut players, &mut viewsheds).join() {


let mut y = 0;
let mut x = 0;
for tile in map.tiles.iter() {
// Render a tile depending upon the tile type
let pt = Point::new(x,y);
if viewshed.visible_tiles.contains(&pt) {
match tile {
TileType::Floor => {
ctx.set(x, y, RGB::from_f32(0.5, 0.5, 0.5),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('.'));
}
TileType::Wall => {
ctx.set(x, y, RGB::from_f32(0.0, 1.0, 0.0),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('#'));
}
}
}

// Move the coordinates


x += 1;
if x > 79 {
x = 0;
y += 1;
}
}
}
}

If you run the example now ( cargo run ), it will show you just what the player can see.
There's no memory, and performance is quite awful - but it's there and about right.

It's clear that we're on the right track, but we need a more e�cient way to do things. It
would be nice if the player could remember the map as they see it, too.

Expanding map to include revealed tiles

72 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

To simulate map memory, we'll extend our Map class to include a revealed_tiles
structure. It's just a bool for each tile on the map - if true, then we know what's there.
Our Map de�nition now looks like this:

#[derive(Default)]
pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>
}

We also need to extend the function that �lls the map to include the new type. In
new_rooms_and_corridors , we extend the Map creation to:

let mut map = Map{


tiles : vec![TileType::Wall; 80*50],
rooms : Vec::new(),
width : 80,
height: 50,
revealed_tiles : vec![false; 80*50]
};

That adds a false value for every tile.

We change the draw_map to look at this value, rather than iterating the component
each time. The function now looks like this:

73 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn draw_map(ecs: &World, ctx : &mut Rltk) {


let map = ecs.fetch::<Map>();

let mut y = 0;
let mut x = 0;
for (idx,tile) in map.tiles.iter().enumerate() {
// Render a tile depending upon the tile type
if map.revealed_tiles[idx] {
match tile {
TileType::Floor => {
ctx.set(x, y, RGB::from_f32(0.5, 0.5, 0.5),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('.'));
}
TileType::Wall => {
ctx.set(x, y, RGB::from_f32(0.0, 1.0, 0.0),
RGB::from_f32(0., 0., 0.), rltk::to_cp437('#'));
}
}
}

// Move the coordinates


x += 1;
if x > 79 {
x = 0;
y += 1;
}
}
}

This will render a black screen, because we're never setting any tiles to be revealed!
So now we extend the VisibilitySystem to know how to mark tiles as revealed. To
do this, it has to check to see if an entity is the player - and if it is, it updates the map's
revealed status:

74 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Position, Map, Player};
extern crate rltk;
use rltk::{field_of_view, Point};

pub struct VisibilitySystem {}

impl<'a> System<'a> for VisibilitySystem {


type SystemData = ( WriteExpect<'a, Map>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
WriteStorage<'a, Position>,
ReadStorage<'a, Player>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, entities, mut viewshed, pos, player) = data;

for (ent,viewshed,pos) in (&entities, &mut viewshed, &pos).join()


{
viewshed.visible_tiles.clear();
viewshed.visible_tiles = field_of_view(Point::new(pos.x,
pos.y), viewshed.range, &*map);

// If this is the player, reveal what they can see


let p : Option<&Player> = player.get(ent);
if let Some(p) = p {
for vis in viewshed.visible_tiles.iter() {
let idx = map.xy_idx(vis.x, vis.y);
map.revealed_tiles[idx] = true;
}
}
}
}
}

The main changes here are that we're getting the Entities list along with components,
and obtaining read-only access to the Players storage. We add those to the list of
things to iterate in the list, and add a let p : Option<&Player> = player.get(ent);
to see if this is the player. The rather cryptic if let Some(p) = p runs only if there is
a Player component. Then we calculate the index, and mark it revealed.

If you run ( cargo run ) the project now, it is MASSIVELY faster than the previous
version, and remembers where you've been.

75 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Speeding it up even more - recalculating


visibility when we need to
It's still not as e�cient as it could be! Lets only update viewsheds when we need to.
Lets add a dirty �ag to our Viewshed component:

#[derive(Component)]
pub struct Viewshed {
pub visible_tiles : Vec<rltk::Point>,
pub range : i32,
pub dirty : bool
}

We'll also update the initialization in main.rs to say that the viewshed is, in fact, dirty:
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .

Our system can be extended to check if the dirty �ag is true, and only recalculate if
it is - and set the dirty �ag to false when it is done. Now we need to set the �ag
when the player moves - because what they can see has changed! We update
try_move_player in player.rs :

pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {


let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
let map = ecs.fetch::<Map>();

for (_player, pos, viewshed) in (&mut players, &mut positions, &mut


viewsheds).join() {
let destination_idx = map.xy_idx(pos.x + delta_x, pos.y +
delta_y);
if map.tiles[destination_idx] != TileType::Wall {
pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));

viewshed.dirty = true;
}
}
}

76 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This should be pretty familiar by now: we've added viewsheds to get write storage,
and included it in the list of component types we are iterating. Then one call sets the
�ag to true after a move.

The game now runs very fast once more, if you type cargo run .

Greying out what we remember, but


can't see
One more extension: we'd like to render the parts of the map we know are there but
can't currently see. So we add a list of what tiles are currently visible to Map :

#[derive(Default)]
pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>
}

Our creation method also needs to know to add all false to it, just like before:
visible_tiles : vec![false; 80*50] . Next, in our VisibilitySystem we clear the
list of visible tiles before we begin iterating - and mark currently visible tiles as we �nd
them. So our code to run when updating the viewshed looks like this:

77 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if viewshed.dirty {
viewshed.dirty = false;
viewshed.visible_tiles.clear();
viewshed.visible_tiles = field_of_view(Point::new(pos.x, pos.y),
viewshed.range, &*map);

// If this is the player, reveal what they can see


let _p : Option<&Player> = player.get(ent);
if let Some(_p) = _p {
for t in map.visible_tiles.iter_mut() { *t = false };
for vis in viewshed.visible_tiles.iter() {
let idx = map.xy_idx(vis.x, vis.y);
map.revealed_tiles[idx] = true;
map.visible_tiles[idx] = true;
}
}
}

Now we adjust the draw_map function to handle revealed but not currently visible
tiles di�erently. The new draw_map function looks like this:

78 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn draw_map(ecs: &World, ctx : &mut Rltk) {


let map = ecs.fetch::<Map>();

let mut y = 0;
let mut x = 0;
for (idx,tile) in map.tiles.iter().enumerate() {
// Render a tile depending upon the tile type

if map.revealed_tiles[idx] {
let glyph;
let mut fg;
match tile {
TileType::Floor => {
glyph = rltk::to_cp437('.');
fg = RGB::from_f32(0.0, 0.5, 0.5);
}
TileType::Wall => {
glyph = rltk::to_cp437('#');
fg = RGB::from_f32(0., 1.0, 0.);
}
}
if !map.visible_tiles[idx] { fg = fg.to_greyscale() }
ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph);
}

// Move the coordinates


x += 1;
if x > 79 {
x = 0;
y += 1;
}
}
}

If you cargo run your project, you will now have visible tiles as slightly cyan �oors
and green walls - and grey as they move out of view. Performance should be great!
Congratulations - you now have a nice, working �eld-of-view system.

79 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Chapter 6 - Monsters

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

A roguelike with no monsters is quite unusual, so lets add some! The good news is
that we've already done some of the work for this: we can render them, and we can

80 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

calculate what they can see. We'll build on the source from the previous chapter, and
get some harmless monsters into play.

Rendering a monster in the center of


each room
We can simply add a Renderable component for each monster (we'll also add a
Viewshed since we'll use it later). In our main function (in main.rs ), add the
following:

for room in map.rooms.iter().skip(1) {


let (x,y) = room.center();
gs.ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('g'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.build();
}

gs.ecs.insert(map);

Notice the skip(1) to ignore the �rst room - we don't want the player starting with a
mob on top of him/her/it! Running this (with cargo run ) produces something like
this:

81 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

That's a really good start! However, we're rendering monsters even if we can't see
them. We probably only want to render the ones we can see. We can do this by
modifying our render loop:

let positions = self.ecs.read_storage::<Position>();


let renderables = self.ecs.read_storage::<Renderable>();
let map = self.ecs.fetch::<Map>();

for (pos, render) in (&positions, &renderables).join() {


let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg,
render.bg, render.glyph) }
}

We get the map from the ECS, and use it to obtain an index - and check if the tile is
visible. If it is - we render the renderable. There's no need for a special case for the
player - since they can generally be expected to see themselves! The result is pretty
good:

82 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Add some monster variety


It's rather dull to only have one monster type, so we'll amend our monster spawner to
be able to create g oblins and o rcs.

Here's the spawner code:

83 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut rng = rltk::RandomNumberGenerator::new();


for room in map.rooms.iter().skip(1) {
let (x,y) = room.center();

let glyph : u8;


let roll = rng.roll_dice(1, 2);
match roll {
1 => { glyph = rltk::to_cp437('g') }
_ => { glyph = rltk::to_cp437('o') }
}

gs.ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: glyph,
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.build();
}

Obviously, when we start adding in combat we'll want more variety - but it's a good
start. Run the program ( cargo run ), and you'll see a roughly 50/50 split between orcs
and goblins.

Making the monsters think


Now to start making the monsters think! For now, they won't actually do much,
beyond pondering their lonely existence. We should start by adding a tag component
to indicate that an entity is a monster. In components.rs we add a simple struct:

#[derive(Component, Debug)]
pub struct Monster {}

Of course, we need to register it in main.rs : gs.ecs.register::<Monster>(); . We


should also amend our spawning code to apply it to monsters:

84 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

gs.ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: glyph,
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
.with(Monster{})
.build();

Now we make a system for monster thought. We'll make a new �le,
monster_ai_system.rs . We'll give it some basically non-existent intelligence:

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Position, Map, Monster};
extern crate rltk;
use rltk::{field_of_view, Point, console};

pub struct MonsterAI {}

impl<'a> System<'a> for MonsterAI {


type SystemData = ( ReadStorage<'a, Viewshed>,
ReadStorage<'a, Position>,
ReadStorage<'a, Monster>);

fn run(&mut self, data : Self::SystemData) {


let (viewshed, pos, monster) = data;

for (viewshed,pos,_monster) in (&viewshed, &pos, &monster).join()


{
console::log("Monster considers their own existence");
}
}
}

Note that we're importing console from rltk - and printing with console::log .
This is a helper provided by RLTK that detects if you are compiling to a regular
program or a Web Assembly; if you are using a regular program, it calls println! and
outputs to the console. If you are in WASM , it outputs to the browser console.

85 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll also extend the system runner in main.rs to call it:

impl State {
fn run_systems(&mut self) {
let mut vis = VisibilitySystem{};
vis.run_now(&self.ecs);
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
self.ecs.maintain();
}
}

The &["visibility_system"] is new - it says "run this after visibility, since we depend
upon its results. At this point, we don't actually care - but we will, so we'll put it in
there now.

If you cargo run your project now, it will be very slow - and your console will �ll up
with "Monster considers their own existence". The AI is running - but it's running every
tick!

Turn-based game, in a tick-based world


To prevent this - and make a turn-based game - we introduce a new concept to the
game state. The game is either "running" or "waiting for input" - so we make an enum
to handle this:

pub enum RunState { Paused, Running }

We add it to the State type:

pub struct State {


pub ecs: World,
pub systems: Dispatcher<'static, 'static>,
pub runstate : RunState
}

We also amend our State creator to include a runstate: RunState::Running .

86 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Now, we change our tick function to only run the simulation when the game isn't
paused - and otherwise to ask for user input:

if self.runstate == RunState::Running {
self.systems.dispatch(&self.ecs);
self.runstate = RunState::Paused;
} else {
self.runstate = player_input(self, ctx);
}

As you can see, player_input now returns a state. Here's the new code for it:

pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {


// Player movement
match ctx.key {
None => { return RunState::Paused } // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut
gs.ecs),
VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad6 => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Numpad8 => try_move_player(0, -1, &mut
gs.ecs),
VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
VirtualKeyCode::Numpad2 => try_move_player(0, 1, &mut gs.ecs),
VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
_ => { return RunState::Paused }
},
}
RunState::Running
}

If you launch cargo run now, the game is back up to speed - and the monsters only
think about what to do when you move. That's a basic turn-based tick loop!

Quiet monsters until they see you

87 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You could let monsters think every time anything moves (and you probably will when
you get into deeper simulation), but for now lets quiet them down a bit - and have
them react if they can see the player.

It's highly likely that systems will often want to know where the player is - so lets add
that as a resource. In main.rs , one line puts it in (I don't recommend doing this for
non-player entities; there are only so many resources available - but the player is one
we use over and over again):

gs.ecs.insert(Point::new(player_x, player_y));

In player.rs , try_move_player() , update the resource when the player moves:

let mut ppos = ecs.write_resource::<Point>();


ppos.x = pos.x;
ppos.y = pos.y;

We can then use that in our monster_ai_system . Here's a working version:

88 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Monster};
extern crate rltk;
use rltk::{Point, console};

pub struct MonsterAI {}

impl<'a> System<'a> for MonsterAI {


type SystemData = ( ReadExpect<'a, Point>,
ReadStorage<'a, Viewshed>,
ReadStorage<'a, Monster>);

fn run(&mut self, data : Self::SystemData) {


let (player_pos, viewshed, monster) = data;

for (viewshed,_monster) in (&viewshed, &monster).join() {


if viewshed.visible_tiles.contains(&*player_pos) {
console::log(format!("Monster shouts insults"));
}
}
}
}

If you cargo run this, you'll be able to move around - and your console will gain
"Monster shouts insults" from time to time when a monster can see you.

Di�erentiating our monsters


Monsters should have names, so we know who is yelling at us! So we create a new
component, Name . In components.rs , we add:

#[derive(Component, Debug)]
pub struct Name {
pub name : String
}

We also register it in main.rs , which you should be comfortable with by now! We'll
also add some commands to add names to our monsters and the player. So our
monster spawner looks like this:

89 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for (i,room) in map.rooms.iter().skip(1).enumerate() {


let (x,y) = room.center();

let glyph : u8;


let name : String;
let roll = rng.roll_dice(1, 2);
match roll {
1 => { glyph = rltk::to_cp437('g'); name = "Goblin".to_string(); }
_ => { glyph = rltk::to_cp437('o'); name = "Orc".to_string(); }
}

gs.ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: glyph,
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.with(Monster{})
.with(Name{ name: format!("{} #{}", &name, i) })
.build();
}

Now we adjust the monster_ai_system to include the monster's name. The new AI
looks like this:

90 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Monster, Name};
extern crate rltk;
use rltk::{Point};

pub struct MonsterAI {}

impl<'a> System<'a> for MonsterAI {


type SystemData = ( ReadExpect<'a, Point>,
ReadStorage<'a, Viewshed>,
ReadStorage<'a, Monster>,
ReadStorage<'a, Name>);

fn run(&mut self, data : Self::SystemData) {


let (player_pos, viewshed, monster, name) = data;

for (viewshed,_monster,name) in (&viewshed, &monster,


&name).join() {
if viewshed.visible_tiles.contains(&*player_pos) {
console::log(&format!("{} shouts insults", name.name));
}
}
}
}

If you cargo run the project, you now see things like Goblin #9 shouts insults - so you
can tell who is shouting.

91 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

And that's a wrap for chapter 6; we've added a variety of foul-mouthed monsters to
hurl insults at your fragile ego! In this chapter, we've begun to see some of the
bene�ts of using an Entity Component System: it was really easy to add newly
rendered monsters, with a bit of variety, and start storing names for things. The
Viewshed code we wrote earlier worked with minimal modi�cation to give visibility to
monsters - and our new monster AI was able to take advantage of what we've already
built to quite e�ciently say bad things to the player.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Dealing Damage (and taking some!)

92 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Now that we have monsters, we want them to be more interesting than just yelling at
you on the console! This chapter will make them chase you, and introduce some basic
game stats to let you �ght your way through the hordes.

Chasing the Player


The �rst thing we need to do is �nish implementing BaseMap for our Map class. In
particular, we need to support get_available_exits - which is used by the
path�nding.

In our Map implementation, we'll need a helper function:

fn is_exit_valid(&self, x:i32, y:i32) -> bool {


if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return
false; }
let idx = (y * self.width) + x;
self.tiles[idx as usize] != TileType::Wall
}

This takes an index, and calculates if it can be entered.

We then implement the trait, using this helper:

93 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn get_available_exits(&self, idx:i32) -> Vec<(i32, f32)> {


let mut exits : Vec<(i32, f32)> = Vec::new();
let x = idx % self.width;
let y = idx / self.width;

// Cardinal directions
if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) };
if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) };
if self.is_exit_valid(x, y-1) { exits.push((idx-self.width, 1.0)) };
if self.is_exit_valid(x, y+1) { exits.push((idx+self.width, 1.0)) };

exits
}

Pretty straight-forward: we evaluate each possible exit, and add it to the exits vector
if it can be taken. Next, we modify the main loop in monster_ai_system :

94 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Monster, Name, Map, Position};
extern crate rltk;
use rltk::{Point, console};

pub struct MonsterAI {}

impl<'a> System<'a> for MonsterAI {


#[allow(clippy::type_complexity)]
type SystemData = ( WriteExpect<'a, Map>,
ReadExpect<'a, Point>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Monster>,
ReadStorage<'a, Name>,
WriteStorage<'a, Position>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, player_pos, mut viewshed, monster, name, mut
position) = data;

for (mut viewshed,_monster,name,mut pos) in (&mut viewshed,


&monster, &name, &mut position).join() {
if viewshed.visible_tiles.contains(&*player_pos) {
console::log(&format!("{} shouts insults", name.name));
let path = rltk::a_star_search(
map.xy_idx(pos.x, pos.y) as i32,
map.xy_idx(player_pos.x, player_pos.y) as i32,
&mut *map
);
if path.success && path.steps.len()>1 {
pos.x = path.steps[1] % map.width;
pos.y = path.steps[1] / map.width;
viewshed.dirty = true;
}
}
}
}
}

We've changed a few things to allow write access, requested access to the map. We've
also added an #[allow...] to tell the linter that we really did mean to use quite so
much in one type! The meat is the a_star_search call; RLTK includes a high-
performance A* implementation, so we're asking it for a path from the monster's

95 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

position to the player. Then we check that the path succeeded, and has more than 2
steps (step 0 is always the current location). If it does, then we move the monster to
that point - and set their viewshed to be dirty.

If you cargo run the project, monsters will now chase the player - and stop if they
lose line-of-sight. We're not preventing monsters from standing on each other - or you
- and we're not having them do anything other than yell at your console - but it's a
good start. It wasn't too hard to get chase mechanics in!

Blocking access
We don't want monsters to walk on top of each other, nor do we want them to get
stuck in a tra�c jam hoping to �nd the player; we'd rather they are willing to try and
�ank the player! We'll accompany this by keeping track of what parts of the map are
blocked.

First, we'll add another vector of bools to our Map :

#[derive(Default)]
pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>
}

We'll also initialize it, just like the other vectors:

96 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut map = Map{


tiles : vec![TileType::Wall; 80*50],
rooms : Vec::new(),
width : 80,
height: 50,
revealed_tiles : vec![false; 80*50],
visible_tiles : vec![false; 80*50],
blocked : vec![false; 80*50]
};

Lets introduce a new function to populate whether or not a tile is blocked. In the Map
implementation:

pub fn populate_blocked(&mut self) {


for (i,tile) in self.tiles.iter_mut().enumerate() {
self.blocked[i] = *tile == TileType::Wall;
}
}

This function is very simple: it sets blocked for a tile to true if its a wall, false
otherwise (we'll expand it when we add more tile types). While we're working with
Map , lets adjust is_exit_valid to use this data:

fn is_exit_valid(&self, x:i32, y:i32) -> bool {


if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return
false; }
let idx = (y * self.width) + x;
!self.blocked[idx as usize]
}

This is quite straightforward: it checks that x and y are within the map, returning
false if the exit is outside of the map (this type of bounds checking is worth doing, it
prevents your program from crashing because you tried to read outside of the the
valid memory area). It then checks the index of the tiles array for the speci�ed
coordinates, and returns the inverse of blocked (the ! is the same as not in most
languages - so read it as "not blocked at idx "). While we're in map , there's one more
function we are going to need:

97 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn clear_content_index(&mut self) {


for content in self.tile_content.iter_mut() {
content.clear();
}
}

This is also quite simple: it iterates (visits) every vector in the tile_content list,
mutably (the iter_mut obtains a mutable iterator). It then tells each vector to clear
itself - remove all content (it doesn't actually guarantee that it will free up the
memory; vectors can keep empty sections ready for more data. This is actually a good
thing, because acquiring new memory is one of the slowest things a program can do -
so it helps keep things running fast).

Now we'll make a new component, BlocksTile . You should know the drill by now; in
Components.rs :

#[derive(Component, Debug)]
pub struct BlocksTile {}

Then register it in main.rs : gs.ecs.register::<BlocksTile>();

We should apply BlocksTile to NPCs - so our NPC creation code becomes:

gs.ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph,
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
.with(Monster{})
.with(Name{ name: format!("{} #{}", &name, i) })
.with(BlocksTile{})
.build();

Lastly, we need to populate the blocked list. We'll probably extend this system later,
so we'll go with a nice generic name map_indexing_system.rs :

98 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Map, Position, BlocksTile};

pub struct MapIndexingSystem {}

impl<'a> System<'a> for MapIndexingSystem {


type SystemData = ( WriteExpect<'a, Map>,
ReadStorage<'a, Position>,
ReadStorage<'a, BlocksTile>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, position, blockers) = data;

map.populate_blocked();
for (position, _blocks) in (&position, &blockers).join() {
let idx = map.xy_idx(position.x, position.y);
map.blocked[idx] = true;
}
}
}

This tells the map to setup blocking from the terrain, and then iterates all entities with
a BlocksTile component, and applies them to the blocked list. We need to register it
with run_systems ; in main.rs :

impl State {
fn run_systems(&mut self) {
let mut mapindex = MapIndexingSystem{};
mapindex.run_now(&self.ecs);
let mut vis = VisibilitySystem{};
vis.run_now(&self.ecs);
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
self.ecs.maintain();
}
}

We didn't specify any dependencies, we're relying upon Specs to �gure out if it can
run concurrently with anything. We do however add it to the dependency list for
MonsterAI - the AI relies on its results, so it has to be done.

99 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you cargo run now, monsters no longer end up on top of each other - but they do
end up on top of the player. We should �x that. We can make the monster only yell
when it is adjacent to the player. In monster_ai_system.rs , add this above the
visibility test:

let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x,


pos.y), *player_pos);
if distance < 1.5 {
// Attack goes here
console::log(&format!("{} shouts insults", name.name));
return;
}

Lastly, we want to stop the player from walking over monsters. In player.rs , we
replace the if statement that looks for walls with:

if !map.blocked[destination_idx] {

Since we already put walls into the blocked list, this should take care of the issue for
now. cargo run shows that monsters now block the player. They block them perfectly
- so a monster that wants to be in your way is an unpassable obstacle!

Allowing Diagonal Movement


It would be nice to be able to bypass the monsters - and diagonal movement is a
mainstay of roguelikes. So lets go ahead and support it. In map.rs 's
get_available_exits function, we add them:

100 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn get_available_exits(&self, idx:i32) -> Vec<(i32, f32)> {


let mut exits : Vec<(i32, f32)> = Vec::new();
let x = idx % self.width;
let y = idx / self.width;

// Cardinal directions
if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) };
if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) };
if self.is_exit_valid(x, y-1) { exits.push((idx-self.width, 1.0)) };
if self.is_exit_valid(x, y+1) { exits.push((idx+self.width, 1.0)) };

// Diagonals
if self.is_exit_valid(x-1, y-1) { exits.push(((idx-self.width)-1,
1.45)); }
if self.is_exit_valid(x+1, y-1) { exits.push(((idx-self.width)+1,
1.45)); }
if self.is_exit_valid(x-1, y+1) { exits.push(((idx+self.width)-1,
1.45)); }
if self.is_exit_valid(x+1, y+1) { exits.push(((idx+self.width)+1,
1.45)); }

exits
}

We also modify the player.rs input code:

101 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {


// Player movement
match ctx.key {
None => { return RunState::Paused } // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left |
VirtualKeyCode::Numpad4 |
VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),

VirtualKeyCode::Right |
VirtualKeyCode::Numpad6 |
VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),

VirtualKeyCode::Up |
VirtualKeyCode::Numpad8 |
VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs),

VirtualKeyCode::Down |
VirtualKeyCode::Numpad2 |
VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),

// Diagonals
VirtualKeyCode::Numpad9 |
VirtualKeyCode::Y => try_move_player(1, -1, &mut gs.ecs),

VirtualKeyCode::Numpad7 |
VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs),

VirtualKeyCode::Numpad3 |
VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs),

VirtualKeyCode::Numpad1 |
VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs),

_ => { return RunState::Paused }


},
}
RunState::Running
}

You can now diagonally dodge around monsters - and they can move/attack
diagonally.

102 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Giving monsters and the player some


combat stats
You probably guessed by now that the way to add stats to entities is with another
component! In components.rs , we add CombatStats . Here's a simple de�nition:

#[derive(Component, Debug)]
pub struct CombatStats {
pub max_hp : i32,
pub hp : i32,
pub defense : i32,
pub power : i32
}

As usual, don't forget to register it in main.rs !

We'll give the Player 30 hit points, 2 defense, and 5 power:

.with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })

Likewise, we'll give the monsters a weaker set of stats (we'll worry about monster
di�erentiation later):

.with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })

Indexing what is where


When traveling the map - as a player or a monster - it's really handy to know what is in
a tile. You can combine it with the visibility system to make intelligent choices with
what can be seen, you can use it to see if you are trying to walk into an enemy's space
(and attack them), and so on. One way to do it would be to iterate the Position
components and see if we hit anything; for low numbers of entities that would be
plenty fast. We'll take a di�erent approach, and make the map_indexing_system help
us. We'll start by adding a �eld to the map:

103 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Default)]
pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>,
pub tile_content : Vec<Vec<Entity>>
}

And we'll add a basic initializer to the new map code:

tile_content : vec![Vec::new(); 80*50]

Then we'll upgrade the indexing system to index all entities by tile:

104 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Map, Position, BlocksTile};

pub struct MapIndexingSystem {}

impl<'a> System<'a> for MapIndexingSystem {


type SystemData = ( WriteExpect<'a, Map>,
ReadStorage<'a, Position>,
ReadStorage<'a, BlocksTile>,
Entities<'a>,);

fn run(&mut self, data : Self::SystemData) {


let (mut map, position, blockers, entities) = data;

map.populate_blocked();
map.clear_content_index();
for (entity, position) in (&entities, &position).join() {
let idx = map.xy_idx(position.x, position.y);

// If they block, update the blocking list


let _p : Option<&BlocksTile> = blockers.get(entity);
if let Some(_p) = _p {
map.blocked[idx] = true;
}

// Push the entity to the appropriate index slot. It's a Copy


// type, so we don't need to clone it (we want to avoid moving
it out of the ECS!)
map.tile_content[idx].push(entity);
}
}
}

Letting the player hit things


Most roguelike characters spend a lot of time hitting things, so let's implement that!
Bump to attack (walking into the target) is the canonical way to do this. We want to
expand try_move_player in player.rs to check to see if a tile we are trying to enter
contains a target.

We'll add a reader for CombatStats to the list of data-stores, and put in a quick

105 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

enemy detector:

let combat_stats = ecs.read_storage::<CombatStats>();


let map = ecs.fetch::<Map>();

for (_player, pos, viewshed) in (&mut players, &mut positions, &mut


viewsheds).join() {
let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);

for potential_target in map.tile_content[destination_idx].iter() {


let target = combat_stats.get(*potential_target);
match target {
None => {}
Some(t) => {
// Attack it
console::log(&format!("From Hell's Heart, I stab thee!"));
return; // So we don't move after attacking
}
}
}

If you cargo run this, you'll see that you can walk up to a mob and try to move onto
it. From Hell's Heart, I stab thee! appears on the console. So the detection works, and
the attack is in the right place.

Player attacking and killing things


We're going to do this in an ECS way, so there's a bit of boilerplate. In components.rs ,
we make a couple of new components:

#[derive(Component, Debug)]
pub struct WantsToMelee {
pub target : Entity
}

#[derive(Component, Debug)]
pub struct SufferDamage {
pub amount : i32
}

106 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

(Don't forget to register them in main.rs !). We modify the player's movement
command to create a component for the player when he/she/it wants to attack
someone:

let mut wants_to_melee = ecs.write_storage::<WantsToMelee>();

...

for potential_target in map.tile_content[destination_idx].iter() {


let target = combat_stats.get(*potential_target);
if let Some(_target) = target {
wants_to_melee.insert(entity, WantsToMelee{ target:
*potential_target }).expect("Add target failed");
return;
}
}

We'll need a melee_combat_system to handle Melee:

107 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{CombatStats, WantsToMelee, Name, SufferDamage};

pub struct MeleeCombatSystem {}

impl<'a> System<'a> for MeleeCombatSystem {


type SystemData = ( Entities<'a>,
WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>
);

fn run(&mut self, data : Self::SystemData) {


let (entities, mut wants_melee, names, combat_stats, mut
inflict_damage) = data;

for (_entity, wants_melee, name, stats) in (&entities,


&wants_melee, &names, &combat_stats).join() {
if stats.hp > 0 {
let target_stats =
combat_stats.get(wants_melee.target).unwrap();
if target_stats.hp > 0 {
let target_name =
names.get(wants_melee.target).unwrap();

let damage = i32::max(0, stats.power -


target_stats.defense);

if damage == 0 {
console::log(&format!("{} is unable to hurt {}",
&name.name, &target_name.name));
} else {
console::log(&format!("{} hits {}, for {} hp.",
&name.name, &target_name.name, damage));
inflict_damage.insert(wants_melee.target,
SufferDamage{ amount: damage }).expect("Unable to do damage");
}
}
}
}

wants_melee.clear();
}

108 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

And we'll need a damage_system to apply the damage (we're separating it out,
because damage could come from any number of sources!):

extern crate specs;


use specs::prelude::*;
use super::{CombatStats, SufferDamage};

pub struct DamageSystem {}

impl<'a> System<'a> for DamageSystem {


type SystemData = ( WriteStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage> );

fn run(&mut self, data : Self::SystemData) {


let (mut stats, mut damage) = data;

for (mut stats, damage) in (&mut stats, &damage).join() {


stats.hp -= damage.amount;
}

damage.clear();
}
}

We'll also add a method to clean up dead entities:

pub fn delete_the_dead(ecs : &mut World) {


let mut dead : Vec<Entity> = Vec::new();
// Using a scope to make the borrow checker happy
{
let combat_stats = ecs.read_storage::<CombatStats>();
let entities = ecs.entities();
for (entity, stats) in (&entities, &combat_stats).join() {
if stats.hp < 1 { dead.push(entity); }
}
}

for victim in dead {


ecs.delete_entity(victim).expect("Unable to delete");
}
}

109 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This is called from our tick command, after the systems run:
damage_system::delete_the_dead(&mut self.ecs); .

If you cargo run now, you can run around the map hitting things - and they vanish
when dead!

Letting the monsters hit you back


Since we've already written systems to handle attacking and damaging, it's relatively
easy to use the same code with monsters - just add a WantsToMelee component and
they can attack/kill the player.

We'll start o� by making the player entity into a game resource, so it can be easily
referenced. Like the player's position, it's something that we're likely to need all over
the place - and since entity IDs are stable, we can rely on it existing. In main.rs , we
change the create_entity for the player to return the entity object:

let player_entity = gs.ecs


.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
.with(Name{name: "Player".to_string() })
.with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
.build();

We then insert it into the world:

gs.ecs.insert(player_entity);

Now we modify the monster_ai_system . There's a bit of clean-up here, and the "hurl
insults" code is completely replaced with a single component insert:

110 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Monster, Map, Position, WantsToMelee};
extern crate rltk;
use rltk::{Point};

pub struct MonsterAI {}

impl<'a> System<'a> for MonsterAI {


#[allow(clippy::type_complexity)]
type SystemData = ( WriteExpect<'a, Map>,
ReadExpect<'a, Point>,
ReadExpect<'a, Entity>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Monster>,
WriteStorage<'a, Position>,
WriteStorage<'a, WantsToMelee>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, player_pos, player_entity, entities, mut viewshed,
monster, mut position, mut wants_to_melee) = data;

for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut


viewshed, &monster, &mut position).join() {
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y),
*player_pos);
if distance < 1.5 {
wants_to_melee.insert(entity, WantsToMelee{ target:
*player_entity }).expect("Unable to insert attack");
}
else if viewshed.visible_tiles.contains(&*player_pos) {
// Path to the player
let path = rltk::a_star_search(
map.xy_idx(pos.x, pos.y) as i32,
map.xy_idx(player_pos.x, player_pos.y) as i32,
&mut *map
);
if path.success && path.steps.len()>1 {
let mut idx = map.xy_idx(pos.x, pos.y);
map.blocked[idx] = false;
pos.x = path.steps[1] % map.width;
pos.y = path.steps[1] / map.width;
idx = map.xy_idx(pos.x, pos.y);

111 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map.blocked[idx] = true;
viewshed.dirty = true;
}
}
}
}
}

If you cargo run now, you can kill monsters - and they can attack you. If a monster
kills you - the game crashes! It crashes, because delete_the_dead has deleted the
player. That's obviously not what we intended. Here's a non-crashing version of
delete_the_dead :

pub fn delete_the_dead(ecs : &mut World) {


let mut dead : Vec<Entity> = Vec::new();
// Using a scope to make the borrow checker happy
{
let combat_stats = ecs.read_storage::<CombatStats>();
let players = ecs.read_storage::<Player>();
let entities = ecs.entities();
for (entity, stats) in (&entities, &combat_stats).join() {
if stats.hp < 1 {
let player = players.get(entity);
match player {
None => dead.push(entity),
Some(_) => console::log("You are dead")
}
}
}
}

for victim in dead {


ecs.delete_entity(victim).expect("Unable to delete");
}
}

We'll worry about ending the game in a later chapter.

Expanding the turn system


If you look closely, you'll see that enemies can �ght back even after they have taken
fatal damage. While that �ts with some Shakespearean dramas (they really should

112 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

give a speech), it's not the kind of tactical play that roguelikes encourage. The problem
is that our game state is just Running and Paused - and we aren't even running the
systems when the player acts. Additionally, systems don't know what phase we are in
- so they can't take that into account.

Let's replace RunState with something more descriptive of each phase:

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn }

If you're running Visual Studio Code with RLS, half your project just turned red. That's
ok, we'll refactor one step at a time. We're going to remove the RunState altogether
from the main GameState :

pub struct State {


pub ecs: World
}

This makes even more red appear! We're doing this, because we're going to make the
RunState into a resource. So in main.rs where we insert other resources, we add:

gs.ecs.insert(RunState::PreRun);

Now to start refactoring Tick . Our new tick function looks like this:

113 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn tick(&mut self, ctx : &mut Rltk) {


ctx.cls();
let mut newrunstate;
{
let runstate = self.ecs.fetch::<RunState>();
newrunstate = *runstate;
}

match newrunstate {
RunState::PreRun => {
self.run_systems();
newrunstate = RunState::AwaitingInput;
}
RunState::AwaitingInput => {
newrunstate = player_input(self, ctx);
}
RunState::PlayerTurn => {
self.run_systems();
newrunstate = RunState::MonsterTurn;
}
RunState::MonsterTurn => {
self.run_systems();
newrunstate = RunState::AwaitingInput;
}
}

{
let mut runwriter = self.ecs.write_resource::<RunState>();
*runwriter = newrunstate;
}
damage_system::delete_the_dead(&mut self.ecs);

draw_map(&self.ecs, ctx);

let positions = self.ecs.read_storage::<Position>();


let renderables = self.ecs.read_storage::<Renderable>();
let map = self.ecs.fetch::<Map>();

for (pos, render) in (&positions, &renderables).join() {


let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg,
render.bg, render.glyph) }
}
}
}

114 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Notice how we now have a state machine going, with a "pre-run" phase for starting
the game! It's much cleaner, and quite obvious what's going on. There's a bit of scope
magic in use to keep the borrow-checker happy: if you declare and use a variable
inside a scope, it is dropped on scope exit (you can also manually drop things, but I
think this is cleaner looking).

In player.rs we simply replace all Paused with AwaitingInput , and Running with
PlayerTurn .

Lastly, we modify monster_ai_system to only run if the state is MonsterTurn


(snippet):

impl<'a> System<'a> for MonsterAI {


#[allow(clippy::type_complexity)]
type SystemData = ( WriteExpect<'a, Map>,
ReadExpect<'a, Point>,
ReadExpect<'a, Entity>,
ReadExpect<'a, RunState>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Monster>,
WriteStorage<'a, Position>,
WriteStorage<'a, WantsToMelee>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, player_pos, player_entity, runstate, entities, mut
viewshed, monster, mut position, mut wants_to_melee) = data;

if *runstate != RunState::MonsterTurn { return; }

If you cargo run the project, it now behaves as you'd expect: the player moves, and
things he/she kills die before they can respond.

Wrapping Up
That was quite the chapter! We added in location indexing, damage, and killing things.
The good news is that this is the hardest part; you now have a simple dungeon bash
game! It's not particularly fun, and you will die (since there's no healing at all) - but the
basics are there.

115 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

User Interface

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In this chapter, we'll add a user interface to the game.

Shrinking the map


We'll start o� by going to map.rs , and adding some constants: MAPWIDTH , MAPHEIGHT
and MAPCOUNT :

const MAPWIDTH : usize = 80;


const MAPHEIGHT : usize = 50;
const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;

Then we'll go through and change every reference to 80*50 to MAPCOUNT , and
references to the map size to use the constants. When this is done and running, we'll
change the MAPHEIGHT to 43 - to give us room at the bottom of the screen for a user
interface panel.

Some minimal GUI elements

116 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll create a new �le, gui.rs to hold our code. We'll go with a really minimal start:

extern crate rltk;


use rltk::{ RGB, Rltk, Console };
extern crate specs;
use specs::prelude::*;

pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {


ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));
}

We add a mod gui to the import block at the top of main.rs , and call it at the end of
tick :

gui::draw_ui(&self.ecs, ctx);

If we cargo run now, we'll see that the map has shrunk - and we have a white box in
place for the panel.

117 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding a health bar


It would help the player out to know how much health they have left. Fortunately,
RLTK provides a convenient helper for this. We'll need to obtain the player's health
from the ECS, and render it. This is pretty easy, and you should be comfortable with it
by now. The code looks like this:

extern crate rltk;


use rltk::{ RGB, Rltk, Console };
extern crate specs;
use specs::prelude::*;
use super::{CombatStats, Player};

pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {


ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));

let combat_stats = ecs.read_storage::<CombatStats>();


let players = ecs.read_storage::<Player>();
for (_player, stats) in (&players, &combat_stats).join() {
let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
ctx.print_color(12, 43, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), &health);

ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp,


RGB::named(rltk::RED), RGB::named(rltk::BLACK));
}
}

Adding a message log


The game log makes sense as a resource: it's available to any system that wants to tell
you something, and there's very little restriction as to what might want to tell you
something. We'll start by modelling the log itself. Make a new �le, gamelog.rs . We'll
start very simply:

118 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct GameLog {


pub entries : Vec<String>
}

In main.rs we add a mod gamelog; line, and insert it as a resource with


gs.ecs.insert(gamelog::GameLog{ entries : vec!["Welcome to Rusty
Roguelike".to_string()] });
. We're inserting a line into the log �le at the start, using the vec! macro for
constructing vectors. That gives us something to display - so we'll start writing the log
display code in gui.rs . In our GUI drawing function, we simply add:

let log = ecs.fetch::<GameLog>();

let mut y = 44;


for s in log.entries.iter() {
if y < 49 { ctx.print(2, y, &s.to_string()); }
y += 1;
}

If you cargo run the project now, you'll see something like this:

119 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Logging attacks
In our melee_combat_system , we add gamelog::GameLog to our imports from super ,
add a read/write accessor for the log ( WriteExpect<'a, GameLog>, ), and extend the
destructuring to include it:
let (entities, mut log, mut wants_melee, names, combat_stats, mut
inflict_damage) = data;
. Then it's just a matter of replacing the print! macros with inserting into the game
log. Here's the resultant code:

120 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{CombatStats, WantsToMelee, Name, SufferDamage,
gamelog::GameLog};

pub struct MeleeCombatSystem {}

impl<'a> System<'a> for MeleeCombatSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( Entities<'a>,
WriteExpect<'a, GameLog>,
WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>
);

fn run(&mut self, data : Self::SystemData) {


let (entities, mut log, mut wants_melee, names, combat_stats, mut
inflict_damage) = data;

for (_entity, wants_melee, name, stats) in (&entities,


&wants_melee, &names, &combat_stats).join() {
if stats.hp > 0 {
let target_stats =
combat_stats.get(wants_melee.target).unwrap();
if target_stats.hp > 0 {
let target_name =
names.get(wants_melee.target).unwrap();

let damage = i32::max(0, stats.power -


target_stats.defense);

if damage == 0 {
log.entries.insert(0, format!("{} is unable to
hurt {}", &name.name, &target_name.name));
} else {
log.entries.insert(0, format!("{} hits {}, for {}
hp.", &name.name, &target_name.name, damage));
inflict_damage.insert(wants_melee.target,
SufferDamage{ amount: damage }).expect("Unable to do damage");
}
}
}
}

121 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

wants_melee.clear();
}
}

Now if you run the game and play a bit ( cargo run , playing is up to you!), you'll see
combat messages in the log:

Notifying of deaths
We can do the same thing with delete_the_dead to notify of deaths. Here's the
�nished code:

122 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn delete_the_dead(ecs : &mut World) {


let mut dead : Vec<Entity> = Vec::new();
// Using a scope to make the borrow checker happy
{
let combat_stats = ecs.read_storage::<CombatStats>();
let players = ecs.read_storage::<Player>();
let names = ecs.read_storage::<Name>();
let entities = ecs.entities();
let mut log = ecs.write_resource::<GameLog>();
for (entity, stats) in (&entities, &combat_stats).join() {
if stats.hp < 1 {
let player = players.get(entity);
match player {
None => {
let victim_name = names.get(entity);
if let Some(victim_name) = victim_name {
log.entries.insert(0, format!("{} is dead",
&victim_name.name));
}
dead.push(entity)
}
Some(_) => console::log("You are dead")
}
}
}
}

for victim in dead {


ecs.delete_entity(victim).expect("Unable to delete");
}
}

Mouse Support and Tooltips


Let's start by looking at how we obtain mouse information from RLTK. It's really easy;
add the following at the bottom of your draw_ui function:

// Draw mouse cursor


let mouse_pos = ctx.mouse_pos();
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::MAGENTA));

123 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This sets the background of the cell at which the mouse is pointed to magenta. As you
can see, mouse information arrives from RLTK as part of the context.

Now we'll introduce a new function, draw_tooltips and call it at the end of draw_ui .
New new function looks like this:

124 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {


let map = ecs.fetch::<Map>();
let names = ecs.read_storage::<Name>();
let positions = ecs.read_storage::<Position>();

let mouse_pos = ctx.mouse_pos();


if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; }
let mut tooltip : Vec<String> = Vec::new();
for (name, position) in (&names, &positions).join() {
if position.x == mouse_pos.0 && position.y == mouse_pos.1 {
tooltip.push(name.name.to_string());
}
}

if !tooltip.is_empty() {
let mut width :i32 = 0;
for s in tooltip.iter() {
if width < s.len() as i32 { width = s.len() as i32; }
}
width += 3;

if mouse_pos.0 > 40 {
let arrow_pos = Point::new(mouse_pos.0 - 2, mouse_pos.1);
let left_x = mouse_pos.0 - width;
let mut y = mouse_pos.1;
for s in tooltip.iter() {
ctx.print_color(left_x, y, RGB::named(rltk::WHITE),
RGB::named(rltk::GREY), &s.to_string());
let padding = (width - s.len() as i32)-1;
for i in 0..padding {
ctx.print_color(arrow_pos.x - i, y,
RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
}
y += 1;
}
ctx.print_color(arrow_pos.x, arrow_pos.y,
RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"->".to_string());
} else {
let arrow_pos = Point::new(mouse_pos.0 + 1, mouse_pos.1);
let left_x = mouse_pos.0 +3;
let mut y = mouse_pos.1;
for s in tooltip.iter() {
ctx.print_color(left_x, y, RGB::named(rltk::WHITE),
RGB::named(rltk::GREY), &s.to_string());
let padding = (width - s.len() as i32)-1;

125 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for i in 0..padding {
ctx.print_color(left_x + s.len() as i32 + i, y,
RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
}
y += 1;
}
ctx.print_color(arrow_pos.x, arrow_pos.y,
RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"<-".to_string());
}
}
}

It starts by obtaining read access to the components we need for tooltips: names and
positions. It also gets read access to the map itself. Then we check that mouse cursor
is actually on the map, and bail out if it isn't - no point in trying to draw tooltips for
something that can never have any!

The remainder says "if we have any tooltips, look at the mouse position" - if its on the
left, we'll put the tooltip to the right, otherwise to the left.

If you cargo run your project now, it looks like this:

126 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Optional post-processing for that truly


retro feeling
Since we're on look and feel, lets consider enabling an RLTK feature: post-processing
to give scanlines and screen burn, for that truly retro feel. It's entirely up to you if you
want to use this! In main.rs , the initial setup simply replaced the �rst init
command with:

let mut context = Rltk::init_simple8x8(80, 50, "Hello Rust World",


"resources");
context.with_post_scanlines(true);

If you choose to do this, the game looks a bit like the classic Caves of Qud:

Wrap up
Now that we have a GUI, it's starting to look pretty good!

127 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Items and Inventory

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

So far, we have maps, monsters, and bashing things! No roguelike "murder hobo"
experience would be complete without items to pick up along the way. This chapter
will add some basic items to the game, along with User Interface elements required to
pick them up, use them and drop them.

Thinking about composing items


A major di�erence between object-oriented and entity-component systems is that
rather than thinking about something as being located on an inheritance tree, you
think about how it composes from components. Ideally, you already have some of the
components ready to use!

So... what makes up an item? Thinking about it, an item can be said to have the
following properties:

It has a Renderable - a way to draw it.


If its on the ground, awaiting pickup - it has a Position .
If its NOT on the ground - say in a backpack, it needs a way to indicate that it it is
stored. We'll start with InPack
It's an item , which implies that it can be picked up. So it'll need an Item

128 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

component of some sort.


If it can be used, it will need some way to indicate that it can be used - and what
to do with it.

Consistently random
Computers are actually really bad at random numbers. Computers are inherently
deterministic - so (without getting into cryptographic stu�) when you ask for a
"random" number, you are actually getting a "really hard to predict next number in a
sequence". The sequence is controlled by a seed - with the same seed, you always get
the same dice rolls!

Since we have an ever-increasing number of things that use randomness, lets go


ahead and make the RNG (Random Number Generator) a resource.

In main.rs , we add:

gs.ecs.insert(rltk::RandomNumberGenerator::new());

We can now access the RNG whenever we need it, without having to pass one around.
Since we're not creating a new one, we can start it with a seed (we'd use seeded
instead of new , and provide a seed). We'll worry about that later; for now, it's just
going to make our code cleaner!

Improved Spawning
One monster per room, always in the middle, makes for rather boring play. We also
need to support spawning items as well as monsters!

To that end, we're going to make a new �le spawner.rs :

129 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate rltk;


use rltk::{ RGB, RandomNumberGenerator };
extern crate specs;
use specs::prelude::*;
use super::{CombatStats, Player, Renderable, Name, Position, Viewshed,
Monster, BlocksTile};

/// Spawns the player and returns his/her entity object.


pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity
{
ecs
.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
})
.with(Player{})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.with(Name{name: "Player".to_string() })
.with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
.build()
}

/// Spawns a random monster at a given location


pub fn random_monster(ecs: &mut World, x: i32, y: i32) {
let roll :i32;
{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
roll = rng.roll_dice(1, 2);
}
match roll {
1 => { orc(ecs, x, y) }
_ => { goblin(ecs, x, y) }
}
}

fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y,


rltk::to_cp437('o'), "Orc"); }
fn goblin(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y,
rltk::to_cp437('g'), "Goblin"); }

fn monster<S : ToString>(ecs: &mut World, x: i32, y: i32, glyph : u8, name

130 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

: S) {
ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph,
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.with(Monster{})
.with(Name{ name : name.to_string() })
.with(BlocksTile{})
.with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
.build();
}

As you can see, we've taken the existing code in main.rs - and wrapped it up in
functions in a di�erent module. We don't have to do this - but it helps keep things tidy.
Since we're going to be expanding our spawning, it's nice to keep things separated
out. Now we modify main.rs to use it:

let player_entity = spawner::player(&mut gs.ecs, player_x, player_y);

gs.ecs.insert(rltk::RandomNumberGenerator::new());
for room in map.rooms.iter().skip(1) {
let (x,y) = room.center();
spawner::random_monster(&mut gs.ecs, x, y);
}

That's de�nitely tidier! cargo run will give you exactly what we had at the end of the
previous chapter.

Spawn All The Things


We're going to extend the function to spawn multiple monsters per room, with 0
being an option. First we change the Map constants which we introduced in the
previous chapter to be public in order to use them in spawner.rs :

131 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub const MAPWIDTH : usize = 80;


pub const MAPHEIGHT : usize = 50;
pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;

In spawner.rs , we create a new function - spawn_room :

/// Fills a room with stuff!


pub fn spawn_room(ecs: &mut World, room : &Rect) {
let mut monster_spawn_points : Vec<usize> = Vec::new();

// Scope to keep the borrow checker happy


{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;

for _i in 0 .. num_monsters {
let mut added = false;
while !added {
let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 -
room.x1))) as usize;
let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 -
room.y1))) as usize;
let idx = (y * MAPWIDTH) + x;
if !monster_spawn_points.contains(&idx) {
monster_spawn_points.push(idx);
added = true;
}
}
}
}

// Actually spawn the monsters


for idx in monster_spawn_points.iter() {
let x = *idx % MAPWIDTH;
let y = *idx / MAPWIDTH;
random_monster(ecs, x as i32, y as i32);
}
}

This obtains the RNG and the map, and rolls a dice for how many monsters it should
spawn. It then keeps trying to add random positions that aren't already occupied,
until su�cient monsters have been created. Each monster is then spawned at the
determined location. The borrow checker isn't at all happy with the idea that we

132 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

mutably access rng , and then pass the ECS itself along: so we introduce a scope to
keep it happy (automatically dropping access to the RNG when we are done with it).

In main.rs , we then replace our monster spawner with:

for room in map.rooms.iter().skip(1) {


spawner::spawn_room(&mut gs.ecs, room);
}

If you cargo run the project now, it will have between 0 and 4 monsters per room. It
can get a little hairy!

Health Potion Entities


We'll improve the chances of surviving for a bit by adding health potions to the game!
We'll start o� by adding some components to help de�ne a potion. In components.rs :

133 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Component, Debug)]
pub struct Item {}

#[derive(Component, Debug)]
pub struct Potion {
pub heal_amount : i32
}

We of course need to register these in main.rs :

gs.ecs.register::<Item>();
gs.ecs.register::<Potion>();

In spawner.rs , we'll add a new function: health_potion :

fn health_potion(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('¡'),
fg: RGB::named(rltk::MAGENTA),
bg: RGB::named(rltk::BLACK),
})
.with(Name{ name : "Health Potion".to_string() })
.with(Item{})
.with(Potion{ heal_amount: 8 })
.build();
}

This is pretty straight-forward: we create an entity with a position, a renderable (we


picked ¡ because it looks a bit like a potion, and my favorite game Dwarf Fortress
uses it), a name, an Item component and a Potion component that speci�es it heals
8 points of damage.

Now we can modify the spawner code to also have a chance to spawn between 0 and
2 items:

134 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn spawn_room(ecs: &mut World, room : &Rect) {


let mut monster_spawn_points : Vec<usize> = Vec::new();
let mut item_spawn_points : Vec<usize> = Vec::new();

// Scope to keep the borrow checker happy


{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;
let num_items = rng.roll_dice(1, MAX_ITEMS + 2) - 3;

for _i in 0 .. num_monsters {
let mut added = false;
while !added {
let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 -
room.x1))) as usize;
let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 -
room.y1))) as usize;
let idx = (y * MAPWIDTH) + x;
if !monster_spawn_points.contains(&idx) {
monster_spawn_points.push(idx);
added = true;
}
}
}

for _i in 0 .. num_items {
let mut added = false;
while !added {
let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 -
room.x1))) as usize;
let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 -
room.y1))) as usize;
let idx = (y * MAPWIDTH) + x;
if !item_spawn_points.contains(&idx) {
item_spawn_points.push(idx);
added = true;
}
}
}
}

// Actually spawn the monsters


for idx in monster_spawn_points.iter() {
let x = *idx % MAPWIDTH;
let y = *idx / MAPWIDTH;

135 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

random_monster(ecs, x as i32, y as i32);


}

// Actually spawn the potions


for idx in item_spawn_points.iter() {
let x = *idx % MAPWIDTH;
let y = *idx / MAPWIDTH;
health_potion(ecs, x as i32, y as i32);
}
}

If you cargo run the project now, rooms now sometimes contain health potions.
Tooltips and rendering "just work" - because they have the components required to
use them.

Picking Up Items
Having potions exist is a great start, but it would be helpful to be able to pick them up!
We'll create a new component in components.rs (and register it in main.rs !), to
represent an item being in someone's backpack:

136 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Component, Debug)]
pub struct InBackpack {
pub owner : Entity
}

We also want to make item collection generic - that is, any entity can pick up an item.
It would be pretty straightforward to just make it work for the player, but later on we
might decide that monsters can pick up loot (introducing a whole new tactical element
- bait!). So we'll also make a component indicating intent in components.rs (and
register it in main.rs ):

#[derive(Component, Debug)]
pub struct WantsToPickupItem {
pub collected_by : Entity,
pub item : Entity
}

Next, we'll put together a system to process WantsToPickupItem notices. We'll make a
new �le, inventory_system.rs :

137 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{WantsToPickupItem, Name, InBackpack, Position,
gamelog::GameLog};

pub struct ItemCollectionSystem {}

impl<'a> System<'a> for ItemCollectionSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
WriteStorage<'a, WantsToPickupItem>,
WriteStorage<'a, Position>,
ReadStorage<'a, Name>,
WriteStorage<'a, InBackpack>
);

fn run(&mut self, data : Self::SystemData) {


let (player_entity, mut gamelog, mut wants_pickup, mut positions,
names, mut backpack) = data;

for pickup in wants_pickup.join() {


positions.remove(pickup.item);
backpack.insert(pickup.item, InBackpack{ owner:
pickup.collected_by }).expect("Unable to insert backpack entry");

if pickup.collected_by == *player_entity {
gamelog.entries.insert(0, format!("You pick up the {}.",
names.get(pickup.item).unwrap().name));
}
}

wants_pickup.clear();
}
}

This iterates the requests to pick up an item, removes their position component, and
adds an InBackpack component assigned to the collector. Don't forget to add it to
the systems list in main.rs :

let mut pickup = ItemCollectionSystem{};


pickup.run_now(&self.ecs);

138 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The next step is to add an input command to pick up an item. g is a popular key for
this, so we'll go with that (we can always change it!). In player.rs , in the ever-growing
match statement of inputs, we add:

VirtualKeyCode::G => get_item(&mut gs.ecs),

As you probably guessed, the next step is to implement get_item :

fn get_item(ecs: &mut World) {


let player_pos = ecs.fetch::<Point>();
let player_entity = ecs.fetch::<Entity>();
let entities = ecs.entities();
let items = ecs.read_storage::<Item>();
let positions = ecs.read_storage::<Position>();
let mut gamelog = ecs.fetch_mut::<GameLog>();

let mut target_item : Option<Entity> = None;


for (item_entity, _item, position) in (&entities, &items,
&positions).join() {
if position.x == player_pos.x && position.y == player_pos.y {
target_item = Some(item_entity);
}
}

match target_item {
None => gamelog.entries.insert(0, "There is nothing here to pick
up.".to_string()),
Some(item) => {
let mut pickup = ecs.write_storage::<WantsToPickupItem>();
pickup.insert(*player_entity, WantsToPickupItem{ collected_by:
*player_entity, item }).expect("Unable to insert want to pickup");
}
}
}

This obtains a bunch of references/accessors from the ECS, and iterates all items with
a position. If it matches the player's position, target_item is set. Then, if
target_item is none - we tell the player that there is nothing to pick up. If it isn't, it
adds a pickup request for the system we just added to use.

If you cargo run the project now, you can press g anywhere to be told that there's

139 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

nothing to get. If you are standing on a potion, it will vanish when you press g ! It's in
our backpack - but we haven't any way to know that other than the log entry.

Listing your inventory


It's a good idea to be able to see your inventory list! This will be a game mode - that is,
another state in which the game loop can �nd itself. So to start, we'll extend RunMode
in main.rs to include it:

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn,
ShowInventory }

The i key is a popular choice for inventory ( b is also popular!), so in player.rs we'll
add the following to the player input code:

VirtualKeyCode::I => return RunState::ShowInventory,

In our tick function in main.rs , we'll add another matching:

RunState::ShowInventory => {
if gui::show_inventory(self, ctx) == gui::ItemMenuResult::Cancel {
newrunstate = RunState::AwaitingInput;
}
}

That naturally leads to implementing show_inventory ! In gui.rs , we add:

140 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub enum ItemMenuResult { Cancel, NoResponse, Selected }

pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> ItemMenuResult


{
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<InBackpack>();

let inventory = (&backpack, &names).join().filter(|item| item.0.owner


== *player_entity );
let count = inventory.count();

let mut y = (25 - (count / 2)) as i32;


ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));
ctx.print_color(18, y-2, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Inventory");
ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "ESCAPE to cancel");

let mut j = 0;
for (_pack, name) in (&backpack, &names).join().filter(|item|
item.0.owner == *player_entity ) {
ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437('('));
ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK),
97+j as u8);
ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437(')'));

ctx.print(21, y, &name.name.to_string());
y += 1;
j += 1;
}

match ctx.key {
None => ItemMenuResult::NoResponse,
Some(key) => {
match key {
VirtualKeyCode::Escape => { ItemMenuResult::Cancel }
_ => ItemMenuResult::NoResponse
}
}
}

141 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This starts out by using the filter feature of Rust iterators to count all items in your
backpack. It then draws an appropriately sized box, and decorates it with a title and
instructions. Next, it iterates all matching items and renders them in a menu format.
Finally, it waits for keyboard input - and if you pressed ESCAPE , indicates that it is time
to close the menu.

If you cargo run your project now, you can see items that you have collected:

Using Items
Now that we can display our inventory, lets make selecting an item actually use it.
We'll extend the menu to return both an item entity and a result:

142 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) ->


(ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<InBackpack>();
let entities = gs.ecs.entities();

let inventory = (&backpack, &names).join().filter(|item| item.0.owner


== *player_entity );
let count = inventory.count();

let mut y = (25 - (count / 2)) as i32;


ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));
ctx.print_color(18, y-2, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Inventory");
ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "ESCAPE to cancel");

let mut equippable : Vec<Entity> = Vec::new();


let mut j = 0;
for (entity, _pack, name) in (&entities, &backpack,
&names).join().filter(|item| item.1.owner == *player_entity ) {
ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437('('));
ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK),
97+j as u8);
ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437(')'));

ctx.print(21, y, &name.name.to_string());
equippable.push(entity);
y += 1;
j += 1;
}

match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) => {
match key {
VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None)
}
_ => {
let selection = rltk::letter_to_option(key);
if selection > -1 && selection < count as i32 {

143 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

return (ItemMenuResult::Selected,
Some(equippable[selection as usize]));
}
(ItemMenuResult::NoResponse, None)
}
}
}
}
}

Our call to show_inventory in main.rs is now invalid, so we'll �x it up:

RunState::ShowInventory => {
let result = gui::show_inventory(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => newrunstate =
RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let names = self.ecs.read_storage::<Name>();
let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
gamelog.entries.insert(0, format!("You try to use {}, but it
isn't written yet", names.get(item_entity) .unwrap().name));
newrunstate = RunState::AwaitingInput;
}
}
}

If you try to use an item in your inventory now, you'll get a log entry that you try to use
it, but we haven't written that bit of code yet. That's a start!

Once again, we want generic code - so that eventually monsters might use potions.
We're going to cheat a little while all items are potions, and just make a potion system;
we'll turn it into something more useful later. So we'll start by creating an "intent"
component in components.rs (and registered in main.rs ):

#[derive(Component, Debug)]
pub struct WantsToDrinkPotion {
pub potion : Entity
}

144 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Add the following to inventory_system.rs :

pub struct PotionUseSystem {}

impl<'a> System<'a> for PotionUseSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
Entities<'a>,
WriteStorage<'a, WantsToDrinkPotion>,
ReadStorage<'a, Name>,
ReadStorage<'a, Potion>,
WriteStorage<'a, CombatStats>
);

fn run(&mut self, data : Self::SystemData) {


let (player_entity, mut gamelog, entities, mut wants_drink, names,
potions, mut combat_stats) = data;

for (entity, drink, stats) in (&entities, &wants_drink, &mut


combat_stats).join() {
let potion = potions.get(drink.potion);
match potion {
None => {}
Some(potion) => {
stats.hp = i32::max(stats.max_hp, stats.hp +
potion.heal_amount);
if entity == *player_entity {
gamelog.entries.insert(0, format!("You drink the
{}, healing {} hp.", names.get(drink.potion).unwrap().name,
potion.heal_amount));
}
entities.delete(drink.potion).expect("Delete failed");
}
}
}

wants_drink.clear();
}
}

And register it in the list of systems to run:

145 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut potions = PotionUseSystem{};


potions.run_now(&self.ecs);

Like other systems we've looked at, this iterates all of the WantsToDrinkPotion intent
objects. It then heals up the drinker by the amount set in the Potion component, and
deletes the potion. Since all of the placement information is attached to the potion
itself, there's no need to chase around making sure it is removed from the
appropriate backpack: the entity ceases to exist, and takes its components with it.

Testing this with cargo run gives a surprise: the potion isn't deleted after use! This is
because the ECS simply marks entities as dead - it doesn't delete them in systems (so
as to not mess up iterators and threading). So after every call to dispatch , we need
to add a call to maintain . In main.ecs :

RunState::PreRun => {
self.run_systems();
self.ecs.maintain();
newrunstate = RunState::AwaitingInput;
}

...

RunState::PlayerTurn => {
self.run_systems();
self.ecs.maintain();
newrunstate = RunState::MonsterTurn;
}
RunState::MonsterTurn => {
self.run_systems();
self.ecs.maintain();
newrunstate = RunState::AwaitingInput;
}

Finally we have to change the RunState::ShowInventory handling if an item was


selected, we create a WantsToDrinkPotion intent:

146 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::ShowInventory => {
let result = gui::show_inventory(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => newrunstate =
RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::
<WantsToDrinkPotion>();
intent.insert(*self.ecs.fetch::<Entity>(),
WantsToDrinkPotion{ potion: item_entity }).expect("Unable to insert
intent");
newrunstate = RunState::PlayerTurn;
}
}
}

NOW if you cargo run the project, you can pickup and drink health potions:

Dropping Items
147 of 856 2019-11-02, 3:59 p.m.
Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You probably want to be able to drop items from your inventory, especially later when
they can be used as bait. We'll follow a similar pattern for this section - create an
intent component, a menu to select it, and a system to perform the drop.

So we create a component (in components.rs ), and register it in main.rs :

#[derive(Component, Debug)]
pub struct WantsToDropItem {
pub item : Entity
}

We add another system to inventory_system.rs :

148 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct ItemDropSystem {}

impl<'a> System<'a> for ItemDropSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
Entities<'a>,
WriteStorage<'a, WantsToDropItem>,
ReadStorage<'a, Name>,
WriteStorage<'a, Position>,
WriteStorage<'a, InBackpack>
);

fn run(&mut self, data : Self::SystemData) {


let (player_entity, mut gamelog, entities, mut wants_drop, names,
mut positions, mut backpack) = data;

for (entity, to_drop) in (&entities, &wants_drop).join() {


let mut dropper_pos : Position = Position{x:0, y:0};
{
let dropped_pos = positions.get(entity).unwrap();
dropper_pos.x = dropped_pos.x;
dropper_pos.y = dropped_pos.y;
}
positions.insert(to_drop.item, Position{ x : dropper_pos.x, y
: dropper_pos.y }).expect("Unable to insert position");
backpack.remove(to_drop.item);

if entity == *player_entity {
gamelog.entries.insert(0, format!("You drop up the {}.",
names.get(to_drop.item).unwrap().name));
}
}

wants_drop.clear();
}
}

Register it in the dispatch builder in main.rs :

let mut drop_items = ItemDropSystem{};


drop_items.run_now(&self.ecs);

149 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll add a new RunState in main.rs :

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn,
ShowInventory, ShowDropItem }

Now in player.rs , we add d for drop to the list of commands:

VirtualKeyCode::D => return RunState::ShowDropItem,

In gui.rs , we need another menu - this time for dropping items:

150 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn drop_item_menu(gs : &mut State, ctx : &mut Rltk) ->


(ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<InBackpack>();
let entities = gs.ecs.entities();

let inventory = (&backpack, &names).join().filter(|item| item.0.owner


== *player_entity );
let count = inventory.count();

let mut y = (25 - (count / 2)) as i32;


ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));
ctx.print_color(18, y-2, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Drop Which Item?");
ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "ESCAPE to cancel");

let mut equippable : Vec<Entity> = Vec::new();


let mut j = 0;
for (entity, _pack, name) in (&entities, &backpack,
&names).join().filter(|item| item.1.owner == *player_entity ) {
ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437('('));
ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK),
97+j as u8);
ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437(')'));

ctx.print(21, y, &name.name.to_string());
equippable.push(entity);
y += 1;
j += 1;
}

match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) => {
match key {
VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None)
}
_ => {
let selection = rltk::letter_to_option(key);
if selection > -1 && selection < count as i32 {

151 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

return (ItemMenuResult::Selected,
Some(equippable[selection as usize]));
}
(ItemMenuResult::NoResponse, None)
}
}
}
}
}

We also need to extend the state handler in main.rs to use it:

RunState::ShowDropItem => {
let result = gui::drop_item_menu(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => newrunstate =
RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::<WantsToDropItem>();
intent.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem{
item: item_entity }).expect("Unable to insert intent");
newrunstate = RunState::PlayerTurn;
}
}
}

If you cargo run the project, you can now press d to drop items! Here's a shot of
rather unwisely dropping a potion while being mobbed:

152 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Render order
You've probably noticed by now that when you walk over a potion, it renders over the
top of you - removing the context for your player completely! We'll �x that by adding a
render_order �eld to Renderables :

#[derive(Component)]
pub struct Renderable {
pub glyph: u8,
pub fg: RGB,
pub bg: RGB,
pub render_order : i32
}

Your IDE is probably now highlighting lots of errors for Renderable components that
were created without this information. We'll add it to various places: the player is 0
(render �rst), monsters 1 (second) and items 2 (last). For example, in the Player
spawner, the Renderable now looks like this:

153 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
render_order: 0
})

To make this do something, we go to our item rendering code in main.rs and add a
sort to the iterators. We referenced the Book of Specs for how to do this! Basically, we
obtain the joined set of Position and Renderable components, and collect them
into a vector. We then sort that vector, and iterate it to render in the appropriate
order. In main.rs , replace the previous entity rendering code with:

let mut data = (&positions, &renderables).join().collect::<Vec<_>>();


data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
for (pos, render) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg,
render.bg, render.glyph) }
}

Wrap Up
This chapter has shown a fair amount of the power of using an ECS: picking up, using
and dropping entities is relatively simple - and once the player can do it, so can
anything else (if you add it to their AI). We've also shown how to order ECS fetches, to
maintain a sensible render order.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Ranged Scrolls and Targeting

154 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the last chapter, we added items and inventory - and a single item type, a health
potion. Now we'll add a second item type: a scroll of magic missile, that lets you zap an
entity at range.

Using components to describe what an


item does
In the last chapter, we pretty much wrote code to ensure that all items were healing
potions. That got things going, but isn't very �exible. So we'll start by breaking down
items into a few more component types. We'll start with a simple �ag component,
Consumable :

#[derive(Component, Debug)]
pub struct Consumable {}

Having this item indicates that using it destroys it (consumed on use). So we replace
the always-called entities.delete(useitem.item).expect("Delete failed"); in our
PotionUseSystem (which we rename ItemUseSystem !) with:

let consumable = consumables.get(useitem.item);


match consumable {
None => {}
Some(_) => {
entities.delete(useitem.item).expect("Delete failed");
}
}

This is quite simple: check if the component has a Consumable tag, and destroy it if it
does. Likewise, we can replace the Potion section with a ProvidesHealing to

155 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

indicate that this is what the potion actually does. In components.rs :

#[derive(Component, Debug)]
pub struct ProvidesHealing {
pub heal_amount : i32
}

And in our ItemUseSystem :

let item_heals = healing.get(useitem.item);


match item_heals {
None => {}
Some(healer) => {
stats.hp = i32::max(stats.max_hp, stats.hp + healer.heal_amount);
if entity == *player_entity {
gamelog.entries.insert(0, format!("You drink the {}, healing
{} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
}
}
}

Drawing that together, our code for creating a potion (in spawner.rs ) looks like this:

fn health_potion(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('¡'),
fg: RGB::named(rltk::MAGENTA),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Health Potion".to_string() })
.with(Item{})
.with(Consumable{})
.with(ProvidesHealing{ heal_amount: 8 })
.build();
}

So we're describing where it is, what it looks like, its name, denoting that it is an item,
consumed on use, and provides 8 points of healing. This is nice and descriptive - and

156 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

future items can mix/match. As we add components, the item system will become
more and more �exible.

Describing Ranged Magic Missile Scrolls


We'll want to add a few more components! In components.rs (and registered in
main.rs ):

#[derive(Component, Debug)]
pub struct Ranged {
pub range : i32
}

#[derive(Component, Debug)]
pub struct InflictsDamage {
pub damage : i32
}

This in turn lets us write a magic_missile_scroll function in spawner.rs , which


e�ectively describes the scroll:

fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437(')'),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Magic Missile Scroll".to_string() })
.with(Item{})
.with(Consumable{})
.with(Ranged{ range: 6 })
.with(InflictsDamage{ damage: 8 })
.build();
}

That neatly lays out the properties of what makes it tick: it has a position, an
appearance, a name, it's an item that is destroyed on use, it has a range of 6 tiles and

157 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

in�icts 8 points of damage. That's what I like about components: after a while, it
sounds more like you are describing a blueprint for a device than writing many lines
of code!

We'll go ahead and add them into the spawn list:

fn random_item(ecs: &mut World, x: i32, y: i32) {


let roll :i32;
{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
roll = rng.roll_dice(1, 2);
}
match roll {
1 => { health_potion(ecs, x, y) }
_ => { magic_missile_scroll(ecs, x, y) }
}
}

Replace the call to health_potion in the item spawning code with a call to
random_item .

If you run the program (with cargo run ) now, you'll �nd scrolls as well as potions
lying around. The components system already provides quite a bit of functionality:

You can see them rendered on the map (thanks to the Renderable and
Position )
You can pick them up and drop them (thank to Item )
You can list them in your inventory
You can call use on them, and they are destroyed: but nothing happens.

158 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Implementing ranged damage for items


We want magic missile to be targeted: you activate it, and then have to select a victim.
This will be another input mode, so we once again extend RunState in main.rs :

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn,
ShowInventory, ShowDropItem,
ShowTargeting { range : i32, item : Entity} }

We'll extend our handler for ShowInventory in main.rs to handle items that are
ranged and induce a mode switch:

159 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::ShowInventory => {
let result = gui::show_inventory(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => newrunstate =
RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let is_ranged = self.ecs.read_storage::<Ranged>();
let is_item_ranged = is_ranged.get(item_entity);
if let Some(is_item_ranged) = is_item_ranged {
newrunstate = RunState::ShowTargeting{ range:
is_item_ranged.range, item: item_entity };
} else {
let mut intent = self.ecs.write_storage::
<WantsToUseItem>();
intent.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem{
item: item_entity }).expect("Unable to insert intent");
newrunstate = RunState::PlayerTurn;
}
}
}
}

So now in main.rs , where we match the appropriate game mode, we can stub in:

RunState::ShowTargeting{range, item} => {


let target = gui::ranged_target(self, ctx, range, item);
}

That naturally leads to actually writing gui::ranged_target . This looks complicated,


but it's actually quite straightforward:

160 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) ->
(ItemMenuResult, Option<Point>) {
let player_entity = gs.ecs.fetch::<Entity>();
let player_pos = gs.ecs.fetch::<Point>();
let viewsheds = gs.ecs.read_storage::<Viewshed>();

ctx.print_color(5, 0, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Select Target:");

// Highlight available target cells


let mut available_cells = Vec::new();
let visible = viewsheds.get(*player_entity);
if let Some(visible) = visible {
// We have a viewshed
for idx in visible.visible_tiles.iter() {
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx);
if distance <= range as f32 {
ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE));
available_cells.push(idx);
}
}
} else {
return (ItemMenuResult::Cancel, None);
}

// Draw mouse cursor


let mouse_pos = ctx.mouse_pos();
let mut valid_target = false;
for idx in available_cells.iter() { if idx.x == mouse_pos.0 && idx.y
== mouse_pos.1 { valid_target = true; } }
if valid_target {
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN));
if ctx.left_click {
return (ItemMenuResult::Selected, Some(Point::new(mouse_pos.0,
mouse_pos.1)));
}
} else {
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED));
if ctx.left_click {
return (ItemMenuResult::Cancel, None);
}
}

(ItemMenuResult::NoResponse, None)

161 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

So we start by obtaining the player's location and viewshed, and iterating cells they
can see. We check the range of the cell versus the range of the item, and if it is in
range - we highlight the cell in blue. We also maintain a list of what cells are possible
to target. Then, we get the mouse position; if it is pointing at a valid target, we light it
up in cyan - otherwise we use red. If you click a valid cell, it returns targeting
information for where you are aiming - otherwise, it cancels.

Now we extend our ShowTargeting code to handle this:

RunState::ShowTargeting{range, item} => {


let result = gui::ranged_target(self, ctx, range);
match result.0 {
gui::ItemMenuResult::Cancel => newrunstate =
RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let mut intent = self.ecs.write_storage::<WantsToUseItem>();
intent.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem{
item, target: result.1 }).expect("Unable to insert intent");
newrunstate = RunState::PlayerTurn;
}
}
}

What's this target ? I added another �eld to WantsToUseItem in components.rs :

#[derive(Component, Debug)]
pub struct WantsToUseItem {
pub item : Entity,
pub target : Option<rltk::Point>
}

So now when you receive a WantsToUseItem , you can now that the user is the owning
entity, the item is the item �eld, and it is aimed at target - if there is one (targeting
doesn't make much sense for healing potions!).

So now we can add another condition to our ItemUseSystem :

162 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// If it inflicts damage, apply it to the target cell


let item_damages = inflict_damage.get(useitem.item);
match item_damages {
None => {}
Some(damage) => {
let target_point = useitem.target.unwrap();
let idx = map.xy_idx(target_point.x, target_point.y);
used_item = false;
for mob in map.tile_content[idx].iter() {
suffer_damage.insert(*mob, SufferDamage{ amount :
damage.damage }).expect("Unable to insert");
if entity == *player_entity {
let mob_name = names.get(*mob).unwrap();
let item_name = names.get(useitem.item).unwrap();
gamelog.entries.insert(0, format!("You use {} on {},
inflicting {} hp.", item_name.name, mob_name.name, damage.damage));
}

used_item = true;
}
}
}

This checks to see if we have an InflictsDamage component on the item - and if it


does, applies the damage to everyone in the targeted cell.

If you cargo run the game, you can now blast entities with your magic missile scrolls!

Introducing Area of E�ect


We'll add another scroll type - Fireball. It's an old favorite, and introduces AoE - Area of
E�ect - damage. We'll start by adding a component to indicate our intent:

#[derive(Component, Debug)]
pub struct AreaOfEffect {
pub radius : i32
}

We'll extend the random_item function in spawner.rs to o�er it as an option:

163 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn random_item(ecs: &mut World, x: i32, y: i32) {


let roll :i32;
{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
roll = rng.roll_dice(1, 3);
}
match roll {
1 => { health_potion(ecs, x, y) }
2 => { fireball_scroll(ecs, x, y) }
_ => { magic_missile_scroll(ecs, x, y) }
}
}

So now we can write a fireball_scroll function to actually spawn them. This is a lot
like the other items:

fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437(')'),
fg: RGB::named(rltk::ORANGE),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Fireball Scroll".to_string() })
.with(Item{})
.with(Consumable{})
.with(Ranged{ range: 6 })
.with(InflictsDamage{ damage: 20 })
.with(AreaOfEffect{ radius: 3 })
.build();
}

Notice that it's basically the same - but we're adding an AreaOfEffect component to
indicate that it is what we want. If you were to cargo run now, you'd see Fireball
scrolls in the game - and they would in�ict damage on a single entity. Clearly, we must
�x that!

In our UseItemSystem , we'll build a new section to �gure out a list of targets for an
e�ect:

164 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Targeting
let mut targets : Vec<Entity> = Vec::new();
match useitem.target {
None => { targets.push( *player_entity ); }
Some(target) => {
let area_effect = aoe.get(useitem.item);
match area_effect {
None => {
// Single target in tile
let idx = map.xy_idx(target.x, target.y);
for mob in map.tile_content[idx].iter() {
targets.push(*mob);
}
}
Some(area_effect) => {
// AoE
let blast_tiles = rltk::field_of_view(target,
area_effect.radius, &*map);
for tile_idx in blast_tiles.iter() {
let idx = map.xy_idx(tile_idx.x, tile_idx.y);
for mob in map.tile_content[idx].iter() {
targets.push(*mob);
}
}
}
}
}
}

This says "if there is no target, apply it to the player". If there is a target, check to see if
it is an Area of E�ect event; if it is - plot a viewshed from that point of the appropriate
radius, and add every entity in the target area. If it isn't, we just get the entities in the
target tile.

So now we need to make the e�ect code generic. We don't want to assume that
e�ects are independent; later on, we may decide that zapping something with a scroll
has all manner of e�ects! So for healing, it looks like this:

165 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// If it heals, apply the healing


let item_heals = healing.get(useitem.item);
match item_heals {
None => {}
Some(healer) => {
for target in targets.iter() {
let stats = combat_stats.get_mut(*target);
if let Some(stats) = stats {
stats.hp = i32::max(stats.max_hp, stats.hp +
healer.heal_amount);
if entity == *player_entity {
gamelog.entries.insert(0, format!("You use the {},
healing {} hp.", names.get(useitem.item).unwrap().name,
healer.heal_amount));
}
}
}
}
}

The damage code is actually simpli�ed, since we've already calculated targets:

// If it inflicts damage, apply it to the target cell


let item_damages = inflict_damage.get(useitem.item);
match item_damages {
None => {}
Some(damage) => {
used_item = false;
for mob in targets.iter() {
suffer_damage.insert(*mob, SufferDamage{ amount :
damage.damage }).expect("Unable to insert");
if entity == *player_entity {
let mob_name = names.get(*mob).unwrap();
let item_name = names.get(useitem.item).unwrap();
gamelog.entries.insert(0, format!("You use {} on {},
inflicting {} hp.", item_name.name, mob_name.name, damage.damage));
}

used_item = true;
}
}
}

166 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you cargo run the project now, you can use magic missile scrolls, �reball scrolls
and health potions.

Confusion Scrolls
Let's add another item - confusion scrolls. These will target a single entity at range,
and make them Confused for a few turns - during which time they will do nothing.
We'll start by describing what we want in the item spawning code:

fn confusion_scroll(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437(')'),
fg: RGB::named(rltk::PINK),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Confusion Scroll".to_string() })
.with(Item{})
.with(Consumable{})
.with(Ranged{ range: 6 })
.with(Confusion{ turns: 4 })
.build();
}

We'll also add it to the item choices:

fn random_item(ecs: &mut World, x: i32, y: i32) {


let roll :i32;
{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
roll = rng.roll_dice(1, 4);
}
match roll {
1 => { health_potion(ecs, x, y) }
2 => { fireball_scroll(ecs, x, y) }
3 => { confusion_scroll(ecs, x, y) }
_ => { magic_missile_scroll(ecs, x, y) }
}
}

167 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll add a new component (and register it!):

#[derive(Component, Debug)]
pub struct Confusion {
pub turns : i32
}

That's enough to have them appear, be triggerable and cause targeting to happen -
but nothing will happen when it is used. We'll add the ability to pass along confusion
to the ItemUseSystem :

// Can it pass along confusion? Note the use of scopes to escape from the
borrow checker!
let mut add_confusion = Vec::new();
{
let causes_confusion = confused.get(useitem.item);
match causes_confusion {
None => {}
Some(confusion) => {
used_item = false;
for mob in targets.iter() {
add_confusion.push((*mob, confusion.turns ));
if entity == *player_entity {
let mob_name = names.get(*mob).unwrap();
let item_name = names.get(useitem.item).unwrap();
gamelog.entries.insert(0, format!("You use {} on {},
confusing them.", item_name.name, mob_name.name));
}
}
}
}
}
for mob in add_confusion.iter() {
confused.insert(mob.0, Confusion{ turns: mob.1 }).expect("Unable to
insert status");
}

Alright! Now we can add the Confused status to anything. We should update the
monster_ai_system to use it. Replace the loop with:

168 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed,


&monster, &mut position).join() {
let mut can_act = true;

let is_confused = confused.get_mut(entity);


if let Some(i_am_confused) = is_confused {
i_am_confused.turns -= 1;
if i_am_confused.turns < 1 {
confused.remove(entity);
}
can_act = false;
}

if can_act {
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y),
*player_pos);
if distance < 1.5 {
wants_to_melee.insert(entity, WantsToMelee{ target:
*player_entity }).expect("Unable to insert attack");
}
else if viewshed.visible_tiles.contains(&*player_pos) {
// Path to the player
let path = rltk::a_star_search(
map.xy_idx(pos.x, pos.y) as i32,
map.xy_idx(player_pos.x, player_pos.y) as i32,
&mut *map
);
if path.success && path.steps.len()>1 {
let mut idx = map.xy_idx(pos.x, pos.y);
map.blocked[idx] = false;
pos.x = path.steps[1] % map.width;
pos.y = path.steps[1] / map.width;
idx = map.xy_idx(pos.x, pos.y);
map.blocked[idx] = true;
viewshed.dirty = true;
}
}
}
}

If this sees a Confused component, it decrements the timer. If the timer hits 0, it
removes it. It then returns, making the monster

The source code for this chapter may be found here

169 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Loading and Saving the Game

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the last few chapters, we've focused on getting a playable (if not massively fun)
game going. You can run around, slay monsters, and make use of various items.
That's a great start! Most games let you stop playing, and come back later to continue.
Fortunately, Rust (and associated libraries) makes it relatively easy.

A Main Menu
If you're going to resume a game, you need somewhere from which to do so! A main
menu also gives you the option to abandon your last save, possibly view credits, and
generally tell the world that your game is here - and written by you. It's an important
thing to have, so we'll put one together.

Being in the menu is a state - so we'll add it to the ever-expanding RunState enum.
We want to include menu state inside it, so the de�nition winds up looking like this:

170 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection }
}

In gui.rs , we add a couple of enum types to handle main menu selections:

#[derive(PartialEq, Copy, Clone)]


pub enum MainMenuSelection { NewGame, LoadGame, Quit }

#[derive(PartialEq, Copy, Clone)]


pub enum MainMenuResult { NoSelection{ selected : MainMenuSelection },
Selected{ selected: MainMenuSelection } }

Your GUI is probably now telling you that main.rs has errors! It's right - we need to
handle the new RunState option. We'll need to change things around a bit to ensure
that we aren't also rendering the GUI and map when in the menu. So we rearrange
tick :

171 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn tick(&mut self, ctx : &mut Rltk) {


let mut newrunstate;
{
let runstate = self.ecs.fetch::<RunState>();
newrunstate = *runstate;
}

ctx.cls();

match newrunstate {
RunState::MainMenu{..} => {}
_ => {
draw_map(&self.ecs, ctx);

{
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();
let map = self.ecs.fetch::<Map>();

let mut data = (&positions, &renderables).join().collect::


<Vec<_>>();
data.sort_by(|&a, &b|
b.1.render_order.cmp(&a.1.render_order) );
for (pos, render) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y,
render.fg, render.bg, render.glyph) }
}

gui::draw_ui(&self.ecs, ctx);
}
}
}
...

We'll also handle the MainMenu state in our large match for RunState :

172 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::MainMenu{ .. } => {
let result = gui::main_menu(self, ctx);
match result {
gui::MainMenuResult::NoSelection{ selected } => newrunstate =
RunState::MainMenu{ menu_selection: selected },
gui::MainMenuResult::Selected{ selected } => {
match selected {
gui::MainMenuSelection::NewGame => newrunstate =
RunState::PreRun,
gui::MainMenuSelection::LoadGame => newrunstate =
RunState::PreRun,
gui::MainMenuSelection::Quit => { ::std::process::exit(0);
}
}
}
}
}

We're basically updating the state with the new menu selection, and if something has
been selected we change the game state. For Quit , we simply terminate the process.
For now, we'll make loading/starting a game do the same thing: go into the PreRun
state to setup the game.

The last thing to do is to write the menu itself. In menu.rs :

173 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {


let runstate = gs.ecs.fetch::<RunState>();

ctx.print_color_centered(15, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");

if let RunState::MainMenu{ menu_selection : selection } = *runstate {


if selection == MainMenuSelection::NewGame {
ctx.print_color_centered(24, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Begin New Game");
} else {
ctx.print_color_centered(24, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Begin New Game");
}

if selection == MainMenuSelection::LoadGame {
ctx.print_color_centered(25, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Load Game");
} else {
ctx.print_color_centered(25, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Load Game");
}

if selection == MainMenuSelection::Quit {
ctx.print_color_centered(26, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Quit");
} else {
ctx.print_color_centered(26, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Quit");
}

match ctx.key {
None => return MainMenuResult::NoSelection{ selected:
selection },
Some(key) => {
match key {
VirtualKeyCode::Escape => { return
MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } }
VirtualKeyCode::Up => {
let newselection;
match selection {
MainMenuSelection::NewGame => newselection =
MainMenuSelection::Quit,
MainMenuSelection::LoadGame => newselection =
MainMenuSelection::NewGame,
MainMenuSelection::Quit => newselection =

174 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

MainMenuSelection::LoadGame
}
return MainMenuResult::NoSelection{ selected:
newselection }
}
VirtualKeyCode::Down => {
let newselection;
match selection {
MainMenuSelection::NewGame => newselection =
MainMenuSelection::LoadGame,
MainMenuSelection::LoadGame => newselection =
MainMenuSelection::Quit,
MainMenuSelection::Quit => newselection =
MainMenuSelection::NewGame
}
return MainMenuResult::NoSelection{ selected:
newselection }
}
VirtualKeyCode::Return => return
MainMenuResult::Selected{ selected : selection },
_ => return MainMenuResult::NoSelection{ selected:
selection }
}
}
}
}

MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }


}

That's a bit of a mouthful, but it displays menu options and lets you select them with
the up/down keys and enter. It's very careful to not modify state itself, to keep things
clear.

Including Serde
Serde is pretty much the gold-standard for serialization in Rust. It makes a lot of
things easier! So the �rst step is to include it. In your project's Cargo.toml �le, we'll
expand the dependencies section to include it:

175 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

[dependencies]
rltk = { git = "https://github.com/thebracket/rltk_rs", features =
["serialization"] }
specs = { version = "0.15.0", features = ["serde"] }
specs-derive = "0.4.0"
serde= { version = "1.0.93", features = ["derive"] }
serde_json = "1.0.39"

It may be worth calling cargo run now - it will take a while, downloading the new
dependencies (and all of their dependencies) and building them for you. It should
keep them around so you don't have to wait this long every time you build.

Adding a "SaveGame" state


We'll extend RunState once more to support game saving:

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection },
SaveGame
}

In tick , we'll add dummy code for now:

RunState::SaveGame => {
newrunstate = RunState::MainMenu{ menu_selection :
gui::MainMenuSelection::LoadGame };
}

In player.rs , we'll add another keyboard handler - escape:

176 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Save and Quit


VirtualKeyCode::Escape => return RunState::SaveGame,

If you cargo run now, you can start a game and press escape to quit to the menu.

Getting started with saving the game


Now that the sca�olding is in place, it's time to actually save something! Lets start
simple, to get a feel for Serde. In the tick function, we extend the save system to just
dump a JSON representation of the map to the console:

RunState::SaveGame => {
let data = serde_json::to_string(&*self.ecs.fetch::<Map>()).unwrap();
println!("{}", data);

newrunstate = RunState::MainMenu{ menu_selection :


gui::MainMenuSelection::LoadGame };
}

We'll also need to add an extern crate serde; to the top of main.rs .

This won't compile, because we need to tell Map to serialize itself! Fortunately, serde
provides some helpers to make this easy. At the top of map.rs , we add
use serde::{Serialize, Deserialize}; . We then decorate the map to derive
serialization and de-serialization code:

177 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Default, Serialize, Deserialize, Clone)]


pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>,

#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content : Vec<Vec<Entity>>
}

Note that we've decorated tile_content with directives to not serialize/de-serialize


it. This prevents us from needing to store the entities, and since this data is rebuilt
every frame - it doesn't matter. The game still won't compile; we need to add similar
decorators to TileType and Rect :

#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]


pub enum TileType {
Wall, Floor
}

#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]


pub struct Rect {
pub x1 : i32,
pub x2 : i32,
pub y1 : i32,
pub y2 : i32
}

Lastly, we should extend the game saving code to dump the map to the console:

let data = serde_json::to_string(&*self.ecs.fetch::<Map>()).unwrap();


println!("{}", data);

If you cargo run the project now, when you hit escape it will dump a huge blob of

178 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

JSON data to the console. That's the game map!

Saving entity state


Now that we've seen how useful serde is, we should start to use it for the game itself.
This is harder than one might expect, because of how specs handles Entity
structures: their ID # is purely synthetic, with no guaranty that you'll get the same one
next time! Also, you may not want to save everything - so specs introduces a concept
of markers to help with this. It winds up being a bit more of a mouthful than it really
needs to be, but gives a pretty powerful serialization system.

Introducing Markers
First of all, in main.rs we'll tell Rust that we'd like to make use of the marker
functionality:

use specs::saveload::{SimpleMarker, SimpleMarkerAllocator};

In components.rs , we'll add a marker type:

pub struct SerializeMe;

Back in main.rs , we'll add SerializeMe to the list of things that we register:

gs.ecs.register::<SimpleMarker<SerializeMe>>();

We'll also add an entry to the ECS resources, which gets used to determine the next
identity:

gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());

Finally, in spawners.rs we tell each entity builder to include the marker. Here's the

179 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

complete entry for the Player :

pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity
{
ecs
.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
render_order: 0
})
.with(Player{})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.with(Name{name: "Player".to_string() })
.with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
.marked::<SimpleMarker<SerializeMe>>()
.build()
}

The new line ( .marked::<SimpleMarker<SerializeMe>>() ) needs to be repeated for


all of our spawners in this �le. It's worth looking at the source for this chapter; to
avoid making a huge chapter full of source code, I've omitted the repeated details.

Serializing components that don't contain an Entity


It's pretty easy to serialize a type that doesn't have an Entity in it: mark it with
#[derive(Component, Serialize, Deserialize, Clone)] . So we go through all the
simple component types in components.rs ; for example, here's Position :

#[derive(Component, Serialize, Deserialize, Clone)]


pub struct Position {
pub x: i32,
pub y: i32,
}

180 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Serializing components that point to entities


Here is where it gets a little messy. There are no provided derive functions for
handling serialization of Entity , so we have to do it the hard way. The good news is
that we're not doing it very often. Here's a helper for InBackpack :

// InBackpack wrapper
#[derive(Serialize, Deserialize, Clone)]
pub struct InBackpackData<M>(M);

impl<M: Marker + Serialize> ConvertSaveload<M> for InBackpack


where
for<'de> M: Deserialize<'de>,
{
type Data = InBackpackData<M>;
type Error = NoError;

fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data,


Self::Error>
where
F: FnMut(Entity) -> Option<M>,
{
let marker = ids(self.owner).unwrap();
Ok(InBackpackData(marker))
}

fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self,


Self::Error>
where
F: FnMut(M) -> Option<Entity>,
{
let entity = ids(data.0).unwrap();
Ok(InBackpack{owner: entity})
}
}

So we start o� by making a "data" class for InBackpack , which simply stores the
entity at which it points. Then we implement convert_info and convert_from to
satisfy Specs' ConvertSaveLoad trait. In convert_into , we use the ids map to get a
saveable ID number for the item, and return an InBackpackData using this marker.
convert_from does the reverse: we get the ID, look up the ID, and return an
InBackpack method.

181 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

So that's not too bad. If you look at the source, we've done this for all of the types that
store Entity data - some of which have other data, or multiple Entity types.

Actually saving something


The code for loading and saving gets large, so we've moved it into
saveload_system.rs . Then include a mod saveload_system; in main.rs , and
replace the SaveGame state with:

RunState::SaveGame => {
saveload_system::save_game(&mut self.ecs);
newrunstate = RunState::MainMenu{ menu_selection :
gui::MainMenuSelection::LoadGame };
}

So... onto implementing save_game . Serde and Specs work decently together, but the
bridge is still pretty roughly de�ned. I kept running into problems like it failing to
compile if I had more than 16 component types! To get around this, I build a macro. I
recommend just copying the macro until you feel ready to learn Rust's (impressive)
macro system.

macro_rules! serialize_individually {
($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => {
$(
SerializeComponents::<NoError,
SimpleMarker<SerializeMe>>::serialize(
&( $ecs.read_storage::<$type>(), ),
&$data.0,
&$data.1,
&mut $ser,
)
.unwrap();
)*
};
}

The short version of what it does is that it takes your ECS as the �rst parameter, and a
tuple with your entity store and "markers" stores in it (you'll see this in a moment).

182 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Every parameter after that is a type - listing a type stored in your ECS. These are
repeating rules, so it issues one SerializeComponent::serialize call per type. It's not
as e�cient as doing them all at once, but it works - and doesn't fall over when you
exceed 16 types! The save_game function then looks like this:

pub fn save_game(ecs : &mut World) {


// Create helper
let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone();
let savehelper = ecs
.create_entity()
.with(SerializationHelper{ map : mapcopy })
.marked::<SimpleMarker<SerializeMe>>()
.build();

// Actually serialize
{
let data = ( ecs.entities(), ecs.read_storage::
<SimpleMarker<SerializeMe>>() );

let writer = File::create("./savegame.json").unwrap();


let mut serializer = serde_json::Serializer::new(writer);
serialize_individually!(ecs, serializer, data, Position,
Renderable, Player, Viewshed, Monster,
Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee,
Item, Consumable, Ranged, InflictsDamage,
AreaOfEffect, Confusion, ProvidesHealing, InBackpack,
WantsToPickupItem, WantsToUseItem,
WantsToDropItem, SerializationHelper
);
}

// Clean up
ecs.delete_entity(savehelper).expect("Crash on cleanup");
}

What's going on here, then?

1. We start by creating a new component type - SerializationHelper that stores


a copy of the map (see, we are using the map stu� from above!). It then creates
a new entity, and gives it the new component - with a copy of the map (the
clone command makes a deep copy). This is needed so we don't need to
serialize the map separately.
2. We enter a block to avoid borrow-checker issues.

183 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

3. We set data to be a tuple, containing the Entity store and ReadStorage for
SimpleMarker . These will be used by the save macro.
4. We open a File called savegame.json in the current directory.
5. We obtain a JSON serializer from Serde.
6. We call the serialize_individually macro with all of our types.
7. We delete the temporary helper entity we created.

If you cargo run and start a game, then save it - you'll �nd a savegame.json �le has
appeared - with your game state in it. Yay!

Restoring Game State


Now that we have the game data, it's time to load it!

Is there a saved game?


First, we need to know if there is a saved game to load. In saveload_system.rs , we
add the following function:

pub fn does_save_exist() -> bool {


Path::new("./savegame.json").exists()
}

Then in gui.rs , we extend the main_menu function to check for the existence of a �le
- and not o�er to load it if it isn't there:

184 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {


let save_exists = super::saveload_system::does_save_exist();
let runstate = gs.ecs.fetch::<RunState>();

ctx.print_color_centered(15, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");

if let RunState::MainMenu{ menu_selection : selection } = *runstate {


if selection == MainMenuSelection::NewGame {
ctx.print_color_centered(24, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Begin New Game");
} else {
ctx.print_color_centered(24, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Begin New Game");
}

if save_exists {
if selection == MainMenuSelection::LoadGame {
ctx.print_color_centered(25, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Load Game");
} else {
ctx.print_color_centered(25, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Load Game");
}
}

if selection == MainMenuSelection::Quit {
ctx.print_color_centered(26, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Quit");
} else {
ctx.print_color_centered(26, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Quit");
}

match ctx.key {
None => return MainMenuResult::NoSelection{ selected:
selection },
Some(key) => {
match key {
VirtualKeyCode::Escape => { return
MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } }
VirtualKeyCode::Up => {
let mut newselection;
match selection {
MainMenuSelection::NewGame => newselection =
MainMenuSelection::Quit,

185 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

MainMenuSelection::LoadGame => newselection =


MainMenuSelection::NewGame,
MainMenuSelection::Quit => newselection =
MainMenuSelection::LoadGame
}
if newselection == MainMenuSelection::LoadGame &&
!save_exists {
newselection = MainMenuSelection::NewGame;
}
return MainMenuResult::NoSelection{ selected:
newselection }
}
VirtualKeyCode::Down => {
let mut newselection;
match selection {
MainMenuSelection::NewGame => newselection =
MainMenuSelection::LoadGame,
MainMenuSelection::LoadGame => newselection =
MainMenuSelection::Quit,
MainMenuSelection::Quit => newselection =
MainMenuSelection::NewGame
}
if newselection == MainMenuSelection::LoadGame &&
!save_exists {
newselection = MainMenuSelection::Quit;
}
return MainMenuResult::NoSelection{ selected:
newselection }
}
VirtualKeyCode::Return => return
MainMenuResult::Selected{ selected : selection },
_ => return MainMenuResult::NoSelection{ selected:
selection }
}
}
}
}

MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }


}

Finally, we'll modify the calling code in main.rs to call game loading:

186 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::MainMenu{ .. } => {
let result = gui::main_menu(self, ctx);
match result {
gui::MainMenuResult::NoSelection{ selected } => newrunstate =
RunState::MainMenu{ menu_selection: selected },
gui::MainMenuResult::Selected{ selected } => {
match selected {
gui::MainMenuSelection::NewGame => newrunstate =
RunState::PreRun,
gui::MainMenuSelection::LoadGame => {
saveload_system::load_game(&mut self.ecs);
newrunstate = RunState::AwaitingInput;
}
gui::MainMenuSelection::Quit => { ::std::process::exit(0);
}
}
}
}
}

Actually loading the game


In saveload_system.rs , we're going to need another macro! This is pretty much the
same as the serialize_individually macro - but reverses the process, and includes
some slight changes:

macro_rules! deserialize_individually {
($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
$(
DeserializeComponents::<NoError, _>::deserialize(
&mut ( &mut $ecs.write_storage::<$type>(), ),
&mut $data.0, // entities
&mut $data.1, // marker
&mut $data.2, // allocater
&mut $de,
)
.unwrap();
)*
};
}

187 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This is called from a new function, load_game :

188 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn load_game(ecs: &mut World) {


{
// Delete everything
let mut to_delete = Vec::new();
for e in ecs.entities().join() {
to_delete.push(e);
}
for del in to_delete.iter() {
ecs.delete_entity(*del).expect("Deletion failed");
}
}

let data = fs::read_to_string("./savegame.json").unwrap();


let mut de = serde_json::Deserializer::from_str(&data);

{
let mut d = (&mut ecs.entities(), &mut ecs.write_storage::
<SimpleMarker<SerializeMe>>(), &mut SimpleMarkerAllocator::
<SerializeMe>::new());

deserialize_individually!(ecs, de, d, Position, Renderable,


Player, Viewshed, Monster,
Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee,
Item, Consumable, Ranged, InflictsDamage,
AreaOfEffect, Confusion, ProvidesHealing, InBackpack,
WantsToPickupItem, WantsToUseItem,
WantsToDropItem, SerializationHelper
);
}

let mut deleteme : Option<Entity> = None;


{
letentities = ecs.entities();
lethelper = ecs.read_storage::<SerializationHelper>();
letplayer = ecs.read_storage::<Player>();
letposition = ecs.read_storage::<Position>();
for(e,h) in (&entities, &helper).join() {
let mut worldmap = ecs.write_resource::<super::map::Map>();
*worldmap = h.map.clone();
worldmap.tile_content = vec![Vec::new();
super::map::MAPCOUNT];
deleteme = Some(e);
}
for (e,_p,pos) in (&entities, &player, &position).join() {
let mut ppos = ecs.write_resource::<rltk::Point>();

189 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

*ppos = rltk::Point::new(pos.x, pos.y);


let mut player_resource = ecs.write_resource::<Entity>();
*player_resource = e;
}
}
ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete
helper");
}

That's quite the mouthful, so lets step through it:

1. Inside a block (to keep the borrow checker happy), we iterate all entities in the
game. We add them to a vector, and then iterate the vector - deleting the
entities. This is a two-step process to avoid invalidating the iterator in the �rst
pass.
2. We open the savegame.json �le, and attach a JSON deserializer.
3. Then we build the tuple for the macro, which requires mutable access to the
entities store, write access to the marker store, and an allocator (from Specs).
4. Now we pass that to the macro we just made, which calls the de-serializer for
each type in turn. Since we saved in the same order, it will pick up everything.
5. Now we go into another block, to avoid borrow con�icts with the previous code
and the entity deletion.
6. We �rst iterate all entities with a SerializationHelper type. If we �nd it, we get
access to the resource storing the map - and replace it. Since we aren't
serializing tile_content , we replace it with an empty set of vectors.
7. Then we �nd the player, by iterating entities with a Player type and a Position
type. We store the world resources for the player entity and his/her position.
8. Finally, we delete the helper entity - so we won't have a duplicate if we save the
game again.

If you cargo run now, you can load your saved game!

Just add permadeath!


It wouldn't really be a roguelike if we let you keep your save game after you reload! So
we'll add one more function to saveload_system :

190 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn delete_save() {
if Path::new("./savegame.json").exists() {
std::fs::remove_file("./savegame.json").expect("Unable to delete file"); }
}

We'll add a call to main.rs to delete the save after we load the game:

gui::MainMenuSelection::LoadGame => {
saveload_system::load_game(&mut self.ecs);
newrunstate = RunState::AwaitingInput;
saveload_system::delete_save();
}

Web Assembly
The example as-is will compile and run on the web assembly ( wasm32 ) platform: but
as soon as you try to save the game, it crashes. Unfortunately (well, fortunately if you
like your computer not being attacked by every website you go to!), wasm is
sandboxed - and doesn't have the ability to save �les locally.

Supporting saving via LocalStorage (a browser/JavaScript feature) is planned for a


future version of RLTK. In the meantime, we'll add some wrappers to avoid the crash -
and simply not actually save the game on wasm32 .

Rust o�ers conditional compilation (if you are familiar with C, it's a lot like the #define
madness you �nd in big, cross-platform libraries). In saveload_system.rs , we'll
modify save_game to only compile on non-web assembly platforms:

#[cfg(not(target_arch = "wasm32"))]
pub fn save_game(ecs : &mut World) {

That # tag is scary looking, but it makes sense if you unwrap it. #[cfg()] means
"only compile if the current con�guration matches the contents of the parentheses.
not() inverts the result of a check, so when we check that
target_arch = "wasm32") (are we compiling for wasm32 ) the result is inverted. The

191 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

end result of this is that the function only compiles if you aren't building for wasm32 .

That's all well and good, but there are calls to that function - so compilation on wasm
will fail. We'll add a stub function to take its place:

#[cfg(target_arch = "wasm32")]
pub fn save_game(_ecs : &mut World) {
}

The #[cfg(target_arch = "wasm32")] pre�x means "only compile this for web
assembly". We've kept the function signature the same, but added a _ before _ecs -
telling the compiler that we intend not to use that variable. Then we keep the function
empty.

The result? You can compile for wasm32 and the save_game function simply doesn't
do anything at all. The rest of the structure remains, so the game correctly returns to
the main menu - but with no resume function.

(Why does the check that the �le exists work? Rust is smart enough to say "no
�lesystem, so the �le can't exist". Thanks, Rust!)

Wrap-up
This has been a long chapter, with quite heavy content. The great news is that we now
have a framework for loading and saving the game whenever we want to. Adding
components has gained some steps: we have to register them in main , tag them for
Serialize, Deserialize , and remember to add them to our component type lists in
saveload_system.rs . That could be easier - but it's a very solid foundation.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Delving Deeper

192 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

We have all the basics of a dungeon crawler now, but only having a single level is a big
limitation! This chapter will introduce depth, with a new dungeon being spawned on
each level down. We'll track the player's depth, and encourage ever-deeper
exploration. What could possibly go wrong for the player?

Indicating - and storing - depth


We'll start by adding the current depth to the map. In map.rs , we adjust the Map
structure to include an integer for depth:

#[derive(Default, Serialize, Deserialize, Clone)]


pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>,
pub depth : i32,

#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content : Vec<Vec<Entity>>
}

i32 is a primitive type, and automatically handled by Serde - the serialization library.
So adding it here automatically adds it to our game save/load mechanism. Our map
creation code also needs to indicate that we are on level 1 of the map. We want to be
able to use the map generator for additional levels, so we add in a parameter also.
The updated function looks like this:

193 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new_map_rooms_and_corridors(new_depth : i32) -> Map {


let mut map = Map{
tiles : vec![TileType::Wall; MAPCOUNT],
rooms : Vec::new(),
width : MAPWIDTH as i32,
height: MAPHEIGHT as i32,
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
tile_content : vec![Vec::new(); MAPCOUNT],
depth: new_depth
};
...

We call this from the setup code in main.rs , so we need to amend the call to the
dungeon builder also:

let map : Map = Map::new_map_rooms_and_corridors(1);

That's it! Our maps now know about depth. You'll want to delete any savegame.json
�les you have lying around, since we've changed the format - loading will fail.

Showing the player their map depth


We'll modify the player's heads-up-display to indicate the current map depth. In
gui.rs , inside the draw_ui function, we add the following:

let map = ecs.fetch::<Map>();


let depth = format!("Depth: {}", map.depth);
ctx.print_color(2, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK),
&depth);

If you cargo run the project now, you'll see that we are showing you your current
depth:

194 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding down stairs


In map.rs , we have an enumeration - TileType - that lists the available tile types. We
want to add a new one: down stairs. Modify the enumeration like this:

#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]


pub enum TileType {
Wall, Floor, DownStairs
}

We also want to be able to render the stairs. map.rs contains draw_map , and adding
a tile type is a relatively simple task:

195 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

match tile {
TileType::Floor => {
glyph = rltk::to_cp437('.');
fg = RGB::from_f32(0.0, 0.5, 0.5);
}
TileType::Wall => {
glyph = rltk::to_cp437('#');
fg = RGB::from_f32(0., 1.0, 0.);
}
TileType::DownStairs => {
glyph = rltk::to_cp437('>');
fg = RGB::from_f32(0., 1.0, 1.0);
}
}

Lastly, we should place the down stairs. We place the up stairs in the center of the �rst
room the map generates - so we'll place the stairs in the center of the last room!
Going back to new_map_rooms_and_corridors in map.rs , we modify it like this:

196 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new_map_rooms_and_corridors(new_depth : i32) -> Map {


let mut map = Map{
tiles : vec![TileType::Wall; MAPCOUNT],
rooms : Vec::new(),
width : MAPWIDTH as i32,
height: MAPHEIGHT as i32,
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
tile_content : vec![Vec::new(); MAPCOUNT],
depth: new_depth
};

const MAX_ROOMS : i32 = 30;


const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;

let mut rng = RandomNumberGenerator::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, map.width - w - 1) - 1;
let y = rng.roll_dice(1, map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in map.rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
map.apply_room_to_map(&new_room);

if !map.rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = map.rooms[map.rooms.len()-
1].center();
if rng.range(0,1) == 1 {
map.apply_horizontal_tunnel(prev_x, new_x, prev_y);
map.apply_vertical_tunnel(prev_y, new_y, new_x);
} else {
map.apply_vertical_tunnel(prev_y, new_y, prev_x);
map.apply_horizontal_tunnel(prev_x, new_x, new_y);
}
}

197 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map.rooms.push(new_room);
}
}

let stairs_position = map.rooms[map.rooms.len()-1].center();


let stairs_idx = map.xy_idx(stairs_position.0, stairs_position.1);
map.tiles[stairs_idx] = TileType::DownStairs;

map
}

If you cargo run the project now, and run around a bit - you can �nd a set of down
stairs! They don't do anything yet, but they are on the map.

Actually going down a level


In player.rs , we have a big match statement that handles user input. Lets bind
going to the next level to the period key (on US keyboards, that's > without the
shift). Add this to the match :

198 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Level changes
VirtualKeyCode::Period => {
if try_next_level(&mut gs.ecs) {
return RunState::NextLevel;
}
}

Of course, now we need to implement try_next_level :

pub fn try_next_level(ecs: &mut World) -> bool {


let player_pos = ecs.fetch::<Point>();
let map = ecs.fetch::<Map>();
let player_idx = map.xy_idx(player_pos.x, player_pos.y);
if map.tiles[player_idx] == TileType::DownStairs {
true
} else {
let mut gamelog = ecs.fetch_mut::<GameLog>();
gamelog.entries.insert(0, "There is no way down from
here.".to_string());
false
}
}

The eagle-eyed programmer will notice that we returned a new RunState -


NextLevel . Since that doesn't exist yet, we'll open main.rs and implement it:

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection },
SaveGame,
NextLevel
}

Your IDE is by now complaining that we haven't actually implemented the new
RunState ! So we go into our ever-growing state handler in main.rs and add:

199 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::NextLevel => {
self.goto_next_level();
newrunstate = RunState::PreRun;
}

We'll add a new impl section for State , so we can attach methods to it. We're �rst
going to create a helper method:

impl State {
fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> {
let entities = self.ecs.entities();
let player = self.ecs.read_storage::<Player>();
let backpack = self.ecs.read_storage::<InBackpack>();
let player_entity = self.ecs.fetch::<Entity>();

let mut to_delete : Vec<Entity> = Vec::new();


for entity in entities.join() {
let mut should_delete = true;

// Don't delete the player


let p = player.get(entity);
if let Some(_p) = p {
should_delete = false;
}

// Don't delete the player's equipment


let bp = backpack.get(entity);
if let Some(bp) = bp {
if bp.owner == *player_entity {
should_delete = false;
}
}

if should_delete {
to_delete.push(entity);
}
}

to_delete
}
}

When we go to the next level, we want to delete all the entities - except for the player

200 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

and whatever equipment the player has. This helper function queries the ECS to
obtain a list of entities for deletion. It's a bit long-winded, but relatively
straightforward: we make a vector, and then iterate all entities. If the entity is the
player, we mark it as should_delete=false . If it is in a backpack (having the
InBackpack component), we check to see if the owner is the player - and if it is, we
don't delete it.

Armed with that, we go to create the goto_next_level function, also inside the
State implementation:

201 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn goto_next_level(&mut self) {
// Delete entities that aren't the player or his/her equipment
let to_delete = self.entities_to_remove_on_level_change();
for target in to_delete {
self.ecs.delete_entity(target).expect("Unable to delete entity");
}

// Build a new map and place the player


let worldmap;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
let current_depth = worldmap_resource.depth;
*worldmap_resource =
Map::new_map_rooms_and_corridors(current_depth + 1);
worldmap = worldmap_resource.clone();
}

// Spawn bad guys


for room in worldmap.rooms.iter().skip(1) {
spawner::spawn_room(&mut self.ecs, room);
}

// Place the player and update resources


let (player_x, player_y) = worldmap.rooms[0].center();
let mut player_position = self.ecs.write_resource::<Point>();
*player_position = Point::new(player_x, player_y);
let mut position_components = self.ecs.write_storage::<Position>();
let player_entity = self.ecs.fetch::<Entity>();
let player_pos_comp = position_components.get_mut(*player_entity);
if let Some(player_pos_comp) = player_pos_comp {
player_pos_comp.x = player_x;
player_pos_comp.y = player_y;
}

// Mark the player's visibility as dirty


let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
let vs = viewshed_components.get_mut(*player_entity);
if let Some(vs) = vs {
vs.dirty = true;
}

// Notify the player and give them some health


let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
gamelog.entries.insert(0, "You descend to the next level, and take a
moment to heal.".to_string());

202 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut player_health_store = self.ecs.write_storage::<CombatStats>();


let player_health = player_health_store.get_mut(*player_entity);
if let Some(player_health) = player_health {
player_health.hp = i32::max(player_health.hp, player_health.max_hp
/ 2);
}
}

This is a long function, but does everything we need. Lets break it down step-by-step:

1. We use the helper function we just wrote to obtain a list of entities to delete, and
ask the ECS to dispose of them.
2. We create a worldmap variable, and enter a new scope. Otherwise, we get issues
with immutable vs. mutable borrowing of the ECS.
3. In this scope, we obtain a writable reference to the resource for the current Map
. We get the current level, and replace the map with a new one - with
current_depth + 1 as the new depth. We then store a clone of this in the outer
variable and exit the scope (avoiding any borrowing/lifetime issues).
4. Now we use the same code we used in the initial setup to spawn bad guys and
items in each room.
5. Now we obtain the location of the �rst room, and update our resources for the
player to set his/her location to the center of it. We also grab the player's
Position component and update it.
6. We obtain the player's Viewshed component, since it will be out of date now
that the entire map has changed around him/her! We mark it as dirty - and will
let the various systems take care of the rest.
7. We give the player a log entry that they have descended to the next level.
8. We obtain the player's health component, and if their health is less than 50% -
boost it to half.

If you cargo run the project now, you can run around and descend levels. Your
depth indicator goes up - telling you that you are doing something right!

203 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrapping Up
This chapter was a bit easier than the last couple! You can now descend through an
e�ectively in�nite (it's really bounded by the size of a 32-bit integer, but good luck
getting through that many levels) dungeon. We've seen how the ECS can help, and
how our serialization work readily expands to include new features like this one as we
add to the project.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Di�culty

204 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Currently, you can advance through multiple dungeon levels - but they all have the
same spawns. There's no ramp-up of di�culty as you advance, and no easy-mode to
get you through the beginning. This chapter aims to change that.

Adding a wait key


An important tactical element of most roguelikes is the ability to skip a turn - let the
monsters come to you (and not get the �rst hit!). As part of turning the game into a
more tactical challenge, lets quickly implement turn skipping. In player.rs (along
with the rest of the input), we'll add numeric keypad 5 and space to be skip:

// Skip Turn
VirtualKeyCode::Numpad5 => return RunState::PlayerTurn,
VirtualKeyCode::Space => return RunState::PlayerTurn,

This adds a nice tactical dimension to the game: you can lure enemies towards you,
and bene�t from tactical placement. Another frequently found feature of roguelikes is
waiting providing some healing if there are no enemies nearby. We'll only implement
that for the player, since mobs suddenly healing up is disconcerting! So we'll change
that to:

// Skip Turn
VirtualKeyCode::Numpad5 => return skip_turn(&mut gs.ecs),
VirtualKeyCode::Space => return skip_turn(&mut gs.ecs),

Now we implement skip_turn :

205 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn skip_turn(ecs: &mut World) -> RunState {


let player_entity = ecs.fetch::<Entity>();
let viewshed_components = ecs.read_storage::<Viewshed>();
let monsters = ecs.read_storage::<Monster>();

let worldmap_resource = ecs.fetch::<Map>();

let mut can_heal = true;


let viewshed = viewshed_components.get(*player_entity).unwrap();
for tile in viewshed.visible_tiles.iter() {
let idx = worldmap_resource.xy_idx(tile.x, tile.y);
for entity_id in worldmap_resource.tile_content[idx].iter() {
let mob = monsters.get(*entity_id);
match mob {
None => {}
Some(_) => { can_heal = false; }
}
}
}

if can_heal {
let mut health_components = ecs.write_storage::<CombatStats>();
let player_hp =
health_components.get_mut(*player_entity).unwrap();
player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp);
}

RunState::PlayerTurn
}

This looks up various entities, and then iterates the player's viewshed using the
tile_content system. It checks what the player can see for monsters; if no monster
is present, it heals the player by 1 hp. This encourages cerebral play - and can be
balanced with the inclusion of a hunger clock at a later date. It also makes the game
really easy - but we're getting to that!

Increased di�culty as you delve: spawn


tables
Thus far, we've been using a simple spawn system: it randomly picks a number of

206 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

monsters and items, and then picks each with an equal weight. That's not much like
"normal" games, which tend to make some things rare - and some things common.
We'll create a generic random_table system, for use in the spawn system. Create a
new �le, random_table.rs and put the following in it:

207 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::RandomNumberGenerator;

pub struct RandomEntry {


name : String,
weight : i32
}

impl RandomEntry {
pub fn new<S:ToString>(name: S, weight: i32) -> RandomEntry {
RandomEntry{ name: name.to_string(), weight }
}
}

#[derive(Default)]
pub struct RandomTable {
entries : Vec<RandomEntry>,
total_weight : i32
}

impl RandomTable {
pub fn new() -> RandomTable {
RandomTable{ entries: Vec::new(), total_weight: 0 }
}

pub fn add<S:ToString>(mut self, name : S, weight: i32) -> RandomTable


{
self.total_weight += weight;
self.entries.push(RandomEntry::new(name.to_string(), weight));
self
}

pub fn roll(&self, rng : &mut RandomNumberGenerator) -> String {


if self.total_weight == 0 { return "None".to_string(); }
let mut roll = rng.roll_dice(1, self.total_weight)-1;
let mut index : usize = 0;

while roll > 0 {


if roll < self.entries[index].weight {
return self.entries[index].name.clone();
}

roll -= self.entries[index].weight;
index += 1;
}

208 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"None".to_string()
}
}

So this creates a new type, random_table . It adds a new method to it, to facilitate
making a new one. It also creates a vector or entries, each of which has a weight and
a name (passing strings around isn't very e�cient, but makes for clear example
code!). It also implements an add function that lets you pass in a new name and
weight, and updates the structure's total_weight . Finally, roll makes a dice roll
from 0 .. total_weight - 1 , and iterates through entries. If the roll is below the
weight, it returns it - otherwise, it reduces the roll by the weight and tests the next
entry. This gives a chance equal to the relative weight of the entry for any given item
in the table. There's a bit of extra work in there to help chain methods together, for
the Rust-like look of chained function calls. We'll use it in spawner.rs to create a new
function, room_table :

fn room_table() -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1)
.add("Health Potion", 7)
.add("Fireball Scroll", 2)
.add("Confusion Scroll", 2)
.add("Magic Missile Scroll", 4)
}

This contains all of the items and monsters we've added so far, with a weight
attached. I wasn't very careful with these weights; we'll play with them later! It does
mean that a call to room_table().roll(rng) will return a random room entry.

Now we simplify a bit. Delete the NUM_MONSTERS , random_monster and random_item


functions in spawner.rs . Then we replace the room spawning code with:

209 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(clippy::map_entry)]
pub fn spawn_room(ecs: &mut World, room : &Rect) {
let spawn_table = room_table();
let mut spawn_points : HashMap<usize, String> = HashMap::new();

// Scope to keep the borrow checker happy


{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let num_spawns = rng.roll_dice(1, MAX_MONSTERS + 3) - 3;

for _i in 0 .. num_spawns {
let mut added = false;
let mut tries = 0;
while !added && tries < 20 {
let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 -
room.x1))) as usize;
let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 -
room.y1))) as usize;
let idx = (y * MAPWIDTH) + x;
if !spawn_points.contains_key(&idx) {
spawn_points.insert(idx, spawn_table.roll(&mut rng));
added = true;
} else {
tries += 1;
}
}
}
}

// Actually spawn the monsters


for spawn in spawn_points.iter() {
let x = (*spawn.0 % MAPWIDTH) as i32;
let y = (*spawn.0 / MAPWIDTH) as i32;

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
_ => {}
}
}
}

210 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Lets work through this:

1. The �rst line tells the Rust linter that we really do like to check a HashMap for
membership and then insert into it - we also set a �ag, which doesn't work well
with its suggestion.
2. We obtain the global random number generator, and set the number of spawns
to be 1d7-3 (for a -2 to 4 range).
3. For each spawn above 0, we pick a random point in the room. We keep picking
random points until we �nd an empty one (or we exceed 20 tries, in which case
we give up). Once we �nd a point, we add it to the spawn list with a location and
a roll from our random table.
4. Then we iterate the spawn list, match on the roll result and spawn monsters and
items.

This is de�nitely cleaner than the previous approach, and now you are less likely to
run into orcs - and more likely to run into goblins and health potions.

A quick cargo run shows you the improved spawn variety.

Increasing the spawn rate as you delve


That gave a nicer distribution, but didn't solve the problem of later levels being of the
same di�culty as earlier ones. A quick and dirty approach is to spawn more entities
as you descend. That still doesn't solve the problem, but it's a start! We'll start by
modifying the function signature of spawn_room to accept the map depth:

pub fn spawn_room(ecs: &mut World, room : &Rect, map_depth: i32) {

Then we'll change the number of entities that spawn to use this:

let num_spawns = rng.roll_dice(1, MAX_MONSTERS + 3) + (map_depth - 1) - 3;

We'll have to change a couple of calls in main.rs to pass in the depth:

211 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for room in map.rooms.iter().skip(1) {


spawner::spawn_room(&mut gs.ecs, room, 1);
}

// Build a new map and place the player


let worldmap;
let current_depth;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
current_depth = worldmap_resource.depth;
*worldmap_resource = Map::new_map_rooms_and_corridors(current_depth +
1);
worldmap = worldmap_resource.clone();
}

// Spawn bad guys


for room in worldmap.rooms.iter().skip(1) {
spawner::spawn_room(&mut self.ecs, room, current_depth+1);
}

If you cargo run now, the �rst level is quite quiet. Di�culty ramps up a bit as you
descend, until you have veritable hordes of monsters!

Increasing the weights by depth


Let's modify the room_table function to include map depth:

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
}

We also change the call to it in spawn_room to use it:

212 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let spawn_table = room_table(map_depth);

A cargo build later, and voila - you have an increasing probability of �nding orcs,
�reball and confusion scrolls as you descend. The total weight of goblins, health
potions and magic missile scrolls remains the same - but because the others change,
their total likelihood diminishes.

Wrapping Up
You now have a dungeon that increases in di�culty as you descend! In the next
chapter, we'll look at giving your character some progression as well (through
equipment), to balance things out.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Equipping The Player

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Now that we have a dungeon with increasing di�culty, it's time to start giving the
player some ways to improve their performance! In this chapter, we'll start with the
most basic of human tasks: equipping a weapon and shield.

Adding some items you can wear/wield

213 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We already have a lot of the item system in place, so we'll build upon the foundation
from previous chapters. Just using components we already have, we can start with the
following in spawners.rs :

fn dagger(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('/'),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Dagger".to_string() })
.with(Item{})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

fn shield(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('('),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Shield".to_string() })
.with(Item{})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

In both cases, we're making a new entity. We give it a Position , because it has to
start somewhere on the map. We assign a Renderable , set to appropriate
CP437/ASCII glyphs. We give them a name, and mark them as items. We can add them
to the spawn table like this:

214 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
}

We can also include them in the system that actually spawns them quite readily:

// Actually spawn the monsters


for spawn in spawn_points.iter() {
let x = (*spawn.0 % MAPWIDTH) as i32;
let y = (*spawn.0 / MAPWIDTH) as i32;

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
_ => {}
}
}

If you cargo run the project now, you can run around and eventually �nd a dagger or
shield. You might consider raising the spawn frequency from 3 to a really big number
while you test! Since we've added the Item tag, you can pick up and drop these items
when you �nd them.

215 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Equipping The Item


Daggers and shields aren't too useful if you can't use them! So lets make them
equippable.

Equippable Component

We need a way to indicate that an item can be equipped. You've probably guessed by
now, but we add a new component! In components.rs , we add:

#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]


pub enum EquipmentSlot { Melee, Shield }

#[derive(Component, Serialize, Deserialize, Clone)]


pub struct Equippable {
pub slot : EquipmentSlot
}

216 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We also have to remember to register it in a few places, now that we have


serialization support (from chapter 11). In main.rs , we add it to the list of registered
components:

gs.ecs.register::<Equippable>();

In saveload_system.rs , we add it to both sets of component lists:

serialize_individually!(ecs, serializer, data, Position, Renderable,


Player, Viewshed, Monster,
Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item,
Consumable, Ranged, InflictsDamage,
AreaOfEffect, Confusion, ProvidesHealing, InBackpack,
WantsToPickupItem, WantsToUseItem,
WantsToDropItem, SerializationHelper, Equippable
);

deserialize_individually!(ecs, de, d, Position, Renderable, Player,


Viewshed, Monster,
Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item,
Consumable, Ranged, InflictsDamage,
AreaOfEffect, Confusion, ProvidesHealing, InBackpack,
WantsToPickupItem, WantsToUseItem,
WantsToDropItem, SerializationHelper, Equippable
);

Finally, we should add the Equippable component to our dagger and shield
functions in spawner.rs :

217 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn dagger(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('/'),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Dagger".to_string() })
.with(Item{})
.with(Equippable{ slot: EquipmentSlot::Melee })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

fn shield(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('('),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Shield".to_string() })
.with(Item{})
.with(Equippable{ slot: EquipmentSlot::Shield })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

Making items equippable

Generally, having a shield in your backpack doesn't help much (obvious "how did you
�t it in there?" questions aside - like many games, we'll gloss over that one!) - so you
have to be able to pick one to equip. We'll start by making another component,
Equipped . This works in a similar fashion to InBackpack - it indicates that an entity is
holding it. Unlike InBackpack , it will indicate what slot is in use. Here's the basic
Equipped component, in components.rs :

218 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// See wrapper below for serialization


#[derive(Component)]
pub struct Equipped {
pub owner : Entity,
pub slot : EquipmentSlot
}

Just like before, we need to register it in main.rs , and include it in the serialization
and deserialization lists in saveload_system.rs . Since this includes an Entity , we'll
also have a to include wrapper/helper code to make serialization work. The wrapper is
a lot like others we've written - it converts Equipped into a tuple for save, and back
again for loading:

// Equipped wrapper
#[derive(Serialize, Deserialize, Clone)]
pub struct EquippedData<M>(M, EquipmentSlot);

impl<M: Marker + Serialize> ConvertSaveload<M> for Equipped


where
for<'de> M: Deserialize<'de>,
{
type Data = EquippedData<M>;
type Error = NoError;

fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data,


Self::Error>
where
F: FnMut(Entity) -> Option<M>,
{
let marker = ids(self.owner).unwrap();
Ok(EquippedData(marker, self.slot))
}

fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self,


Self::Error>
where
F: FnMut(M) -> Option<Entity>,
{
let entity = ids(data.0).unwrap();
Ok(Equipped{owner: entity, slot : data.1})
}
}

219 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Actually equipping the item

Now we want to make it possible to actually equip the item. Doing so will
automatically unequip any item in the same slot. We'll do this through the same
interface we already have for using items, so we don't have disparate menus
everywhere. Open inventory_system.rs , and we'll edit ItemUseSystem . We'll start by
expanding the list of systems we are referencing:

impl<'a> System<'a> for ItemUseSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
ReadExpect<'a, Map>,
Entities<'a>,
WriteStorage<'a, WantsToUseItem>,
ReadStorage<'a, Name>,
ReadStorage<'a, Consumable>,
ReadStorage<'a, ProvidesHealing>,
ReadStorage<'a, InflictsDamage>,
WriteStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, AreaOfEffect>,
WriteStorage<'a, Confusion>,
ReadStorage<'a, Equippable>,
WriteStorage<'a, Equipped>,
WriteStorage<'a, InBackpack>
);

fn run(&mut self, data : Self::SystemData) {


let (player_entity, mut gamelog, map, entities, mut wants_use,
names,
consumables, healing, inflict_damage, mut combat_stats, mut
suffer_damage,
aoe, mut confused, equippable, mut equipped, mut backpack) =
data;

Now, after target acquisition, add the following block:

220 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// If it is equippable, then we want to equip it - and unequip whatever


else was in that slot
let item_equippable = equippable.get(useitem.item);
match item_equippable {
None => {}
Some(can_equip) => {
let target_slot = can_equip.slot;
let target = targets[0];

// Remove any items the target has in the item's slot


let mut to_unequip : Vec<Entity> = Vec::new();
for (item_entity, already_equipped, name) in (&entities,
&equipped, &names).join() {
if already_equipped.owner == target && already_equipped.slot
== target_slot {
to_unequip.push(item_entity);
if target == *player_entity {
gamelog.entries.insert(0, format!("You unequip {}.",
name.name));
}
}
}
for item in to_unequip.iter() {
equipped.remove(*item);
backpack.insert(*item, InBackpack{ owner: target
}).expect("Unable to insert backpack entry");
}

// Wield the item


equipped.insert(useitem.item, Equipped{ owner: target, slot:
target_slot }).expect("Unable to insert equipped component");
backpack.remove(useitem.item);
if target == *player_entity {
gamelog.entries.insert(0, format!("You equip {}.",
names.get(useitem.item).unwrap().name));
}
}
}

This starts by matching to see if we can equip the item. If we can, it looks up the target
slot for the item and looks to see if there is already an item in that slot. If there, it
moves it to the backpack. Lastly, it adds an Equipped component to the item entity
with the owner (the player right now) and the appropriate slot.

221 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Lastly, you may remember that when the player moves to the next level we delete a
lot of entities. We want to include Equipped by the player as a reason to keep an item
in the ECS. In main.rs , we modify entities_to_remove_on_level_change as follows:

fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> {


let entities = self.ecs.entities();
let player = self.ecs.read_storage::<Player>();
let backpack = self.ecs.read_storage::<InBackpack>();
let player_entity = self.ecs.fetch::<Entity>();
let equipped = self.ecs.read_storage::<Equipped>();

let mut to_delete : Vec<Entity> = Vec::new();


for entity in entities.join() {
let mut should_delete = true;

// Don't delete the player


let p = player.get(entity);
if let Some(_p) = p {
should_delete = false;
}

// Don't delete the player's equipment


let bp = backpack.get(entity);
if let Some(bp) = bp {
if bp.owner == *player_entity {
should_delete = false;
}
}

let eq = equipped.get(entity);
if let Some(eq) = eq {
if eq.owner == *player_entity {
should_delete = false;
}
}

if should_delete {
to_delete.push(entity);
}
}

to_delete
}

222 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you cargo run the project now, you can run around picking up the new items - and
you can equip them. They don't do anything, yet - but at least you can swap them in
and out. The game log will show equipping and unequipping.

Granting combat bonuses

Logically, a shield should provide some protection against incoming damage - and
being stabbed with a dagger should hurt more than being punched! To facilitate this,
we'll add some more components (this should be a familiar song by now). In
components.rs :

223 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Component, Serialize, Deserialize, Clone)]


pub struct MeleePowerBonus {
pub power : i32
}

#[derive(Component, Serialize, Deserialize, Clone)]


pub struct DefenseBonus {
pub defense : i32
}

We also need to remember to register them in main.rs , and saveload_system.rs .


We can then modify our code in spawner.rs to add these components to the right
items:

224 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn dagger(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('/'),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Dagger".to_string() })
.with(Item{})
.with(Equippable{ slot: EquipmentSlot::Melee })
.with(MeleePowerBonus{ power: 2 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

fn shield(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('('),
fg: RGB::named(rltk::CYAN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Shield".to_string() })
.with(Item{})
.with(Equippable{ slot: EquipmentSlot::Shield })
.with(DefenseBonus{ defense: 1 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

Notice how we've added the component to each? Now we need to modify the
melee_combat_system to apply these bonuses. We do this by adding some additional
ECS queries to our system:

225 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl<'a> System<'a> for MeleeCombatSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( Entities<'a>,
WriteExpect<'a, GameLog>,
WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, MeleePowerBonus>,
ReadStorage<'a, DefenseBonus>,
ReadStorage<'a, Equipped>
);

fn run(&mut self, data : Self::SystemData) {


let (entities, mut log, mut wants_melee, names, combat_stats, mut
inflict_damage, melee_power_bonuses, defense_bonuses, equipped) = data;

for (entity, wants_melee, name, stats) in (&entities,


&wants_melee, &names, &combat_stats).join() {
if stats.hp > 0 {
let mut offensive_bonus = 0;
for (_item_entity, power_bonus, equipped_by) in
(&entities, &melee_power_bonuses, &equipped).join() {
if equipped_by.owner == entity {
offensive_bonus += power_bonus.power;
}
}

let target_stats =
combat_stats.get(wants_melee.target).unwrap();
if target_stats.hp > 0 {
let target_name =
names.get(wants_melee.target).unwrap();

let mut defensive_bonus = 0;


for (_item_entity, defense_bonus, equipped_by) in
(&entities, &defense_bonuses, &equipped).join() {
if equipped_by.owner == wants_melee.target {
defensive_bonus += defense_bonus.defense;
}
}

let damage = i32::max(0, (stats.power +


offensive_bonus) - (target_stats.defense + defensive_bonus));

226 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This is a big chunk of code, so lets go through it:

1. We've added MeleePowerBonus , DefenseBonus and Equipped readers to the


system.
2. Once we've determined that the attacker is alive, we set offensive_bonus to 0.
3. We iterate all entities that have a MeleePowerBonus and an Equipped entry. If
they are equipped by the attacker, we add their power bonus to
offensive_bonus .
4. Once we've determined that the defender is alive, we set defensive_bonus to 0.
5. We iterate all entities that have a DefenseBonus and an Equipped entry. If they
are equipped by the target, we add their defense to the defense_bonus .
6. When we calculate damage, we add the o�ense bonus to the power side - and
add the defense bonus to the defense side.

If you cargo run now, you'll �nd that using your dagger makes you hit harder - and
using your shield makes you su�er less damage.

Unequipping the item

Now that you can equip items, and remove the by swapping, you may want to stop
holding an item and return it to your backpack. In a game as simple as this one, this
isn't strictly necessary - but it is a good option to have for the future. We'll bind the R
key to remove an item, since that key is available. In player.rs , add this to the input
code:

VirtualKeyCode::R => return RunState::ShowRemoveItem,

Now we add ShowRemoveItem to RunState in main.rs :

227 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection },
SaveGame,
NextLevel,
ShowRemoveItem
}

And we add a handler for it in tick :

RunState::ShowRemoveItem => {
let result = gui::remove_item_menu(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => newrunstate =
RunState::AwaitingInput,
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::
<WantsToRemoveItem>();
intent.insert(*self.ecs.fetch::<Entity>(), WantsToRemoveItem{
item: item_entity }).expect("Unable to insert intent");
newrunstate = RunState::PlayerTurn;
}
}
}

We'll implement a new component in components.rs (see the source code for the
serialization handler; it's a cut-and-paste of the handler for wanting to drop an item,
with the names changed):

#[derive(Component, Debug)]
pub struct WantsToRemoveItem {
pub item : Entity
}

228 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

As usual, it has to be registered in main.rs and saveload_system.rs .

Now in gui.rs , we'll implement remove_item_menu . It's almost exactly the same as
the item dropping menu, but changing what is queries and the heading (it'd be a great
idea to make these into more generic functions some time!):

229 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn remove_item_menu(gs : &mut State, ctx : &mut Rltk) ->


(ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<Equipped>();
let entities = gs.ecs.entities();

let inventory = (&backpack, &names).join().filter(|item| item.0.owner


== *player_entity );
let count = inventory.count();

let mut y = (25 - (count / 2)) as i32;


ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));
ctx.print_color(18, y-2, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Remove Which Item?");
ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "ESCAPE to cancel");

let mut equippable : Vec<Entity> = Vec::new();


let mut j = 0;
for (entity, _pack, name) in (&entities, &backpack,
&names).join().filter(|item| item.1.owner == *player_entity ) {
ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437('('));
ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK),
97+j as u8);
ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK),
rltk::to_cp437(')'));

ctx.print(21, y, &name.name.to_string());
equippable.push(entity);
y += 1;
j += 1;
}

match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) => {
match key {
VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None)
}
_ => {
let selection = rltk::letter_to_option(key);
if selection > -1 && selection < count as i32 {

230 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

return (ItemMenuResult::Selected,
Some(equippable[selection as usize]));
}
(ItemMenuResult::NoResponse, None)
}
}
}
}
}

Next, we should extend inventory_system.rs to support removing items.


Fortunately, this is a very simple system:

pub struct ItemRemoveSystem {}

impl<'a> System<'a> for ItemRemoveSystem {


#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
WriteStorage<'a, WantsToRemoveItem>,
WriteStorage<'a, Equipped>,
WriteStorage<'a, InBackpack>
);

fn run(&mut self, data : Self::SystemData) {


let (entities, mut wants_remove, mut equipped, mut backpack) =
data;

for (entity, to_remove) in (&entities, &wants_remove).join() {


equipped.remove(to_remove.item);
backpack.insert(to_remove.item, InBackpack{ owner: entity
}).expect("Unable to insert backpack");
}

wants_remove.clear();
}
}

Lastly, we add it to the systems in main.rs :

231 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl State {
fn run_systems(&mut self) {
let mut mapindex = MapIndexingSystem{};
mapindex.run_now(&self.ecs);
let mut vis = VisibilitySystem{};
vis.run_now(&self.ecs);
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
let mut melee = MeleeCombatSystem{};
melee.run_now(&self.ecs);
let mut damage = DamageSystem{};
damage.run_now(&self.ecs);
let mut pickup = ItemCollectionSystem{};
pickup.run_now(&self.ecs);
let mut itemuse = ItemUseSystem{};
itemuse.run_now(&self.ecs);
let mut drop_items = ItemDropSystem{};
drop_items.run_now(&self.ecs);
let mut item_remove = ItemRemoveSystem{};
item_remove.run_now(&self.ecs);

self.ecs.maintain();
}
}

Now if you cargo run , you can pick up a dagger or shield and equip it. Then you can
press R to remove it.

Adding some more powerful gear later


Lets add a couple more items, in spawner.rs :

232 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn longsword(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('/'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Longsword".to_string() })
.with(Item{})
.with(Equippable{ slot: EquipmentSlot::Melee })
.with(MeleePowerBonus{ power: 4 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

fn tower_shield(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('('),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Tower Shield".to_string() })
.with(Item{})
.with(Equippable{ slot: EquipmentSlot::Shield })
.with(DefenseBonus{ defense: 3 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

We're going to add a quick �x to random_table.rs to ignore entries with 0 or lower


spawn chances:

233 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn add<S:ToString>(mut self, name : S, weight: i32) -> RandomTable {


if weight > 0 {
self.total_weight += weight;
self.entries.push(RandomEntry::new(name.to_string(), weight));
}
self
}

And back in spawner.rs , we'll add them to the loot table - with a chance of appearing
later in the dungeon:

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
.add("Longsword", map_depth - 1)
.add("Tower Shield", map_depth - 1)
}

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
_ => {}
}

Now as you descend further, you can �nd better weapons and shields!

234 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The game over screen


We're nearly at the end of the basic tutorial, so lets make something happen when
you die - rather than locking up in a console loop. In the �le damage_system.rs , we'll
edit the match statement on player for delete_the_dead :

match player {
None => {
let victim_name = names.get(entity);
if let Some(victim_name) = victim_name {
log.entries.insert(0, format!("{} is dead",
&victim_name.name));
}
dead.push(entity)
}
Some(_) => {
let mut runstate = ecs.write_resource::<RunState>();
*runstate = RunState::GameOver;
}
}

Of course, we now have to go to main.rs and add the new state:

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection },
SaveGame,
NextLevel,
ShowRemoveItem,
GameOver
}

We'll add that to the state implementation, also in main.rs :

235 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::GameOver => {
let result = gui::game_over(ctx);
match result {
gui::GameOverResult::NoSelection => {}
gui::GameOverResult::QuitToMenu => {
self.game_over_cleanup();
newrunstate = RunState::MainMenu{ menu_selection:
gui::MainMenuSelection::NewGame };
}
}
}

That's relatively straightforward: we call game_over to render the menu, and when
you quit we delete everything in the ECS. Lastly, in gui.rs we'll implement
game_over :

#[derive(PartialEq, Copy, Clone)]


pub enum GameOverResult { NoSelection, QuitToMenu }

pub fn game_over(ctx : &mut Rltk) -> GameOverResult {


ctx.print_color_centered(15, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Your journey has ended!");
ctx.print_color_centered(17, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "One day, we'll tell you all about how you
did.");
ctx.print_color_centered(18, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "That day, sadly, is not in this chapter..");

ctx.print_color_centered(20, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Press any key to return to the menu.");

match ctx.key {
None => GameOverResult::NoSelection,
Some(_) => GameOverResult::QuitToMenu
}
}

Lastly, we'll handle game_over_cleanup :

236 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn game_over_cleanup(&mut self) {
// Delete everything
let mut to_delete = Vec::new();
for e in self.ecs.entities().join() {
to_delete.push(e);
}
for del in to_delete.iter() {
self.ecs.delete_entity(*del).expect("Deletion failed");
}

// Build a new map and place the player


let worldmap;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
*worldmap_resource = Map::new_map_rooms_and_corridors(1);
worldmap = worldmap_resource.clone();
}

// Spawn bad guys


for room in worldmap.rooms.iter().skip(1) {
spawner::spawn_room(&mut self.ecs, room, 1);
}

// Place the player and update resources


let (player_x, player_y) = worldmap.rooms[0].center();
let player_entity = spawner::player(&mut self.ecs, player_x,
player_y);
let mut player_position = self.ecs.write_resource::<Point>();
*player_position = Point::new(player_x, player_y);
let mut position_components = self.ecs.write_storage::<Position>();
let mut player_entity_writer = self.ecs.write_resource::<Entity>();
*player_entity_writer = player_entity;
let player_pos_comp = position_components.get_mut(player_entity);
if let Some(player_pos_comp) = player_pos_comp {
player_pos_comp.x = player_x;
player_pos_comp.y = player_y;
}

// Mark the player's visibility as dirty


let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
let vs = viewshed_components.get_mut(player_entity);
if let Some(vs) = vs {
vs.dirty = true;
}
}

237 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This should look familiar from our serialization work when loading the game. It's very
similar, but it generates a new player.

If you cargo run now, and die - you'll get a message informing you that the game is
done, and sending you back to the menu.

Wrapping Up
That's it for the �rst section of the tutorial. It sticks relatively closely to the Python
tutorial, and takes you from "hello rust" to a moderately fun Roguelike. I hope you've
enjoyed it! Stay tuned, I hope to add a section 2 soon.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Section 2 - Stretch Goals

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

238 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I've been enjoying writing this tutorial, and people are using it (thank you!) - so I
decided to keep adding content. Section 2 is more of a smorgasbord of content than a
structured tutorial. I'll keep adding content as we try to build a great roguelike as a
community.

Please feel free to contact me (I'm @herberticus on Twitter) if you have any
questions, ideas for improvements, or things you'd like me to add. Also, sorry about
all the Patreon spam - hopefully someone will �nd this su�ciently useful to feel like
throwing a co�ee or two my way. :-)

Copyright (C) 2019, Herbert Wolverson.

Nicer Walls

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

So far, we've used a very traditional rendering style for the map. Hash symbols for
walls, periods for �oors. It looks pretty nice, but games like Dwarf Fortress do a lovely
job of using codepage 437's line-drawing characters to make the walls of the dungeon
look smooth. This short chapter will show how to use a bitmask to calculate
appropriate walls, and render them appropriately. As usual, we'll start from the code
from the previous chapter (chapter 1.14).

Counting neighbors to build our bitset


We have a decent map rendering system in map.rs , speci�cally the function
draw_map . If you �nd the section that matches tile by type, we can start by
extending the Wall selection:

239 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

TileType::Wall => {
glyph = wall_glyph(&*map, x, y);
fg = RGB::from_f32(0., 1.0, 0.);
}

This requires the wall_glyph function, so lets write it:

fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {


if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 {
return 35; }
let mut mask : u8 = 0;

if is_revealed_and_wall(map, x, y - 1) { mask +=1; }


if is_revealed_and_wall(map, x, y + 1) { mask +=2; }
if is_revealed_and_wall(map, x - 1, y) { mask +=4; }
if is_revealed_and_wall(map, x + 1, y) { mask +=8; }

match mask {
0 => { 9 } // Pillar because we can't see neighbors
1 => { 186 } // Wall only to the north
2 => { 186 } // Wall only to the south
3 => { 186 } // Wall to the north and south
4 => { 205 } // Wall only to the west
5 => { 188 } // Wall to the north and west
6 => { 187 } // Wall to the south and west
7 => { 185 } // Wall to the north, south and west
8 => { 205 } // Wall only to the east
9 => { 200 } // Wall to the north and east
10 => { 201 } // Wall to the south and east
11 => { 204 } // Wall to the north, south and east
12 => { 205 } // Wall to the east and west
13 => { 202 } // Wall to the east, west, and south
14 => { 203 } // Wall to the east, west, and north
_ => { 35 } // We missed one?
}
}

Lets step through this function:

1. If we are at the map bounds, we aren't going to risk stepping outside of them -
so we return a # symbol (ASCII 35).
2. Now we create an 8-bit unsigned integer to act as our bitmask . We're interested

240 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

in setting individual bits, and only need four of them - so an 8-bit number is
perfect.
3. Next, we check each of the 4 directions and add to the mask. We're adding
numbers corresponding to each of the �rst four bits in binary - so 1,2,4,8. This
means that our �nal number will store whether or not we have each of the four
possible neighbors. For example, a value of 3 means that we have neighbors to
the north and south.
4. Then we match on the resulting mask bit, and return the appropriate line-
drawing character from the codepage 437 character set

This function in turn calls is_revealed_and_wall , so we'll write that too! It's very
simple:

fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {


let idx = map.xy_idx(x, y);
map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx]
}

It simply checks to see if a tile is revealed, and if it is a wall - if both are true, it returns
true - otherwise it returns false.

If you cargo run the project now, you get a nicer looking set of walls:

241 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Bloodstains

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Our character lives the life of a "murder-hobo", looting and slaying at will - so it only
makes sense that the pristine dungeon will start to resemble a charnel house. It also
gives us a bridge into a future chapter, in which we'll start to add some particle and

242 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

visual e�ects (in ASCII/CP437) to the game.

Storing the blood


Tiles either have blood or they don't, so it makes sense to attach them to the map as a
set . So at the top of map.rs , we'll include a new storage type - HashSet :

use std::collections::HashSet;

In the map de�nition, we'll include a HashSet of usize (to represent tile indices)
types for blood:

#[derive(Default, Serialize, Deserialize, Clone)]


pub struct Map {
pub tiles : Vec<TileType>,
pub rooms : Vec<Rect>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>,
pub depth : i32,
pub bloodstains : HashSet<usize>,

#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content : Vec<Vec<Entity>>
}

And in the new map generator, we'll initialize it:

243 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut map = Map{


tiles : vec![TileType::Wall; MAPCOUNT],
rooms : Vec::new(),
width : MAPWIDTH as i32,
height: MAPHEIGHT as i32,
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
tile_content : vec![Vec::new(); MAPCOUNT],
depth: new_depth,
bloodstains: HashSet::new()
};

Rendering the blood


We'll indicate a bloodstain by changing a tile background to a dark red. We don't want
to be too "in your face" with the e�ect, and we don't want to hide the tile content - so
that should be su�cient. We'll also not show blood that isn't in visual range, to keep it
understated. In map.rs , the render section now looks like this:

244 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if map.revealed_tiles[idx] {
let glyph;
let mut fg;
let mut bg = RGB::from_f32(0., 0., 0.);
match tile {
TileType::Floor => {
glyph = rltk::to_cp437('.');
fg = RGB::from_f32(0.0, 0.5, 0.5);
}
TileType::Wall => {
glyph = wall_glyph(&*map, x, y);
fg = RGB::from_f32(0., 1.0, 0.);
}
TileType::DownStairs => {
glyph = rltk::to_cp437('>');
fg = RGB::from_f32(0., 1.0, 1.0);
}
}
if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.);
}
if !map.visible_tiles[idx] {
fg = fg.to_greyscale();
bg = RGB::from_f32(0., 0., 0.); // Don't show stains out of visual
range
}
ctx.set(x, y, fg, bg, glyph);
}

Blood for the blood god


Now we need to add blood to the scene! We'll mark a tile as bloody whenever
someone takes damage in it. We'll adjust the DamageSystem in damage_system.rs to
set the bloodstain:

245 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl<'a> System<'a> for DamageSystem {


type SystemData = ( WriteStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, Position>,
WriteExpect<'a, Map>,
Entities<'a> );

fn run(&mut self, data : Self::SystemData) {


let (mut stats, mut damage, positions, mut map, entities) = data;

for (entity, mut stats, damage) in (&entities, &mut stats,


&damage).join() {
stats.hp -= damage.amount;
let pos = positions.get(entity);
if let Some(pos) = pos {
let idx = map.xy_idx(pos.x, pos.y);
map.bloodstains.insert(idx);
}
}

damage.clear();
}
}

If you cargo run your project, the map starts to show signs of battle!

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

246 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Particle E�ects in ASCII

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

There's no real visual feedback for your actions - you hit something, and it either goes
away, or it doesn't. Bloodstains give a good impression of what previously happened in
a location - but it would be nice to give some sort of instant reaction to your actions.
These need to be fast, non-blocking (so you don't have to wait for the animation to
�nish to keep playing), and not too intrusive. Particles are a good �t for this, so we'll
implement a simple ASCII/CP437 particle system.

Particle component
As usual, we'll start out by thinking about what a particle is. Typically it has a position,
something to render, and a lifetime (so it goes away). We've already written two out of
three of those, so lets go ahead and create a ParticleLifetime component. In
components.rs :

#[derive(Component, Serialize, Deserialize, Clone)]


pub struct ParticleLifetime {
pub lifetime_ms : f32
}

We have to register this in all the usual places: main.rs and saveload_system.rs
(twice).

Grouping particle code together

247 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll make a new �le, particle_system.rs . It won't be a regular system, because we


need access to the RLTK Context object - but it will have to provide services to other
systems.

The �rst thing to support is making particles vanish after their lifetime. So we start
with the following in particle_system.rs :

use specs::prelude::*;
use super::{ Rltk, ParticleLifetime};

pub fn cull_dead_particles(ecs : &mut World, ctx : &Rltk) {


let mut dead_particles : Vec<Entity> = Vec::new();
{
// Age out particles
let mut particles = ecs.write_storage::<ParticleLifetime>();
let entities = ecs.entities();
for (entity, mut particle) in (&entities, &mut particles).join() {
particle.lifetime_ms -= ctx.frame_time_ms;
if particle.lifetime_ms < 0.0 {
dead_particles.push(entity);
}
}
}
for dead in dead_particles.iter() {
ecs.delete_entity(*dead).expect("Particle will not die");
}
}

Then we modify the render loop in main.rs to call it:

ctx.cls();
particle_system::cull_dead_particles(&mut self.ecs, ctx);

Spawning particles via a service


Let's extend particle_system.rs to o�er a builder system: you obtain a
ParticleBuilder and add requests to it, and then create your particles as a batch
together. We'll o�er the particle system as a resource - so it's available anywhere. This

248 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

avoids having to add much intrusive code into each system, and lets us handle the
actual particle spawning as a single (fast) batch.

Our basic ParticleBuilder looks like this. We haven't done anything to actually add
any particles yet, but this provides the requestor service:

struct ParticleRequest {
x: i32,
y: i32,
fg: RGB,
bg: RGB,
glyph: u8,
lifetime: f32
}

pub struct ParticleBuilder {


requests : Vec<ParticleRequest>
}

impl ParticleBuilder {
pub fn new() -> ParticleBuilder {
ParticleBuilder{ requests : Vec::new() }
}

pub fn request(&mut self, x:i32, y:i32, fg: RGB, bg:RGB, glyph: u8,
lifetime: f32) {
self.requests.push(
ParticleRequest{
x, y, fg, bg, glyph, lifetime
}
);
}
}

In main.rs , we'll turn it into a resource:

gs.ecs.insert(particle_system::ParticleBuilder::new());

Now, we'll return to particle_system.rs and build an actual system to spawn


particles. The system looks like this:

249 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct ParticleSpawnSystem {}

impl<'a> System<'a> for ParticleSpawnSystem {


#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
WriteStorage<'a, Position>,
WriteStorage<'a, Renderable>,
WriteStorage<'a, ParticleLifetime>,
WriteExpect<'a, ParticleBuilder>
);

fn run(&mut self, data : Self::SystemData) {


let (entities, mut positions, mut renderables, mut particles, mut
particle_builder) = data;
for new_particle in particle_builder.requests.iter() {
let p = entities.create();
positions.insert(p, Position{ x: new_particle.x, y:
new_particle.y }).expect("Unable to inser position");
renderables.insert(p, Renderable{ fg: new_particle.fg, bg:
new_particle.bg, glyph: new_particle.glyph, render_order: 0
}).expect("Unable to insert renderable");
particles.insert(p, ParticleLifetime{ lifetime_ms:
new_particle.lifetime }).expect("Unable to insert lifetime");
}

particle_builder.requests.clear();
}
}

This is a very simple service: it iterates the requests, and creates an entity for each
particle with the component parameters from the request. Then it clears the builder
list. The last step is to add it to the system schedule in main.rs :

250 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut gs = State {


ecs: World::new(),
systems : DispatcherBuilder::new()
.with(MapIndexingSystem{}, "map_indexing_system", &[])
.with(VisibilitySystem{}, "visibility_system", &[])
.with(MonsterAI{}, "monster_ai", &["visibility_system",
"map_indexing_system"])
.with(MeleeCombatSystem{}, "melee_combat", &["monster_ai"])
.with(DamageSystem{}, "damage", &["melee_combat"])
.with(ItemCollectionSystem{}, "pickup", &["melee_combat"])
.with(ItemUseSystem{}, "potions", &["melee_combat"])
.with(ItemDropSystem{}, "drop_items", &["melee_combat"])
.with(ItemRemoveSystem{}, "remove_items", &["melee_combat"])
.with(particle_system::ParticleSpawnSystem{}, "spawn_particles",
&["potions", "melee_combat"])
.build(),
};

We've made it depend upon likely particle spawners. We'll have to be a little careful to
avoid accidentally making it concurrent with anything that might add to it.

Actually spawning some particles for combat


Lets start by spawning a particle whenever someone attacks. Open up
melee_combat_system.rs , and we'll add ParticleBuilder to the list of requested
resources for the system. First, the includes:

use super::{CombatStats, WantsToMelee, Name, SufferDamage,


gamelog::GameLog, MeleePowerBonus, DefenseBonus, Equipped,
particle_system::ParticleBuilder, Position};

Then, a WriteExpect to be able to write to the resource:

251 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

type SystemData = ( Entities<'a>,


WriteExpect<'a, GameLog>,
WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, MeleePowerBonus>,
ReadStorage<'a, DefenseBonus>,
ReadStorage<'a, Equipped>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>
);

And the expanded list of resources for the run method itself:

let (entities, mut log, mut wants_melee, names, combat_stats, mut


inflict_damage,
melee_power_bonuses, defense_bonuses, equipped, mut particle_builder,
positions) = data;

Finally, we'll add the request:

let pos = positions.get(wants_melee.target);


if let Some(pos) = pos {
particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE),
rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0);
}

let damage = i32::max(0, (stats.power + offensive_bonus) -


(target_stats.defense + defensive_bonus));

If you cargo run now, you'll see a relatively subtle particle feedback to show that
melee combat occurred. This de�nitely helps with the feel of gameplay, and is
su�ciently non-intrusive that we aren't making our other systems too confusing.

252 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding e�ects to item use


It would be great to add similar e�ects to item use, so lets do it! In
inventory_system.rs , we'll expand the ItemUseSystem introduction to include the
ParticleBuilder :

impl<'a> System<'a> for ItemUseSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
ReadExpect<'a, Map>,
Entities<'a>,
WriteStorage<'a, WantsToUseItem>,
ReadStorage<'a, Name>,
ReadStorage<'a, Consumable>,
ReadStorage<'a, ProvidesHealing>,
ReadStorage<'a, InflictsDamage>,
WriteStorage<'a, CombatStats>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, AreaOfEffect>,
WriteStorage<'a, Confusion>,
ReadStorage<'a, Equippable>,
WriteStorage<'a, Equipped>,
WriteStorage<'a, InBackpack>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>
);

#[allow(clippy::cognitive_complexity)]
fn run(&mut self, data : Self::SystemData) {
let (player_entity, mut gamelog, map, entities, mut wants_use,
names,
consumables, healing, inflict_damage, mut combat_stats, mut
suffer_damage,
aoe, mut confused, equippable, mut equipped, mut backpack, mut
particle_builder, positions) = data;

We'll start by showing a heart when you drink a healing potion. In the healing section:

253 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount);


if entity == *player_entity {
gamelog.entries.insert(0, format!("You use the {}, healing {} hp.",
names.get(useitem.item).unwrap().name, healer.heal_amount));
}
used_item = true;

let pos = positions.get(*target);


if let Some(pos) = pos {
particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::GREEN),
rltk::RGB::named(rltk::BLACK), rltk::to_cp437('♥'), 200.0);
}

We can use a similar e�ect for confusion - only with a magenta question mark. In the
confusion section:

gamelog.entries.insert(0, format!("You use {} on {}, confusing them.",


item_name.name, mob_name.name));

let pos = positions.get(*mob);


if let Some(pos) = pos {
particle_builder.request(pos.x, pos.y,
rltk::RGB::named(rltk::MAGENTA), rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('?'), 200.0);
}

We should also use a particle to indicate that damage was in�icted. In the damage
section of the system:

gamelog.entries.insert(0, format!("You use {} on {}, inflicting {} hp.",


item_name.name, mob_name.name, damage.damage));

let pos = positions.get(*mob);


if let Some(pos) = pos {
particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::RED),
rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0);
}

Lastly, if an e�ect hits a whole area (for example, a �reball) it would be good to
indicate what the area is. In the targeting section of the system, add:

254 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for mob in map.tile_content[idx].iter() {


targets.push(*mob);
}
particle_builder.request(tile_idx.x, tile_idx.y,
rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('░'), 200.0);

That wasn't too hard, was it? If you cargo run your project now, you'll see various
visual e�ects �ring.

Adding an indicator for missing a turn due to


confusion
Lastly, we'll repeat the confused e�ect on monsters when it is their turn and they skip
due to being confused. This should make it less confusing as to why they stand
around. In monster_ai_system.rs , we �rst modify the system header to request the
appropriate helper:

255 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl<'a> System<'a> for MonsterAI {


#[allow(clippy::type_complexity)]
type SystemData = ( WriteExpect<'a, Map>,
ReadExpect<'a, Point>,
ReadExpect<'a, Entity>,
ReadExpect<'a, RunState>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Monster>,
WriteStorage<'a, Position>,
WriteStorage<'a, WantsToMelee>,
WriteStorage<'a, Confusion>,
WriteExpect<'a, ParticleBuilder>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, player_pos, player_entity, runstate, entities, mut
viewshed,
monster, mut position, mut wants_to_melee, mut confused, mut
particle_builder) = data;

Then we add in a request at the end of the confusion test:

can_act = false;

particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA),


rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'),
200.0);

We don't need to worry about getting the Position component here, because we
already get it as part of the loop. If you cargo run your project now, and �nd a
confusion scroll - you have visual feedback as to why a goblin isn't chasing you
anymore:

256 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrap Up
That's it for visual e�ects for now. We've given the game a much more visceral feel,
with feedback given for actions. That's a big improvement, and goes a long way to
modernizing an ASCII interface!

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Adding a hunger clock and food

About this tutorial

257 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Hunger clocks are a controversial feature of a lot of roguelikes. They can really irritate
the player if you are spending all of your time looking for food, but they also drive you
forward - so you can't sit around without exploring more. Resting to heal becomes
more of a risk/reward system, in particular. This chapter will implement a basic
hunger clock for the player.

Adding a hunger clock component


We'll be adding a hunger clock to the player, so the �rst step is to make a component
to represent it. In components.rs :

#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]


pub enum HungerState { WellFed, Normal, Hungry, Starving }

#[derive(Component, Serialize, Deserialize, Clone)]


pub struct HungerClock {
pub state : HungerState,
pub duration : i32
}

As with all components, it needs to be registered in main.rs and


saveload_system.rs . In spawners.rs , we'll extend the player function to add a
hunger clock to the player:

258 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity
{
ecs
.create_entity()
.with(Position { x: player_x, y: player_y })
.with(Renderable {
glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW),
bg: RGB::named(rltk::BLACK),
render_order: 0
})
.with(Player{})
.with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true
})
.with(Name{name: "Player".to_string() })
.with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
.with(HungerClock{ state: HungerState::WellFed, duration: 20 })
.marked::<SimpleMarker<SerializeMe>>()
.build()
}

There's now a hunger clock component in place, but it doesn't do anything!

Adding a hunger system


We'll make a new �le, hunger_system.rs and implement a hunger clock system. It's
quite straightforward:

259 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use specs::prelude::*;
use super::{HungerClock, RunState, HungerState, SufferDamage,
gamelog::GameLog};

pub struct HungerSystem {}

impl<'a> System<'a> for HungerSystem {


#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
WriteStorage<'a, HungerClock>,
ReadExpect<'a, Entity>, // The player
ReadExpect<'a, RunState>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, GameLog>
);

fn run(&mut self, data : Self::SystemData) {


let (entities, mut hunger_clock, player_entity, runstate, mut
inflict_damage, mut log) = data;

for (entity, mut clock) in (&entities, &mut hunger_clock).join() {


let mut proceed = false;

match *runstate {
RunState::PlayerTurn => {
if entity == *player_entity {
proceed = true;
}
}
RunState::MonsterTurn => {
if entity != *player_entity {
proceed = true;
}
}
_ => proceed = false
}

if proceed {
clock.duration -= 1;
if clock.duration < 1 {
match clock.state {
HungerState::WellFed => {
clock.state = HungerState::Normal;
clock.duration = 200;

260 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if entity == *player_entity {
log.entries.insert(0, "You are no longer
well fed.".to_string());
}
}
HungerState::Normal => {
clock.state = HungerState::Hungry;
clock.duration = 200;
if entity == *player_entity {
log.entries.insert(0, "You are
hungry.".to_string());
}
}
HungerState::Hungry => {
clock.state = HungerState::Starving;
clock.duration = 200;
if entity == *player_entity {
log.entries.insert(0, "You are
starving!".to_string());
}
}
HungerState::Starving => {
// Inflict damage from hunger
if entity == *player_entity {
log.entries.insert(0, "Your hunger pangs
are getting painful! You suffer 1 hp damage.".to_string());
}
inflict_damage.insert(entity, SufferDamage{
amount: 1 }).expect("Unable to do damage");
}
}
}
}
}
}
}

It works by iterating all entities that have a HungerClock . If they are the player, it only
takes e�ect in the PlayerTurn state; likewise, if they are a monster, it only takes place
in their turn (in case we want hungry monsters later!). The duration of the current
state is reduced on each run-through. If it hits 0, it moves one state down - or if you
are starving, damages you.

Now we need to add it to the list of systems running in main.rs :

261 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl State {
fn run_systems(&mut self) {
let mut mapindex = MapIndexingSystem{};
mapindex.run_now(&self.ecs);
let mut vis = VisibilitySystem{};
vis.run_now(&self.ecs);
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
let mut melee = MeleeCombatSystem{};
melee.run_now(&self.ecs);
let mut damage = DamageSystem{};
damage.run_now(&self.ecs);
let mut pickup = ItemCollectionSystem{};
pickup.run_now(&self.ecs);
let mut itemuse = ItemUseSystem{};
itemuse.run_now(&self.ecs);
let mut drop_items = ItemDropSystem{};
drop_items.run_now(&self.ecs);
let mut item_remove = ItemRemoveSystem{};
item_remove.run_now(&self.ecs);
let mut hunger = hunger_system::HungerSystem{};
hunger.run_now(&self.ecs);
let mut particles = particle_system::ParticleSpawnSystem{};
particles.run_now(&self.ecs);

self.ecs.maintain();
}
}

If you cargo run now, and hit wait a lot - you'll starve to death.

Displaying the status


It would be nice to know your hunger state! We'll modify draw_ui in gui.rs to show
it:

262 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {


ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK));

let combat_stats = ecs.read_storage::<CombatStats>();


let players = ecs.read_storage::<Player>();
let hunger = ecs.read_storage::<HungerClock>();
for (_player, stats, hc) in (&players, &combat_stats, &hunger).join()
{
let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
ctx.print_color(12, 43, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), &health);

ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp,


RGB::named(rltk::RED), RGB::named(rltk::BLACK));

match hc.state {
HungerState::WellFed => ctx.print_color(71, 42,
RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"),
HungerState::Normal => {}
HungerState::Hungry => ctx.print_color(71, 42,
RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"),
HungerState::Starving => ctx.print_color(71, 42,
RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"),
}
}
...

If you cargo run your project, this gives quite a pleasant display:

Adding in food
It's all well and good starving to death, but players will �nd it frustrating if they always
start do die after 620 turns (and su�er consequences before that! 620 may sound like
a lot, but it's common to use a few hundred moves on a level, and we aren't trying to
make food the primary game focus). We'll introduce a new item, Rations . We have
most of the components needed for this already, but we need a new one to indicate

263 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

that an item ProvidesFood . In components.rs :

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct ProvidesFood {}

We will, as always, need to register this in main.rs and saveload_system.rs .

Now, in spawner.rs we'll create a new function to make rations:

fn rations(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('%'),
fg: RGB::named(rltk::GREEN),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Rations".to_string() })
.with(Item{})
.with(ProvidesFood{})
.with(Consumable{})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

We'll also add it to the spawn table (quite common):

264 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
.add("Longsword", map_depth - 1)
.add("Tower Shield", map_depth - 1)
.add("Rations", 10)
}

And to the spawn code:

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Rations" => rations(ecs, x, y),
_ => {}
}

If you cargo run now, you will encounter rations that you can pickup and drop. You
can't, however, eat them! We'll add that to inventory_system.rs . Here's the relevant
portion (see the tutorial source for the full version):

265 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// It it is edible, eat it!


let item_edible = provides_food.get(useitem.item);
match item_edible {
None => {}
Some(_) => {
used_item = true;
let target = targets[0];
let hc = hunger_clocks.get_mut(target);
if let Some(hc) = hc {
hc.state = HungerState::WellFed;
hc.duration = 20;
gamelog.entries.insert(0, format!("You eat the {}.",
names.get(useitem.item).unwrap().name));
}
}
}

If you cargo run now, you can run around - �nd rations, and eat them to reset the
hunger clock!

266 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding a bonus for being well fed


It would be nice if being Well Fed does something! We'll give you a temporary +1 to
your power when you are fed. This encourages the player to eat - even though they
don't have to (sneakily making it harder to survive on lower levels as food becomes
less plentiful). In melee_combat_system.rs we add:

let hc = hunger_clock.get(entity);
if let Some(hc) = hc {
if hc.state == HungerState::WellFed {
offensive_bonus += 1;
}
}

And that's it! You get a +1 power bonus for being full of rations.

Preventing healing when hungry or starving


As another bene�t to food, we'll prevent you from wait-healing while hungry or
starving (this also balances the healing system we added earlier). In player.rs , we
modify skip_turn :

let hunger_clocks = ecs.read_storage::<HungerClock>();


let hc = hunger_clocks.get(*player_entity);
if let Some(hc) = hc {
match hc.state {
HungerState::Hungry => can_heal = false,
HungerState::Starving => can_heal = false,
_ => {}
}
}

if can_heal {

Wrap-Up

267 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We now have a working hunger clock system. You may want to tweak the durations to
suit your taste (or skip it completely if it isn't your cup of tea) - but it's a mainstay of
the genre, so it's good to have it included in the tutorials.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

Magic Mapping

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

A really common item in roguelikes is the scroll of magic mapping. You read it, and the
dungeon is revealed. Fancier roguelikes have nice graphics for it. In this chapter, we'll
start by making it work - and then make it pretty!

Adding a magic map component


We have everything we need except for an indicator that an item is a scroll (or any
other item, really) of magic mapping. So in components.rs we'll add a component for
it:

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct MagicMapper {}

As always, we need to register it in main.rs and saveload_system.rs . We'll head

268 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

over to spawners.rs and create a new function for it, as well as adding it to the loot
tables:

fn magic_mapping_scroll(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437(')'),
fg: RGB::named(rltk::CYAN3),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Scroll of Magic Mapping".to_string() })
.with(Item{})
.with(MagicMapper{})
.with(Consumable{})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

And the loot table:

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
.add("Longsword", map_depth - 1)
.add("Tower Shield", map_depth - 1)
.add("Rations", 10)
.add("Magic Mapping Scroll", 400)
}

Notice that we've given it a weight of 400 - absolutely ridiculous. We'll �x it later, for
now we really want to spawn the scroll so that we can test it! Lastly, we add it to the
actual spawn function:

269 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Rations" => rations(ecs, x, y),
"Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
_ => {}
}

If you were to cargo run now, you'd likely �nd scrolls you can pick up - but they
won't do anything.

Mapping the level - the simple version


We'll modify inventory_system.rs to detect if you just used a mapping scroll, and
reveal the whole map:

// If its a magic mapper...


let is_mapper = magic_mapper.get(useitem.item);
match is_mapper {
None => {}
Some(_) => {
used_item = true;
for r in map.revealed_tiles.iter_mut() {
*r = true;
}
gamelog.entries.insert(0, "The map is revealed to
you!".to_string());
}
}

There are some framework changes also (see the source); we've done this often

270 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

enough, I don't think it needs repeating here again. If you cargo run the project now,
�nd a scroll (they are everywhere) and use it - the map is instantly revealed:

Making it pretty
While the code presented there is e�ective, it isn't visually attractive. It's nice to
include �u� in games, and let the user be pleasantly surprised by the beauty of an
ASCII terminal from time to time! We'll start by modifying inventory_system.rs
again:

271 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// If its a magic mapper...


let is_mapper = magic_mapper.get(useitem.item);
match is_mapper {
None => {}
Some(_) => {
used_item = true;
gamelog.entries.insert(0, "The map is revealed to
you!".to_string());
*runstate = RunState::MagicMapReveal{ row : 0};
}
}

Notice that instead of modifying the map, we are just changing the game state to
mapping mode. We don't actually support doing that yet, so lets go into the state
mapper in main.rs and modify PlayerTurn to handle it:

RunState::PlayerTurn => {
self.systems.dispatch(&self.ecs);
self.ecs.maintain();
match *self.ecs.fetch::<RunState>() {
RunState::MagicMapReveal{ .. } => newrunstate =
RunState::MagicMapReveal{ row: 0 },
_ => newrunstate = RunState::MonsterTurn
}
}

While we're here, lets add the state to RunState :

272 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection },
SaveGame,
NextLevel,
ShowRemoveItem,
GameOver,
MagicMapReveal { row : i32 }
}

We also add some logic to the tick loop for the new state:

RunState::MagicMapReveal{row} => {
let mut map = self.ecs.fetch_mut::<Map>();
for x in 0..MAPWIDTH {
let idx = map.xy_idx(x as i32,row);
map.revealed_tiles[idx] = true;
}
if row as usize == MAPHEIGHT-1 {
newrunstate = RunState::MonsterTurn;
} else {
newrunstate = RunState::MagicMapReveal{ row: row+1 };
}
}

This is pretty straightforward: it reveals the tiles on the current row, and then if we
haven't hit the bottom of the map - it adds to row. If we have, it returns to where we
were - MonsterTurn . If you cargo run now, �nd a magic mapping scroll and use it,
the map fades in nicely:

273 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Remember to lower the spawn priority!


In spawners.rs we are currently spawning magic mapping scrolls everywhere. That's
probably not what we want! Edit the spawn table to have a much lower priority:

274 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
.add("Longsword", map_depth - 1)
.add("Tower Shield", map_depth - 1)
.add("Rations", 10)
.add("Magic Mapping Scroll", 2)
}

Wrap Up
This was a relatively quick chapter, but we now have another staple of the roguelike
genre: magic mapping.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.

REX Paint Main Menu

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

275 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Our main menu is really boring, and not a good way to attract players! This chapter
will spice it up a bit.

REX Paint
Grid Sage Games (the amazing u/Kyzrati on Reddit) provide a lovely tool for Codepage
437 image editing called REX Paint. RLTK has built-in support for using the output
from this editor. As they used to say on the BBC's old kids show Blue Peter - here's one
I made earlier.

I cheated a bit; I found a CC0 image, resized it to 80x50 in the GIMP, and used a tool I
wrote years ago to convert the PNG to a REX Paint �le. Still, I like the result. You can
�nd the REX Paint �le in the resources folder.

Loading REX Assets

276 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll introduce a new �le, rex_assets.rs to store our REX sprites. The �le looks like
this:

use rltk::{rex::XpFile};

rltk::embedded_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");

pub struct RexAssets {


pub menu : XpFile
}

impl RexAssets {
#[allow(clippy::new_without_default)]
pub fn new() -> RexAssets {
rltk::link_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");

RexAssets{
menu : XpFile::from_resource("../../resources
/SmallDungeon_80x50.xp").unwrap()
}
}
}

Very simple - it de�nes a structure, and loads the dungeon graphic into it when new is
called. We'll also insert it into Specs as a resource so we can access our sprites
anywhere. There are some new concepts here:

1. We're using rltk::embedded_resource! to include the �le in our binary. This


gets around having to ship the binary with your executable (and makes life
easier in wasm land).
2. #[allow(clippy::new_without_default)] tells the linter to stop telling me to
write a default implementation, when we don't need one!
3. rltk::link_resource! is the second-half the the embedded resource; the �rst
stores it in memory, this one tells RLTK where to �nd it.
menu : XpFile::from_resource("../../resources
4.
/SmallDungeon_80x50.xp").unwrap()
loads the Rex paint �le from memory.

In main.rs :

277 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

gs.ecs.insert(rex_assets::RexAssets::new());

Now we open up gui.rs and �nd the main_menu function. We'll add two lines before
we start printing menu content:

let assets = gs.ecs.fetch::<RexAssets>();


ctx.render_xp_sprite(&assets.menu, 0, 0);

The result ( cargo run to see it) is a good start at a menu!

Improving the look of the menu - adding a box and


borders
To make it look a little snazzier, we'll work on spacing - and add a box for the menu
and text. Replace the current title rendering code with:

278 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

ctx.draw_box_double(24, 18, 31, 10, RGB::named(rltk::WHEAT),


RGB::named(rltk::BLACK));
ctx.print_color_centered(20, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");
ctx.print_color_centered(21, RGB::named(rltk::CYAN),
RGB::named(rltk::BLACK), "by Herbert Wolverson");
ctx.print_color_centered(22, RGB::named(rltk::GRAY),
RGB::named(rltk::BLACK), "Use Up/Down Arrows and Enter");

If you cargo run now, your menu looks like this:

That's quite a bit better!

Fixing the spacing


You'll notice that if you don't have a saved game to load, there is an annoying gap
between menu items. This is an easy �x, by keeping track of the y position we have
used while we render the menu. Here's the new menu rendering code:

279 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut y = 24;


if let RunState::MainMenu{ menu_selection : selection } = *runstate {
if selection == MainMenuSelection::NewGame {
ctx.print_color_centered(y, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Begin New Game");
} else {
ctx.print_color_centered(y, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Begin New Game");
}
y += 1;

if save_exists {
if selection == MainMenuSelection::LoadGame {
ctx.print_color_centered(y, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Load Game");
} else {
ctx.print_color_centered(y, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Load Game");
}
y += 1;
}

if selection == MainMenuSelection::Quit {
ctx.print_color_centered(y, RGB::named(rltk::MAGENTA),
RGB::named(rltk::BLACK), "Quit");
} else {
ctx.print_color_centered(y, RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK), "Quit");
}
...

If you cargo run now, it looks better:

280 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Simple Traps

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

281 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Most roguelikes, like their D&D precursors, feature traps in the dungeon. Walk down
an innocent looking hallway, and oops - an arrow �ies out and hits you. This chapter
will implement some simple traps, and then examine some of the game implications
they bring.

What is a trap?
Most traps follow the pattern of: you might see the trap (or you might not!), you enter
the tile anyway, the trap goes o� and something happens (damage, teleport, etc.). So
traps can be logically divided into three sections:

An appearance (which we already support), which may or may not be discovered


(which we don't, yet).
A trigger - if you enter the trap's tile, something happens.
An e�ect - which we've touched on with magic items.

Let's work our way through getting components into place for these, in turn.

Rendering a basic bear trap


A lot of roguelikes use ^ for a trap, so we'll do the same. We have all the components
required to render a basic object, so we'll make a new spawning function (in
spawners.rs ). It's pretty much the minimum to put a glyph on the map:

282 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn bear_trap(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('^'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Bear Trap".to_string() })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

We'll also add it into the list of things that can spawn:

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
.add("Longsword", map_depth - 1)
.add("Tower Shield", map_depth - 1)
.add("Rations", 10)
.add("Magic Mapping Scroll", 2)
.add("Bear Trap", 2)
}

283 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Rations" => rations(ecs, x, y),
"Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
"Bear Trap" => bear_trap(ecs, x, y),
_ => {}
}

If you cargo run the project now, occasionally you will run into a red ^ - and it will
be labeled "Bear Trap" on the mouse-over. Not massively exciting, but a good start!
Note that for testing, we'll up the spawn frequency from 2 to 100 - LOTS of traps,
making debugging easier. Remember to lower it later!

But you don't always spot the trap!


It is pretty easy if you can always know that a trap awaits you! So we want to make
traps hidden by default, and come up with a way to sometimes locate traps when you
are near them. Like most things in an ECS driven world, analyzing the text gives a
great clue as to what components you need. In this case, we need to go into
components.rs and create a new component - Hidden :

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct Hidden {}

As usual, we need to register it in main.rs and in saveload_system.rs . We'll also


give the property to our new bear trap:

284 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn bear_trap(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('^'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Bear Trap".to_string() })
.with(Hidden{})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

Now, we want to modify the object renderer to not show things that are hidden. The
Specs Book provides a great clue as to how to exclude a component from a join, so we
do that (in main.rs ):

let mut data = (&positions, &renderables, !&hidden).join().collect::


<Vec<_>>();
data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
for (pos, render, _hidden) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg,
render.bg, render.glyph) }
}

Notice that we've added a ! ("not" symbol) to the join - we're saying that entities
must not have the Hidden component if we are to render them.

If you cargo run the project now, the bear traps are no longer visible. However, they
show up in tool tips (which may be perhaps as well, we know they are there!). We'll
exclude them from tool-tips also. In gui.rs , we amend the draw_tooltips function:

285 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {


let map = ecs.fetch::<Map>();
let names = ecs.read_storage::<Name>();
let positions = ecs.read_storage::<Position>();
let hidden = ecs.read_storage::<Hidden>();

let mouse_pos = ctx.mouse_pos();


if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; }
let mut tooltip : Vec<String> = Vec::new();
for (name, position, _hidden) in (&names, &positions, !&hidden).join()
{
if position.x == mouse_pos.0 && position.y == mouse_pos.1 {
tooltip.push(name.name.to_string());
}
}
...

Now if you cargo run , you'll have no idea that traps are present. Since they don't do
anything yet - they may as well not exist!

Adding entry triggers


A trap should trigger when an entity walks onto them. So in components.rs , we'll
create an EntryTrigger (as usual, we'll also register it in main.rs and
saveload_system.rs ):

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct EntryTrigger {}

We'll give bear traps a trigger (in spawner.rs ):

286 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn bear_trap(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('^'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Bear Trap".to_string() })
.with(Hidden{})
.with(EntryTrigger{})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

We also need to have traps �re their trigger when an entity enters them. We'll add
another component, EntityMoved to indicate that an entity has moved this turn. In
components.rs (and remembering to register in main.rs and saveload_system.rs ):

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct EntityMoved {}

Now, we scour the codebase to add an EntityMoved component every time an entity
moves. In player.rs , we handle player movement in the try_move_player function.
At the top, we'll gain write access to the relevant component store:

let mut entity_moved = ecs.write_storage::<EntityMoved>();

Then when we've determined that the player did, in fact, move - we'll insert the
EntityMoved component:

entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert


marker");

The other location that features movement is the Monster AI. So in


monster_ai_system.rs , we do something similar. We add a WriteResource for the
EntityMoved component, and insert one after the monster moves. The source code

287 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for the AI is getting a bit long, so I recommend you look at the source �le directly for
this one (here).

Lastly, we need a system to make triggers actually do something. We'll make a new �le,
trigger_system.rs :

288 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{EntityMoved, Position, EntryTrigger, Hidden, Map, Name,
gamelog::GameLog};

pub struct TriggerSystem {}

impl<'a> System<'a> for TriggerSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Map>,
WriteStorage<'a, EntityMoved>,
ReadStorage<'a, Position>,
ReadStorage<'a, EntryTrigger>,
WriteStorage<'a, Hidden>,
ReadStorage<'a, Name>,
Entities<'a>,
WriteExpect<'a, GameLog>);

fn run(&mut self, data : Self::SystemData) {


let (map, mut entity_moved, position, entry_trigger, mut hidden,
names, entities, mut log) = data;

// Iterate the entities that moved and their final position


for (entity, mut _entity_moved, pos) in (&entities, &mut
entity_moved, &position).join() {
let idx = map.xy_idx(pos.x, pos.y);
for entity_id in map.tile_content[idx].iter() {
if entity != *entity_id { // Do not bother to check
yourself for being a trap!
let maybe_trigger = entry_trigger.get(*entity_id);
match maybe_trigger {
None => {},
Some(_trigger) => {
// We triggered it
let name = names.get(*entity_id);
if let Some(name) = name {
log.entries.insert(0, format!("{}
triggers!", &name.name));
}

hidden.remove(*entity_id); // The trap is no


longer hidden
}
}
}

289 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}

// Remove all entity movement markers


entity_moved.clear();
}
}

This is relatively straightforward if you've been through the previous chapters:

1. We iterate all entities that have a Position and an EntityMoved component.


2. We obtain the map index for their location.
3. We iterate the tile_content index to see what's in the new tile.
4. We look to see if there is a trap there.
5. If there is, we get its name and notify the player (via the log) that a trap activated.
6. We remove the hidden component from the trap, since we now know that it is
there.

We also have to go into main.rs and insert code to run the system. It goes after the
Monster AI, since monsters can move - but we might output damage, so that system
needs to run later:

...
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
let mut triggers = trigger_system::TriggerSystem{};
triggers.run_now(&self.ecs);
...

Traps that hurt


So that gets us a long way: traps can be sprinkled around the level, and trigger when
you enter their target tile. It would help if the trap did something! We actually have a
decent number of component types to describe the e�ect. In spawner.rs , we'll
extend the bear trap to include some damage:

290 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn bear_trap(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('^'),
fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Bear Trap".to_string() })
.with(Hidden{})
.with(EntryTrigger{})
.with(InflictsDamage{ damage: 6 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

We'll also extend the trigger_system to apply the damage:

// If the trap is damage inflicting, do it


let damage = inflicts_damage.get(*entity_id);
if let Some(damage) = damage {
particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE),
rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0);
inflict_damage.insert(entity, SufferDamage{ amount: damage.damage
}).expect("Unable to do damage");
}

If you cargo run now, you can move around - and walking into a trap will damage
you. If a monster walks into a trap, it damages them too! It even plays the particle
e�ect for attacking.

Bear traps only snap once


Some traps, like a bear trap (think a spring with spikes) really only �re once. That
seems like a useful property to model for our trigger system, so we'll add a new
component (to components.rs , main.rs and saveload_system.rs ):

291 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct SingleActivation {}

We'll also add it to the Bear Trap function in spawner.rs :

.with(SingleActivation{})

Now we modify the trigger_system to apply it. Note that we remove the entities
after looping through them, to avoid confusing our iterators.

292 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{EntityMoved, Position, EntryTrigger, Hidden, Map, Name,
gamelog::GameLog,
InflictsDamage, particle_system::ParticleBuilder, SufferDamage,
SingleActivation};

pub struct TriggerSystem {}

impl<'a> System<'a> for TriggerSystem {


#[allow(clippy::type_complexity)]
type SystemData = ( ReadExpect<'a, Map>,
WriteStorage<'a, EntityMoved>,
ReadStorage<'a, Position>,
ReadStorage<'a, EntryTrigger>,
WriteStorage<'a, Hidden>,
ReadStorage<'a, Name>,
Entities<'a>,
WriteExpect<'a, GameLog>,
ReadStorage<'a, InflictsDamage>,
WriteExpect<'a, ParticleBuilder>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, SingleActivation>);

fn run(&mut self, data : Self::SystemData) {


let (map, mut entity_moved, position, entry_trigger, mut hidden,
names, entities, mut log, inflicts_damage, mut
particle_builder,
mut inflict_damage, single_activation) = data;

// Iterate the entities that moved and their final position


let mut remove_entities : Vec<Entity> = Vec::new();
for (entity, mut _entity_moved, pos) in (&entities, &mut
entity_moved, &position).join() {
let idx = map.xy_idx(pos.x, pos.y);
for entity_id in map.tile_content[idx].iter() {
if entity != *entity_id { // Do not bother to check
yourself for being a trap!
let maybe_trigger = entry_trigger.get(*entity_id);
match maybe_trigger {
None => {},
Some(_trigger) => {
// We triggered it
let name = names.get(*entity_id);
if let Some(name) = name {

293 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

log.entries.insert(0, format!("{}
triggers!", &name.name));
}

hidden.remove(*entity_id); // The trap is no


longer hidden

// If the trap is damage inflicting, do it


let damage = inflicts_damage.get(*entity_id);
if let Some(damage) = damage {
particle_builder.request(pos.x, pos.y,
rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK),
rltk::to_cp437('‼'), 200.0);
inflict_damage.insert(entity,
SufferDamage{ amount: damage.damage }).expect("Unable to do damage");
}

// If it is single activation, it needs to be


removed
let sa = single_activation.get(*entity_id);
if let Some(_sa) = sa {
remove_entities.push(*entity_id);
}
}
}
}
}
}

// Remove any single activation traps


for trap in remove_entities.iter() {
entities.delete(*trap).expect("Unable to delete trap");
}

// Remove all entity movement markers


entity_moved.clear();
}
}

If you cargo run now (I recommend cargo run --release - it's getting slower!), you
can be hit by a bear trap - take some damage, and the trap goes away.

Spotting Traps

294 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We have a pretty functional trap system now, but it's annoying to randomly take
damage for no apparent reason - because you had no way to know that a trap was
there. It's also quite unfair, since there's no way to guard against it. We'll implement a
chance to spot traps. At some point in the future, this might be tied to an attribute or
skill - but for now, we'll go with a dice roll. That's a bit nicer than asking everyone to
carry a 10 foot pole with them at all times (like some early D&D games!).

Since the visibility_system already handles revealing tiles, why not make it
potentially reveal hidden things, too? Here's the code for visibility_system.rs :

295 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

extern crate specs;


use specs::prelude::*;
use super::{Viewshed, Position, Map, Player, Hidden, gamelog::GameLog};
extern crate rltk;
use rltk::{field_of_view, Point};

pub struct VisibilitySystem {}

impl<'a> System<'a> for VisibilitySystem {


#[allow(clippy::type_complexity)]
type SystemData = ( WriteExpect<'a, Map>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Position>,
ReadStorage<'a, Player>,
WriteStorage<'a, Hidden>,
WriteExpect<'a, rltk::RandomNumberGenerator>,
WriteExpect<'a, GameLog>,
ReadStorage<'a, Name>,);

fn run(&mut self, data : Self::SystemData) {


let (mut map, entities, mut viewshed, pos, player,
mut hidden, mut rng, mut log, names) = data;

for (ent,viewshed,pos) in (&entities, &mut viewshed, &pos).join()


{
if viewshed.dirty {
viewshed.dirty = false;
viewshed.visible_tiles = field_of_view(Point::new(pos.x,
pos.y), viewshed.range, &*map);

// If this is the player, reveal what they can see


let _p : Option<&Player> = player.get(ent);
if let Some(_p) = _p {
for t in map.visible_tiles.iter_mut() { *t = false };
for vis in viewshed.visible_tiles.iter() {
let idx = map.xy_idx(vis.x, vis.y);
map.revealed_tiles[idx] = true;
map.visible_tiles[idx] = true;

// Chance to reveal hidden things


for e in map.tile_content[idx].iter() {
let maybe_hidden = hidden.get(*e);
if let Some(_maybe_hidden) = maybe_hidden {
if rng.roll_dice(1,24)==1 {

296 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let name = names.get(*e);


if let Some(name) = name {
log.entries.insert(0, format!("You
spotted a {}.", &name.name));
}
hidden.remove(*e);
}
}
}
}
}
}
}
}
}

So why a 1 in 24 chance to spot traps? I played around until it felt about right. 1 in 6
(my �rst choice) was too good. Since your viewshed updates whenever you move, you
have a high chance of spotting traps as you move around. Like a lot of things in game
design: sometimes you just have to play with it until it feels right!

If you cargo run now, you can walk around - and sometimes spot traps. Monsters
won't reveal traps, unless they fall into them.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Section 3 - Procedurally Generating


Maps

About this tutorial

297 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

This started out as part of section 2, but I realized it was a large, open topic. The larger
roguelike games, such as Dungeon Crawl Stone Soup, Cogmind, Caves of Qud, etc. all
have a variety of maps. Section 3 is all about map building, and will cover many of the
available algorithms for procedurally building interesting maps.

Copyright (C) 2019, Herbert Wolverson.

Refactor: Generic Map Interface

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

So far, we've really just had one map design. It's di�erent every time (unless you hit a
repeat random seed), which is a great start - but the world of procedural generation
leaves so many more possibilities. Over the next few chapters, we'll start building a
few di�erent map types.

Refactoring the builder - De�ning an Interface


Up until now, all of our map generation code has sat in the map.rs �le. That's �ne for
a single style, but what if we want to have lots of styles? This is the perfect time to
create a proper builder system! If you look at the map generation code in main.rs ,
we have the beginnings of an interface de�ned:

298 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We call Map::new_map_rooms_and_corridors , which builds a set of rooms.


We pass that to spawner::spawn_room to populate each room.
We then place the player in the �rst room.

To better organize our code, we'll make a module. Rust lets you make a directory, with
a �le in it called mod.rs - and that directory is now a module. Modules are exposed
through mod and pub mod , and provide a way to keep parts of your code together.
The mod.rs �le provides an interface - that is, a list of what is provided by the module,
and how to interact with it. Other �les in the module can do whatever they want,
safely isolated from the rest of the code.

So, we'll create a directory (o� of src ) called map_builders . In that directory, we'll
create an empty �le called mod.rs . We're trying to de�ne an interface, so we'll start
with a skeleton. In mod.rs :

use super::Map;

trait MapBuilder {
fn build(new_depth: i32) -> Map;
}

The use of trait is new! A trait is like an interface in other languages: you are saying
that any other type can implement the trait, and can then be treated as a variable of
that type. Rust by Example has a great section on traits, as does The Rust Book. What
we're stating is that anything can declare itself to be a MapBuilder - and that includes
a promise that they will provide a build function that takes in an ECS World object,
and returns a map.

Open up map.rs , and add a new function - called, appropriately enough, new :

299 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

/// Generates an empty map, consisting entirely of solid walls


pub fn new(new_depth : i32) -> Map {
Map{
tiles : vec![TileType::Wall; MAPCOUNT],
rooms : Vec::new(),
width : MAPWIDTH as i32,
height: MAPHEIGHT as i32,
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
tile_content : vec![Vec::new(); MAPCOUNT],
depth: new_depth,
bloodstains: HashSet::new()
}
}

We'll need this for other map generators, and it makes sense for a Map to know how
to return a new one as a constructor - without having to encapsulate all the logic for
map layout. The idea is that any Map will work basically the same way, irrespective of
how we've decided to populate it.

Now we'll create a new �le, also inside the map_builders directory. We'll call it
simple_map.rs - and it'll be where we put the existing map generation system. We'll
also put a skeleton in place here:

use super::MapBuilder;
use super::Map;
use specs::prelude::*;

pub struct SimpleMapBuilder {}

impl MapBuilder for SimpleMapBuilder {


fn build(new_depth: i32) -> Map {
Map::new(new_depth)
}
}

This simply returns an unusable, solid map. We'll �esh out the details in a bit - lets get
the interface working, �rst.

Now, back in map_builders/mod.rs we add a public function. For now, it just calls the

300 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

builder in SimpleMapBuilder :

pub fn build_random_map(new_depth: i32) -> Map {


SimpleMapBuilder::build(new_depth)
}

Finally, we'll tell main.rs to actually include the module:

pub mod map_builders;

Ok, so that was a fair amount of work to not actually do anything - but we've gained a
clean interface o�ering map creation (via a single function), and setup a trait to
require that our map builders work in a similar fashion. That's a good start.

Fleshing out the Simple Map Builder


Now we start moving functionality out of map.rs into our SimpleMapBuilder . We'll
start by adding another �le to map_builders - common.rs . This will hold functions that
used to be part of the map, and are now commonly used when building.

The �le looks like this:

301 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{Map, Rect, TileType};


use std::cmp::{max, min};

pub fn apply_room_to_map(map : &mut Map, room : &Rect) {


for y in room.y1 +1 ..= room.y2 {
for x in room.x1 + 1 ..= room.x2 {
let idx = map.xy_idx(x, y);
map.tiles[idx] = TileType::Floor;
}
}
}

pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) {


for x in min(x1,x2) ..= max(x1,x2) {
let idx = map.xy_idx(x, y);
if idx > 0 && idx < map.width as usize * map.height as usize {
map.tiles[idx as usize] = TileType::Floor;
}
}
}

pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) {


for y in min(y1,y2) ..= max(y1,y2) {
let idx = map.xy_idx(x, y);
if idx > 0 && idx < map.width as usize * map.height as usize {
map.tiles[idx as usize] = TileType::Floor;
}
}
}

These are exactly the same as the functions from map.rs , but with map passed as a
mutable reference (so you are working on the original, rather than a new one) and all
vestiges of self gone. These are free functions - that is, they are functions available
from anywhere, not tied to a type. The pub fn means they are public within the
module - unless we add pub use to the module itself, they aren't passed out of the
module to the main program. This helps keeps code organized.

Now that we have these helpers, we can start porting the map builder itself. In
simple_map.rs , we start by �eshing out the build function a bit:

302 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl MapBuilder for SimpleMapBuilder {


fn build(new_depth: i32) -> Map {
let mut map = Map::new(new_depth);
SimpleMapBuilder::rooms_and_corridors(&mut map);
map
}
}

We're calling a new function, rooms_and_corridors . Lets build it:

303 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl SimpleMapBuilder {
fn rooms_and_corridors(map : &mut Map) {
const MAX_ROOMS : i32 = 30;
const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;

let mut rng = RandomNumberGenerator::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, map.width - w - 1) - 1;
let y = rng.roll_dice(1, map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in map.rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
apply_room_to_map(map, &new_room);

if !map.rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = map.rooms[map.rooms.len()-
1].center();
if rng.range(0,1) == 1 {
apply_horizontal_tunnel(map, prev_x, new_x,
prev_y);
apply_vertical_tunnel(map, prev_y, new_y, new_x);
} else {
apply_vertical_tunnel(map, prev_y, new_y, prev_x);
apply_horizontal_tunnel(map, prev_x, new_x,
new_y);
}
}

map.rooms.push(new_room);
}
}

let stairs_position = map.rooms[map.rooms.len()-1].center();


let stairs_idx = map.xy_idx(stairs_position.0, stairs_position.1);
map.tiles[stairs_idx] = TileType::DownStairs;
}
}

304 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You'll notice that this is built as a method attached to the SimpleMapBuilder


structure. It isn't part of the trait, so we can't de�ne it there - but we want to keep it
separated from other builders, which might have their own functions. The code itself
should look eerily familiar: it's the same as the generator in map.rs , but with map as
a variable rather than being generated inside the function.

This is only the �rst half of generation, but it's a good start! Now go to map.rs , and
delete the entire new_map_rooms_and_corridors function. Also delete the ones we
replicated in common.rs . The map.rs �le looks much cleaner now, without any
references to map building strategy! Of course, your compiler/IDE is probably telling
you that we've broken a bunch of stu�. That's ok - and a normal part of "refactoring" -
the process of changing code to be easier to work with.

There are three lines in main.rs that are now �agged by the compiler.

We can replace
*worldmap_resource = Map::new_map_rooms_and_corridors(current_depth +
1);
with
*worldmap_resource = map_builders::build_random_map(current_depth +
1);
.
*worldmap_resource = Map::new_map_rooms_and_corridors(1); can become
*worldmap_resource = map_builders::build_random_map(1); .
let map : Map = Map::new_map_rooms_and_corridors(1); transforms to
let map : Map = map_builders::build_random_map(1); .

If you cargo run now, you'll notice: the game is exactly the same! That's good: we've
successfully refactored functionality out of Map and into map_builders .

Placing the Player


If you look in main.rs , pretty much every time we build a map - we then look for the
�rst room, and use it to place the player. It's quite possible that we won't want to use
the same strategy in future maps, so we should indicate where the player goes when
we build the map. Lets expand our interface in map_builders/mod.rs to also return a
position:

305 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

trait MapBuilder {
fn build(new_depth: i32) -> (Map, Position);
}

pub fn build_random_map(new_depth: i32) -> (Map, Position) {


SimpleMapBuilder::build(new_depth)
}

Notice that we're using a tuple to return two values at once. We've talked about those
earlier, but this is a great example of why they are useful! We now need to go into
simple_map to make the build function actually return the correct data. The
de�nition of build in simple_map.rs now looks like this:

fn build(new_depth: i32) -> (Map, Position) {


let mut map = Map::new(new_depth);
let playerpos = SimpleMapBuilder::rooms_and_corridors(&mut map);
(map, playerpos)
}

We'll update the signature of rooms_and_corridors :

fn rooms_and_corridors(map : &mut Map) -> Position {

And we'll add a last line to return the center of room 0:

let start_pos = map.rooms[0].center();


Position{ x: start_pos.0, y: start_pos.1 }

This has, of course, broken the code we updated in main.rs . We can quickly take care
of that! The �rst error can be taken care of with the following code:

306 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Build a new map and place the player


let worldmap;
let current_depth;
let player_start;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
current_depth = worldmap_resource.depth;
let (newmap, start) = map_builders::build_random_map(current_depth +
1);
*worldmap_resource = newmap;
player_start = start;
worldmap = worldmap_resource.clone();
}

// Spawn bad guys


for room in worldmap.rooms.iter().skip(1) {
spawner::spawn_room(&mut self.ecs, room, current_depth+1);
}

// Place the player and update resources


let (player_x, player_y) = (player_start.x, player_start.y);

Notice how we use destructuring to retrieve both the map and the start position from
the builder. We then put these in the appropriate places. Since assignment in Rust is a
move operation, this is pretty e�cient - and the compiler can get rid of temporary
assignments for us.

We do the same again on the second error (around line 369). It's almost exactly the
same code, so feel free to check the source code for this chapter if you are stuck.

Lastly, the �nal error can be simply replaced like this:

let (map, player_start) = map_builders::build_random_map(1);


let (player_x, player_y) = (player_start.x, player_start.y);

Alright, lets cargo run that puppy! If all went well, then... nothing has changed. We've
made a signi�cant gain, however: our map building strategy now determines the
player's starting point on a level, not the map itself.

307 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Cleaning up room spawning


It's quite possible that we won't have the concept of rooms in some map designs, so
we also want to move spawning to be a function of the map builder. We'll add a
generic spawner to the interface in map_builders/mod.rs :

trait MapBuilder {
fn build(new_depth: i32) -> (Map, Position);
fn spawn(map : &Map, ecs : &mut World, new_depth: i32);
}

Simple enough: it requires the ECS (since we're adding entities) and the map. We'll
also add a public function, spawn to provide an external interface to layout out the
monsters:

pub fn spawn(map : &mut Map, ecs : &mut World, new_depth: i32) {


SimpleMapBuilder::spawn(map, ecs, new_depth);
}

Now we open simple_map.rs and actually implement spawn . Fortunately, it's very
simple:

fn spawn(map : &mut Map, ecs : &mut World) {


for room in map.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, 1);
}
}

Now, we can go into main.rs and �nd every time we loop through calling
spawn_room and replace it with a call to map_builders::spawn .

Once again, cargo run should give you the same game we've been looking at for 22
chapters!

Maintaining builder state

308 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you look closely at what we have so far, there's one problem: the builder has no way
of knowing what should be used for the second call to the builder (spawning things).
That's because our functions are stateless - we don't actually create a builder and give
it a way to remember anything. Since we want to support a wide variety of builders,
we should correct that.

This introduces a new Rust concept: dynamic dispatch. The Rust Book has a good
section on this if you are familiar with the concept. If you've previously used an Object
Oriented Programming language, then you will have encountered this also. The basic
idea is that you have a "base object" that speci�es an interface - and multiple objects
implement the functions from the interface. You can then, at run-time (when the
program runs, rather than when it compiles) put any object that implements the
interface into a variable typed by the interface - and when you call the methods from
the interface, the implementation runs from the actual type. This is nice because your
underlying program doesn't have to know about the actual implementations - just
how to talk to the interface. That helps keep your program clean.

Dynamic dispatch does come with a cost, which is why Entity Component Systems
(and Rust in general) prefer not to use it for performance-critical code. There's actually
two costs:

1. Since you don't know what type the object is up-front, you have to allocate it via
a pointer. Rust makes this easy by providing the Box system (more on that in a
moment), but there is a cost: rather than just jumping to a readily de�ned piece
of memory (which your CPU/memory can generally �gure out easily in advance
and make sure the cache is ready) the code has to follow the pointer - and then
run what it �nds at the end of the pointer. That's why some C++ programmers
call -> (dereference operator) the "cache miss operator". Simply by being
boxed, your code is slowed down by a tiny amount.
2. Since multiple types can implement methods, the computer needs to know
which one to run. It does this with a vtable - that is, a "virtual table" of method
implementations. So each call has to check the table, �nd out which method to
run, and then run from there. That's another cache miss, and more time for your
CPU to �gure out what to do.

In this case, we're just generating the map - and making very few calls into the builder.
That makes the slowdown acceptable, since it's really small and not being run
frequently. You wouldn't want to do this in your main loop, if you can avoid it!

So - implementation. We'll start by changing our trait to be public, and have the

309 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

methods accept an &mut self - which means "this method is a member of the trait,
and should receive access to self - the attached object when we call it. The code
looks like this:

pub trait MapBuilder {


fn build_map(&mut self, new_depth: i32) -> (Map, Position);
fn spawn_entities(&mut self, map : &Map, ecs : &mut World, new_depth:
i32);
}

Notice that I've also taken the time to make the names a bit more descriptive! Now we
replace our free function calls with a factory function: it creates a MapBuilder and
returns it. The name is a bit of a lie until we have more map implementations - it
claims to be random, but when there's only one choice it's not hard to guess which
one it will pick (just ask Soviet election systems!):

pub fn random_builder() -> Box<dyn MapBuilder> {


// Note that until we have a second map type, this isn't even slighlty
random
Box::new(SimpleMapBuilder{})
}

Notice that it doesn't return a MapBuilder - rather it returns a Box<dyn MapBuilder> !


That's rather convoluted (and in earlier versions of Rust, the dyn is optional). A Box is
a type wrapped in a pointer, whose size may not be known at compile time. It's the
same as a C++ MapBuilder * - it points to a MapBuilder rather than actually being
one. The dyn is a �ag to say "this should use dynamic dispatch"; the code will work
without it (it will be inferred), but it's good practice to �ag that you are doing
something complicated/expensive here.

The function simply returns Box::new(SimpleMapBuilder{}) . This is actually two calls,


now: we make a box with Box::new(...) , and we place an empty SimpleMapBuilder
into the box.

Over in main.rs , we once again have to change all three calls to the map builder. We
now need to use the following pattern:

1. Obtain a boxed MapBuilder object, from the factory.


2. Call build_map as a method - that is, a function attached to the object.

310 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

3. Call spawn_entities also as a method.

The implementation from goto_next_level now reads as follows:

// Build a new map and place the player


let mut builder = map_builders::random_builder(current_depth + 1);
let worldmap;
let current_depth;
let player_start;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
current_depth = worldmap_resource.depth;
let (newmap, start) = builder.build_map(current_depth + 1);
*worldmap_resource = newmap;
player_start = start;
worldmap = worldmap_resource.clone();
}

// Spawn bad guys


builder.spawn_entities(&worldmap, &mut self.ecs, current_depth+1);

It's not very di�erent, but now we're keeping the builder object around - so
subsequent calls to the builder will apply to the same implementation (sometimes
called "concrete object" - the object that actually physically exists).

If we were to add 5 more map builders, the code in main.rs wouldn't care! We can
add them to the factory, and the rest of the program is blissfully unaware of the
workings of the map builder. This is a very good example of how dynamic dispatch
can be useful: you have a clearly de�ned interface, and the rest of the program
doesn't need to understand the inner workings.

Adding a constructor to SimpleMapBuilder


We're currently making a SimpleMapBuilder as an empty object. What if it needs to
keep track of some data? In case we need it, lets add a simple constructor to it and
use that instead of a blank object. In simple_map.rs , modify the struct
implementation as follows:

311 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl SimpleMapBuilder {
pub fn new(new_depth : i32) -> SimpleMapBuilder {
SimpleMapBuilder{}
}
...

That simply returns an empty object for now. In mod.rs , change the
random_map_builder function to use it:

pub fn random_builder(new_depth : i32) -> Box<dyn MapBuilder> {


// Note that until we have a second map type, this isn't even slighlty
random
Box::new(SimpleMapBuilder::new(new_depth))
}

This hasn't gained us anything, but is a bit cleaner - when you write more maps, they
may do something in their constructors!

Cleaning up the trait - simple, obvious steps and


single return types
Now that we've come this far, lets extend the trait a bit to obtain the player's position
in one function, the map in another, and build/spawn separately. Using small
functions tends to make the code easier to read, which is a worthwhile goal in and of
itself. In mod.rs , we change the interface as follows:

pub trait MapBuilder {


fn build_map(&mut self);
fn spawn_entities(&mut self, ecs : &mut World);
fn get_map(&mut self) -> Map;
fn get_starting_position(&mut self) -> Position;
}

There's a few things to note here:

1. build_map no longer returns anything at all. We're using it as a function to build

312 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map state.
2. spawn_entities no longer asks for a Map parameter. Since all map builders
have to implement a map in order to make sense, we're going to assume that
the map builder has one.
3. get_map returns a map. Again, we're assuming that the builder implementation
keeps one.
4. get_starting_position also assumes that the builder will keep one around.

Obviously, our SimpleMapBuilder now needs to be modi�ed to work this way. We'll
start by modifying the struct to include the required variables. This is the map
builder's state - and since we're doing dynamic object-oriented code, the state
remains attached to the object. Here's the code from simple_map.rs :

pub struct SimpleMapBuilder {


map : Map,
starting_position : Position,
depth: i32
}

Next, we'll implement the getter functions. These are very simple: they simply return
the variables from the structure's state:

impl MapBuilder for SimpleMapBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}
...

We'll also update the constructor to create the state:

313 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new(new_depth : i32) -> SimpleMapBuilder {


SimpleMapBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth
}
}

This also simpli�es build_map and spawn_entities :

fn build_map(&mut self) {
SimpleMapBuilder::rooms_and_corridors();
}

fn spawn_entities(&mut self, ecs : &mut World) {


for room in self.map.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, self.depth);
}
}

Lastly, we need to modify rooms_and_corridors to work with this interface:

314 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn rooms_and_corridors(&mut self) {
const MAX_ROOMS : i32 = 30;
const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;

let mut rng = RandomNumberGenerator::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, self.map.width - w - 1) - 1;
let y = rng.roll_dice(1, self.map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in self.map.rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
apply_room_to_map(&mut self.map, &new_room);

if !self.map.rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) =
self.map.rooms[self.map.rooms.len()-1].center();
if rng.range(0,1) == 1 {
apply_horizontal_tunnel(&mut self.map, prev_x, new_x,
prev_y);
apply_vertical_tunnel(&mut self.map, prev_y, new_y,
new_x);
} else {
apply_vertical_tunnel(&mut self.map, prev_y, new_y,
prev_x);
apply_horizontal_tunnel(&mut self.map, prev_x, new_x,
new_y);
}
}

self.map.rooms.push(new_room);
}
}

let stairs_position = self.map.rooms[self.map.rooms.len()-1].center();


let stairs_idx = self.map.xy_idx(stairs_position.0,
stairs_position.1);
self.map.tiles[stairs_idx] = TileType::DownStairs;

315 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let start_pos = self.map.rooms[0].center();


self.starting_position = Position{ x: start_pos.0, y: start_pos.1 };
}

This is very similar to what we had before, but now uses self.map to refer to its own
copy of the map, and stores the player position in self.starting_position .

The calls into the new code in main.rs once again change. The call from
goto_next_level now looks like this:

let mut builder;


let worldmap;
let current_depth;
let player_start;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
current_depth = worldmap_resource.depth;
builder = map_builders::random_builder(current_depth + 1);
builder.build_map();
*worldmap_resource = builder.get_map();
player_start = builder.get_starting_position();
worldmap = worldmap_resource.clone();
}

// Spawn bad guys


builder.spawn_entities(&mut self.ecs);

We basically repeat those changes for the others (see the source). We now have a
pretty comfortable interface into the map builder: it exposes enough to be easy to
use, without exposing the details of the magic it uses to actually build the map!

If you cargo run the project now: once again, nothing visible has changed - it still
works the way it did before. When you are refactoring, that's a good thing!

So why do maps still have rooms?


Rooms don't actually do much in the game itself: they are an artifact of how we build
the map. It's quite possible that later map builders won't actually care about rooms, at

316 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

least not in the "here's a rectangle, we're calling a room" sense. Lets try and move that
abstraction out of the map, and also out of the spawner.

As a �rst step, in map.rs we remove the rooms structure completely:

#[derive(Default, Serialize, Deserialize, Clone)]


pub struct Map {
pub tiles : Vec<TileType>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>,
pub depth : i32,
pub bloodstains : HashSet<usize>,

#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content : Vec<Vec<Entity>>
}

We also remove it from the new function. Take a look at your IDE, and you'll notice
that you've only broken code in simple_map.rs ! We weren't using the rooms
anywhere else - which is a pretty big clue that they don't belong in the map we're
passing around throughout the main program.

We can �x simple_map by putting rooms into the builder rather than the map. We'll
put it into the structure:

pub struct SimpleMapBuilder {


map : Map,
starting_position : Position,
depth: i32,
rooms: Vec<Rect>
}

This requires that we �xup the constructor:

317 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new(new_depth : i32) -> SimpleMapBuilder {


SimpleMapBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
rooms: Vec::new()
}
}

The spawn function becomes:

fn spawn_entities(&mut self, ecs : &mut World) {


for room in self.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, self.depth);
}
}

And we replace every instance of map.rooms with self.rooms in


rooms_and_corridors :

318 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn rooms_and_corridors(&mut self) {
const MAX_ROOMS : i32 = 30;
const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;

let mut rng = RandomNumberGenerator::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, self.map.width - w - 1) - 1;
let y = rng.roll_dice(1, self.map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in self.rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
apply_room_to_map(&mut self.map, &new_room);

if !self.rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = self.rooms[self.rooms.len()-
1].center();
if rng.range(0,1) == 1 {
apply_horizontal_tunnel(&mut self.map, prev_x, new_x,
prev_y);
apply_vertical_tunnel(&mut self.map, prev_y, new_y,
new_x);
} else {
apply_vertical_tunnel(&mut self.map, prev_y, new_y,
prev_x);
apply_horizontal_tunnel(&mut self.map, prev_x, new_x,
new_y);
}
}

self.rooms.push(new_room);
}
}

let stairs_position = self.rooms[self.rooms.len()-1].center();


let stairs_idx = self.map.xy_idx(stairs_position.0,
stairs_position.1);
self.map.tiles[stairs_idx] = TileType::DownStairs;

319 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let start_pos = self.rooms[0].center();


self.starting_position = Position{ x: start_pos.0, y: start_pos.1 };
}

Once again, cargo run the project: and nothing should have changed.

Wrap-up
This was an interesting chapter to write, because the objective is to �nish with code
that operates exactly as it did before - but with the map builder cleaned into its own
module, completely isolated from the rest of the code. That gives us a great starting
point to start building new map builders, without having to change the game itself.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required). There isn't a lot of
point, since refactoring aims to not change the visible
result!
Copyright (C) 2019, Herbert Wolverson.

Map Construction Test Harness

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

320 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

As we're diving into generating new and interesting maps, it would be helpful to
provide a way to see what the algorithms are doing. This chapter will build a test
harness to accomplish this, and extend the SimpleMapBuilder from the previous
chapter to support it. This is going to be a relatively large task, and we'll learn some
new techniques along the way!

Cleaning up map creation - Do Not Repeat Yourself


In main.rs , we essentially have the same code three times. When the program starts,
we insert a map into the world. When we change level, or �nish the game - we do the
same. The last two have di�erent semantics (since we're updating the world rather
than inserting for the �rst time) - but it's basically redundant repetition.

We'll start by changing the �rst one to insert placeholder values rather than the actual
values we intend to use. This way, the World has the slots for the data - it just isn't all
that useful yet. Here's a version with the old code commented out:

gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());

gs.ecs.insert(Map::new(1));
gs.ecs.insert(Point::new(0, 0));
gs.ecs.insert(rltk::RandomNumberGenerator::new());

/*let mut builder = map_builders::random_builder(1);


builder.build_map();
let player_start = builder.get_starting_position();
let map = builder.get_map();
let (player_x, player_y) = (player_start.x, player_start.y);
builder.spawn_entities(&mut gs.ecs);
gs.ecs.insert(map);
gs.ecs.insert(Point::new(player_x, player_y));*/

let player_entity = spawner::player(&mut gs.ecs, 0, 0);


gs.ecs.insert(player_entity);

So instead of building the map, we put a placeholder into the World resources. That's
obviously not very useful for actually starting the game, so we also need a function to
do the actual building and update the resources. Not entirely coincidentally, that
function is the same as the other two places from which we currently update the map!

321 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

In other words, we can roll those into this function, too. So in the implementation of
State , we add:

fn generate_world_map(&mut self, new_depth : i32) {


let mut builder = map_builders::random_builder(new_depth);
builder.build_map();
let player_start;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
*worldmap_resource = builder.get_map();
player_start = builder.get_starting_position();
}

// Spawn bad guys


builder.spawn_entities(&mut self.ecs);

// Place the player and update resources


let (player_x, player_y) = (player_start.x, player_start.y);
let mut player_position = self.ecs.write_resource::<Point>();
*player_position = Point::new(player_x, player_y);
let mut position_components = self.ecs.write_storage::<Position>();
let player_entity = self.ecs.fetch::<Entity>();
let player_pos_comp = position_components.get_mut(*player_entity);
if let Some(player_pos_comp) = player_pos_comp {
player_pos_comp.x = player_x;
player_pos_comp.y = player_y;
}

// Mark the player's visibility as dirty


let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
let vs = viewshed_components.get_mut(*player_entity);
if let Some(vs) = vs {
vs.dirty = true;
}
}

Now we can get rid of the commented out code, and simplify our �rst call quite a bit:

322 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

gs.ecs.insert(Map::new(1));
gs.ecs.insert(Point::new(0, 0));
gs.ecs.insert(rltk::RandomNumberGenerator::new());
let player_entity = spawner::player(&mut gs.ecs, 0, 0);
gs.ecs.insert(player_entity);
gs.ecs.insert(RunState::MainMenu{ menu_selection:
gui::MainMenuSelection::NewGame });
gs.ecs.insert(gamelog::GameLog{ entries : vec!["Welcome to Rusty
Roguelike".to_string()] });
gs.ecs.insert(particle_system::ParticleBuilder::new());
gs.ecs.insert(rex_assets::RexAssets::new());

gs.generate_world_map(1);

We can also go to the various parts of the code that call the same code we just added
to generate_world_map and greatly simplify them by using the new function. We can
replace goto_next_level with:

323 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn goto_next_level(&mut self) {
// Delete entities that aren't the player or his/her equipment
let to_delete = self.entities_to_remove_on_level_change();
for target in to_delete {
self.ecs.delete_entity(target).expect("Unable to delete entity");
}

// Build a new map and place the player


let current_depth;
{
let worldmap_resource = self.ecs.fetch::<Map>();
current_depth = worldmap_resource.depth;
}
self.generate_world_map(current_depth + 1);

// Notify the player and give them some health


let player_entity = self.ecs.fetch::<Entity>();
let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
gamelog.entries.insert(0, "You descend to the next level, and take a
moment to heal.".to_string());
let mut player_health_store = self.ecs.write_storage::<CombatStats>();
let player_health = player_health_store.get_mut(*player_entity);
if let Some(player_health) = player_health {
player_health.hp = i32::max(player_health.hp, player_health.max_hp
/ 2);
}
}

Likewise, we can clean up game_over_cleanup :

324 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn game_over_cleanup(&mut self) {
// Delete everything
let mut to_delete = Vec::new();
for e in self.ecs.entities().join() {
to_delete.push(e);
}
for del in to_delete.iter() {
self.ecs.delete_entity(*del).expect("Deletion failed");
}

// Spawn a new player


{
let player_entity = spawner::player(&mut self.ecs, 0, 0);
let mut player_entity_writer = self.ecs.write_resource::
<Entity>();
*player_entity_writer = player_entity;
}

// Build a new map and place the player


self.generate_world_map(1);
}

And there we go - cargo run gives the same game we've had for a while, and we've
cut out a bunch of code. Refactors that make things smaller rock!

Making a generator
It's surprisingly di�cult to combine two paradigms, sometimes:

The graphical "tick" nature of RLTK (and the underlying GUI environment)
encourages you to do everything fast, in one fell swoop.
Actually visualizing progress while you generate a map encourages you to run in
lots of phases as a "state machine", yielding map results along the way.

My �rst thought was to use coroutines, speci�cally Generators. They really are ideal for
this type of thing: you can write code in a function that runs synchronously (in order)
and "yields" values as the computation continues. I even went so far as to get a
working implementation - but it required nightly support (unstable, un�nished Rust)
and didn't play nicely with web assembly. So I scrapped it. There's a lesson here:

325 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

sometimes the tooling isn't quite ready for what you really want!

Instead, I decided to go with a more traditional route. Maps can take a "snapshot"
while they generate, and that big pile of snapshots can be played frame-by-frame in
the visualizer. This isn't quite as nice as a coroutine, but it works and is stable. Those
are desirable traits!

To get started, we should make sure that visualizing map generation is entirely
optional. When you ship your game to players, you probably don't want to show them
the whole map while they get started - but while you are working on map algorithms,
it's very valuable. So towards the top of main.rs , we add a constant:

const SHOW_MAPGEN_VISUALIZER : bool = true;

A constant is just that: a variable that cannot change once the program has started.
Rust makes read-only constants pretty easy, and the compiler generally optimizes
them out completely since the value is known ahead of time. In this case, we're stating
that a bool called SHOW_MAPGEN_VISUALIZER is true . The idea is that we can set it to
false when we don't want to display our map generation progress.

With that in place, it's time to add snapshot support to our map builder interface. In
map_builders/mod.rs we extend the interface a bit:

pub trait MapBuilder {


fn build_map(&mut self);
fn spawn_entities(&mut self, ecs : &mut World);
fn get_map(&self) -> Map;
fn get_starting_position(&self) -> Position;
fn get_snapshot_history(&self) -> Vec<Map>;
fn take_snapshot(&mut self);
}

Notice the new entries: get_snapshot_history and take_snapshot . The former will
be used to ask the generator for its history of map frames; the latter tells generators
to support taking snapshots (and leaves it up to them how they do it).

This is a good time to mention one major di�erence between Rust and C++ (and other
languages that provide Object Oriented Programming support). Rust traits do not
support adding variables to the trait signature. So you can't include a

326 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

history : Vec<Map> within the trait, even if that's exactly what you're using to store
the snapshot in all the implementations. I honestly don't know why this is the case,
but it's workable - just an odd departure from OOP norms.

Inside simple_map.rs , we need to implement these methods for our


SimpleMapBuilder . We start by adding supporting variables to our struct :

pub struct SimpleMapBuilder {


map : Map,
starting_position : Position,
depth: i32,
rooms: Vec<Rect>,
history: Vec<Map>
}

Notice that we've added history: Vec<Map> to the structure. It's what it says on the
tin: a vector (resizable array) of Map structures. The idea is that we'll keep adding
copies of the map into it for each "frame" of map generation.

Onto the trait implementations:

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

This is very simple: we return a copy of the history vector to the caller. We also need:

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}
}

We �rst check to see if we're using the snapshot feature (no point in wasting memory
if we aren't!). If we are, we take a copy of the current map, iterate every
revealed_tiles cell and set it to true (so the map render will display everything,

327 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

including inaccessible walls), and add it to the history list.

We can now call self.take_snapshot() at any point during map generation, and it
gets added as a frame to the map generator. In simple_map.rs we add a couple of
calls after we add rooms or corridors:

...
if ok {
apply_room_to_map(&mut self.map, &new_room);
self.take_snapshot();

if !self.rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = self.rooms[self.rooms.len()-1].center();
if rng.range(0,1) == 1 {
apply_horizontal_tunnel(&mut self.map, prev_x, new_x, prev_y);
apply_vertical_tunnel(&mut self.map, prev_y, new_y, new_x);
} else {
apply_vertical_tunnel(&mut self.map, prev_y, new_y, prev_x);
apply_horizontal_tunnel(&mut self.map, prev_x, new_x, new_y);
}
}

self.rooms.push(new_room);
self.take_snapshot();
}
...

Rendering the visualizer


Visualizing map development is another game state, so we add it to our RunState
enumeration in main.rs :

328 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub enum RunState { AwaitingInput,
PreRun,
PlayerTurn,
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range : i32, item : Entity},
MainMenu { menu_selection : gui::MainMenuSelection },
SaveGame,
NextLevel,
ShowRemoveItem,
GameOver,
MagicMapReveal { row : i32 },
MapGeneration
}

Visualization actually requires a few variables, but I ran into a problem: one of the
variables really should be the next state to which we transition after visualizing. We
might be building a new map from one of three sources (new game, game over, next
level) - and they have di�erent states following the generation. Unfortunately, you
can't put a second RunState into the �rst one - Rust gives you cycle errors, and it
won't compile. You can use a Box<RunState> - but that doesn't work with RunState
deriving from Copy ! I fought this for a while, and settled on adding to State instead:

pub struct State {


pub ecs: World,
mapgen_next_state : Option<RunState>,
mapgen_history : Vec<Map>,
mapgen_index : usize,
mapgen_timer : f32
}

We've added:

mapgen_next_state - which is where the game should go next.


mapgen_history - a copy of the map history frames to play.
mapgen_index - how far through the history we are during playback.
mapgen_timer - used for frame timing during playback.

Since we've modi�ed State , we also have to modify our creation of the State

329 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

object:

let mut gs = State {


ecs: World::new(),
mapgen_next_state : Some(RunState::MainMenu{ menu_selection:
gui::MainMenuSelection::NewGame }),
mapgen_index : 0,
mapgen_history: Vec::new(),
mapgen_timer: 0.0
};

We've made the next state the same as the starting state we have been using: so the
game will render map creation and then go to the menu. We can change our initial
state to MapGeneration :

gs.ecs.insert(RunState::MapGeneration{} );

Now we need to implement the renderer. In our tick function, we add the following
state:

match newrunstate {
RunState::MapGeneration => {
if !SHOW_MAPGEN_VISUALIZER {
newrunstate = self.mapgen_next_state.unwrap();
}
ctx.cls();
draw_map(&self.mapgen_history[self.mapgen_index], ctx);

self.mapgen_timer += ctx.frame_time_ms;
if self.mapgen_timer > 300.0 {
self.mapgen_timer = 0.0;
self.mapgen_index += 1;
if self.mapgen_index == self.mapgen_history.len() {
newrunstate = self.mapgen_next_state.unwrap();
}
}
}
...

This is relatively straight-forward:

330 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. If the visualizer isn't enabled, simply transition to the next state immediately.
2. Clear the screen.
3. Call draw_map , with the map history from our state - at the current frame.
4. Add the frame duration to the mapgen_timer , and if it is greater than 300ms:
1. Set the timer back to 0.
2. Increment the frame counter.
3. If the frame counter has reached the end of our history, transition to the
next game state.

The eagle-eyed reader will have noticed a subtle change here. draw_map didn't used
to take a map - it would pull it from the ECS! In map.rs , the beginning of draw_map
changes to:

pub fn draw_map(map : &Map, ctx : &mut Rltk) {

Our regular call to draw_map in tick also changes to:

draw_map(&self.ecs.fetch::<Map>(), ctx);

This is a tiny change that allowed us to render whatever Map structure we need!

Lastly, we need to actually give the visualizer some data to render. We adjust
generate_world_map to reset the various mapgen_ variables, clear the history, and
retrieve the snapshot history once it has run:

fn generate_world_map(&mut self, new_depth : i32) {


self.mapgen_index = 0;
self.mapgen_timer = 0.0;
self.mapgen_history.clear();
let mut builder = map_builders::random_builder(new_depth);
builder.build_map();
self.mapgen_history = builder.get_snapshot_history();
let player_start;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
*worldmap_resource = builder.get_map();
player_start = builder.get_starting_position();
}

331 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you cargo run the project now, you get to watch the simple map generator build
your level before you start.

Wrap-Up
This �nishes building the test harness - you can watch maps spawn, which should
make generating maps (the topic of the next few chapters) a lot more intuitive.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

332 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

BSP Room Dungeons

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

A popular method of map generation uses "binary space partition" to sub-divide your
map into rectangles of varying size, and then link the resulting rooms together into
corridors. You can go a long way with this method: Nethack uses it extensively,
Dungeon Crawl: Stone Soup uses it sometimes, and my project - One Knight in the
Dungeon - uses it for sewer levels. This chapter will use the visualizer from the
previous chapter to walk you through using this technique.

Implementing a new map - subdivided BSP, the


boilerplate
We'll start by making a new �le in map_builders - bsp_dungeon.rs . We start by
making the basic BspDungeonBuilder struct:

pub struct BspDungeonBuilder {


map : Map,
starting_position : Position,
depth: i32,
rooms: Vec<Rect>,
history: Vec<Map>,
rects: Vec<Rect>
}

This is basically the same as the one from SimpleMapBuilder - and we've kept the
rooms vector, because this method uses a concept of rooms as well. We've added a
rects vector: the algorithm uses this a lot, so it's helpful to make it available

333 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

throughout the implementation. We'll see why it's needed shortly.

Now we implement the MapBuilder trait to the type:

impl MapBuilder for BspDungeonBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
// We should do something here
}

fn spawn_entities(&mut self, ecs : &mut World) {


for room in self.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, self.depth);
}
}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}
}
}

This is also pretty much the same as SimpleMapBuilder , but build_map has a
comment reminding us to write some code. If you ran the generator right now, you'd
get a solid blob of walls - and no content whatsoever.

We also need to implement a constructor for BspMapBuilder . Once again, it's basically
the same as SimpleMapBuilder :

334 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl BspDungeonBuilder {
pub fn new(new_depth : i32) -> BspDungeonBuilder {
BspDungeonBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
rooms: Vec::new(),
history: Vec::new(),
rects: Vec::new()
}
}
}

Lastly, we'll open map_builders/mod.rs and change the random_builder function to


always return our new map type:

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


// Note that until we have a second map type, this isn't even slighlty
random
Box::new(BspDungeonBuilder::new(new_depth))
}

Once again, this isn't in the slightest bit random - but it's far easier to develop a
feature that always runs, rather than keeping trying until it picks the one we want to
debug!

Building the map creator


We'll worry about swapping out map types later. Onto making the map! Note that this
implementation is ported from my C++ game, One Knight in the Dungeon. We'll start
with room generation. Inside our impl BspMapBuilder , we add a new function:

335 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

self.rects.clear();
self.rects.push( Rect::new(2, 2, self.map.width-5, self.map.height-5)
); // Start with a single map-sized rectangle
let first_room = self.rects[0];
self.add_subrects(first_room); // Divide the first room

// Up to 240 times, we get a random rectangle and divide it. If its


possible to squeeze a
// room in there, we place it and add it to the rooms list.
let mut n_rooms = 0;
while n_rooms < 240 {
let rect = self.get_random_rect(&mut rng);
let candidate = self.get_random_sub_rect(rect, &mut rng);

if self.is_possible(candidate) {
apply_room_to_map(&mut self.map, &candidate);
self.rooms.push(candidate);
self.add_subrects(rect);
self.take_snapshot();
}

n_rooms += 1;
}
let start = self.rooms[0].center();
self.starting_position = Position{ x: start.0, y: start.1 };
}

So what on Earth does this do?

1. We clear the rects structure we created as part of the builder. This will be used
to store rectangles derived from the overall map.
2. We create the "�rst room" - which is really the whole map. We've trimmed a bit
to add some padding to the sides of the map.
3. We call add_subrects , passing it the rectangle list - and the �rst room. We'll
implement that in a minute, but what it does is: it divides the rectangle into four
quadrants, and adds each of the quadrants to the rectangle list.
4. Now we setup a room counter, so we don't in�nitely loop.
5. While that counter is less than 240 (a relatively arbitrary limit that gives fun
results):
1. We call get_random_rect to retrieve a random rectangle from the

336 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

rectangles list.
2. We call get_random_sub_rect using this rectangle as an outer boundary. It
creates a random room from 3 to 10 tiles in size (on each axis), somewhere
within the parent rectangle.
3. We ask is_possible if the candidate can be drawn to the map; every tile
must be within the map boundaries, and not already a room. If it IS
possible:
1. We mark it on the map.
2. We add it to the rooms list.
3. We call add_subrects to sub-divide the rectangle we just used (not
the candidate!).

There's quite a few support functions in play here, so lets go through them.

fn add_subrects(&mut self, rect : Rect) {


let width = i32::abs(rect.x1 - rect.x2);
let height = i32::abs(rect.y1 - rect.y2);
let half_width = i32::max(width / 2, 1);
let half_height = i32::max(height / 2, 1);

self.rects.push(Rect::new( rect.x1, rect.y1, half_width, half_height


));
self.rects.push(Rect::new( rect.x1, rect.y1 + half_height, half_width,
half_height ));
self.rects.push(Rect::new( rect.x1 + half_width, rect.y1, half_width,
half_height ));
self.rects.push(Rect::new( rect.x1 + half_width, rect.y1 +
half_height, half_width, half_height ));
}

The function add_subrects is core to the BSP (Binary Space Partition) approach: it
takes a rectangle, and divides the width and height in half. It then creates four new
rectangles, one for each quadrant of the original. These are added to the rects list.
Graphically:

337 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

############### ###############
# # # 1 + 2 #
# # # + #
# 0 # -> #+++++++++++++#
# # # 3 + 4 #
# # # + #
############### ###############

Next up is get_random_rect :

fn get_random_rect(&mut self, rng : &mut RandomNumberGenerator) -> Rect {


if self.rects.len() == 1 { return self.rects[0]; }
let idx = (rng.roll_dice(1, self.rects.len() as i32)-1) as usize;
self.rects[idx]
}

This is a simple function. If there is only one rectangle in the rects list, it returns the
�rst one. Otherwise, it rolls a dice for of 1d(size of rects list) and returns the
rectangle found at the random index.

Next up is get_random_sub_rect :

fn get_random_sub_rect(&self, rect : Rect, rng : &mut


RandomNumberGenerator) -> Rect {
let mut result = rect;
let rect_width = i32::abs(rect.x1 - rect.x2);
let rect_height = i32::abs(rect.y1 - rect.y2);

let w = i32::max(3, rng.roll_dice(1, i32::min(rect_width, 10))-1) + 1;


let h = i32::max(3, rng.roll_dice(1, i32::min(rect_height, 10))-1) +
1;

result.x1 += rng.roll_dice(1, 6)-1;


result.y1 += rng.roll_dice(1, 6)-1;
result.x2 = result.x1 + w;
result.y2 = result.y1 + h;

result
}

So this takes a rectangle as the parameter, and makes a mutable copy to use as the
result. It calculates the width and height of the rectangle, and then produces a random

338 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

width and height inside that rectangle - but no less than 3 tiles in size and no more
than 10 on each dimension. You can tweak those numbers to change your desired
room size. It then shunts the rectangle a bit, to provide some random placement
(otherwise, it would always be against the sides of the sub-rectangle). Finally, it
returns the result. Graphically:

############### ########
# # # 1 #
# # # #
# 0 # -> ########
# #
# #
###############

Finally, the is_possible function:

fn is_possible(&self, rect : Rect) -> bool {


let mut expanded = rect;
expanded.x1 -= 2;
expanded.x2 += 2;
expanded.y1 -= 2;
expanded.y2 += 2;

let mut can_build = true;

for y in expanded.y1 ..= expanded.y2 {


for x in expanded.x1 ..= expanded.x2 {
if x > self.map.width-2 { can_build = false; }
if y > self.map.height-2 { can_build = false; }
if x < 1 { can_build = false; }
if y < 1 { can_build = false; }
if can_build {
let idx = self.map.xy_idx(x, y);
if self.map.tiles[idx] != TileType::Wall {
can_build = false;
}
}
}
}

can_build
}

339 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This is a little more complicated, but makes sense when you break it down:

1. Take a rectangle as a target, representing the room we are looking at.


2. Create a mutable copy of the rectangle called expanded . We then expand the
rectangle out by 2 tiles in each direction, to prevent rooms from overlapping.
3. We iterate every x and y coordinate in the rectangle:
1. If x or y are out of the map boundaries, we mark can_build as false -
this won't work.
2. If we still can build it, we look at the existing map - if it isn't a solid wall,
then we've overlapped an existing room, and mark that we can't build.
4. We return the result of can_build .

So now that we've implemented all of these, the overall algorithm is more obvious:

1. We start with a single rectangle covering the entire map.


2. We sub-divide it, so now our map has 5 rectangles - one for each quadrant, one
for the map as a whole.
3. We use a counter to ensure that we don't loop forever (we'll reject a lot of
rooms). While we can still add rooms, we:
1. Obtain a random rectangle from the rectangles list. Initially, this will be one
of the quadrants - or the whole map. This list will keep growing as we add
subdivisions.
2. We generate a random sub-rectangle inside this rectangle.
3. We look to see if that's a possible room. If it is, we:
1. Apply the room to the map (build it).
2. Add it to the rooms list.
3. Sub-divide the new rectangle into quadrants and add those to our
rectangles list.
4. Store a snapshot for the visualizer.

This tends to give a nice spread of rooms, and they are guaranteed not to overlap.
Very Nethack like!

If you cargo run now, you will be in a room with no exits. You'll get to watch rooms
appear around the map in the visualizer. That's a great start.

340 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding in corridors
Now, we sort the rooms by left coordinate. You don't have to do this, but it helps
make connected rooms line up.

self.rooms.sort_by(|a,b| a.x1.cmp(&b.x1) );

sort_by takes a closure - that is, an inline function (known as a "lambda" in other
languages) as a parameter. You could specify a whole other function if you wanted to,
or implement traits on Rect to make it sortable - but this is easy enough. It sorts by
comparing the x1 value of each rectangle.

Now we'll add some corridors:

341 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Now we want corridors


for i in 0..self.rooms.len()-1 {
let room = self.rooms[i];
let next_room = self.rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 -
room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 -
room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 -
next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 -
next_room.y2))-1);
self.draw_corridor(start_x, start_y, end_x, end_y);
self.take_snapshot();
}

This iterates the rooms list, ignoring the last one. It fetches the current room, and the
next one in the list and calculates a random location ( start_x / start_y and end_x /
end_y ) within each room. It then calls the mysterious draw_corridor function with
these coordinates. Draw corridor adds a line from the start to the end, using only
north/south or east/west (it can give 90-degree bends). It won't give you a staggered,
hard to navigate perfect line like Bresenham would. We also take a snapshot.

The draw_corridor function is quite simple:

342 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn draw_corridor(&mut self, x1:i32, y1:i32, x2:i32, y2:i32) {


let mut x = x1;
let mut y = y1;

while x != x2 || y != y2 {
if x < x2 {
x += 1;
} else if x > x2 {
x -= 1;
} else if y < y2 {
y += 1;
} else if y > y2 {
y -= 1;
}

let idx = self.map.xy_idx(x, y);


self.map.tiles[idx] = TileType::Floor;
}
}

It takes a start and end point, and creates mutable x and y variables equal to the
starting location. Then it keeps going until x and y match end end of the line. For
each iteration, if x is less than the ending x - it goes left. If x is greater than the
ending x - it goes right. Same for y , but with up and down. This gives straight
corridors with a single corner.

Don't forget the stairs (I nearly did!)


Finally, we need to wrap up and create the exit:

// Don't forget the stairs


let stairs = self.rooms[self.rooms.len()-1].center();
let stairs_idx = self.map.xy_idx(stairs.0, stairs.1);
self.map.tiles[stairs_idx] = TileType::DownStairs;

We place the exit in the last room, guaranteeing that the poor player has a ways to
walk.

343 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you cargo run now, you'll see something like this:

Randomizing the dungeon per level


Rather than always using the BSP sewer algorithm, we would like to sometimes use
one or the other. In map_builders/mod.rs , replace the build function:

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 2);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

Now when you play, it's a coin toss what type of map you encounter. The spawn
functions for the types are the same - so we're not going to worry about map builder

344 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

state until the next chapter.

Wrap-Up
You've refactored your map building into a new module, and built a simple BSP
(Binary Space Partitioning) based map. The game randomly picks a map type, and you
have more variety. The next chapter will further refactor map generation, and
introduce another technique.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

BSP Interior Design

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the last chapter, we used binary space partition (BSP) to build a dungeon with
rooms. BSP is �exible, and can help you with a lot of problems; in this example, we're
going to modify BSP to design an interior dungeon - completely inside a rectangular
structure (for example, a castle) and with no wasted space other than interior walls.

The code for this chapter is converted from One Knight in the Dungeon's prison levels.

345 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Sca�olding
We'll start by making a new �le, map_builders/bsp_interior.rs and putting in the
same initial boilerplate that we used in the previous chapter:

346 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map, Rect, apply_room_to_map,


TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER};
use rltk::RandomNumberGenerator;
use specs::prelude::*;

pub struct BspInteriorBuilder {


map : Map,
starting_position : Position,
depth: i32,
rooms: Vec<Rect>,
history: Vec<Map>,
rects: Vec<Rect>
}

impl MapBuilder for BspInteriorBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
// We should do something here
}

fn spawn_entities(&mut self, ecs : &mut World) {


for room in self.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, self.depth);
}
}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}

347 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}

impl BspInteriorBuilder {
pub fn new(new_depth : i32) -> BspInteriorBuilder {
BspInteriorBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
rooms: Vec::new(),
history: Vec::new(),
rects: Vec::new()
}
}
}

We'll also change our random builder function in map_builders/mod.rs to once again
lie to the user and always "randomly" pick the new algorithm:

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 2);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(BspInteriorBuilder::new(new_depth))
}

Subdividing into rooms


We're not going to achieve a perfect subdivision due to rounding issues, but we can
get pretty close. Certainly good enough for a game! We put together a build function
that is quite similar to the one from the previous chapter:

348 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

self.rects.clear();
self.rects.push( Rect::new(1, 1, self.map.width-2, self.map.height-2)
); // Start with a single map-sized rectangle
let first_room = self.rects[0];
self.add_subrects(first_room, &mut rng); // Divide the first room

let rooms = self.rects.clone();


for r in rooms.iter() {
let room = *r;
//room.x2 -= 1;
//room.y2 -= 1;
self.rooms.push(room);
for y in room.y1 .. room.y2 {
for x in room.x1 .. room.x2 {
let idx = self.map.xy_idx(x, y);
if idx > 0 && idx < ((self.map.width * self.map.height)-1)
as usize {
self.map.tiles[idx] = TileType::Floor;
}
}
}
self.take_snapshot();
}

let start = self.rooms[0].center();


self.starting_position = Position{ x: start.0, y: start.1 };
}

Lets look at what this does:

1. We create a new random number generator.


2. We clear the rects list, and add a rectangle covering the whole map we intend
to use.
3. We call a magical function add_subrects on this rectangle. More on that in a
minute.
4. We copy the rooms list, to avoid borring issues.
5. For each room, we add it to the rooms list - and carve it out of the map. We also
take a snapshot.
6. We start the player in the �rst room.

349 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The add_subrects function in this case does all the hard work:

fn add_subrects(&mut self, rect : Rect, rng : &mut RandomNumberGenerator)


{
// Remove the last rect from the list
if !self.rects.is_empty() {
self.rects.remove(self.rects.len() - 1);
}

// Calculate boundaries
let width = rect.x2 - rect.x1;
let height = rect.y2 - rect.y1;
let half_width = width / 2;
let half_height = height / 2;

let split = rng.roll_dice(1, 4);

if split <= 2 {
// Horizontal split
let h1 = Rect::new( rect.x1, rect.y1, half_width-1, height );
self.rects.push( h1 );
if half_width > MIN_ROOM_SIZE { self.add_subrects(h1, rng); }
let h2 = Rect::new( rect.x1 + half_width, rect.y1, half_width,
height );
self.rects.push( h2 );
if half_width > MIN_ROOM_SIZE { self.add_subrects(h2, rng); }
} else {
// Vertical split
let v1 = Rect::new( rect.x1, rect.y1, width, half_height-1 );
self.rects.push(v1);
if half_height > MIN_ROOM_SIZE { self.add_subrects(v1, rng); }
let v2 = Rect::new( rect.x1, rect.y1 + half_height, width,
half_height );
self.rects.push(v2);
if half_height > MIN_ROOM_SIZE { self.add_subrects(v2, rng); }
}
}

Lets take a look at what this function does:

1. If the rects list isn't empty, we remove the last item from the list. This has the
e�ect of removing the last rectangle we added - so when we start, we are
removing the rectangle covering the whole map. Later on, we are removing a
rectangle because we are dividing it. This way, we won't have overlaps.

350 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

2. We calculate the width and height of the rectangle, and well as half of the width
and height.
3. We roll a dice. There's a 50% chance of a horizontal or vertical split.
4. If we're splitting horizontally:
1. We make h1 - a new rectangle. It covers the left half of the parent
rectangle.
2. We add h1 to the rects list.
3. If half_width is bigger than MIN_ROOM_SIZE , we recursively call
add_subrects again, with h1 as the target rectangle.
4. We make h2 - a new rectangle covering the right side of the parent
rectangle.
5. We add h2 to the rects list.
6. If half_width is bigger than MIN_ROOM_SIZE , we recursively call
add_subrects again, with h2 as the target rectangle.
5. If we're splitting vertically, it's the same as (4) - but with top and bottom
rectangles.

Conceptually, this starts with a rectangle:

#################################
# #
# #
# #
# #
# #
# #
# #
# #
# #
#################################

A horizontal split would yield the following:

351 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#################################
# # #
# # #
# # #
# # #
# # #
# # #
# # #
# # #
# # #
#################################

The next split might be vertical:

#################################
# # #
# # #
# # #
# # #
################ #
# # #
# # #
# # #
# # #
#################################

This repeats until we have a lot of small rooms.

You can cargo run the code right now, to see the rooms appearing.

352 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding some doorways


It's all well and good to have rooms, but without doors connecting them it's not going
to be a very fun experience! Fortunately, the exact same code from the previous
chapter will work here, also.

353 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Now we want corridors


for i in 0..self.rooms.len()-1 {
let room = self.rooms[i];
let next_room = self.rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 -
room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 -
room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 -
next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 -
next_room.y2))-1);
self.draw_corridor(start_x, start_y, end_x, end_y);
self.take_snapshot();
}

This in turn calls the unchanged draw_corridor function:

fn draw_corridor(&mut self, x1:i32, y1:i32, x2:i32, y2:i32) {


let mut x = x1;
let mut y = y1;

while x != x2 || y != y2 {
if x < x2 {
x += 1;
} else if x > x2 {
x -= 1;
} else if y < y2 {
y += 1;
} else if y > y2 {
y -= 1;
}

let idx = self.map.xy_idx(x, y);


self.map.tiles[idx] = TileType::Floor;
}
}

Don't forget the stairs (I nearly did, AGAIN!)

354 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Finally, we need to wrap up and create the exit:

// Don't forget the stairs


let stairs = self.rooms[self.rooms.len()-1].center();
let stairs_idx = self.map.xy_idx(stairs.0, stairs.1);
self.map.tiles[stairs_idx] = TileType::DownStairs;

We place the exit in the last room, guaranteeing that the poor player has a ways to
walk.

If you cargo run now, you'll see something like this:

Restoring randomness - again


Lastly, we go back to map_builders/mod.rs and edit our random_builder to once
gain provide a random dungeon per level:

355 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 3);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

Wrap Up
This type of dungeon can represent an interior, maybe of a space ship, a castle, or
even a home. You can tweak dimensions, door placement, and bias the splitting as
you see �t - but you'll get a map that makes most of the available space usable by the
game. It's probably worth being sparing with these levels (or incorporating them into
other levels) - they can lack variety, even though they are random.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Cellular Automata Maps

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

356 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Sometimes, you need a break from rectangular rooms. You might want a nice, organic
looking cavern; a winding forest trail, or a spooky quarry. One Knight in the Dungeon
uses cellular automata for this purpose, inspired by this excellent article. This chapter
will help you create natural looking maps.

Sca�olding
Once again, we're going to take a bunch of code from the previous tutorial and re-use
it for the new generator. Create a new �le, map_builders/cellular_automota.rd and
place the following in it:

357 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map, Rect, apply_room_to_map,


TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER};
use rltk::RandomNumberGenerator;
use specs::prelude::*;

const MIN_ROOM_SIZE : i32 = 8;

pub struct CellularAutomotaBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>
}

impl MapBuilder for CellularAutomotaBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
//self.build(); - we should write this
}

fn spawn_entities(&mut self, ecs : &mut World) {


// We need to rewrite this, too.
}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}
}
}

358 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl CellularAutomotaBuilder {
pub fn new(new_depth : i32) -> CellularAutomotaBuilder {
CellularAutomotaBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
}
}
}

Once again, we'll make the name random_builder a lie and only return the one we're
working on:

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 3);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(CellularAutomotaBuilder::new(new_depth))
}

Putting together the basic map


The �rst step is to make the map completely chaotic, with about 55% of tiles being
solid. You can tweak that number for di�erent e�ects, but I quite like the result.
Here's the build function:

359 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// First we completely randomize the map, setting 55% of it to be


floor.
for y in 1..self.map.height-1 {
for x in 1..self.map.width-1 {
let roll = rng.roll_dice(1, 100);
let idx = self.map.xy_idx(x, y);
if roll > 55 { self.map.tiles[idx] = TileType::Floor }
else { self.map.tiles[idx] = TileType::Wall }
}
}
self.take_snapshot();
}

This makes a mess of an unusable level. Walls and �oors everywhere with no rhyme or
reason to them - and utterly unplayable. That's ok, because cellular automata is
designed to make a level out of noise. It works by iterating each cell, counting the
number of neighbors, and turning walls into �oors or walls based on density. Here's a
working builder:

360 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// First we completely randomize the map, setting 55% of it to be


floor.
for y in 1..self.map.height-1 {
for x in 1..self.map.width-1 {
let roll = rng.roll_dice(1, 100);
let idx = self.map.xy_idx(x, y);
if roll > 55 { self.map.tiles[idx] = TileType::Floor }
else { self.map.tiles[idx] = TileType::Wall }
}
}
self.take_snapshot();

// Now we iteratively apply cellular automota rules


for _i in 0..15 {
let mut newtiles = self.map.tiles.clone();

for y in 1..self.map.height-1 {
for x in 1..self.map.width-1 {
let idx = self.map.xy_idx(x, y);
let mut neighbors = 0;
if self.map.tiles[idx - 1] == TileType::Wall { neighbors
+= 1; }
if self.map.tiles[idx + 1] == TileType::Wall { neighbors
+= 1; }
if self.map.tiles[idx - self.map.width as usize] ==
TileType::Wall { neighbors += 1; }
if self.map.tiles[idx + self.map.width as usize] ==
TileType::Wall { neighbors += 1; }
if self.map.tiles[idx - (self.map.width as usize - 1)] ==
TileType::Wall { neighbors += 1; }
if self.map.tiles[idx - (self.map.width as usize + 1)] ==
TileType::Wall { neighbors += 1; }
if self.map.tiles[idx + (self.map.width as usize - 1)] ==
TileType::Wall { neighbors += 1; }
if self.map.tiles[idx + (self.map.width as usize + 1)] ==
TileType::Wall { neighbors += 1; }

if neighbors > 4 || neighbors == 0 {


newtiles[idx] = TileType::Wall;
}
else {
newtiles[idx] = TileType::Floor;

361 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}
}

self.map.tiles = newtiles.clone();
self.take_snapshot();
}
}

This is actually very simple:

1. We randomize the map, as above.


2. We count from 0 to 9, for 10 iterations of the algorithm.
3. For each iteration:
1. We take a copy of the map tiles, placing it into newtiles . We do this so we
aren't writing to the tiles we are counting, which gives a very odd map.
2. We iterate every cell on the map, and count the number of tiles
neighboring the tile that are walls.
3. If there are more than 4, or zero, neighboring walls - then the tile (in
newtiles ) becomes a wall. Otherwise, it becomes a �oor.
4. We copy the newtiles back into the map .
5. We take a snapshot.

This is a very simple algorithm - but produces quite beautiful results. Here it is in
action:

362 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Picking a starting point


Picking a starting point for the player is a little more di�cult than it has been in
previous chapters. We don't have a list of rooms to query! Instead, we'll start in the
middle and move left until we hit some open space. The code for this is quite simple:

// Find a starting point; start at the middle and walk left until we find
an open tile
self.starting_position = Position{ x: self.map.width / 2, y :
self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
}

363 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Placing an exit - and culling unreachable areas


We want the exit to be quite a way away from the player. We also don't want to keep
areas that the player absolutely can't reach. Fortunately, the process to �nd an exit
and the process to �nd orphans are quite similar. We can use a Dijkstra Map. If you
haven't already read it, I recommend reading The Incredible Power of Dijkstra Maps.
Fortunately, RLTK implements a very fast version of Dijkstra for you, so you won't
have to �ght with the algorithm. Here's the code:

// Find all tiles we can reach from the starting point


let map_starts : Vec<i32> = vec![start_idx as i32];
let dijkstra_map = rltk::DijkstraMap::new(self.map.width, self.map.height,
&map_starts , &self.map, 200.0);
let mut exit_tile = (0, 0.0f32);
for (i, tile) in self.map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor {
let distance_to_start = dijkstra_map.map[i];
// We can't get to this tile - so we'll make it a wall
if distance_to_start == std::f32::MAX {
*tile = TileType::Wall;
} else {
// If it is further away than our current exit candidate, move
the exit
if distance_to_start > exit_tile.1 {
exit_tile.0 = i;
exit_tile.1 = distance_to_start;
}
}
}
}
self.take_snapshot();

self.map.tiles[exit_tile.0] = TileType::DownStairs;
self.take_snapshot();

This is a dense piece of code that does a lot, lets walk through it:

1. We create a vector called map_starts and give it a single value: the tile index on
which the player starts. Dijkstra maps can have multiple starting points (distance
0), so this has to be a vector even though there is only one choice.
2. We ask RLTK to make a Dijkstra Map for us. It has dimensions that match the
main map, uses the starts, has read access to the map itself, and we'll stop

364 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

counting at 200 steps (a safety feature in case of runaways!)


3. We set an exit_tile tuple to 0 and 0.0 . The �rst zero is the tile index of the
exit, the second zero is the distance to the exit.
4. We iterate the map tiles, using Rust's awesome enumerate feature. By adding
.enumerate() to the end of a range iteration, it adds the cell index as the �rst
parameter in a tuple. We then de-structure to obtain both the tile and the index.
5. If the tile is a �oor,
6. We obtain the distance to the starting point from the Dijkstra map.
7. If the distance is the maximum value for an f32 (a marker the Dijkstra map uses
for "unreachable"), then it doesn't need to be a �oor at all - nobody can get
there. So we turn it into a wall.
8. If the distance is greater than the distance in our exit_tile tuple, we store
both the new distance and the new tile index.
9. Once we've visited every tile, we take a snapshot to show the removed area.
10. We set the tile at the exit_tile (most distant reachable tile) to be a downward
staircase.

If you cargo run , you actually have quite a playable map now! There's just one
problem: there are no other entities on the map.

Populating our cave: freeing the spawn system from


rooms.
If we were feeling lazy, we could simply iterate the map - �nd open spaces and have a
random chance to spawn something. But that's not really very much fun. It makes
more sense for monsters to be grouped together, with some "dead spaces" so you
can catch your breath (and regain some health).

As a �rst step, we're going to revisit how we spawn entities. Right now, pretty much
everything that isn't the player arrives into the world via the spawner.rs -provided
spawn_room function. It has served us well up to now, but we want to be a bit more
�exible; we might want to spawn in corridors, we might want to spawn in semi-open
areas that don't �t a rectangle, and so on. Also, a look over spawn_room shows that it
does several things in one function - which isn't the best design. A �nal objective is to
keep the spawn_room interface available - so we can still use it, but to also o�er more
detailed options.

365 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The �rst thing we'll do is separate out the actual spawning:

/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)


fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
let x = (*spawn.0 % MAPWIDTH) as i32;
let y = (*spawn.0 / MAPWIDTH) as i32;

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Rations" => rations(ecs, x, y),
"Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
"Bear Trap" => bear_trap(ecs, x, y),
_ => {}
}
}

Now we can replace the last for loop in spawn_room with the following:

// Actually spawn the monsters


for spawn in spawn_points.iter() {
spawn_entity(ecs, &spawn);
}

Now, we'll replace spawn_room with a simpli�ed version that calls our theoretical
function:

366 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn spawn_room(ecs: &mut World, room : &Rect, map_depth: i32) {


let mut possible_targets : Vec<usize> = Vec::new();
{ // Borrow scope - to keep access to the map separated
let map = ecs.fetch::<Map>();
for y in room.y1 + 1 .. room.y2 {
for x in room.x1 + 1 .. room.x2 {
let idx = map.xy_idx(x, y);
if map.tiles[idx] == TileType::Floor {
possible_targets.push(idx);
}
}
}
}

spawn_region(ecs, &possible_targets, map_depth);


}

This function maintains the same interface/signature as the previous call - so our old
code will still work. Instead of actually spawning anything, it builds a vector of all of
the tiles in the room (checking that they are �oors - something we didn't do before;
monsters in walls is no longer possible!). It then calls a new function, spawn_region
that accepts a similar signature - but wants a list of available tiles into which it can
spawn things. Here's the new function:

367 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn spawn_region(ecs: &mut World, area : &[usize], map_depth: i32) {


let spawn_table = room_table(map_depth);
let mut spawn_points : HashMap<usize, String> = HashMap::new();
let mut areas : Vec<usize> = Vec::from(area);

// Scope to keep the borrow checker happy


{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1,
MAX_MONSTERS + 3) + (map_depth - 1) - 3);
if num_spawns == 0 { return; }

for _i in 0 .. num_spawns {
let array_index = if areas.len() == 1 { 0usize } else {
(rng.roll_dice(1, areas.len() as i32)-1) as usize };
let map_idx = areas[array_index];
spawn_points.insert(map_idx, spawn_table.roll(&mut rng));
areas.remove(array_index);
}
}

// Actually spawn the monsters


for spawn in spawn_points.iter() {
spawn_entity(ecs, &spawn);
}
}

This is similar to the previous spawning code, but not quite the same (although the
results are basically the same!). We'll go through it, just to be sure we understand
what we're doing:

1. We obtain a spawn table for the current map depth.


2. We setup a HashMap called spawn_points , listing pairs of data (map index and
name tag) for everything we've decided to spawn.
3. We create a new Vector of areas, copied from the passed in slice. (A slice is a
"view" of an array or vector). We're making a new one so we aren't modifying the
parent area list. The caller might want to use that data for something else, and
it's good to avoid changing people's data without asking. Changing data without
warning is called a "side e�ect", and it's good to avoid them in general (unless
you actually want them).
4. We make a new scope, because Rust doesn't like us using the ECS to obtain the
random number generator, and then using it later to spawn entities. The scope

368 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

makes Rust "forget" our �rst borrow as soon as it ends.


5. We obtain a random number generator from the ECS.
6. We calculate the number of entities to spawn. This is the same random function
as we used before, but we've added an i32::min call: we want the smaller of
EITHER the number of available tiles, OR the random calculation. This way, we'll
never try to spawn more entities than we have room for.
7. If the number to spawn is zero, we bail out of the function (nothing to do, here!).
8. Repeating for zero to the number of spawns (minus 1 - we're not using an
inclusive range):
1. We pick an array_index from areas. If there is only one entry, we use it.
Otherwise, we roll a dice (from 1 to the number of entries, subtract one
because the array is zero-based).
2. The map_idx (location in the map tiles array) is the value located at the
array_index index of the array. So we obtain that.
3. We insert a spawn into the spawn_points map, listing both the index and a
random roll on the spawn table.
4. We remove the entry we just used from areas - that way, we can't
accidentally pick it again. Note that we're not checking to see if the array is
empty: in step 6 above, we guaranteed that we won't spawn more entities
than we have room for, so (at least in theory) that particular bug can't
happen!

The best way to test this is to uncomment out the random_builder code (and
comment the CellularAutomotaBuilder entry) and give it a go. It should play just like
before. Once you've tested it, go back to always spawning the map type we're working
on.

Grouped placement in our map - Enter the Voronoi!


Voronoi Diagrams are a wonderfully useful piece of math. Given a group of points, it
builds a diagram of regions surrounding each point (which could be random, or might
mean something; that's the beauty of math, it's up to you!) - with no empty space.
We'd like to do something similar for our maps: subdivide the map into random
regions, and spawn inside those regions. Fortunately, RLTK provides a type of noise to
help with that: cellular noise.

First of all, what is noise. "Noise" in this case doesn't refer to the loud heavy metal you

369 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

accidentally pipe out of your patio speakers at 2am while wondering what a stereo
receiver you found in your new house does (true story...); it refers to random data -
like the noise on old analog TVs if you didn't tune to a station (ok, I'm showing my age
there). Like most things random, there's lots of ways to make it not-really-random and
group it into useful patterns. A noise library provides lots of types of noise.
Perlin/Simplex noise makes really good approximations of landscapes. White noise
looks like someone randomly threw paint at a piece of paper. Cellular Noise randomly
places points on a grid, and then plots Voronoi diagrams around them. We're
interested in the latter.

This is a somewhat complicated way to do things, so we'll take it a step at a time. Lets
start by adding a structure to store generated areas into our
CellularAutomotaBuilder structure:

pub struct CellularAutomotaBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>
}

In new , we also have to initialize it:

impl CellularAutomotaBuilder {
pub fn new(new_depth : i32) -> CellularAutomotaBuilder {
CellularAutomotaBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new()
}
}
...

The idea here is that we have a HashMap (dictionary in other languages) keyed on the
ID number of an area. The area consists of a vector of tile ID numbers. Ideally, we'd
generate 20-30 distinct areas all with spaces to spawn entities into.

370 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Here's the next section of the build code:

// Now we build a noise map for use in spawning entities later


let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as u64);
noise.set_noise_type(rltk::NoiseType::Cellular);
noise.set_frequency(0.08);
noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan);

for y in 1 .. self.map.height-1 {
for x in 1 .. self.map.width-1 {
let idx = self.map.xy_idx(x, y);
if self.map.tiles[idx] == TileType::Floor {
let cell_value_f = noise.get_noise(x as f32, y as f32) *
10240.0;
let cell_value = cell_value_f as i32;

if self.noise_areas.contains_key(&cell_value) {
self.noise_areas.get_mut(&cell_value).unwrap().push(idx);
} else {
self.noise_areas.insert(cell_value, vec![idx]);
}
}
}
}

Since this is quite complicated, lets walk through it:

1. We create a new FastNoise object, from RLTK's port of Auburns' excellent


FastNoise library.
2. We specify that we want Cellular noise. That's the same as Voronoi noise in this
case.
3. We specify a frequency of 0.08 . This number was found by playing with
di�erent values!
4. We specify the Manhattan distance function. There are three to choose from,
and they give di�ering shapes. Manhattan tends to favor elongated shapes,
which I like for this purpose. Try all three and see what you like.
5. We iterate the whole map:
1. We get the idx of the tile, in the map's tiles vectors.
2. We check to make sure it's a �oor - and skip if it isn't.
3. We query FastNoise for a noise value for the coordinates (converting
them to f32 �oating point numbers, because the library likes �oats). We

371 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

multiply by 10240.0 because the default is very small numbers - and this
brings it up into a reasonable range.
4. We convert the result to an integer.
5. If the noise_areas map contains the area number we just generated, we
add the tile index to the vector.
6. If the noise_areas map DOENS'T contain the area number we just
generated, we make a new vector of tile indices with the map index
number in it.

This generates between 20 and 30 areas quite consistently, and they only contain
valid �oor tiles. So the last remaining job is to actually spawn some entities. We
update our spawn_entities function:

fn spawn_entities(&mut self, ecs : &mut World) {


for area in self.noise_areas.iter() {
spawner::spawn_region(ecs, area.1, self.depth);
}
}

This is quite simple: it iterates through each area, and calls the new spawn_region
with the vector of available map tiles for that region.

The game is now quite playable on these new maps:

372 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Restoring randomness
Once again, we should restore randomness to our map building. In
map_builders/mod.rs :

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 4);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

373 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrap-Up
We've made a pretty nice map generator, and �xed our dependency upon rooms.
Cellular Automata are a really �exible algorithm, and can be used for all kinds of
organic looking maps. With a bit of tweaking to the rules, you can make a really large
variety of maps.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Drunkard's Walk Maps

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Ever wondered what would happen if an Umber Hulk (or other tunneling creature) got
really drunk, and went on a dungeon carving bender? The Drunkard's Walk algorithm
answers the question - or more precisely, what would happen if a whole bunch of
monsters had far too much to drink. As crazy it sounds, this is a good way to make
organic dungeons.

Initial sca�olding

374 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

As usual, we'll start with sca�olding from the previous map tutorials. We've done it
enough that it should be old hat by now! In map_builders/drunkard.rs , build a new
DrunkardsWalkBuilder class. We'll keep the zone-based placement from Cellular
Automata - but remove the map building code. Here's the sca�olding:

375 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map,


TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER};
use rltk::RandomNumberGenerator;
use specs::prelude::*;
use std::collections::HashMap;

pub struct DrunkardsWalkBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>
}

impl MapBuilder for DrunkardsWalkBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
self.build();
}

fn spawn_entities(&mut self, ecs : &mut World) {


for area in self.noise_areas.iter() {
spawner::spawn_region(ecs, area.1, self.depth);
}
}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}

376 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}

impl DrunkardsWalkBuilder {
pub fn new(new_depth : i32) -> DrunkardsWalkBuilder {
DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new()
}
}

#[allow(clippy::map_entry)]
fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// Set a central starting point


self.starting_position = Position{ x: self.map.width / 2, y:
self.map.height / 2 };
let start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);

// Find all tiles we can reach from the starting point


let map_starts : Vec<i32> = vec![start_idx as i32];
let dijkstra_map = rltk::DijkstraMap::new(self.map.width,
self.map.height, &map_starts , &self.map, 200.0);
let mut exit_tile = (0, 0.0f32);
for (i, tile) in self.map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor {
let distance_to_start = dijkstra_map.map[i];
// We can't get to this tile - so we'll make it a wall
if distance_to_start == std::f32::MAX {
*tile = TileType::Wall;
} else {
// If it is further away than our current exit
candidate, move the exit
if distance_to_start > exit_tile.1 {
exit_tile.0 = i;
exit_tile.1 = distance_to_start;
}
}
}
}
self.take_snapshot();

377 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Place the stairs


self.map.tiles[exit_tile.0] = TileType::DownStairs;
self.take_snapshot();

// Now we build a noise map for use in spawning entities later


let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as
u64);
noise.set_noise_type(rltk::NoiseType::Cellular);
noise.set_frequency(0.08);

noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan);

for y in 1 .. self.map.height-1 {
for x in 1 .. self.map.width-1 {
let idx = self.map.xy_idx(x, y);
if self.map.tiles[idx] == TileType::Floor {
let cell_value_f = noise.get_noise(x as f32, y as f32)
* 10240.0;
let cell_value = cell_value_f as i32;

if self.noise_areas.contains_key(&cell_value) {

self.noise_areas.get_mut(&cell_value).unwrap().push(idx);
} else {
self.noise_areas.insert(cell_value, vec![idx]);
}
}
}
}
}
}

We've kept a lot of the work from the Cellular Automata chapter, since it can help us
here also. We also go into map_builders/mod.rs and once again force the "random"
system to pick our new code:

378 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 4);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(DrunkardsWalkBuilder::new(new_depth))
}

Don't Repeat Yourself (The DRY principle)


Since we're re-using the exact code from Cellular Automata, we should take the
common code and put it into map_builders/common.rs . This saves typing, saves the
compiler from repeatedly remaking the same code (increasing your program size). So
in common.rs , we refactor the common code into some functions. In common.rs , we
create a new function - remove_unreachable_areas_returning_most_distant :

379 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

/// Searches a map, removes unreachable areas and returns the most distant
tile.
pub fn remove_unreachable_areas_returning_most_distant(map : &mut Map,
start_idx : usize) -> usize {
map.populate_blocked();
let map_starts : Vec<i32> = vec![start_idx as i32];
let dijkstra_map = rltk::DijkstraMap::new(map.width, map.height,
&map_starts , map, 200.0);
let mut exit_tile = (0, 0.0f32);
for (i, tile) in map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor {
let distance_to_start = dijkstra_map.map[i];
// We can't get to this tile - so we'll make it a wall
if distance_to_start == std::f32::MAX {
*tile = TileType::Wall;
} else {
// If it is further away than our current exit candidate,
move the exit
if distance_to_start > exit_tile.1 {
exit_tile.0 = i;
exit_tile.1 = distance_to_start;
}
}
}
}

exit_tile.0
}

We'll make a second function, generate_voronoi_spawn_regions :

380 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

/// Generates a Voronoi/cellular noise map of a region, and divides it


into spawn regions.
#[allow(clippy::map_entry)]
pub fn generate_voronoi_spawn_regions(map: &Map, rng : &mut
rltk::RandomNumberGenerator) -> HashMap<i32, Vec<usize>> {
let mut noise_areas : HashMap<i32, Vec<usize>> = HashMap::new();
let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as
u64);
noise.set_noise_type(rltk::NoiseType::Cellular);
noise.set_frequency(0.08);

noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan);

for y in 1 .. map.height-1 {
for x in 1 .. map.width-1 {
let idx = map.xy_idx(x, y);
if map.tiles[idx] == TileType::Floor {
let cell_value_f = noise.get_noise(x as f32, y as f32) *
10240.0;
let cell_value = cell_value_f as i32;

if noise_areas.contains_key(&cell_value) {
noise_areas.get_mut(&cell_value).unwrap().push(idx);
} else {
noise_areas.insert(cell_value, vec![idx]);
}
}
}
}

noise_areas
}

Plugging these into our build function lets us reduce the boilerplate section
considerably:

381 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Find all tiles we can reach from the starting point


let exit_tile = remove_unreachable_areas_returning_most_distant(&mut
self.map, start_idx);
self.take_snapshot();

// Place the stairs


self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();

// Now we build a noise map for use in spawning entities later


self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);

In the example, I've gone back to the cellular_automata section and done the same.

This is basically the same code we had before (hence, it isn't explained here), but
wrapped in a function (and taking a mutable map reference - so it changes the map
you give it, and the starting point as parameters).

Walking Drunkards
The basic idea behind the algorithm is simple:

1. Pick a central starting point, and convert it to a �oor.


2. We count how much of the map is �oor space, and iterate until we have
converted a percentage (we use 50% in the example) of the map to �oors.
1. Spawn a drunkard at the starting point. The drunkard has a "lifetime" and a
"position".
2. While the drunkard is still alive:
1. Decrement the drunkard's lifetime (I like to think that they pass out
and sleep).
2. Roll a 4-sided dice.
1. If we rolled a 1, move the drunkard North.
2. If we rolled a 2, move the drunkard South.
3. If we rolled a 3, move the drunkard East.
4. If we rolled a 4, move the drunkard West.
3. The tile on which the drunkard landed becomes a �oor.

That's really all there is to it: we keep spawning drunkards until we have su�cient

382 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map coverage. Here's an implementation:

383 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Set a central starting point


self.starting_position = Position{ x: self.map.width / 2, y:
self.map.height / 2 };
let start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
self.map.tiles[start_idx] = TileType::Floor;

let total_tiles = self.map.width * self.map.height;


let desired_floor_tiles = (total_tiles / 2) as usize;
let mut floor_tile_count = self.map.tiles.iter().filter(|a| **a ==
TileType::Floor).count();
let mut digger_count = 0;
let mut active_digger_count = 0;

while floor_tile_count < desired_floor_tiles {


let mut did_something = false;
let mut drunk_x = self.starting_position.x;
let mut drunk_y = self.starting_position.y;
let mut drunk_life = 400;

while drunk_life > 0 {


let drunk_idx = self.map.xy_idx(drunk_x, drunk_y);
if self.map.tiles[drunk_idx] == TileType::Wall {
did_something = true;
}
self.map.tiles[drunk_idx] = TileType::DownStairs;

let stagger_direction = rng.roll_dice(1, 4);


match stagger_direction {
1 => { if drunk_x > 2 { drunk_x -= 1; } }
2 => { if drunk_x < self.map.width-2 { drunk_x += 1; } }
3 => { if drunk_y > 2 { drunk_y -=1; } }
_ => { if drunk_y < self.map.height-2 { drunk_y += 1; } }
}

drunk_life -= 1;
}
if did_something {
self.take_snapshot();
active_digger_count += 1;
}

digger_count += 1;
for t in self.map.tiles.iter_mut() {
if *t == TileType::DownStairs {

384 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

*t = TileType::Floor;
}
}
floor_tile_count = self.map.tiles.iter().filter(|a| **a ==
TileType::Floor).count();
}
println!("{} dwarves gave up their sobriety, of whom {} actually found a
wall.", digger_count, active_digger_count);

This implementation expands a lot of things out, and could be much shorter - but for
clarity, we've left it large and obvious. We've also made a bunch of things into
variables that could be constants - it's easier to read, and is designed to be easy to
"play" with values. It also prints a status update to the console, showing what
happened.

If you cargo run now, you'll get a pretty nice open map:

Managing The Diggers' Alcoholism


There's a lot of ways to tweak the "drunkard's walk" algorithm to generate di�erent

385 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map types. Since these can produce radically di�erent maps, lets customize the
interface to the algorithm to provide a few di�erent ways to run. We'll start by
creating a struct to hold the parameter sets:

#[derive(PartialEq, Copy, Clone)]


pub enum DrunkSpawnMode { StartingPoint, Random }

pub struct DrunkardSettings {


pub spawn_mode : DrunkSpawnMode
}

Now we'll modify new and the structure itself to accept it:

pub struct DrunkardsWalkBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>,
settings : DrunkardSettings
}

...

impl DrunkardsWalkBuilder {
pub fn new(new_depth : i32, settings: DrunkardSettings) ->
DrunkardsWalkBuilder {
DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings
}
}
...

We'll also modify the "random" builder to take settings:

386 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Box::new(DrunkardsWalkBuilder::new(new_depth, DrunkardSettings{
spawn_mode: DrunkSpawnMode::StartingPoint }))

Now we have a mechanism to tune the inebriation of our diggers!

Varying the drunken rambler's starting point


We alluded to it in the previous section with the creation of DrunkSpawnMode - we're
going to see what happens if we change the way drunken diggers - after the �rst -
spawn. Change the random_builder to DrunkSpawnMode::Random , and then modify
build (in drunkard.rs ) to use it:

...
while floor_tile_count < desired_floor_tiles {
let mut did_something = false;
let mut drunk_x;
let mut drunk_y;
match self.settings.spawn_mode {
DrunkSpawnMode::StartingPoint => {
drunk_x = self.starting_position.x;
drunk_y = self.starting_position.y;
}
DrunkSpawnMode::Random => {
if digger_count == 0 {
drunk_x = self.starting_position.x;
drunk_y = self.starting_position.y;
} else {
drunk_x = rng.roll_dice(1, self.map.width - 3) + 1;
drunk_y = rng.roll_dice(1, self.map.height - 3) + 1;
}
}
}
let mut drunk_life = 400;
...

This is a relatively easy change: if we're in "random" mode, the starting position for
the drunkard is the center of the map for the �rst digger (to ensure that we have
some space around the stairs), and then a random map location for each subsequent

387 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

iteration. It produces maps like this:

This is a much more spread out map. Less of a big central area, and more like a
sprawling cavern. A handy variation!

Modifying how long it takes for the drunkard to pass


out
Another parameter to tweak is how long the drunkard stays awake. This can seriously
change the character of the resultant map. We'll add it into the settings:

pub struct DrunkardSettings {


pub spawn_mode : DrunkSpawnMode,
pub drunken_lifetime : i32
}

We'll tell the random_builder function to use a shorter lifespan:

388 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Box::new(DrunkardsWalkBuilder::new(new_depth, DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100
}))

And we'll modify the build code to actually use it:

let mut drunk_life = self.settings.drunken_lifetime;

That's a simple change - and drastically alters the nature of the resulting map. Each
digger can only go one quarter the distance of the previous ones (stronger beer!), so
they tend to carve out less of the map. That leads to more iterations, and since they
start randomly you tend to see more distinct map areas forming - and hope they join
up (if they don't, they will be culled at the end).

cargo run with the 100 lifespan, randomly placed drunkards produces something
like this:

389 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Changing the desired �ll percentage


Lastly, we'll play with how much of the map we want to cover with �oors. The lower
the number, the more walls (and less open areas) you generate. We'll once again
modify DrunkardSettings :

pub struct DrunkardSettings {


pub spawn_mode : DrunkSpawnMode,
pub drunken_lifetime : i32,
pub floor_percent: f32
}

We also change one line in our builder:

let desired_floor_tiles = (self.settings.floor_percent * total_tiles as


f32) as usize;

We previously had desired_floor_tiles as total_tiles / 2 - which would be


represented by 0.5 in the new system. Lets try changing that to 0.4 in
random_builder :

Box::new(DrunkardsWalkBuilder::new(new_depth, DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 200,
floor_percent: 0.4
}))

If you cargo run now, you'll see that we have even fewer open areas forming:

390 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Building some preset constructors


Now that we've got these parameters to play with, lets make a few more constructors
to remove the need for the caller in mod.rs to know about the algorithm details:

391 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new(new_depth : i32, settings: DrunkardSettings) ->


DrunkardsWalkBuilder {
DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings
}
}

pub fn open_area(new_depth : i32) -> DrunkardsWalkBuilder {


DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::StartingPoint,
drunken_lifetime: 400,
floor_percent: 0.5
}
}
}

pub fn open_halls(new_depth : i32) -> DrunkardsWalkBuilder {


DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 400,
floor_percent: 0.5
}
}
}

pub fn winding_passages(new_depth : i32) -> DrunkardsWalkBuilder {


DrunkardsWalkBuilder{
map : Map::new(new_depth),

392 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100,
floor_percent: 0.4
}
}
}

Now we can modify our random_builder function to be once again random - and
o�er three di�erent map types:

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 7);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

Wrap-Up
And we're done with drunken map building (words I never expected to type...)! It's a
very �exible algorithm, and can be used to make a lot of di�erent map types. It also
combines well with other algorithms, as we'll see in future chapters.

The source code for this chapter may be found here

393 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Maze/Labyrinth Generation

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

A mainstay of dungeon crawl games is the good old-fashioned labyrinth, often


featuring a Minotaur. Dungeon Crawl: Stone Soup has a literal minotaur labyrinth,
Tome 4 has sand-worm mazes, One Knight has an elven hedge maze. These levels can
be annoying for the player, and should be used sparingly: a lot of players don't really
enjoy the tedium of exploring to �nd an exit. This chapter will show you how to make
a labyrinth!

Sca�olding
Once again, we'll use the previous chapter as sca�olding - and set our "random"
builder to use the new design. In map_builders/maze.rs , place the following code:

394 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map,


TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER,
remove_unreachable_areas_returning_most_distant,
generate_voronoi_spawn_regions};
use rltk::RandomNumberGenerator;
use specs::prelude::*;
use std::collections::HashMap;

pub struct MazeBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>
}

impl MapBuilder for MazeBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
self.build();
}

fn spawn_entities(&mut self, ecs : &mut World) {


for area in self.noise_areas.iter() {
spawner::spawn_region(ecs, area.1, self.depth);
}
}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}

395 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

self.history.push(snapshot);
}
}
}

impl MazeBuilder {
pub fn new(new_depth : i32) -> MazeBuilder {
MazeBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new()
}
}

#[allow(clippy::map_entry)]
fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// Find a starting point; start at the middle and walk left until
we find an open tile
self.starting_position = Position{ x: self.map.width / 2, y :
self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
}
self.take_snapshot();

// Find all tiles we can reach from the starting point


let exit_tile =
remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
self.take_snapshot();

// Place the stairs


self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();

// Now we build a noise map for use in spawning entities later


self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut
rng);
}

396 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

And in random_builder ( map_builders/mod.rs ):

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 7);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(MazeBuilder::new(new_depth))
}

Actually building a maze


There are lots of good maze building algorithms out there, all guaranteed to give you
a perfectly solvable maze. In One Knight in the Dungeon, I based my maze building code
o� of a relatively standard implementation - Cyucelen's mazeGenerator. It's an
interesting algorithm because - like a lot of maze algorithms - it assumes that walls
are part of the tile grid, rather than having separate wall entities. That isn't going to
work for the type of tile map we are using, so we generate the grid at half the
resolution of the actual map, and generate walls based upon wall adjacency
information in the grid.

The algorithm started as C++ code with pointers everywhere, and took a bit of time to
port. The most basic structure in the algorithm: the Cell . Cells are tiles on the map:

397 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

const TOP : usize = 0;


const RIGHT : usize = 1;
const BOTTOM : usize = 2;
const LEFT : usize = 3;

#[derive(Copy, Clone)]
struct Cell {
row: i32,
column: i32,
walls: [bool; 4],
visited: bool,
}

We de�ne four constants: TOP, RIGHT, BOTTOM and LEFT and assign them to the
numbers 0..3 . We use these whenever the algorithm wants to refer to a direction.
Looking at Cell , it is relatively simple:

row and column de�ne where the cell is on the map.


walls is an array , with a bool for each of the directions we've de�ned. Rust
arrays (static, you can't resize them like a vector ) are de�ned with the syntax
[TYPE ; NUMBER_OF_ELEMENTS] . Most of the time we just use vectors because
we like the dynamic sizing; in this case, the number of elements is known ahead
of time, so using the lower-overhead type makes sense.
visited - a bool indicating whether we've previously looked at the cell.

Cell also de�nes some methods. The �rst is its constructor:

impl Cell {
fn new(row: i32, column: i32) -> Cell {
Cell{
row,
column,
walls: [true, true, true, true],
visited: false
}
}
...

This is a simple constructor: it makes a cell with walls in each direction, and not
previously visited. Cells also de�ne a function called remove_walls :

398 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

unsafe fn remove_walls(&mut self, next : *mut Cell) {


let x = self.column - (*(next)).column;
let y = self.row - (*(next)).row;

if x == 1 {
self.walls[LEFT] = false;
(*(next)).walls[RIGHT] = false;
}
else if x == -1 {
self.walls[RIGHT] = false;
(*(next)).walls[LEFT] = false;
}
else if y == 1 {
self.walls[TOP] = false;
(*(next)).walls[BOTTOM] = false;
}
else if y == -1 {
self.walls[BOTTOM] = false;
(*(next)).walls[TOP] = false;
}
}

Uh oh, there's some new stu� here:

The terrifying looking unsafe keyword says "this function uses code that Rust
cannot prove to be memory-safe". In other words, it is the Rust equivalent of
typing here be dragons .
We need the unsafe �ag because of next : *mut Cell . We've not used a raw
pointer before - we're into C/C++ land, now! The syntax is relatively
straightforward; the * indicates that it is a pointer (rather than a reference). The
mut indicates that we can change the contents (that's what makes it unsafe;
Rust can't verify that the pointer is actually valid, so if we change something we
might really break things!), and it points to a Cell . A reference is actually a type
of pointer; it points to a variable, and if you access it - you are reading the
variable to which it points (so there's only one copy existing). With a reference,
you can change the value with *myref = 1 - the * dereferences the reference,
giving you the original area of memory. Rust can check that this is valid with the
borrow checker. More on this in a moment.
We set x to be our column value, minus the column value of the cell we are
pointing at. The (*(next)).column is horrible syntax, and should discourage
anyone from using pointers (I think that's the point). The �rst parentheses

399 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

indicate that we're modifying the type; the * dereferences the pointer encased
in the �nal set of parentheses. Since we're changing a value pointed to by a
pointer, this is inherently unsafe (and makes the function not compile if we don't
use the unsafe �ag), and also means: BE REALLY CAREFUL.
We do the same with y - but with row values.
If x is equal to 1, then the pointer's column must be greater than our column
value. In other words, the next cell is to the right of our current location. So we
remove the wall to the right.
Likewise, if x is -1 , then we must be going left - so we remove the wall to the
right.
Once again, if y is 1 , we must be going up. So we remove the walls to the top.
Finally, if y is -1 , we must be going down - so we remove the walls below us.

Whew! Cell is done. Now to actually use it. In our maze algorithm, Cell is part of
Grid . Here's the basic Grid de�nition:

struct Grid<'a> {
width: i32,
height: i32,
cells: Vec<Cell>,
backtrace: Vec<usize>,
current: usize,
rng : &'a mut RandomNumberGenerator
}

Some commentary on Grid :

The <'a> is a lifetime speci�er. We have to specify one so that Rust's borrow
checker can ensure that the Grid will not expire before we delete the
RandomNumberGenerator . Because we're passing a mutable reference to the
caller's RNG, Rust needs this to ensure that the RNG doesn't go away before
we're �nished with it. This type of bug often a�ects C/C++ users, so Rust made it
really hard to mess up. Unfortunately, the price of making it hard to get wrong is
some ugly syntax!
We have a width and height de�ning the size of the maze.
Cells are just a Vector of the Cell type we de�ned earlier.
backtrace is used by the algorithm for recursively back-tracking to ensure that
every cell has been processed. It's just a vector of cell indices - the index into
the cells vector.

400 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

current is used by the algorithm to tell which Cell we're currently working
with.
rng is the reason for the ugly lifetime stu�; we want to use the random number
generator built in the build function, so we store a reference to it here.
Because obtaining a random number changes the content of the variable, we
have to store a mutable reference. The really ugly &'a mut indicates that it is a
reference, with the lifetime 'a (de�ned above) and is mutable/changeable.

Grid implements quite a few methods. First up, the constructor:

impl<'a> Grid<'a> {
fn new(width: i32, height:i32, rng: &mut RandomNumberGenerator) ->
Grid {
let mut grid = Grid{
width,
height,
cells: Vec::new(),
backtrace: Vec::new(),
current: 0,
rng
};

for row in 0..height {


for column in 0..width {
grid.cells.push(Cell::new(row, column));
}
}

grid
}
...

Notice that once again we had to use some ugly syntax for the lifetime! The
constructor itself is quite simple: it makes a new Grid structure with the speci�ed
width and height , a new vector of cells, a new (empty) backtrace vector, sets
current to 0 and stores the random number generator reference. Then it iterates
the rows and columns of the grid, pushing new Cell structures to the cells vector,
numbered by their location.

The Grid also implements calculate_index :

401 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn calculate_index(&self, row: i32, column: i32) -> i32 {


if row < 0 || column < 0 || column > self.width-1 || row >
self.height-1 {
-1
} else {
column + (row * self.width)
}
}

This is very similar to our map 's xy_idx function: it takes a row and column
coordinate, and returns the array index at which one can �nd the cell. It also does
some bounds checking, and returns -1 if the coordinates are invalid. Next, we
provide get_available_neighbors :

fn get_available_neighbors(&self) -> Vec<usize> {


let mut neighbors : Vec<usize> = Vec::new();

let current_row = self.cells[self.current].row;


let current_column = self.cells[self.current].column;

let neighbor_indices : [i32; 4] = [


self.calculate_index(current_row -1, current_column),
self.calculate_index(current_row, current_column + 1),
self.calculate_index(current_row + 1, current_column),
self.calculate_index(current_row, current_column - 1)
];

for i in neighbor_indices.iter() {
if *i != -1 && !self.cells[*i as usize].visited {
neighbors.push(*i as usize);
}
}

neighbors
}

This function provides the available exits from the current cell. It works by obtaining
the row and column coordinates of the current cell, and then puts a call to
calculate_index into an array (corresponding to the directions we de�ned with
Cell ). It �nally iterates the array, and if the values are valid (greater than -1 ), and we
haven't been there before (the visited check) it pushes them into the neighbors list.

402 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

It then returns neighbors . A call to this for any cell address will return a vector
listing all of the adjacent cells to which we can travel (ignoring walls). We �rst use this
in find_next_cell :

fn find_next_cell(&mut self) -> Option<usize> {


let neighbors = self.get_available_neighbors();
if !neighbors.is_empty() {
if neighbors.len() == 1 {
return Some(neighbors[0]);
} else {
return Some(neighbors[(self.rng.roll_dice(1, neighbors.len()
as i32)-1) as usize]);
}
}
None
}

This function is interesting in that it returns an Option . It's possible that there is
nowhere to go from the current cell - in which case it returns None . Otherwise, it
returns Some with the array index of the next destination. It works by:

Obtain a list of neighbors for the current cell.


If there are neighbors:
If there is only one neighbor, return it.
If there are multiple neighbors, pick one and random and return it.
If there are no neighbors, return None .

We use this from generate_maze :

403 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn generate_maze(&mut self, generator : &mut MazeBuilder) {


loop {
self.cells[self.current].visited = true;
let next = self.find_next_cell();

match next {
Some(next) => {
self.cells[next].visited = true;
self.backtrace.insert(0, self.current);
unsafe {
let next_cell : *mut Cell = &mut self.cells[next];
let current_cell = &mut self.cells[self.current];
current_cell.remove_walls(next_cell);
}
self.current = next;
}
None => {
if !self.backtrace.is_empty() {
self.current = self.backtrace[0];
self.backtrace.remove(0);
} else {
break;
}
}
}

self.copy_to_map(&mut generator.map);
generator.take_snapshot();
}
}

So now we're onto the actual algorithm! Lets step through it to understand how it
works:

1. We start with a loop . We haven't used one of these before (you can read about
them here). Basically, a loop runs forever - until it hits a break statement.
2. We set the value of visited in the current cell to true .
3. We add the current cell to the beginning of the backtrace list.
4. We call find_next_cell and set its index in the variable next . If this is our �rst
run, we'll get a random direction from the starting cell. Otherwise, we get an exit
from the current cell we're visiting.
5. If next has a value, then:

404 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. Get a pointer to the next_cell . If you remember, remove_walls takes a


pointer. This is why we jump through those hoops: the borrow checker
really doesn't like us borrowing values from two elements of the array at
once. So to keep it happy, we use a pointer.
2. We also obtain a reference to the current cell.
3. We call remove_walls on the current cell, referencing the next cell. The
good news is that we're being careful, so we can be sure that we aren't
going to hit a null pointer error. Rust cannot know that, so we use the
unsafe block to indicate that we know what we're doing!
6. If next does not have a value (it's equal to None ), we:
1. If backtrace isn't empty, we set current to the �rst value in the
backtrace list.
2. If backtrace is empty, we've �nished - so we break out of the loop.
7. Finally, we call copy_to_map - which copies the maze to the map (more on that
below), and take a snapshot for the iterative map generation renderer.

So why does that work?

The �rst few iterations will get a non-visited neighbor, carving a clear path
through the maze. Each step along the way, the cell we've visited is added to
backtrace . This is e�ectively a drunken walk through the maze, but ensuring
that we cannot return to a cell.
When we hit a point at which we have no neighbors (we've hit the end of the
maze), the algorithm will change current to the �rst entry in our backtrace
list. It will then randomly walk from there, �lling in more cells.
If that point can't go anywhere, it works back up the backtrace list.
This repeats until every cell has been visited, meaning that backtrace and
neighbors are both empty. We're done!

The best way to understand this is to watch it in action:

405 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Finally, there's the copy_to_map function:

fn copy_to_map(&self, map : &mut Map) {


// Clear the map
for i in map.tiles.iter_mut() { *i = TileType::Wall; }

for cell in self.cells.iter() {


let x = cell.column + 1;
let y = cell.row + 1;
let idx = map.xy_idx(x * 2, y * 2);

map.tiles[idx] = TileType::Floor;
if !cell.walls[TOP] { map.tiles[idx - map.width as usize] =
TileType::Floor }
if !cell.walls[RIGHT] { map.tiles[idx + 1] = TileType::Floor }
if !cell.walls[BOTTOM] { map.tiles[idx + map.width as usize] =
TileType::Floor }
if !cell.walls[LEFT] { map.tiles[idx - 1] = TileType::Floor }
}
}

This is where the mismatch between Grid/Cell and our map format is resolved:

406 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

each Cell in the maze structure can have walls in any of the four major directions.
Our map doesn't work that way: walls aren't part of a tile, they are a tile. So we double
the size of the Grid , and write carve �oors where walls aren't present. Lets walk
through this function:

1. We set all cells in the map to be a solid wall.


2. For each cell in the grid, we:
1. Calculate x as the cell's column value, plus one.
2. Calculate y as the cell's row value, plus one.
3. Set idx to map.xy_idx of DOUBLE the x and y values: so spread each
cell out.
4. We set the map tile at idx to be a �oor.
5. If the Cell we're referencing does not have a TOP wall, we set the map tile
above our idx tile to be a �oor.
6. We repeat that for the other directions.

Speeding up the generator


We're wasting a lot of time by snapshotting at every iteration - we're building a huge
list of snapshot maps. That was great for learning the algorithm, but simply takes too
long when playing the game. We'll modify our generate_maze function to count
iterations, and only log every 10th:

407 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn generate_maze(&mut self, generator : &mut MazeBuilder) {


let mut i = 0;
loop {
self.cells[self.current].visited = true;
let next = self.find_next_cell();

match next {
Some(next) => {
self.cells[next].visited = true;
self.backtrace.insert(0, self.current);
unsafe {
let next_cell : *mut Cell = &mut self.cells[next];
let current_cell = &mut self.cells[self.current];
current_cell.remove_walls(next_cell);
}
self.current = next;
}
None => {
if !self.backtrace.is_empty() {
self.current = self.backtrace[0];
self.backtrace.remove(0);
} else {
break;
}
}
}

if i % 50 == 0 {
self.copy_to_map(&mut generator.map);
generator.take_snapshot();
}
i += 1;
}
}

This brings the generator up to a reasonable speed, and you can still watch the maze
develop.

Finding the exit


Fortunately, our current algorithm will start you at Cell (1,1) - which corresponds to

408 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map location (2,2). So in build , we can easily specify a starting point:

self.starting_position = Position{ x: 2, y : 2 };
let start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
self.take_snapshot();

We can then use the same code we've used in the last two examples to �nd an exit:

// Find all tiles we can reach from the starting point


let exit_tile = remove_unreachable_areas_returning_most_distant(&mut
self.map, start_idx);
self.take_snapshot();

// Place the stairs


self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();

// Now we build a noise map for use in spawning entities later


self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);

This is also a great test of the library's Dijkstra map code. It can solve a maze very
quickly!

Restoring the randomness


Once again, we should restore random_builder to be random:

409 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 8);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(MazeBuilder::new(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

Wrap-Up
In this chapter, we've built a maze. It's a guaranteed solvable maze, so there's no risk
of a level that you can't beat. You still have to use this type of map with caution: they
make good one-o� maps, and can really annoy players!

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Di�usion-Limited Aggregation

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to

410 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Di�usion-Limited Aggregation (DLA) is a fancy name for a constrained form of the


drunken walk. It makes organic looking maps, with more of an emphasis on a central
area and "arms" coming out of it. With some tricks, it can be made to look quite alien -
or quite real. See this excellent article on Rogue Basin.

Sca�olding
We'll create a new �le, map_builders/dla.rs and put the sca�olding in from previous
projects. We'll name the builder DLABuilder . We'll also keep the voronoi spawn code,
it will work �ne for this application. Rather than repeat the sca�olding code blocks
from previous chapters, we'll jump straight in. If you get stuck, you can check the
source code for this chapter here.

Algorithm Tuning Knobs


In the last chapter, we introduced the idea of adding parameters to our builder. We'll
do the same again for DLA - there's a few algorithm variants that can produce
di�erent map styles. We'll introduce the following enumerations:

#[derive(PartialEq, Copy, Clone)]


pub enum DLAAlgorithm { WalkInwards, WalkOutwards, CentralAttractor }

#[derive(PartialEq, Copy, Clone)]


pub enum DLASymmetry { None, Horizontal, Vertical, Both }

Our builder will include one more, brush size:

411 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct DLABuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>,
algorithm : DLAAlgorithm,
brush_size: i32,
symmetry: DLASymmetry,
floor_percent: f32
}

This should be pretty self-explanatory by now if you've been through the other
chapters:

We're supporting three algorithms, WalkInwards , WalkOutwards ,


CentralAttractor . We'll cover these in detail shortly.
We've added symmetry , which can be either None , Horizontal , Vertical or
Both . Symmetry can be used to make some beautiful results with this
algorithm, and we'll cover that later in the article.
We've also added brush_size , which speci�es how many �oor tiles we "paint"
onto the map in one go. We'll look at this at the end of the chapter.
We've included floor_percent from the Drunkard's Walk chapter.

Our new function needs to include these parameters:

pub fn new(new_depth : i32) -> DLABuilder {


DLABuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
algorithm: DLAAlgorithm::WalkInwards,
brush_size: 1,
symmetry: DLASymmetry::None,
floor_percent: 0.25
}
}

We'll make some type constructors once we've mastered the algorithms and their

412 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

variants!

Walking Inwards
The most basic form of Di�usion-Limited Aggregation works like this:

1. Dig a "seed" area around your central starting point.


2. While the number of �oor tiles is less than your desired total:
1. Select a starting point at random for your digger.
2. Use the "drunkard's walk" algorithm to move randomly.
3. If the digger hit a �oor tile, then the previous tile they were in also becomes
a �oor and the digger stops.

Very simple, and not too hard to implement:

413 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// Carve a starting seed


self.starting_position = Position{ x: self.map.width/2, y :
self.map.height/2 };
let start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
self.take_snapshot();
self.map.tiles[start_idx] = TileType::Floor;
self.map.tiles[start_idx-1] = TileType::Floor;
self.map.tiles[start_idx+1] = TileType::Floor;
self.map.tiles[start_idx-self.map.width as usize] = TileType::Floor;
self.map.tiles[start_idx+self.map.width as usize] = TileType::Floor;

// Random walker
let total_tiles = self.map.width * self.map.height;
let desired_floor_tiles = (self.floor_percent * total_tiles as f32) as
usize;
let mut floor_tile_count = self.map.tiles.iter().filter(|a| **a ==
TileType::Floor).count();
while floor_tile_count < desired_floor_tiles {

match self.algorithm {
DLAAlgorithm::WalkInwards => {
let mut digger_x = rng.roll_dice(1, self.map.width - 3) +
1;
let mut digger_y = rng.roll_dice(1, self.map.height - 3) +
1;
let mut prev_x = digger_x;
let mut prev_y = digger_y;
let mut digger_idx = self.map.xy_idx(digger_x, digger_y);
while self.map.tiles[digger_idx] == TileType::Wall {
prev_x = digger_x;
prev_y = digger_y;
let stagger_direction = rng.roll_dice(1, 4);
match stagger_direction {
1 => { if digger_x > 2 { digger_x -= 1; } }
2 => { if digger_x < self.map.width-2 { digger_x
+= 1; } }
3 => { if digger_y > 2 { digger_y -=1; } }
_ => { if digger_y < self.map.height-2 { digger_y
+= 1; } }
}
digger_idx = self.map.xy_idx(digger_x, digger_y);

414 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
self.paint(prev_x, prev_y);
}
_ => {}
...

The only new thing here is the call to paint . We'll be extending it later (to handle
brush sizes), but here's a temporary implementation:

fn paint(&mut self, x: i32, y: i32) {


let digger_idx = self.map.xy_idx(x, y);
self.map.tiles[digger_idx] = TileType::Floor;
}

If you cargo run this, you will get a pretty cool looking dungeon:

Walking outwards
A second variant of this algorithm reverses part of the process:

415 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. Dig a "seed" area around your central starting point.


2. While the number of �oor tiles is less than your desired total:
1. Set the digger to the starting central location.
2. Use the "drunkard's walk" algorithm to move randomly.
3. If the digger hit a wall tile, then that tile becomes a �oor - and the digger
stops.

So instead of marching inwards, our brave diggers are marching outwards.


Implementing this is quite simple, and can be added to the match sequence of
algorithms in build :

...
DLAAlgorithm::WalkOutwards => {
let mut digger_x = self.starting_position.x;
let mut digger_y = self.starting_position.y;
let mut digger_idx = self.map.xy_idx(digger_x, digger_y);
while self.map.tiles[digger_idx] == TileType::Floor {
let stagger_direction = rng.roll_dice(1, 4);
match stagger_direction {
1 => { if digger_x > 2 { digger_x -= 1; } }
2 => { if digger_x < self.map.width-2 { digger_x += 1; } }
3 => { if digger_y > 2 { digger_y -=1; } }
_ => { if digger_y < self.map.height-2 { digger_y += 1; } }
}
digger_idx = self.map.xy_idx(digger_x, digger_y);
}
self.paint(digger_x, digger_y);
}
_ => {}

There aren't any new concepts in this code, and if you understood Drunkard's Walk - it
should be pretty self explanatory. If you adjust the constructor to use it, and call
cargo run it looks pretty good:

416 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Central Attractor
This variant is again very similar, but slightly di�erent. Instead of moving randomly,
your particles path from a random point towards the middle:

1. Dig a "seed" area around your central starting point.


2. While the number of �oor tiles is less than your desired total:
1. Select a starting point at random for your digger.
2. Plot a line to the center of the map, and keep it.
3. Traverse the line. If the digger hit a �oor tile, then the previous tile they
were in also becomes a �oor and the digger stops.

Again, this is relatively easy to implement:

417 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
DLAAlgorithm::CentralAttractor => {
let mut digger_x = rng.roll_dice(1, self.map.width - 3) + 1;
let mut digger_y = rng.roll_dice(1, self.map.height - 3) + 1;
let mut prev_x = digger_x;
let mut prev_y = digger_y;
let mut digger_idx = self.map.xy_idx(digger_x, digger_y);

let mut path = rltk::line2d(


rltk::LineAlg::Bresenham,
rltk::Point::new( digger_x, digger_y ),
rltk::Point::new( self.starting_position.x,
self.starting_position.y )
);

while self.map.tiles[digger_idx] == TileType::Wall && !path.is_empty()


{
prev_x = digger_x;
prev_y = digger_y;
digger_x = path[0].x;
digger_y = path[0].y;
path.remove(0);
digger_idx = self.map.xy_idx(digger_x, digger_y);
}
self.paint(prev_x, prev_y);
}

If you adjust the constructor to use this algorithm, and cargo run the project you get
a map that is more focused around a central point:

418 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Implementing Symmetry

Tyger Tyger, burning bright,


In the forests of the night;
What immortal hand or eye,
Could frame thy fearful symmetry?

(William Blake, The Tyger)

Symmetry can transform a random map into something that looks designed - but
quite alien. It often looks quite insectoid or reminiscent of a Space Invaders enemy.
This can make for some fun-looking levels!

Lets modify the paint function to handle symmetry:

419 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn paint(&mut self, x: i32, y:i32) {


match self.symmetry {
DLASymmetry::None => self.apply_paint(x, y),
DLASymmetry::Horizontal => {
let center_x = self.map.width / 2;
if x == center_x {
self.apply_paint(x, y);
} else {
let dist_x = i32::abs(center_x - x);
self.apply_paint(center_x + dist_x, y);
self.apply_paint(center_x - dist_x, y);
}
}
DLASymmetry::Vertical => {
let center_y = self.map.height / 2;
if y == center_y {
self.apply_paint(x, y);
} else {
let dist_y = i32::abs(center_y - y);
self.apply_paint(x, center_y + dist_y);
self.apply_paint(x, center_y - dist_y);
}
}
DLASymmetry::Both => {
let center_x = self.map.width / 2;
let center_y = self.map.height / 2;
if x == center_x && y == center_y {
self.apply_paint(x, y);
} else {
let dist_x = i32::abs(center_x - x);
self.apply_paint(center_x + dist_x, y);
self.apply_paint(center_x - dist_x, y);
let dist_y = i32::abs(center_y - y);
self.apply_paint(x, center_y + dist_y);
self.apply_paint(x, center_y - dist_y);
}
}
}

This is a longer function that it really needs to be, in the name of clarity. Here's how it
works:

1. We match on the current symmetry setting.


2. If it is None , we simply call apply_paint with the destination tile.

420 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

3. If it is Horizontal :
1. We check to see if we are on the tile - if we are, just apply the paint once.
2. Otherwise, obtain the horizontal distance from the center.
3. Paint at center_x - distance and center_x + distance to paint
symmetrically on the x axis.
4. If it is Vertical :
1. We check to see if we are on the tile - if we are, just apply the paint once
(this helps with odd numbers of tiles by reducing rounding issues).
2. Otherwise, obtain the vertical distance from the center.
3. Paint at center_y - distance and center_y + distance .
5. If it is Both - then do both steps.

You'll notice that we're calling apply_paint rather than actually painting. That's
because we've also implemented brush_size :

fn apply_paint(&mut self, x: i32, y: i32) {


match self.brush_size {
1 => {
let digger_idx = self.map.xy_idx(x, y);
self.map.tiles[digger_idx] = TileType::Floor;
}

_ => {
let half_brush_size = self.brush_size / 2;
for brush_y in y-half_brush_size .. y+half_brush_size {
for brush_x in x-half_brush_size .. x+half_brush_size {
if brush_x > 1 && brush_x < self.map.width-1 &&
brush_y > 1 && brush_y < self.map.height-1 {
let idx = self.map.xy_idx(brush_x, brush_y);
self.map.tiles[idx] = TileType::Floor;
}
}
}
}
}
}

This is quite simple:

1. If brush size is 1, we just paint a �oor tile.


2. Otherwise, we loop through the brush size - and paint, performing bounds-

421 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

checking to ensure we aren't painting o� the map.

In your constructor, use the CentralAttractor algorithm - and enable symmetry with
Horizontal . If you cargo run now, you get a map not unlike a cranky insectoid:

Playing with Brush Sizes


Using a larger brush is ensures that you don't get too many 1x1 areas (that can be
�ddly to navigate), and gives a more planned look to the map. Now that we've already
implemented brush size, modify your constructor like this:

422 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new(new_depth : i32) -> DLABuilder {


DLABuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
algorithm: DLAAlgorithm::WalkInwards,
brush_size: 2,
symmetry: DLASymmetry::None,
floor_percent: 0.25
}
}

With this simple change, our map looks much more open:

Providing a few constructors


Rather than pollute the random_builder function with algorithm details, we'll make

423 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

constructors for each of the major algorithms we used in this chapter:

424 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn walk_inwards(new_depth : i32) -> DLABuilder {


DLABuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
algorithm: DLAAlgorithm::WalkInwards,
brush_size: 1,
symmetry: DLASymmetry::None,
floor_percent: 0.25
}
}

pub fn walk_outwards(new_depth : i32) -> DLABuilder {


DLABuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
algorithm: DLAAlgorithm::WalkOutwards,
brush_size: 2,
symmetry: DLASymmetry::None,
floor_percent: 0.25
}
}

pub fn central_attractor(new_depth : i32) -> DLABuilder {


DLABuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
algorithm: DLAAlgorithm::CentralAttractor,
brush_size: 2,
symmetry: DLASymmetry::None,
floor_percent: 0.25
}
}

pub fn insectoid(new_depth : i32) -> DLABuilder {


DLABuilder{
map : Map::new(new_depth),

425 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
algorithm: DLAAlgorithm::CentralAttractor,
brush_size: 2,
symmetry: DLASymmetry::Horizontal,
floor_percent: 0.25
}
}

Randomizing the map builder, once again


Now we can modify random_builder in map_builders/mod.rs to actually be random
once more - and o�er even more types of map!

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 12);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(MazeBuilder::new(new_depth)),
8 => Box::new(DLABuilder::walk_inwards(new_depth)),
9 => Box::new(DLABuilder::walk_outwards(new_depth)),
10 => Box::new(DLABuilder::central_attractor(new_depth)),
11 => Box::new(DLABuilder::insectoid(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

Wrap-up
This chapter has introduced another, very �exible, map builder for your arsenal. Great

426 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for making maps that feel like they were carved from the rock (or hewn from the
forest, mined from the asteroid, etc.), it's another great way to introduce variety into
your game.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Adding Symmetry and Brush Size as


Library Functions

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the previous chapter on Di�usion-Limited Aggregation, we introduced two new


concepts for map building: symmetry and brush size. These readily apply to other
algorithms, so we're going to take a moment to move them into library functions (in
map_builders/common.rs ), make them generic, and demonstrate how they can alter
the Drunkard's Walk.

Building the library versions


We'll start by moving the DLASymmetry enumeration out of dla.rs and into

427 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

common.rs . We'll also change its name, since we are no longer binding it to a speci�c
algorithm:

#[derive(PartialEq, Copy, Clone)]


pub enum Symmetry { None, Horizontal, Vertical, Both }

At the end of common.rs , we can add the following:

428 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn paint(map: &mut Map, mode: Symmetry, brush_size: i32, x: i32,


y:i32) {
match mode {
Symmetry::None => apply_paint(map, brush_size, x, y),
Symmetry::Horizontal => {
let center_x = map.width / 2;
if x == center_x {
apply_paint(map, brush_size, x, y);
} else {
let dist_x = i32::abs(center_x - x);
apply_paint(map, brush_size, center_x + dist_x, y);
apply_paint(map, brush_size, center_x - dist_x, y);
}
}
Symmetry::Vertical => {
let center_y = map.height / 2;
if y == center_y {
apply_paint(map, brush_size, x, y);
} else {
let dist_y = i32::abs(center_y - y);
apply_paint(map, brush_size, x, center_y + dist_y);
apply_paint(map, brush_size, x, center_y - dist_y);
}
}
Symmetry::Both => {
let center_x = map.width / 2;
let center_y = map.height / 2;
if x == center_x && y == center_y {
apply_paint(map, brush_size, x, y);
} else {
let dist_x = i32::abs(center_x - x);
apply_paint(map, brush_size, center_x + dist_x, y);
apply_paint(map, brush_size, center_x - dist_x, y);
let dist_y = i32::abs(center_y - y);
apply_paint(map, brush_size, x, center_y + dist_y);
apply_paint(map, brush_size, x, center_y - dist_y);
}
}
}
}

fn apply_paint(map: &mut Map, brush_size: i32, x: i32, y: i32) {


match brush_size {
1 => {
let digger_idx = map.xy_idx(x, y);

429 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

map.tiles[digger_idx] = TileType::Floor;
}

_ => {
let half_brush_size = brush_size / 2;
for brush_y in y-half_brush_size .. y+half_brush_size {
for brush_x in x-half_brush_size .. x+half_brush_size {
if brush_x > 1 && brush_x < map.width-1 && brush_y > 1
&& brush_y < map.height-1 {
let idx = map.xy_idx(brush_x, brush_y);
map.tiles[idx] = TileType::Floor;
}
}
}
}
}
}

This shouldn't be a surprise: it's the exact same code we had in dla.rs - but with the
&mut self removed and instead taking parameters.

Modifying dla.rs to use it


It's relatively simple to modify dla.rs to use it. Replace all DLASymmetry references
with Symmetry . Replace all calls to self.paint(x, y) with
paint(&mut self.map, self.symmetry, self.brush_size, x, y); . You can check
the source code to see the changes - no need to repeat them all here. Make sure to
include paint and Symmetry in the list of included functions at the top, too.

Like a lot of refactoring, the proof of the pudding is that if you cargo run your code -
nothing has changed! We won't bother with a screenshot to show that it's the same as
last time!

Modifying Drunkard's Walk to use it


We'll start by modifying the DrunkardSettings struct to accept the two new features:

430 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct DrunkardSettings {


pub spawn_mode : DrunkSpawnMode,
pub drunken_lifetime : i32,
pub floor_percent: f32,
pub brush_size: i32,
pub symmetry: Symmetry
}

The compiler will complain that we aren't setting these in our constructors, so we'll
add some default values:

pub fn open_area(new_depth : i32) -> DrunkardsWalkBuilder {


DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::StartingPoint,
drunken_lifetime: 400,
floor_percent: 0.5,
brush_size: 1,
symmetry: Symmetry::None
}
}
}

We need to make similar changes to the other constructors - just adding brush_size
and symmetry to each of the DrunkardSettings builders.

We also need to replace the line:

self.map.tiles[drunk_idx] = TileType::DownStairs;

With:

431 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

paint(&mut self.map, self.settings.symmetry, self.settings.brush_size,


drunk_x, drunk_y);
self.map.tiles[drunk_idx] = TileType::DownStairs;

The double-draw retains the function of adding > symbols to show you the walker's
path, while retaining the overdraw of the paint function.

Making a wider-carving drunk


To test this out, we'll add a new constructor to drunkard.rs :

pub fn fat_passages(new_depth : i32) -> DrunkardsWalkBuilder {


DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100,
floor_percent: 0.4,
brush_size: 2,
symmetry: Symmetry::None
}
}
}

We'll also quickly modify random_builder in map_builders/mod.rs to showcase this


one:

432 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 12);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(MazeBuilder::new(new_depth)),
8 => Box::new(DLABuilder::walk_inwards(new_depth)),
9 => Box::new(DLABuilder::walk_outwards(new_depth)),
10 => Box::new(DLABuilder::central_attractor(new_depth)),
11 => Box::new(DLABuilder::insectoid(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(DrunkardsWalkBuilder::fat_passages(new_depth))
}

This shows an immediate change in the map generation:

Notice how the "fatter" digging area gives more open halls. It also runs in half the

433 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

time, since we exhaust the desired �oor count much more quickly.

Adding Symmetry
Like DLA, symmetrical drunkards can make interesting looking maps. We'll add one
more constructor:

pub fn fearful_symmetry(new_depth : i32) -> DrunkardsWalkBuilder {


DrunkardsWalkBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100,
floor_percent: 0.4,
brush_size: 1,
symmetry: Symmetry::Both
}
}
}

We also modify our random_builder function to use it:

434 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 12);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(MazeBuilder::new(new_depth)),
8 => Box::new(DLABuilder::walk_inwards(new_depth)),
9 => Box::new(DLABuilder::walk_outwards(new_depth)),
10 => Box::new(DLABuilder::central_attractor(new_depth)),
11 => Box::new(DLABuilder::insectoid(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth))
}

cargo run will render results something like these:

435 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Notice how the symmetry is applied (really fast - we're blasting out the �oor tiles,
now!) - and then unreachable areas are culled, getting rid of part of the map. This is
quite a nice map!

Restoring Randomness Once More


Once again, we add our new algorithms to the random_builder function in
map_builders/mod.rs :

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 14);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)),
8 => Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)),
9 => Box::new(MazeBuilder::new(new_depth)),
10 => Box::new(DLABuilder::walk_inwards(new_depth)),
11 => Box::new(DLABuilder::walk_outwards(new_depth)),
12 => Box::new(DLABuilder::central_attractor(new_depth)),
13 => Box::new(DLABuilder::insectoid(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

We're up to 14 algorithms, now! We have an increasingly varied game!

Wrap-Up
This chapter has demonstrated a very useful tool for the game programmer: �nding a
handy algorithm, making it generic, and using it in other parts of your code. It's rare to
guess exactly what you need up-front (and there's a lot to be said for "you won't need

436 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

it" - implementing things when you do need them), so it's a valuable weapon in our
arsenal to be able to quickly refactor our code for reuse.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Voronoi Hive/Cell Maps

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

We've touched on Voronoi diagrams before, in our spawn placement. In this section,
we'll use them to make a map. The algorithm basically subdivides the map into
regions, and places walls between them. The result is a bit like a hive. You can play
with the distance/adjacency algorithm to adjust the results.

Sca�olding
We'll make sca�olding like in the previous chapters, making voronoi.rs with the
structure VoronoiBuilder in it. We'll also adjust our random_builder function to only
return VoronoiBuilder for now.

437 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Building a Voronoi Diagram


In previous usages, we've skimmed over how to actually make a Voronoi diagram -
and relied on the FastNoise library inside rltk . That's all well and good, but it
doesn't really show us how it works - and gives very limited opportunities to tweak it.
So - we'll make our own.

The �rst step in making some Voronoi noise it to populate a set of "seeds". These are
randomly chosen (but not duplicate) points on the map. We'll make the number of
seeds a variable so it can be tweaked later. Here's the code:

let n_seeds = 64;


let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new();

while voronoi_seeds.len() < n_seeds {


let vx = rng.roll_dice(1, self.map.width-1);
let vy = rng.roll_dice(1, self.map.height-1);
let vidx = self.map.xy_idx(vx, vy);
let candidate = (vidx, rltk::Point::new(vx, vy));
if !voronoi_seeds.contains(&candidate) {
voronoi_seeds.push(candidate);
}
}

This makes a vector , each entry containing a tuple . Inside that tuple, we're storing
an index to the map location, and a Point with the x and y coordinates in it (we
could skip saving those and calculate from the index if we wanted, but I feel that this
is clearer). Then we randomly determine a position, check to see that we haven't
already rolled that location, and add it. We repeat the process until we have the
desired number of seeds. 64 is quite a lot, but will give a relatively dense hive-like
structure.

The next step is to determine each cell's Voronoi membership:

438 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut voroni_distance = vec![(0, 0.0f32) ; n_seeds];


let mut voronoi_membership : Vec<i32> = vec![0 ; self.map.width as usize *
self.map.height as usize];
for (i, vid) in voronoi_membership.iter_mut().enumerate() {
let x = i as i32 % self.map.width;
let y = i as i32 / self.map.width;

for (seed, pos) in voronoi_seeds.iter().enumerate() {


let distance = rltk::DistanceAlg::PythagorasSquared.distance2d(
rltk::Point::new(x, y),
pos.1
);
voroni_distance[seed] = (seed, distance);
}

voroni_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());

*vid = voroni_distance[0].0 as i32;


}

In this block of code, we:

1. Create a new vector , called voronoi_distance . It contains tuples of a usize


and a f32 (�oat), and is pre-made with n_seeds entries. We could make this for
every iteration, but it's a lot faster to reuse the same one. We create it zeroed.
2. We create a new voronoi_membership vector, containing one entry per tile on
the map. We set them all to 0. We'll use this to store which Voronoi cell the tile
belongs to.
3. For every tile in voronoi_membership , we obtain an enumerator (index number)
and the value. We have this mutably, so we can make changes.
1. We calculate the x and y position of the tile from the enumerator ( i ).
2. For each entry in the voronoi_seeds structure, we obtain the index (via
enumerate() ) and the position tuple.
1. We calculate the distance from the seed to the current tile, using the
PythagorasSquared algorithm.
2. We set voronoi_distance[seed] to the seed index and the distance.
3. We sort the voronoi_distance vector by the distance, so the closest seed
will be the �rst entry.
4. We set the tile's vid (Voronoi ID) to the �rst entry in the
voronoi_distance list.

439 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You can summarize that in English more easily: each tile is given membership of the
Voronoi group to whom's seed it is physically closest.

Next, we use this to draw the map:

for y in 1..self.map.height-1 {
for x in 1..self.map.width-1 {
let mut neighbors = 0;
let my_idx = self.map.xy_idx(x, y);
let my_seed = voronoi_membership[my_idx];
if voronoi_membership[self.map.xy_idx(x-1, y)] != my_seed {
neighbors += 1; }
if voronoi_membership[self.map.xy_idx(x+1, y)] != my_seed {
neighbors += 1; }
if voronoi_membership[self.map.xy_idx(x, y-1)] != my_seed {
neighbors += 1; }
if voronoi_membership[self.map.xy_idx(x, y+1)] != my_seed {
neighbors += 1; }

if neighbors < 2 {
self.map.tiles[my_idx] = TileType::Floor;
}
}
self.take_snapshot();
}

In this code, we visit every tile except for the very outer edges. We count how many
neighboring tiles are in a di�erent Voronoi group. If the answer is 0, then it is entirely
in the group: so we can place a �oor. If the answer is 1, it only borders 1 other group -
so we can also place a �oor (to ensure we can walk around the map). Otherwise, we
leave the tile as a wall.

Then we run the same culling and placement code we've used before. If you
cargo run the project now, you will see a pleasant structure:

440 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Tweaking the Hive


There are two obvious variables to expose to the builder: the number of seeds, and
the distance algorithm to use. We'll update the structure signature to include these:

#[derive(PartialEq, Copy, Clone)]


pub enum DistanceAlgorithm { Pythagoras, Manhattan, Chebyshev }

pub struct VoronoiCellBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>,
n_seeds: usize,
distance_algorithm: DistanceAlgorithm
}

Then we'll update the Voronoi code to use them:

441 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// Make a Voronoi diagram. We'll do this the hard way to learn about
the technique!
let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new();

while voronoi_seeds.len() < self.n_seeds {


let vx = rng.roll_dice(1, self.map.width-1);
let vy = rng.roll_dice(1, self.map.height-1);
let vidx = self.map.xy_idx(vx, vy);
let candidate = (vidx, rltk::Point::new(vx, vy));
if !voronoi_seeds.contains(&candidate) {
voronoi_seeds.push(candidate);
}
}

let mut voroni_distance = vec![(0, 0.0f32) ; self.n_seeds];


let mut voronoi_membership : Vec<i32> = vec![0 ; self.map.width as
usize * self.map.height as usize];
for (i, vid) in voronoi_membership.iter_mut().enumerate() {
let x = i as i32 % self.map.width;
let y = i as i32 / self.map.width;

for (seed, pos) in voronoi_seeds.iter().enumerate() {


let distance;
match self.distance_algorithm {
DistanceAlgorithm::Pythagoras => {
distance =
rltk::DistanceAlg::PythagorasSquared.distance2d(
rltk::Point::new(x, y),
pos.1
);
}
DistanceAlgorithm::Manhattan => {
distance = rltk::DistanceAlg::Manhattan.distance2d(
rltk::Point::new(x, y),
pos.1
);
}
DistanceAlgorithm::Chebyshev => {
distance = rltk::DistanceAlg::Chebyshev.distance2d(
rltk::Point::new(x, y),
pos.1
);

442 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}
voroni_distance[seed] = (seed, distance);
}

voroni_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());

*vid = voroni_distance[0].0 as i32;


}

for y in 1..self.map.height-1 {
for x in 1..self.map.width-1 {
let mut neighbors = 0;
let my_idx = self.map.xy_idx(x, y);
let my_seed = voronoi_membership[my_idx];
if voronoi_membership[self.map.xy_idx(x-1, y)] != my_seed {
neighbors += 1; }
if voronoi_membership[self.map.xy_idx(x+1, y)] != my_seed {
neighbors += 1; }
if voronoi_membership[self.map.xy_idx(x, y-1)] != my_seed {
neighbors += 1; }
if voronoi_membership[self.map.xy_idx(x, y+1)] != my_seed {
neighbors += 1; }

if neighbors < 2 {
self.map.tiles[my_idx] = TileType::Floor;
}
}
self.take_snapshot();
}
...

As a test, lets change the constructor to use Manhattan distance. The results will look
something like this:

443 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Notice how the lines are straighter, and less organic looking. That's what Manhattan
distance does: it calculates distance like a Manhattan Taxi Driver - number of rows
plus number of columns, rather than a straight line distance.

Restoring Randomness
So we'll put a couple of constructors in for each of the noise types:

444 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn pythagoras(new_depth : i32) -> VoronoiCellBuilder {


VoronoiCellBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
n_seeds: 64,
distance_algorithm: DistanceAlgorithm::Pythagoras
}
}

pub fn manhattan(new_depth : i32) -> VoronoiCellBuilder {


VoronoiCellBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
n_seeds: 64,
distance_algorithm: DistanceAlgorithm::Manhattan
}
}

Then we'll restore the random_builder to once again be random:

445 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 16);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)),
8 => Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)),
9 => Box::new(MazeBuilder::new(new_depth)),
10 => Box::new(DLABuilder::walk_inwards(new_depth)),
11 => Box::new(DLABuilder::walk_outwards(new_depth)),
12 => Box::new(DLABuilder::central_attractor(new_depth)),
13 => Box::new(DLABuilder::insectoid(new_depth)),
14 => Box::new(VoronoiCellBuilder::pythagoras(new_depth)),
15 => Box::new(VoronoiCellBuilder::manhattan(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}
}

Wrap-Up
That's another algorithm under our belts! We really have enough to write a pretty
good roguelike now, but there are still more to come!

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

446 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Waveform Collapse

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

A few years ago, Waveform Collapse (WFC) exploded onto the procedural generation
scene. Apparently magical, it took images in - and made a similar image. Demos
showed it spitting out great looking game levels, and the amazing Caves of Qud
started using it for generating fun levels. The canonical demonstrations - along with
the original algorithm in C# and various explanatory links/ports - may be found here.

In this chapter, we're going to implement Waveform Collapse from scratch - and apply
it to making fun Roguelike levels. Note that there is a crate with the original algorithm
available ( wfc , accompanied by wfc-image ); it seemed pretty good in testing, but I
had problems making it work with Web Assembly. I also didn't feel that I was really
teaching the algorithm by saying "just import this". It's a longer chapter, but by the end
you should feel comfortable with the algorithm.

So what does WFC really do?


Waveform Collapse is unlike the map generation algorithms we've used so far in that
it doesn't actually make maps. It takes source data in (we'll use other maps!), scans
them, and builds a new map featuring elements made exclusively from the source
data. It operates in a few phases:

1. It reads the incoming data. In the original implementation, this was a PNG �le. In
our implementation, this is a Map structure like others we've worked with; we'll
also implement a REX Paint reader to load maps.
2. It divides the source image into "tiles", and optionally makes more tiles by
mirroring the tiles it reads along one or two axes.
3. It either loads or builds a "constraints" graph. This is a set of rules specifying

447 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

which tiles can go next to each other. In an image, this may be derived from tile
adjacency. In a Roguelike map, connectivity of exits is a good metric. For a tile-
based game, you might carefully build a layout of what can go where.
4. It then divides the output image into tile-sized chunks, and sets them all to
"empty". The �rst tile placed will be pretty random, and then it selects areas and
examines tile data that is already known - placing down tiles that are compatible
with what is already there. Eventually, it's placed all of the tiles - and you have a
map/image!

The name "Waveform Collapse" refers to the Quantum Physics idea that a particle
may have not actually have a state until you look at it. In the algorithm, tiles don't
really coalesce into being until you pick one to examine. So there is a slight similarity to
Quantum Physics. In reality, though - the name is a triumph of marketing. The
algorithm is what is known as a solver - given a set of constraints, it iterates through
possible solutions until the constraints are solved. This isn't a new concept - Prolog is
an entire programming language based around this idea, and it �rst hit the scene in
1972. So in a way, it's older than me!

Getting started: Rust support for complex modules


All our previous algorithms were small enough to �t into one source code �le, without
too much paging around to �nd the relevant bit of code. Waveform Collapse is
complicated enough that it deserves to be broken into multiple �les - in much the
same was as the map_builders module was broken into a module - WFC will be
divided into its own module . The module will still live inside map_builders - so in a
way it's really a sub-module.

Rust makes it pretty easy to break any module into multiple �les: you create a
directory inside the parent module, and put a �le in it called mod.rs . You can then put
more �les in the folder, and so long as you enable them (with mod myfile ) and use
the contents (with use myfile::MyElement ) it works just like a single �le.

So to get started, inside your map_builders directory - make a new directory called
waveform_collapse . Add a �le, mod.rs into it. You should have a source tree like this:

448 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

\ src
\ map_builders
\ waveform_collapse
+ mod.rs
bsp_dungeon.rs
(etc)
main.rs
(etc)

We'll populate mod.rs with a skeletal implementation similar to previous chapters:

449 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map, TileType, Position, spawner,


SHOW_MAPGEN_VISUALIZER,
generate_voronoi_spawn_regions,
remove_unreachable_areas_returning_most_distant};
use rltk::RandomNumberGenerator;
use specs::prelude::*;

pub struct WaveformCollapseBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>
}

impl MapBuilder for WaveformCollapseBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
self.build();
}

fn spawn_entities(&mut self, ecs : &mut World) {


for area in self.noise_areas.iter() {
spawner::spawn_region(ecs, area.1, self.depth);
}
}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);

450 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}
}

impl WaveformCollapseBuilder {
pub fn new(new_depth : i32) -> WaveformCollapseBuilder {
WaveformCollapseBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new()
}
}

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

// TODO: Builder goes here

// Find a starting point; start at the middle and walk left until
we find an open tile
self.starting_position = Position{ x: self.map.width / 2, y :
self.map.height / 2 };
/*let mut start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
}*/
self.take_snapshot();

// Find all tiles we can reach from the starting point


let exit_tile =
remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
self.take_snapshot();

// Place the stairs


self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();

// Now we build a noise map for use in spawning entities later


self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut
rng);
}

451 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll also modify map_builders/mod.rs 's random_builder function to always return


the algorithm we're currently working with:

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*
let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 16);
match builder {
1 => Box::new(BspDungeonBuilder::new(new_depth)),
2 => Box::new(BspInteriorBuilder::new(new_depth)),
3 => Box::new(CellularAutomotaBuilder::new(new_depth)),
4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
7 => Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)),
8 => Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)),
9 => Box::new(MazeBuilder::new(new_depth)),
10 => Box::new(DLABuilder::walk_inwards(new_depth)),
11 => Box::new(DLABuilder::walk_outwards(new_depth)),
12 => Box::new(DLABuilder::central_attractor(new_depth)),
13 => Box::new(DLABuilder::insectoid(new_depth)),
14 => Box::new(VoronoiCellBuilder::pythagoras(new_depth)),
15 => Box::new(VoronoiCellBuilder::manhattan(new_depth)),
_ => Box::new(SimpleMapBuilder::new(new_depth))
}*/
Box::new(WaveformCollapseBuilder::new(new_depth))
}

This will give you an empty map (all walls) if you cargo run it - but it's a good starting
point.

Loading the source image - REX Paint


You may remember back in section 2 we loaded a REX Paint �le to use as the main
menu screen. We're going to do similar here, but we're going to turn it into a playable
map. It's a deliberately odd map to help illustrate what you can do with this algorithm.
Here's the original in REX Paint:

452 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I've tried to include some interesting shapes, a silly face, and plenty of corridors and
di�erent sized rooms. Here's a second REX Paint �le, designed to be more like the old
board game The Sorcerer's Cave, of which the algorithm reminds me - tiles with 1 exit,
2 exits, 3 exits and 4. It would be easy to make these prettier, but we'll keep it simple
for demonstration purposes.

453 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

These �les are found in the resources directory, as wfc-demo1.xp and


wfc-demo2.xp . One thing I love about REX Paint: the �les are tiny (102k and 112k
respectively). To make accessing them easier - and avoid having to ship them with the
executable when you publish your �nished game, we'll embed them into our game.
We did this previously for the main menu. Modify rex_assets.xp to include the new
�les:

454 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{rex::XpFile};

rltk::embedded_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");
rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
rltk::embedded_resource!(WFC_DEMO_IMAGE2, "../../resources/wfc-demo2.xp");

pub struct RexAssets {


pub menu : XpFile
}

impl RexAssets {
#[allow(clippy::new_without_default)]
pub fn new() -> RexAssets {
rltk::link_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");
rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-
demo1.xp");
rltk::link_resource!(WFC_DEMO_IMAGE2, "../../resources/wfc-
demo2.xp");

RexAssets{
menu : XpFile::from_resource("../../resources
/SmallDungeon_80x50.xp").unwrap()
}
}
}

Finally, we should load the map itself! Inside the waveform_collapse directory, make
a new �le: image_loader.rs :

455 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::rex::XpFile;
use super::{Map, TileType};

/// Loads a RexPaint file, and converts it into our map format
pub fn load_rex_map(new_depth: i32, xp_file : &XpFile) -> Map {
let mut map : Map = Map::new(new_depth);

for layer in &xp_file.layers {


for y in 0..layer.height {
for x in 0..layer.width {
let cell = layer.get(x, y).unwrap();
if x < map.width as usize && y < map.height as usize {
let idx = map.xy_idx(x as i32, y as i32);
match cell.ch {
32 => map.tiles[idx] = TileType::Floor, // #
35 => map.tiles[idx] = TileType::Wall, // #
_ => {}
}
}
}
}
}

map
}

This is really simple, and if you remember the main menu graphic tutorial it should be
quite self-explanatory. This function:

1. Accepts arguments for new_depth (because maps want it) and a reference to an
XpFile - a REX Paint map. It will be made completely solid, walls everywhere by
the constructor.
2. It creates a new map, using the new_depth parameter.
3. For each layer in the REX Paint �le (there should be only one at this point):
1. For each y and x on that layer:
1. Load the tile information for that coordinate.
2. Ensure that we're within the map boundaries (in case we have a
mismatch in sizes).
3. Calculate the tiles index for the cell.
4. Match on the cell glyph; if its a # (35) we place a wall, if its a space
(32) we place a �oor.

456 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Now we can modify our build function (in mod.rs ) to load the map:

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

self.map = load_rex_map(self.depth,
&rltk::rex::XpFile::from_resource("../../resources/wfc-
demo1.xp").unwrap());
self.take_snapshot();

// Find a starting point; start at the middle and walk left until we
find an open tile
self.starting_position = Position{ x: self.map.width / 2, y :
self.map.height / 2 };
...

At the top, we have to tell it to use the new image_loader �le:

mod image_loader;
use image_loader::*;

Note that we're not putting pub in front of these: we're using them, but not exposing
them outside of the module. This helps us keep our code clean, and our compile
times short!

In and of itself, this is cool - we can now load any REX Paint designed level and play it!
If you cargo run now, you'll �nd that you can play the new map:

457 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll make use of this in later chapters for vaults, prefabs and pre-designed levels - but
for now, we'll just use it as source data for later in the Waveform Collapse
implementation.

Carving up our map into tiles


We discussed earlier that WFC works by carving the original image into chunks/tiles,
and optionally �ipping them in di�erent directions. It does this as the �rst part of
building constraints - how the map can be laid out. So now we need to start carving up
our image.

We'll start by picking a tile size (we're going to call it chunk_size ). We'll make it a
constant for now (it'll become tweakable later), and start with a size of 7 - because
that was the size of the tiles in our second REX demo �le. We'll also call a function we'll
write in a moment:

458 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
let mut rng = RandomNumberGenerator::new();

const CHUNK_SIZE :i32 = 7;

self.map = load_rex_map(self.depth,
&rltk::rex::XpFile::from_resource("../../resources/wfc-
demo2.xp").unwrap());
self.take_snapshot();

let patterns = build_patterns(&self.map, CHUNK_SIZE, true, true);


...

Since we're dealing with constraints, we'll make a new �le in our
map_builders/waveform_collapse directory - constraints.rs . We're going to make
a function called build_patterns :

459 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{TileType, Map};


use std::collections::HashSet;

pub fn build_patterns(map : &Map, chunk_size: i32, include_flipping: bool,


dedupe: bool) -> Vec<Vec<TileType>> {
let chunks_x = map.width / chunk_size;
let chunks_y = map.height / chunk_size;
let mut patterns = Vec::new();

for cy in 0..chunks_y {
for cx in 0..chunks_x {
// Normal orientation
let mut pattern : Vec<TileType> = Vec::new();
let start_x = cx * chunk_size;
let end_x = (cx+1) * chunk_size;
let start_y = cy * chunk_size;
let end_y = (cy+1) * chunk_size;

for y in start_y .. end_y {


for x in start_x .. end_x {
let idx = map.xy_idx(x, y);
pattern.push(map.tiles[idx]);
}
}
patterns.push(pattern);

if include_flipping {
// Flip horizontal
pattern = Vec::new();
for y in start_y .. end_y {
for x in start_x .. end_x {
let idx = map.xy_idx(end_x - (x+1), y);
pattern.push(map.tiles[idx]);
}
}
patterns.push(pattern);

// Flip vertical
pattern = Vec::new();
for y in start_y .. end_y {
for x in start_x .. end_x {
let idx = map.xy_idx(x, end_y - (y+1));
pattern.push(map.tiles[idx]);
}
}

460 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

patterns.push(pattern);

// Flip both
pattern = Vec::new();
for y in start_y .. end_y {
for x in start_x .. end_x {
let idx = map.xy_idx(end_x - (x+1), end_y -
(y+1));
pattern.push(map.tiles[idx]);
}
}
patterns.push(pattern);
}
}
}

// Dedupe
if dedupe {
println!("Pre de-duplication, there are {} patterns",
patterns.len());
let set: HashSet<Vec<TileType>> = patterns.drain(..).collect(); //
dedup
patterns.extend(set.into_iter());
println!("There are {} patterns", patterns.len());
}

patterns
}

That's quite the mouthful of a function, so let's walk through it:

1. At the top, we're importing some items from elsewhere in the project: Map ,
TileType , and the built-in collection HashMap .
2. We declare our build_patterns function, with parameters for a reference to the
source map, the chunk_size to use (tile size), and �ags ( bool variables) for
include_flipping and dedupe . These indicate which features we'd like to use
when reading the source map. We're returning a vector , containing a series of
vector s of di�erent TileType s. The outer container holds each pattern. The
inner vector holds the TileType s that make up the pattern itself.
3. We determine how many chunks there are in each direction and store it in
chunks_x and chunks_y .
4. We create a new vector called patterns . This will hold the result of the
function; we don't declare it's type, because Rust is smart enough to see that

461 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

we're returning it at the end of the function - and can �gure out what type it is
for us.
5. We iterate every vertical chunk in the variable cy :
1. We iterate every horizontal chunk in the variable cx :
1. We make a new vector to hold this pattern.
2. We calculate start_x , end_x , start_y and end_y to hold the four
corner coordinates of this chunk - on the original map.
3. We iterate the pattern in y / x order (to match our map format), read
in the TileType of each map tile within the chunk, and add it to the
pattern.
4. We push the pattern to the patterns result vector.
5. If include_flipping is set to true (because we'd like to �ip our tiles,
making more tiles!):
1. Repeat iterating y / x in di�erent orders, giving 3 more tiles.
Each is added to the patterns result vector.
6. If dedupe is set, then we are "de-duplicating" the pattern bu�er. Basically,
removing any pattern that occurs more than once. This is good for a map with
lots of wasted space, if you don't want to make an equally sparse result map. We
de-duplicate by adding the patterns into a HashMap (which can only store one of
each entry) and then reading it back out again.

For this to compile, we have to make TileType know how to convert itself into a hash.
HashMap uses "hashes" (basically a checksum of the contained values) to determine if
an entry is unique, and to help �nd it. In map.rs , we can simply add one more
derived attribute to the TileType enumeration:

#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]


pub enum TileType {
Wall, Floor, DownStairs
}

This code should get you every 7x7 tile within your source �le - but it'd be great to be
able to prove that it works! As Reagan's speech-writer once wrote, Trust - But Verify. In
constraints.rs , we'll add another function: render_pattern_to_map :

462 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn render_pattern_to_map(map : &mut Map, pattern: &Vec<TileType>,


chunk_size: i32, start_x : i32, start_y: i32) {
let mut i = 0usize;
for tile_y in 0..chunk_size {
for tile_x in 0..chunk_size {
let map_idx = map.xy_idx(start_x + tile_x, start_y + tile_y);
map.tiles[map_idx] = pattern[i];
map.visible_tiles[map_idx] = true;
i += 1;
}
}
}

This is pretty simple: iterate the pattern, and copy to a location on the map - o�set by
the start_x and start_y coordinates. Note that we're also marking the tile as
visible - this will make the renderer display our tiles in color.

Now we just need to display our tiles as part of the snapshot system. In
waveform_collapse/mod.rs add a new function as part of the implementation of
WaveformCollapseBuilder (underneath build ). It's a member function because it
needs access to the take_snapshot command:

463 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn render_tile_gallery(&mut self, patterns: &Vec<Vec<TileType>>,


chunk_size: i32) {
self.map = Map::new(0);
let mut counter = 0;
let mut x = 1;
let mut y = 1;
while counter < patterns.len() {
render_pattern_to_map(&mut self.map, &patterns[counter],
chunk_size, x, y);

x += chunk_size + 1;
if x + chunk_size > self.map.width {
// Move to the next row
x = 1;
y += chunk_size + 1;

if y + chunk_size > self.map.height {


// Move to the next page
self.take_snapshot();
self.map = Map::new(0);

x = 1;
y = 1;
}
}

counter += 1;
}
self.take_snapshot();
}

Now, we need to call it. In build :

let patterns = build_patterns(&self.map, CHUNK_SIZE, true, true);


self.render_tile_gallery(&patterns, CHUNK_SIZE);

Also, comment out some code so that it doesn't crash from not being able to �nd a
starting point:

464 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut start_idx = self.map.xy_idx(self.starting_position.x,


self.starting_position.y);
/*while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
}*/

If you cargo run now, it'll show you the tile patterns from map sample 2:

Notice how �ipping has given us multiple variants of each tile. If we change the image
loading code to load wfc-demo1 (by changing the loader to
self.map = load_rex_map(self.depth,
&rltk::rex::XpFile::from_resource("../../resources/wfc-
demo1.xp").unwrap());
), we get chunks of our hand-drawn map:

465 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Building the constraints matrix


Now we need to begin to tell the algorithm how it can place tiles next to one another.
We could go for a simple "what's next to it on the original image?" algorithm, but that
would ignore a key factor in roguelike maps: connectivity. We're far more interested in
the ability to go from point A to point B than we are in overall aesthetics! So we need to
write a constraint builder that takes into account connectivity.

We'll start by extending builder in mod.rs to call a hypothetical function we'll


implement in a second:

let patterns = build_patterns(&self.map, CHUNK_SIZE, true, true);


self.render_tile_gallery(&patterns, CHUNK_SIZE);
let constraints = patterns_to_constaints(patterns, CHUNK_SIZE);

This gives us the signature of a new method, patterns_to_constraints to add to


constraints.rs . We're also going to need a new type and a helper function. We'll use
these in other places, so we're going to add a new �le to the waveform_collapse
folder - common.rs .

use super::TileType;

#[derive(PartialEq, Eq, Hash, Clone)]


pub struct MapChunk {
pub pattern : Vec<TileType>,
pub exits: [Vec<bool>; 4],
pub has_exits: bool,
pub compatible_with: [Vec<usize>; 4]
}

pub fn tile_idx_in_chunk(chunk_size: i32, x:i32, y:i32) -> usize {


((y * chunk_size) + x) as usize
}

We're de�ning MapChunk to be a structure, containing the actual pattern, a structure


of exits (more on that in a moment), a bool to say we have any exits, and a structure
called compatible_with (more on that in a second, too). We're also de�ning
tile_idx_in_chunk - which is just like map.xy_idx - but constrained to a small tile
type.

466 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Now we'll write patterns_to_constraints in constraints.rs :

467 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn patterns_to_constaints(patterns: Vec<Vec<TileType>>, chunk_size :


i32) -> Vec<MapChunk> {
// Move into the new constraints object
let mut constraints : Vec<MapChunk> = Vec::new();
for p in patterns {
let mut new_chunk = MapChunk{
pattern: p,
exits: [ Vec::new(), Vec::new(), Vec::new(), Vec::new() ],
has_exits : true,
compatible_with: [ Vec::new(), Vec::new(), Vec::new(),
Vec::new() ]
};
for exit in new_chunk.exits.iter_mut() {
for _i in 0..chunk_size {
exit.push(false);
}
}

let mut n_exits = 0;


for x in 0..chunk_size {
// Check for north-bound exits
let north_idx = tile_idx_in_chunk(chunk_size, x, 0);
if new_chunk.pattern[north_idx] == TileType::Floor {
new_chunk.exits[0][x as usize] = true;
n_exits += 1;
}

// Check for south-bound exits


let south_idx = tile_idx_in_chunk(chunk_size, x,
chunk_size-1);
if new_chunk.pattern[south_idx] == TileType::Floor {
new_chunk.exits[1][x as usize] = true;
n_exits += 1;
}

// Check for west-bound exits


let west_idx = tile_idx_in_chunk(chunk_size, 0, x);
if new_chunk.pattern[west_idx] == TileType::Floor {
new_chunk.exits[2][x as usize] = true;
n_exits += 1;
}

// Check for east-bound exits


let east_idx = tile_idx_in_chunk(chunk_size, chunk_size-1, x);
if new_chunk.pattern[east_idx] == TileType::Floor {

468 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

new_chunk.exits[3][x as usize] = true;


n_exits += 1;
}
}

if n_exits == 0 {
new_chunk.has_exits = false;
}

constraints.push(new_chunk);
}

// Build compatibility matrix


let ch = constraints.clone();
for c in constraints.iter_mut() {
for (j,potential) in ch.iter().enumerate() {
// If there are no exits at all, it's compatible
if !c.has_exits || !potential.has_exits {
for compat in c.compatible_with.iter_mut() {
compat.push(j);
}
} else {
// Evaluate compatibilty by direction
for (direction, exit_list) in
c.exits.iter_mut().enumerate() {
let opposite;
match direction {
0 => opposite = 1, // Our North, Their South
1 => opposite = 0, // Our South, Their North
2 => opposite = 3, // Our West, Their East
_ => opposite = 2 // Our East, Their West
}

let mut it_fits = false;


let mut has_any = false;
for (slot, can_enter) in exit_list.iter().enumerate()
{
if *can_enter {
has_any = true;
if potential.exits[opposite][slot] {
it_fits = true;
}
}
}
if it_fits {
c.compatible_with[direction].push(j);

469 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
if !has_any {
// There's no exits on this side, we don't care
what goes there
for compat in c.compatible_with.iter_mut() {
compat.push(j);
}
}
}
}
}
}

constraints
}

This is a really big function, but clearly broken down into sections. Let's take the time to
walk through what it actually does:

1. It accepts a �rst parameter, patterns as Vec<Vec<TileType>> - the type we


used to build our patterns. A second parameter, chunk_size is the same as
we've used before. It returns a vector of the new MapChunk type. A MapChunk
is a pattern, but with additional exit and compatibility information added to it. So
we're promising that given a set of pattern graphics, we're going to add all the
navigation information to it and return the patterns as a set of chunks.
2. It makes a new Vec of type MapChunk called constraints . This is our result -
we'll be adding to it, and returning it to the caller at the end.
3. Now we iterate every pattern in patterns , calling it p (to save typing). For each
pattern:
1. We make a new MapChunk . The pattern �eld gets a copy of our pattern.
exits is an array (�xed size set; in this case of size 4) of vectors, so we
insert 4 empty vectors into it. compatible_with is also an array of vectors,
so we set those to new - empty - vectors. We set has_exits to true - we'll
set that later.
2. We iterate from 0 to chunk_size , and add false into each exits �eld of
the new map chunk. The exits structure represents one entry per
possible direction (North, South, West, East) - so it needs one entry per size
of the chunk to represent each possible exit tile in that direction. We'll
check for actual connectivity later - for now, we just want placeholders for
each direction.
3. We set n_exits to 0, and make it mutable - so we can add to it later. We'll

470 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

be counting the total number of exits on the way through.


4. We iterate x from 0 to chunk_size , and for each value of x :
1. We check for north-bound exits. These are always at the location
(x, 0) within the chunk - so we calculate the tile index to check as
tile_idx_in_chunk(chunk_size, x, 0) . If that tile is a �oor, we add
one to n_exits and set new_chunk.exits[0][x] to true .
2. We do the same for south-bound exits. These are always at the
location (x, chunk_size-1) , so we calculate the chunk index to be
tile_idx_in_chunk(chunk_size, x, chunk_size-1) . If that tile is a
�oor, we add one to n_exits and set new_chunks.exits[1][x] to
true .
3. We do the same again for west-bound, which are at location (0,x) .
4. We do the same again for east-bound, which are at location
(chunk_size-1,0) .
5. If n_exits is 0, we set new_chunk.has_exits to 0 - there's no way in or out
of this chunk!
6. We push new_chunk to the constraints result vector.
4. Now it's time to build a compatibility matrix! The idea here is to match which
tiles can be placed to which other tiles, by matching exits on adjacent edges.
5. To avoid borrow-checker issues, we take a copy of the existing constraints with
let ch = constraints.clone(); . Rust isn't a big fan of both reading from and
writing to the same vector at once - so this avoids us having to do a dance to
keep it separated.
6. For each constraint in or results vector constraints , named c we:
1. Iterate every constraint in ch , our copy of the constraints vector, as
potential . We add an enumerator, j to tell us how it is indexed.
1. If neither c (the constraint we are editing) or potential (the
constraint we are examining) has exits, then we make it compatible
with everything. We do this to increase the chances of a map being
successfully resolved and still featuring these tiles (otherwise, they
would never be chosen). To add compatibility with everything, we add
j to the compatibile_with structure for all four directions. So c can
be placed next to potential in any direction.
2. Otherwise, we iterate through all four exit directions on c :
1. We set opposite to the reciprocal of the direction we're
evaluating; so North goes to South, East to West, etc.
2. We setup two mutable variables, it_fits and has_any - and

471 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

set both to false . We'll use these in the next steps. it_fits
means that there are one or more matching exits between c 's
exit tiles and potential 's entry tiles. has_any means that c
has any exits at all in this direction. We distinguish between the
two because if there are no exits in that direction, we don't care
what the neighbor is - we can't a�ect it. If there are exits, then
we only want to be compatible with tiles you can actually visit.
3. We iterate c 's exits, keeping both a slot (the tile number we
are evaluating) and the value of the exit tile ( can_enter ).
You'll remember that we've set these to true if they are a �oor
- and false otherwise - so we're iterating possible exits.
1. If can_enter is true , then we set has_any to true - it has
an exit in that direction.
2. We check potential_exits.exits[opposite][slot] - that
is that matching exit on the other tile, in the opposite
direction to the way we're going. If there is a match-up,
then you can go from tile c to tile potential in our
current direction ! That lets us set it_fits to true.
4. If it_fits is true , then there is a compatibility between the
tiles: we add j to c 's compatible_with vector for the current
direction.
5. If has_any is false , then we don't care about adjacency in this
direction - so we add j to the compatibility matrix for all
directions, just like we did for a tile with no exits.
7. Finally, we return our constraints results vector.

That's quite a complicated algorithm, so we don't really want to trust that I got it right.
We'll verify exit detection by adjusting our tile gallery code to show exits. In build ,
tweak the rendering order and what we're passing to render_tile_gallery :

let patterns = build_patterns(&self.map, CHUNK_SIZE, true, true);


let constraints = patterns_to_constaints(patterns, CHUNK_SIZE);
self.render_tile_gallery(&constraints, CHUNK_SIZE);

We also need to modify render_tile_gallery :

472 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn render_tile_gallery(&mut self, constraints: &Vec<MapChunk>, chunk_size:


i32) {
self.map = Map::new(0);
let mut counter = 0;
let mut x = 1;
let mut y = 1;
while counter < constraints.len() {
render_pattern_to_map(&mut self.map, &constraints[counter],
chunk_size, x, y);

x += chunk_size + 1;
if x + chunk_size > self.map.width {
// Move to the next row
x = 1;
y += chunk_size + 1;

if y + chunk_size > self.map.height {


// Move to the next page
self.take_snapshot();
self.map = Map::new(0);

x = 1;
y = 1;
}
}

counter += 1;
}
self.take_snapshot();
}

This requires that we modify our render_pattern_to_map function, also:

473 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn render_pattern_to_map(map : &mut Map, chunk: &MapChunk, chunk_size:


i32, start_x : i32, start_y: i32) {
let mut i = 0usize;
for tile_y in 0..chunk_size {
for tile_x in 0..chunk_size {
let map_idx = map.xy_idx(start_x + tile_x, start_y + tile_y);
map.tiles[map_idx] = chunk.pattern[i];
map.visible_tiles[map_idx] = true;
i += 1;
}
}

for (x,northbound) in chunk.exits[0].iter().enumerate() {


if *northbound {
let map_idx = map.xy_idx(start_x + x as i32, start_y);
map.tiles[map_idx] = TileType::DownStairs;
}
}
for (x,southbound) in chunk.exits[1].iter().enumerate() {
if *southbound {
let map_idx = map.xy_idx(start_x + x as i32, start_y +
chunk_size -1);
map.tiles[map_idx] = TileType::DownStairs;
}
}
for (x,westbound) in chunk.exits[2].iter().enumerate() {
if *westbound {
let map_idx = map.xy_idx(start_x, start_y + x as i32);
map.tiles[map_idx] = TileType::DownStairs;
}
}
for (x,eastbound) in chunk.exits[2].iter().enumerate() {
if *eastbound {
let map_idx = map.xy_idx(start_x + chunk_size - 1, start_y + x
as i32);
map.tiles[map_idx] = TileType::DownStairs;
}
}
}

Now that we have the demo framework running, we can cargo run the project - and
see the tiles from wfc-demo2.xp correctly highlighting the exits:

474 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The wfc-demo1.xp exits are also highlighted:

That's great! Our exit �nder is working correctly.

Building the Solver


Do you remember the old books of logic problems you used to be able to buy for long
trips? "Fred is a lawyer, Mary is a doctor, and Jim is unemployed. Fred can't sit next to
unemployed people, because he's snooty. Mary likes everyone. How should you
arrange their seating?" This is an example of the type of constrained problem a solver is
designed to help with. Building our map is no di�erent - we're reading the constraints
matrix (which we built above) to determine which tiles we can place in any given area.
Because it's a roguelike, and we want something di�erent every time, we want to
inject some randomness - and get a di�erent but valid map every time.

475 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Let's extend our build function to call a hypothetical solver:

let patterns = build_patterns(&self.map, CHUNK_SIZE, true, true);


let constraints = patterns_to_constaints(patterns, CHUNK_SIZE);
self.render_tile_gallery(&constraints, CHUNK_SIZE);

self.map = Map::new(self.depth);
loop {
let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE,
&self.map);
while !solver.iteration(&mut self.map, &mut rng) {
self.take_snapshot();
}
self.take_snapshot();
if solver.possible { break; } // If it has hit an impossible
condition, try again
}

We make a freshly solid map (since we've been using it for rendering tile demos, and
don't want to pollute the �nal map with a demo gallery!). Then we loop (the Rust
loop that runs forever until something calls break ). Inside that loop, we create a
solver for a copy of the constraints matrix (we copy it in case we have to go through
repeatedly; otherwise, we'd have to move it in and move it out again). We repeatedly
call the solver's iteration function, taking a snapshot each time - until it reports that
it is done. If the solver gave up and said it wasn't possible, we try again.

We'll start by adding solver.rs to our waveform_collapse directory. The solver


needs to keep its own state: that is, as it iterates through, it needs to know how far it
has come. We'll support this by making Solver into a struct:

pub struct Solver {


constraints: Vec<MapChunk>,
chunk_size : i32,
chunks : Vec<Option<usize>>,
chunks_x : usize,
chunks_y : usize,
remaining : Vec<(usize, i32)>, // (index, # neighbors)
pub possible: bool
}

It stores the constraints we've been building, the chunk_size we're using, the

476 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

chunks we're resolving (more on that in a second), the number of chunks it can �t
onto the target map ( chunks_x , and chunks_y ), a remaining vector (more on that,
too), and a possible indicator to indicate whether or not it gave up.

chunks is a vector of Option<usize> . The usize value is the index of the chunk. It's
an option because we may not have �lled it in, yet - so it might be None or
Some(usize) . This nicely represents the "quantum waveform collapse" nature of the
problem - it either exists or it doesn't, and we don't know until we look at it!

remaining is a vector of all of the chunks, with their index. It's a tuple - we store the
chunk index in the �rst entry, and the number of existing neighbors in the second.
We'll use that to help decide which chunk to �ll in next, and remove it from the
remaining list when we've added one.

We'll need to implement methods for Solver , too. new is a basic constructor:

impl Solver {
pub fn new(constraints: Vec<MapChunk>, chunk_size: i32, map : &Map) ->
Solver {
let chunks_x = (map.width / chunk_size) as usize;
let chunks_y = (map.height / chunk_size) as usize;
let mut remaining : Vec<(usize, i32)> = Vec::new();
for i in 0..(chunks_x*chunks_y) {
remaining.push((i, 0));
}

Solver {
constraints,
chunk_size,
chunks: vec![None; chunks_x * chunks_y],
chunks_x,
chunks_y,
remaining,
possible: true
}
}
...

It calculates the size (for chunks_x and chunks_y ), �lls remaining with every tile and
no neighbors, and chunks with None values. This sets us up for our solving run! We
also need a helper function called chunk_idx :

477 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn chunk_idx(&self, x:usize, y:usize) -> usize {


((y * self.chunks_x) + x) as usize
}

This is a lot like xy_idx in map , or tile_idx_in_chunk in common - but is constrained


by the number of chunks we can �t onto our map. We'll also rely on count_neighbors
:

478 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn count_neighbors(&self, chunk_x:usize, chunk_y:usize) -> i32 {


let mut neighbors = 0;

if chunk_x > 0 {
let left_idx = self.chunk_idx(chunk_x-1, chunk_y);
match self.chunks[left_idx] {
None => {}
Some(_) => {
neighbors += 1;
}
}
}

if chunk_x < self.chunks_x-1 {


let right_idx = self.chunk_idx(chunk_x+1, chunk_y);
match self.chunks[right_idx] {
None => {}
Some(_) => {
neighbors += 1;
}
}
}

if chunk_y > 0 {
let up_idx = self.chunk_idx(chunk_x, chunk_y-1);
match self.chunks[up_idx] {
None => {}
Some(_) => {
neighbors += 1;
}
}
}

if chunk_y < self.chunks_y-1 {


let down_idx = self.chunk_idx(chunk_x, chunk_y+1);
match self.chunks[down_idx] {
None => {}
Some(_) => {
neighbors += 1;
}
}
}
neighbors
}

479 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This function could be a lot smaller, but I've left it spelling out every step for clarity. It
looks at a chunk, and determines if it has a created (not set to None ) chunk to the
North, South, East and West.

Finally, we get to the iteration function - which does the hard work:

480 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn iteration(&mut self, map: &mut Map, rng : &mut


super::RandomNumberGenerator) -> bool {
if self.remaining.is_empty() { return true; }

// Populate the neighbor count of the remaining list


let mut remain_copy = self.remaining.clone();
let mut neighbors_exist = false;
for r in remain_copy.iter_mut() {
let idx = r.0;
let chunk_x = idx % self.chunks_x;
let chunk_y = idx / self.chunks_x;
let neighbor_count = self.count_neighbors(chunk_x, chunk_y);
if neighbor_count > 0 { neighbors_exist = true; }
*r = (r.0, neighbor_count);
}
remain_copy.sort_by(|a,b| b.1.cmp(&a.1));
self.remaining = remain_copy;

// Pick a random chunk we haven't dealt with yet and get its index,
remove from remaining list
let remaining_index = if !neighbors_exist {
(rng.roll_dice(1, self.remaining.len() as i32)-1) as usize
} else {
0usize
};
let chunk_index = self.remaining[remaining_index].0;
self.remaining.remove(remaining_index);

let chunk_x = chunk_index % self.chunks_x;


let chunk_y = chunk_index / self.chunks_x;

let mut neighbors = 0;


let mut options : Vec<Vec<usize>> = Vec::new();

if chunk_x > 0 {
let left_idx = self.chunk_idx(chunk_x-1, chunk_y);
match self.chunks[left_idx] {
None => {}
Some(nt) => {
neighbors += 1;

options.push(self.constraints[nt].compatible_with[3].clone());
}
}
}

481 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if chunk_x < self.chunks_x-1 {


let right_idx = self.chunk_idx(chunk_x+1, chunk_y);
match self.chunks[right_idx] {
None => {}
Some(nt) => {
neighbors += 1;

options.push(self.constraints[nt].compatible_with[2].clone());
}
}
}

if chunk_y > 0 {
let up_idx = self.chunk_idx(chunk_x, chunk_y-1);
match self.chunks[up_idx] {
None => {}
Some(nt) => {
neighbors += 1;

options.push(self.constraints[nt].compatible_with[1].clone());
}
}
}

if chunk_y < self.chunks_y-1 {


let down_idx = self.chunk_idx(chunk_x, chunk_y+1);
match self.chunks[down_idx] {
None => {}
Some(nt) => {
neighbors += 1;

options.push(self.constraints[nt].compatible_with[0].clone());
}
}
}

if neighbors == 0 {
// There is nothing nearby, so we can have anything!
let new_chunk_idx = (rng.roll_dice(1, self.constraints.len() as
i32)-1) as usize;
self.chunks[chunk_index] = Some(new_chunk_idx);
let left_x = chunk_x as i32 * self.chunk_size as i32;
let right_x = (chunk_x as i32+1) * self.chunk_size as i32;
let top_y = chunk_y as i32 * self.chunk_size as i32;
let bottom_y = (chunk_y as i32+1) * self.chunk_size as i32;

482 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut i : usize = 0;


for y in top_y .. bottom_y {
for x in left_x .. right_x {
let mapidx = map.xy_idx(x, y);
let tile = self.constraints[new_chunk_idx].pattern[i];
map.tiles[mapidx] = tile;
i += 1;
}
}
}
else {
// There are neighbors, so we try to be compatible with them
let mut options_to_check : HashSet<usize> = HashSet::new();
for o in options.iter() {
for i in o.iter() {
options_to_check.insert(*i);
}
}

let mut possible_options : Vec<usize> = Vec::new();


for new_chunk_idx in options_to_check.iter() {
let mut possible = true;
for o in options.iter() {
if !o.contains(new_chunk_idx) { possible = false; }
}
if possible {
possible_options.push(*new_chunk_idx);
}
}

if possible_options.is_empty() {
println!("Oh no! It's not possible!");
self.possible = false;
return true;
} else {
let new_chunk_idx = if possible_options.len() == 1 { 0 }
else { rng.roll_dice(1, possible_options.len() as i32)-1
};

self.chunks[chunk_index] = Some(new_chunk_idx as usize);


let left_x = chunk_x as i32 * self.chunk_size as i32;
let right_x = (chunk_x as i32+1) * self.chunk_size as i32;
let top_y = chunk_y as i32 * self.chunk_size as i32;
let bottom_y = (chunk_y as i32+1) * self.chunk_size as i32;

483 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut i : usize = 0;


for y in top_y .. bottom_y {
for x in left_x .. right_x {
let mapidx = map.xy_idx(x, y);
let tile = self.constraints[new_chunk_idx as
usize].pattern[i];
map.tiles[mapidx] = tile;
i += 1;
}
}
}
}

false
}

This is another really big function, but once again that's because I tried to keep it easy
to read. Let's walk through the algorithm:

1. If there is nothing left in remaining , we return that we have completed the map.
possible is true, because we actually �nished the problem.
2. We take a clone of remaining to avoid borrow checker issues.
3. We iterate our copy of remaining , and for each remaining chunk:
1. We determine it's x and y location from the chunk index.
2. We call count_neighbors to determine how many (if any) neighboring
chunks have been resolved.
3. If any neighbors were found, we set neighbors_exist to true - telling the
algorithm that it has run at least once.
4. We update the copy of the remaining list to include the same index as
before, and the new neighbor count.
4. We sort our copy of remaining by the number of neighbors, descending - so the
chunk with the most neighbors is �rst.
5. We copy our clone of remaining back to our actual remaining list.
6. We want to create a new variable, remaining_index - to indicate which chunk
we're going to work on, and where it is in the remaining vector. If we haven't
made any tiles yet, we pick our starting point at random. Otherwise, we pick the
�rst entry in the remaining list - which will be the one with the most neighbors.
7. We obtain chunk_idx from the remaining list at the selected index, and
remove that chunk from the list.

484 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

8. Now we calculate chunk_x and chunk_y to tell us where it is on the new map.
9. We set a mutable variable, neighbors to 0; we'll be counting neighbors again.
10. We create a mutable variable called Options . It has the rather strange type
Vec<Vec<usize>> - it is a vector of vectors, each of which contains an array
index ( usize ). We'll be storing compatible options for each direction in here - so
we need the outer vector for directions, and the inner vector for options. These
index the constraints vector.
11. If it isn't the left-most chunk on the map, it may have a chunk to the west - so we
calculate the index of that chunk. If a chunk to the west exists (isn't None ), then
we add it's east bound compatible_with list to our Options vector. We
increment neighbors to indicate that we found a neighbor.
12. We repeat for the east - if it isn't the right-most chunk on the map. We increment
neighbors to indicate that we found a neighbor.
13. We repeat for the south - if it isn't the bottom chunk on the map. We increment
neighbors to indicate that we found a neighbor.
14. We repeat for the north - if it isn't the top chunk on the map. We increment
neighbors to indicate that we found a neighbor.
15. If there are no neighbors, we:
1. Find a random tile from constraints .
2. Figure out the bounds of where we are placing the tile in left_x , right_x
, top_y , and bottom_y .
3. Copy the selected tile to the map.
16. If there are neighbors, we:
1. Insert all of the options from each direction into a HashSet . We used
HashSet to de-duplicate our tiles earlier, and this is what we're doing here:
we're removing all duplicate options, so we don't evaluate them
repeatedly.
2. We make a new vector called possible_options . For each option in the
HashSet :
1. Set a mutable variable called possible to true .
2. Check each directions' options , and if it is compatible with its
neighbors preferences - add it to possible_options .
3. If possible_options is empty - then we've hit a brick wall, and can't add
any more tiles. We set possible to false in the parent structure and bail
out!
4. Otherwise, we pick a random entry from possible_options and draw it to
the map.

485 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

So while it's a long function, it isn't a really complicated one. It looks for possible
combinations for each iteration, and tries to apply them - giving up and returning
failure if it can't �nd one.

The caller is already taking snapshots of each iteration, so if we cargo run the project
with our wfc-test1.xp �le we get something like this:

Not the greatest map, but you can watch the solver chug along - placing tiles one at a
time. Now lets try it with wfc-test2.xmp , a set of tiles designed for tiling:

486 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

This is kind-of fun - it lays it out like a jigsaw, and eventually gets a map! The map isn't
as well connected as one might hope, the edges with no exit lead to a smaller play
area (which is culled at the end). It's still a good start!

Reducing the chunk size


We can signi�cantly improve the resulting map in this case by reducing our
CHUNK_SIZE constant to 3. Running it with test map 1 produces something like this:

487 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

That's a much more interesting map! You can try it with wfc-test2.xp as well:

488 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Once again, it's an interesting and playable map! The problem is that we've got such a
small chunk size that there really aren't all that many interesting options for adjacency
- 3x3 grids really limits the amount of variability you can have on your map! So we'll
try wfc-test1.xp with a chunk size of 5:

That's more like it! It's not dissimilar from a map we might try and generate in another
fashion.

Taking advantage of the ability to read other map


types
Rather than loading one of our .xp �les, lets feed in the results of a
CellularAutomata run, and use that as the seed with a large (8) chunk. This is
surprisingly easy with the structure we have! In our build function:

489 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

const CHUNK_SIZE :i32 = 8;

let mut ca = super::CellularAutomotaBuilder::new(0);


ca.build_map();
self.map = ca.get_map();
for t in self.map.tiles.iter_mut() {
if *t == TileType::DownStairs { *t = TileType::Floor; }
}

Notice that we're removing down stairs - the Cellular Automata generator will place
one, and we don't want stairs everywhere! This gives a very pleasing result:

Improving adjacency - and increasing the risk of


rejection!
What we have already is quite a workable solution - you can make decent maps with
it, especially when you use other generators as the seed. On winding jigsaw maps, it's
not generating the adjacency we'd like. There's a small risk by making the matcher

490 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

more speci�c that we will see some failures, but lets give it a go anyway. In our code
that builds a compatibility matrix, �nd the comment
There's no exits on this side and replace the section with this code:

if !has_any {
// There's no exits on this side, let's match only if
// the other edge also has no exits
let matching_exit_count = potential.exits[opposite].iter().filter(|a|
!**a).count();
if matching_exit_count == 0 {
c.compatible_with[direction].push(j);
}
}

Run against the our cellular automata example, we see a bit of a change:

It also looks pretty good with our map test 1:

491 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Overall, that change is a winner! It doesn't look very good with our jigsaw puzzle
anymore; there just aren't enough tiles to make good patterns.

O�ering di�erent build options to the game


We're going to o�er three modes to our random_builder function: TestMap (just the
REX Paint map), and Derived (run on an existing algorithm). So, in mod.rs we add an
enumeration and extend our structure to hold some related data:

492 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub enum WaveformMode { TestMap, Derived }

pub struct WaveformCollapseBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>,
mode : WaveformMode,
derive_from : Option<Box<dyn MapBuilder>>
}

We'll extend our new constructor to include these:

impl WaveformCollapseBuilder {
pub fn new(new_depth : i32, mode : WaveformMode, derive_from :
Option<Box<dyn MapBuilder>>) -> WaveformCollapseBuilder {
WaveformCollapseBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
mode,
derive_from
}
}

Then we'll add some functionality into the top of our build function:

493 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
if self.mode == WaveformMode::TestMap {
self.map = load_rex_map(self.depth,
&rltk::rex::XpFile::from_resource("../../resources/wfc-
demo1.xp").unwrap());
self.take_snapshot();
return;
}

let mut rng = RandomNumberGenerator::new();

const CHUNK_SIZE :i32 = 8;

let prebuilder = &mut self.derive_from.as_mut().unwrap();


prebuilder.build_map();
self.map = prebuilder.get_map();
for t in self.map.tiles.iter_mut() {
if *t == TileType::DownStairs { *t = TileType::Floor; }
}
self.take_snapshot();
...

Now we'll add a couple of constructors to make it easier for random_builder to not
have to know about the innards of the WFC algorithm:

pub fn test_map(new_depth: i32) -> WaveformCollapseBuilder {


WaveformCollapseBuilder::new(new_depth, WaveformMode::TestMap, None)
}

pub fn derived_map(new_depth: i32, builder: Box<dyn MapBuilder>) ->


WaveformCollapseBuilder {
WaveformCollapseBuilder::new(new_depth, WaveformMode::Derived,
Some(builder))
}

Lastly, we'll modify our random_builder (in map_builders/mod.rs ) to sometimes


return the test map - and sometimes run WFC on whatever map we've created:

494 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 17);
let mut result : Box<dyn MapBuilder>;
match builder {
1 => { result = Box::new(BspDungeonBuilder::new(new_depth)); }
2 => { result = Box::new(BspInteriorBuilder::new(new_depth)); }
3 => { result = Box::new(CellularAutomotaBuilder::new(new_depth));
}
4 => { result =
Box::new(DrunkardsWalkBuilder::open_area(new_depth)); }
5 => { result =
Box::new(DrunkardsWalkBuilder::open_halls(new_depth)); }
6 => { result =
Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)); }
7 => { result =
Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)); }
8 => { result =
Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)); }
9 => { result = Box::new(MazeBuilder::new(new_depth)); }
10 => { result = Box::new(DLABuilder::walk_inwards(new_depth)); }
11 => { result = Box::new(DLABuilder::walk_outwards(new_depth)); }
12 => { result =
Box::new(DLABuilder::central_attractor(new_depth)); }
13 => { result = Box::new(DLABuilder::insectoid(new_depth)); }
14 => { result =
Box::new(VoronoiCellBuilder::pythagoras(new_depth)); }
15 => { result =
Box::new(VoronoiCellBuilder::manhattan(new_depth)); }
16 => { result =
Box::new(WaveformCollapseBuilder::test_map(new_depth)); }
_ => { result = Box::new(SimpleMapBuilder::new(new_depth)); }
}

if rng.roll_dice(1, 3)==1 {
result = Box::new(WaveformCollapseBuilder::derived_map(new_depth,
result));
}

result
}

That's quite a change. We roll a 17-sided dice (wouldn't it be nice if those really
existed?), and pick a builder - as before, but with the option to use the .xp �le from

495 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

wfc_test1.xp . We store it in result . Then we roll 1d3 ; if it comes up 1 , we wrap


the builder in the WaveformCollapseBuilder in derived mode - so it will take the
original map and rebuild it with WFC. E�ectively, we just added another 17 options!

Cleaning Up Dead Code Warnings


Let's take a moment to do a little housekeeping on our code.

There are quite a few warnings in the project when you compile. They are almost all
"this function is never used" (or equivalent). Since we're building a library of map
builders, it's ok to not always call the constructors. You can add an annotation above a
function de�nition - #[allow(dead_code)] to tell the compiler to stop worrying about
this. For example, in drunkard.rs :

impl DrunkardsWalkBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32, settings: DrunkardSettings) ->
DrunkardsWalkBuilder {

I've gone through and applied these where necessary in the example code to silence
the compiler.

Cleaning Up Unused Embedded Files


We're not using wfc-test2.xp anymore, so lets remove it from rex-assets.rs :

496 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{rex::XpFile};

rltk::embedded_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");
rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");

pub struct RexAssets {


pub menu : XpFile
}

impl RexAssets {
#[allow(clippy::new_without_default)]
pub fn new() -> RexAssets {
rltk::link_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");
rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-
demo1.xp");

RexAssets{
menu : XpFile::from_resource("../../resources
/SmallDungeon_80x50.xp").unwrap()
}
}
}

This saves a little bit of space in the resulting binary (never a bad thing: smaller
binaries �t into your CPU's cache better, and generally run faster).

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Prefabricated Levels and Level Sections

497 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Despite being essentially pseudorandom (that is, random - but constrained in a way
that makes for a fun, cohesive game), many roguelikes feature some hand-crafted
content. Typically, these can be divided into a few categories:

Hand-crafted levels - the whole level is premade, the content static. These are
typically used very sparingly, for big set-piece battles essential to the story.
Hand-crafted level sections - some of the level is randomly created, but a large
part is pre-made. For example, a fortress might be a "set piece", but the
dungeon leading up to it is random. Dungeon Crawl Stone Soup uses these
extensively - you sometimes run into areas that you recognize because they are
prefabricated - but the dungeon around them is clearly random. Cogmind uses
these for parts of the caves (I'll avoid spoilers). Caves of Qud has a few set-piece
levels that appear to be built around a number of prefabricated parts. Some
systems call this mechanism "vaults" - but the name can also apply to the third
category.
Hand-crafted rooms (also called Vaults in some cases). The level is largely
random, but when sometimes a room �ts a vault - so you put one there.

The �rst category is special and should be used sparingly (otherwise, your players will
just learn an optimal strategy and power on through it - and may become bored from
lack of variety). The other categories bene�t from either providing lots of vaults (so
there's a ton of content to sprinkle around, meaning the game doesn't feel too similar
each time you play) or being rare - so you only occasionally see them (for the same
reason).

Some Clean Up
In the Waveform Collapse chapter, we loaded a pre-made level - without any entities
(those are added later). It's not really very nice to hide a map loader inside WFC - since
that isn't it's primary purpose - so we'll start by removing it:

498 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll start by deleting the �le map_builders/waveform_collapse/image_loader.rs .


We'll be building a better one in a moment.

Now we edit the start of mod.rs in ``map_builders/waveform_collapse`:

499 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map, TileType, Position, spawner,


SHOW_MAPGEN_VISUALIZER,
generate_voronoi_spawn_regions,
remove_unreachable_areas_returning_most_distant};
use rltk::RandomNumberGenerator;
use specs::prelude::*;
use std::collections::HashMap;
mod common;
use common::*;
mod constraints;
use constraints::*;
mod solver;
use solver::*;

/// Provides a map builder using the Waveform Collapse algorithm.


pub struct WaveformCollapseBuilder {
map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
noise_areas : HashMap<i32, Vec<usize>>,
derive_from : Option<Box<dyn MapBuilder>>
}
...

impl WaveformCollapseBuilder {
/// Generic constructor for waveform collapse.
/// # Arguments
/// * new_depth - the new map depth
/// * derive_from - either None, or a boxed MapBuilder, as output by
`random_builder`
pub fn new(new_depth : i32, derive_from : Option<Box<dyn MapBuilder>>)
-> WaveformCollapseBuilder {
WaveformCollapseBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history: Vec::new(),
noise_areas : HashMap::new(),
derive_from
}
}

/// Derives a map from a pre-existing map builder.


/// # Arguments

500 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

/// * new_depth - the new map depth


/// * derive_from - either None, or a boxed MapBuilder, as output by
`random_builder`
pub fn derived_map(new_depth: i32, builder: Box<dyn MapBuilder>) ->
WaveformCollapseBuilder {
WaveformCollapseBuilder::new(new_depth, Some(builder))
}
...

We've removed all references to image_loader , removed the test map constructor,
and removed the ugly mode enumeration. WFC is now exactly what it says on the tin,
and nothing else. Lastly, we'll modify random_builder to not use the test map
anymore:

501 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 16);
let mut result : Box<dyn MapBuilder>;
match builder {
1 => { result = Box::new(BspDungeonBuilder::new(new_depth)); }
2 => { result = Box::new(BspInteriorBuilder::new(new_depth)); }
3 => { result = Box::new(CellularAutomotaBuilder::new(new_depth));
}
4 => { result =
Box::new(DrunkardsWalkBuilder::open_area(new_depth)); }
5 => { result =
Box::new(DrunkardsWalkBuilder::open_halls(new_depth)); }
6 => { result =
Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)); }
7 => { result =
Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)); }
8 => { result =
Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)); }
9 => { result = Box::new(MazeBuilder::new(new_depth)); }
10 => { result = Box::new(DLABuilder::walk_inwards(new_depth)); }
11 => { result = Box::new(DLABuilder::walk_outwards(new_depth)); }
12 => { result =
Box::new(DLABuilder::central_attractor(new_depth)); }
13 => { result = Box::new(DLABuilder::insectoid(new_depth)); }
14 => { result =
Box::new(VoronoiCellBuilder::pythagoras(new_depth)); }
15 => { result =
Box::new(VoronoiCellBuilder::manhattan(new_depth)); }
_ => { result = Box::new(SimpleMapBuilder::new(new_depth)); }
}

if rng.roll_dice(1, 3)==1 {
result = Box::new(WaveformCollapseBuilder::derived_map(new_depth,
result));
}

result
}

Skeletal Builder

502 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll start with a very basic skeleton, similar to those used before. We'll make a new
�le, prefab_builder.rs in map_builders :

503 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MapBuilder, Map, TileType, Position, spawner,


SHOW_MAPGEN_VISUALIZER,
draw_corridor};
use rltk::RandomNumberGenerator;
use specs::prelude::*;

pub struct PrefabBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
}

impl MapBuilder for PrefabBuilder {


fn get_map(&self) -> Map {
self.map.clone()
}

fn get_starting_position(&self) -> Position {


self.starting_position.clone()
}

fn get_snapshot_history(&self) -> Vec<Map> {


self.history.clone()
}

fn build_map(&mut self) {
self.build();
}

fn spawn_entities(&mut self, ecs : &mut World) {


}

fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}
}
}

impl PrefabBuilder {

504 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn new(new_depth : i32) -> PrefabBuilder {


PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new()
}
}

fn build(&mut self) {
}
}

Prefab builder mode 1 - hand-crafted levels


We're going to support multiple modes for the prefab-builder, so lets bake that in at
the beginning. In prefab_builder.rs :

#[derive(PartialEq, Clone)]
#[allow(dead_code)]
pub enum PrefabMode {
RexLevel{ template : &'static str }
}

pub struct PrefabBuilder {


map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
mode: PrefabMode
}

This is new - an enum with variables? This works because under the hood, Rust
enumerations are actually unions. They can hold whatever you want to put in there,
and the type is sized to hold the largest of the options. It's best used sparingly in tight
code, but for things like con�guration it is a very clean way to pass in data. We should
also update the constructor to create the new types:

505 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl PrefabBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32) -> PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-
demo1.xp" }
}
}
...

Including the map template path in the mode makes for easier reading, even if it is
slightly more complicated. We're not �lling the PrefabBuilder with variables for all of
the options we might use - we're keeping them separated. That's generally good
practice - it makes it much more obvious to someone who reads your code what's
going on.

Now we'll re-implement the map reader we previously deleted from


image_loader.rs - only we'll add it as a member function for PrefabBuilder , and
use the enclosing class features rather than passing Map and new_depth in and out:

506 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
fn load_rex_map(&mut self, path: &str) {
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap();

for layer in &xp_file.layers {


for y in 0..layer.height {
for x in 0..layer.width {
let cell = layer.get(x, y).unwrap();
if x < self.map.width as usize && y < self.map.height as
usize {
let idx = self.map.xy_idx(x as i32, y as i32);
match (cell.ch as u8) as char {
' ' => self.map.tiles[idx] = TileType::Floor, //
space
'#' => self.map.tiles[idx] = TileType::Wall, // #
_ => {}
}
}
}
}
}
}

That's pretty straightforward, more or less a direct port of the one form the Waveform
Collapse chapter. Now lets start making our build function:

507 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template)
}

// Find a starting point; start at the middle and walk left until we
find an open tile
self.starting_position = Position{ x: self.map.width / 2, y :
self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
}
self.take_snapshot();
}

Notice that we've copied over the �nd starting point code; we'll improve that at some
point, but for now it ensures you can play your level. We haven't spawned anything -
so you will be alone in the level. There's also a slightly di�erent usage of match here -
we're using the variable in the enum. The code PrefabMode::RexLevel{template}
says "match RexLevel , but with any value of template - and make that value
available via the name template in the match scope". You could use _ to match any
value if you didn't want to access it. Rust's pattern matching system is really
impressive - you can do a lot with it!

Lets modify our random_builder function to always call this type of map (so we don't
have to test over and over in the hopes of getting the one we want!). In
map_builders/mod.rs :

508 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


/*
let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 16);
let mut result : Box<dyn MapBuilder>;
match builder {
1 => { result = Box::new(BspDungeonBuilder::new(new_depth)); }
2 => { result = Box::new(BspInteriorBuilder::new(new_depth)); }
3 => { result = Box::new(CellularAutomotaBuilder::new(new_depth));
}
4 => { result =
Box::new(DrunkardsWalkBuilder::open_area(new_depth)); }
5 => { result =
Box::new(DrunkardsWalkBuilder::open_halls(new_depth)); }
6 => { result =
Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)); }
7 => { result =
Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)); }
8 => { result =
Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)); }
9 => { result = Box::new(MazeBuilder::new(new_depth)); }
10 => { result = Box::new(DLABuilder::walk_inwards(new_depth)); }
11 => { result = Box::new(DLABuilder::walk_outwards(new_depth)); }
12 => { result =
Box::new(DLABuilder::central_attractor(new_depth)); }
13 => { result = Box::new(DLABuilder::insectoid(new_depth)); }
14 => { result =
Box::new(VoronoiCellBuilder::pythagoras(new_depth)); }
15 => { result =
Box::new(VoronoiCellBuilder::manhattan(new_depth)); }
_ => { result = Box::new(SimpleMapBuilder::new(new_depth)); }
}

if rng.roll_dice(1, 3)==1 {
result = Box::new(WaveformCollapseBuilder::derived_map(new_depth,
result));
}

result*/

Box::new(PrefabBuilder::new(new_depth))
}

If you cargo run your project now, you can run around the (otherwise deserted)
demo map:

509 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Populating the test map with prefabbed entities


Let's pretend that our test map is some sort of super-duper end-game map. We'll take
a copy and call it wfc-populated.xp . Then we'll splat a bunch of monster and item
glyphs around it:

510 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The color coding is completely optional, but I put it in for clarity. You'll see we have an
@ to indicate the player start, a > to indicate the exit, and a bunch of g goblins, o
orcs, ! potions, % rations and ^ traps. Not too bad a map, really.

We'll add wfc-populated.xp to our resources folder, and extend rex_assets.rs to


load it:

511 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use rltk::{rex::XpFile};

rltk::embedded_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");
rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
rltk::embedded_resource!(WFC_POPULATED, "../../resources/wfc-
populated.xp");

pub struct RexAssets {


pub menu : XpFile
}

impl RexAssets {
#[allow(clippy::new_without_default)]
pub fn new() -> RexAssets {
rltk::link_resource!(SMALL_DUNGEON, "../../resources
/SmallDungeon_80x50.xp");
rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-
demo1.xp");
rltk::link_resource!(WFC_POPULATED, "../../resources/wfc-
populated.xp");

RexAssets{
menu : XpFile::from_resource("../../resources
/SmallDungeon_80x50.xp").unwrap()
}
}
}

We also want to be able to list out spawns that are required by the map. Looking in
spawner.rs , we have an established tuple format for how we pass spawns - so we'll
use it in the struct:

#[allow(dead_code)]
pub struct PrefabBuilder {
map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
mode: PrefabMode,
spawns: Vec<(usize, String)>
}

512 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Now we'll modify our constructor to use the new map, and initialize spawns :

impl PrefabBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32) -> PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-
populated.xp" },
spawns: Vec::new()
}
}
...

To make use of the function in spawner.rs that accepts this type of data, we need to
make it public. So we open up the �le, and add the word pub to the function
signature:

/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)


pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
...

We'll then modify our PrefabBuilder 's spawn_entities function to make use of this
data:

fn spawn_entities(&mut self, ecs : &mut World) {


for entity in self.spawns.iter() {
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
}
}

We do a bit of a dance with references just to work with the previous function
signature (and not have to change it, which would change lots of other code). So far,
so good - it reads the spawn list, and requests that everything in the list be placed
onto the map. Now would be a good time to add something to the list! We'll want to
modify our load_rex_map to handle the new data:

513 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
fn load_rex_map(&mut self, path: &str) {
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap();

for layer in &xp_file.layers {


for y in 0..layer.height {
for x in 0..layer.width {
let cell = layer.get(x, y).unwrap();
if x < self.map.width as usize && y < self.map.height as
usize {
let idx = self.map.xy_idx(x as i32, y as i32);
// We're doing some nasty casting to make it easier to
type things like '#' in the match
match (cell.ch as u8) as char {
' ' => self.map.tiles[idx] = TileType::Floor,
'#' => self.map.tiles[idx] = TileType::Wall,
'@' => {
self.map.tiles[idx] = TileType::Floor;
self.starting_position = Position{ x:x as i32,
y:y as i32 };
}
'>' => self.map.tiles[idx] = TileType::DownStairs,
'g' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Goblin".to_string()));
}
'o' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Orc".to_string()));
}
'^' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Bear
Trap".to_string()));
}
'%' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx,
"Rations".to_string()));
}
'!' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Health
Potion".to_string()));
}

514 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

_ => {
println!("Unknown glyph loading map: {}",
(cell.ch as u8) as char);
}
}
}
}
}
}
}

This recognizes the extra glyphs, and prints a warning to the console if we've loaded
one we forgot to handle. Note that for entities, we're setting the tile to Floor and
then adding the entity type. That's because we can't overlay two glyphs on the same
tile - but it stands to reason that the entity is standing on a �oor.

Lastly, we need to modify our build function to not move the exit and the player. We
simply wrap the fallback code in an if statement to detect if we've set a
starting_position (we're going to require that if you set a start, you also set an exit):

515 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template)
}
self.take_snapshot();

// Find a starting point; start at the middle and walk left until we
find an open tile
if self.starting_position.x == 0 {
self.starting_position = Position{ x: self.map.width / 2, y :
self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x,
self.starting_position.y);
}
self.take_snapshot();

// Find all tiles we can reach from the starting point


let exit_tile =
remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
self.take_snapshot();

// Place the stairs


self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();
}
}

If you cargo run the project now, you start in the speci�ed location - and entities
spawn around you.

516 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Rex-free prefabs
It's possible that you don't like Rex Paint (don't worry, I won't tell Kyzrati!), maybe you
are on a platform that doesn't support it - or maybe you'd just like to not have to rely
on an external tool. We'll extend our reader to also support string output for maps.
This will be handy later when we get to small room prefabs/vaults.

I cheated a bit, and opened the wfc-populated.xp �le in Rex and typed ctrl-t to
save in TXT format. That gave me a nice Notepad friendly map �le:

517 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I also realized that prefab_builder was going to outgrow a single �le! Fortunately,
Rust makes it pretty easy to turn a module into a multi-�le monster. In map_builders ,

518 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I made a new directory called prefab_builder . I then moved prefab_builder.rs


into it, and renamed it mod.rs . The game compiles and runs exactly as before.

Make a new �le in your prefab_builder folder, and name it prefab_levels.rs . We'll
paste in the map de�nition, and decorate it a bit:

519 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


pub struct PrefabLevel {
pub template : &'static str,
pub width : usize,
pub height: usize
}

pub const WFC_POPULATED : PrefabLevel = PrefabLevel{


template : LEVEL_MAP,
width: 80,
height: 43
};

const LEVEL_MAP : &str =


"
###############################################################################
#### ######### #    # #######       #########  ####    #####                ###
#### ######### ###### #######   o   #########  #### ## #####                ###
#                        ####       #########   ### ##         o            ###
#### ######### ###       ####       #######         ## #####                ###
#### ######### ###       ####       ####### #   ### ## #####                ###
#### ######### ###       ####       ####### #######    #####     o          ###
##          ## ###       ####       ####### ################                ###
##          ## ###   o   ###### ########### #   ############                ###
##          ## ###       ###### ###########     ###                         ###
##    %                  ###### ########### #   ###   !   ##                ###
##          ## ###              ######   ## #######       ##                ###
##          ## ###       ## ### #####     # ########################      #####
##          ## ###       ## ### #####     # #   ######################    #####
### ## ####### ###### ##### ### ####          o ###########     ######    #####
### ## ####### ###### ####   ## ####        #   #########         ###### ######
######                  ####### ####            ######     !    !    ### #    #
#####                     ##### ####        #   ######               ### ######
####                            #####     # ##########               ### ######
####           !           ### ######     # ##########      o##o     ### #   ##
####                       ### #######   ## #   ######               ###   g ##
####   ######### ########## %  ######## ###################     ######## ##   #
### ### ######## ##########    ######## #################### ##########   #   #
## ##### ######   #########    ########          ########### #######   # g#   #
## #####           ###############      ###      ########### #######   ####   #
## ##### ####       ############## ######## g  g ########### ####         # ^ #
### ###^####         ############# ########      #####       ####      # g#   #
####   ######       ###            ########      ##### g     ####   !  ####^^ #
###############################################################################
";

520 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

So we start by de�ning a new struct type: PrefabLevel . This holds a map template,
a width and a height. Then we make a constant, WFC_POPULATED and create an always-
available level de�nition in it. Lastly, we paste our Notepad �le into a new constant,
currently called MY_LEVEL . This is a big string, and will be stored like any other string.

Lets modify the mode to also allow this type:

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum PrefabMode {
RexLevel{ template : &'static str },
Constant{ level : prefab_levels::PrefabLevel }
}

We'll modify our build function to also handle this match pattern:

fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template),
PrefabMode::Constant{level} => self.load_ascii_map(&level)
}
self.take_snapshot();
...

And modify our constructor to use it:

impl PrefabBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32) -> PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::Constant{level :
prefab_levels::WFC_POPULATED},
spawns: Vec::new()
}
}

521 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Now we need to create a loader that can handle it. We'll modify our load_rex_map to
share some code with it, so we aren't typing everything repeatedly - and make our
new load_ascii_map function:

522 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn char_to_map(&mut self, ch : char, idx: usize) {


match ch {
' ' => self.map.tiles[idx] = TileType::Floor,
'#' => self.map.tiles[idx] = TileType::Wall,
'@' => {
let x = idx as i32 % self.map.width;
let y = idx as i32 / self.map.width;
self.map.tiles[idx] = TileType::Floor;
self.starting_position = Position{ x:x as i32, y:y as i32 };
}
'>' => self.map.tiles[idx] = TileType::DownStairs,
'g' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Goblin".to_string()));
}
'o' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Orc".to_string()));
}
'^' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Bear Trap".to_string()));
}
'%' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Rations".to_string()));
}
'!' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Health Potion".to_string()));
}
_ => {
println!("Unknown glyph loading map: {}", (ch as u8) as char);
}
}
}

#[allow(dead_code)]
fn load_rex_map(&mut self, path: &str) {
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap();

for layer in &xp_file.layers {


for y in 0..layer.height {
for x in 0..layer.width {
let cell = layer.get(x, y).unwrap();

523 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if x < self.map.width as usize && y < self.map.height as


usize {
let idx = self.map.xy_idx(x as i32, y as i32);
// We're doing some nasty casting to make it easier to
type things like '#' in the match
self.char_to_map(cell.ch as u8 as char, idx);
}
}
}
}
}

#[allow(dead_code)]
fn load_ascii_map(&mut self, level: &prefab_levels::PrefabLevel) {
// Start by converting to a vector, with newlines removed
let mut string_vec : Vec<char> = level.template.chars().filter(|a| *a
!= '\r' && *a !='\n').collect();
for c in string_vec.iter_mut() { if *c as u8 == 160u8 { *c = ' '; } }

let mut i = 0;
for ty in 0..level.height {
for tx in 0..level.width {
if tx < self.map.width as usize && ty < self.map.height as
usize {
let idx = self.map.xy_idx(tx as i32, ty as i32);
self.char_to_map(string_vec[i], idx);
}
i += 1;
}
}
}

The �rst thing to notice is that the giant match in load_rex_map is now a function -
char_to_map . Since we're using the functionality more than once, this is good
practice: now we only have to �x it once if we messed it up! Otherwise, load_rex_map
is pretty much the same. Our new function is load_ascii_map . It starts with some
ugly code that bears explanation:

let mut string_vec : Vec<char> = level.template.chars().filter(|a| *a


1.
!= '\r' && *a !='\n').collect();
is a common Rust pattern, but isn't really self-explanatory at all. It chains
methods together, in left-to-right order. So it's really a big collection of
instructions glued together:
1. let mut string_vec : Vec<char> is just saying "make a variable named

524 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

string_vec , or the type Vec<char> and let me edit it.


2. level.template is the string in which our level template lives.
3. .chars() turns the string into an iterator - the same as when we've
previously typed myvector.iter() .
4. .filter(|a| *a != '\r' && *a !='\n') is interesting. Filters take a
lambda function in, and keep any entries that return true . So in this case,
we're stripping out \r and \n - the two newline characters. We'll keep
everything else.
5. .collect() says "take the results of everything before me, and put them
into a vector."
2. We then mutably iterate the string vector, and turn the character 160 into
spaces. I honestly have no idea why the text is reading spaces as character 160
and not 32, but we'll roll with it and just convert it.
3. We then iterate y from 0 to the speci�ed height.
1. We then iterate x from 0 to the speci�ed width.
1. If the x and y values are within the map we're creating, we calculate
the idx for the map tile - and call our char_to_map function to
translate it.

If you cargo run now, you'll see exactly the same as before - but instead of loading
the Rex Paint �le, we've loaded it from the constant ASCII in prefab_levels.rs .

Building a level section


Your brave adventurer emerges from the twisting tunnels, and comes across the walls of an
ancient underground forti�cation! That's the stu� of great D&D stories, and also an
occasional occurrence in games such as Dungeon Crawl: Stone Soup. It's quite likely
that what actually happened is your brave adventurer emerges from a procedurally
generated map and �nds a level section prefab!

We'll extend our mapping system to explicitly support this: a regular builder makes a
map, and then a sectional prefab replaces part of the map with your exciting premade
content. We'll start by making a new �le (in map_builders/prefab_builder ) called
prefab_sections.rs , and place a description of what we want:

525 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub enum HorizontalPlacement { Left, Center, Right }

#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub enum VerticalPlacement { Top, Center, Bottom }

#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub struct PrefabSection {
pub template : &'static str,
pub width : usize,
pub height: usize,
pub placement : (HorizontalPlacement, VerticalPlacement)
}

#[allow(dead_code)]
pub const UNDERGROUND_FORT : PrefabSection = PrefabSection{
template : RIGHT_FORT,
width: 15,
height: 43,
placement: ( HorizontalPlacement::Right, VerticalPlacement::Top )
};

#[allow(dead_code)]
const RIGHT_FORT : &str = "
     #         
  #######      
  #     #      
  #     #######
  #  g        #
  #     #######
  #     #      
  ### ###      
    # #        
    # #        
    # ##       
    ^          
    ^          
    # ##       
    # #        
    # #        
    # #        
    # #        

526 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

  ### ###      
  #     #      
  #     #      
  #  g  #      
  #     #      
  #     #      
  ### ###      
    # #        
    # #        
    # #        
    # ##       
    ^          
    ^          
    # ##       
    # #        
    # #        
    # #        
  ### ###      
  #     #      
  #     #######
  #  g        #
  #     #######
  #     #      
  #######      
     #         
";

So we have RIGHT_FORT as a string, describing a forti�cation we might encounter.


We've built a structure, PrefabSection which includes placement hints, and a
constant for our actual fort ( UNDERGROUND_FORT ) specifying that we'd like to be at the
right of the map, at the top (the vertical doesn't really matter in this example, because
it is the full size of the map).

Level sections are di�erent from builders we've made before, because they take a
completed map - and replace part of it. We've done something similar with Waveform
Collapse, so we'll adopt a similar pattern. We'll start by modifying our PrefabBuilder
to know about the new type of map decoration:

527 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum PrefabMode {
RexLevel{ template : &'static str },
Constant{ level : prefab_levels::PrefabLevel },
Sectional{ section : prefab_sections::PrefabSection }
}

#[allow(dead_code)]
pub struct PrefabBuilder {
map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
mode: PrefabMode,
spawns: Vec<(usize, String)>,
previous_builder : Option<Box<dyn MapBuilder>>
}

As much as I'd love to put the previous_builder into the enum, I kept running into
lifetime problems. Perhaps there's a way to do it (and some kind reader will help me
out?), but for now I've put it into PrefabBuilder . The requested map section is in the
parameter, however. We also update our constructor to use this type of map:

impl PrefabBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32, previous_builder : Option<Box<dyn
MapBuilder>>) -> PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::Sectional{ section:
prefab_sections::UNDERGROUND_FORT },
spawns: Vec::new(),
previous_builder
}
}
...

Over in map_builders/mod.rs 's random_builder , we'll modify the builder to �rst run

528 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

a Cellular Automata map, and then apply the sectional:

Box::new(
PrefabBuilder::new(
new_depth,
Some(
Box::new(
CellularAutomotaBuilder::new(new_depth)
)
)
)
)

This could be one line, but I've separated it out due to the sheer number of
parentheses.

Next, we update our match statement (in build() ) to actually call the builder:

fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template),
PrefabMode::Constant{level} => self.load_ascii_map(&level),
PrefabMode::Sectional{section} => self.apply_sectional(&section)
}
self.take_snapshot();
...

Now, we'll write apply_sectional :

529 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn apply_sectional(&mut self, section :


&prefab_sections::PrefabSection) {
// Build the map
let prev_builder = self.previous_builder.as_mut().unwrap();
prev_builder.build_map();
self.starting_position = prev_builder.get_starting_position();
self.map = prev_builder.get_map().clone();
self.take_snapshot();

use prefab_sections::*;

let string_vec = PrefabBuilder::read_ascii_to_vec(section.template);

// Place the new section


let chunk_x;
match section.placement.0 {
HorizontalPlacement::Left => chunk_x = 0,
HorizontalPlacement::Center => chunk_x = (self.map.width / 2) -
(section.width as i32 / 2),
HorizontalPlacement::Right => chunk_x = (self.map.width-1) -
section.width as i32
}

let chunk_y;
match section.placement.1 {
VerticalPlacement::Top => chunk_y = 0,
VerticalPlacement::Center => chunk_y = (self.map.height / 2) -
(section.height as i32 / 2),
VerticalPlacement::Bottom => chunk_y = (self.map.height-1) -
section.height as i32
}
println!("{},{}", chunk_x, chunk_y);

let mut i = 0;
for ty in 0..section.height {
for tx in 0..section.width {
if tx < self.map.width as usize && ty < self.map.height as
usize {
let idx = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 +
chunk_y);
self.char_to_map(string_vec[i], idx);
}
i += 1;
}
}

530 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

self.take_snapshot();
}

This a lot like other code we've written, but lets step through it anyway:

1. let prev_builder = self.previous_builder.as_mut().unwrap(); is quite the


mouthful. The previous builder is an Option - but if we're calling this code, it has
to have a value. So we want to unwrap it (which will panic and crash if there is
no value), but we can't! The borrow checker will complain if we just call
previous_builder.unwrap - so we have to inject an as_mut() in there, which
Option provides for just this purpose.
2. We call build_map on the previous builder, to construct the base map.
3. We copy the starting position from the previous builder to our new builder.
4. We copy the map from the previous builder to our self (the new builder).
5. We call read_ascii_to_vec , which is the same as the string-to-vector code from
the level example; we've actually updated the level example to use it also, in the
source code.
6. We create two variables, chunk_x and chunk_y and query the section's
placement preference to determine where to put the new chunk.
7. We iterate the section just like when we were iterating a level earlier - but adding
chunk_x to tx and chunk_y to ty to o�set the section inside the level.

If you cargo run the example now, you'll see a map built with a cave - and a
forti�cation to the right.

531 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You may also notice that there aren't any entities at all, outside of the prefab area!

Adding entities to sectionals


Spawning and determining spawn points have been logically separated, to help keep
the map generation code clean. Di�erent maps can have their own strategies for
placing entities, so there isn't a straightforward method to simply suck in the data
from the previous algorithms and add to it. There should be, and it should enable
�ltering and all manner of tweaking with later "meta-map builders" (such as WFC or
this one). We've stumbled upon a clue for a good interface in the code that places
entities in prefabs: the spawn system already supports tuples of
(position, type string) . We'll use that as the basis for the new setup.

We'll start by opening up map_builders/mod.rs and editing the MapBuilder trait:

532 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub trait MapBuilder {


fn build_map(&mut self);
fn get_map(&self) -> Map;
fn get_starting_position(&self) -> Position;
fn get_snapshot_history(&self) -> Vec<Map>;
fn take_snapshot(&mut self);
fn get_spawn_list(&self) -> &Vec<(usize, String)>;

fn spawn_entities(&mut self, ecs : &mut World) {


for entity in self.get_spawn_list().iter() {
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
}
}
}

Congratulations, half your source code just turned red in your IDE. That's the danger
of changing a base interface - you wind up implementing it everywhere. Also, the setup
of spawn_entities has changed - there is now a default implementation.
Implementers of the trait can override it if they want to - but otherwise they don't
actually need to write it anymore. Since everything should be available via the
get_spawn_list function, the trait has everything it needs to provide that
implementation.

We'll go back to simple_map and update it to obey the new trait rules. We'll extend
the SimpleMapBuiler structure to feature a spawn list:

pub struct SimpleMapBuilder {


map : Map,
starting_position : Position,
depth: i32,
rooms: Vec<Rect>,
history: Vec<Map>,
spawn_list: Vec<(usize, String)>
}

The get_spawn_list implementation is trivial:

fn get_spawn_list(&self) -> &Vec<(usize, String)> {


&self.spawn_list
}

533 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Now for the fun part. Previously, we didn't consider spawning until the call to
spawn_entities . Lets remind ourselves what it does (it's been a while!):

fn spawn_entities(&mut self, ecs : &mut World) {


for room in self.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, self.depth);
}
}

It iterates all the rooms, and spawns entities inside the rooms. We're using that
pattern a lot, so it's time to visit spawn_room in spawner.rs . We'll modify it to spawn
into a spawn_list rather than directly onto the map. So we open up spawner.rs , and
modify spawn_room and spawn_region (since they are intertwined, we'll �x them
together):

534 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

/// Fills a room with stuff!


pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room :
&Rect, map_depth: i32, spawn_list : &mut Vec<(usize, String)>) {
let mut possible_targets : Vec<usize> = Vec::new();
{ // Borrow scope - to keep access to the map separated
for y in room.y1 + 1 .. room.y2 {
for x in room.x1 + 1 .. room.x2 {
let idx = map.xy_idx(x, y);
if map.tiles[idx] == TileType::Floor {
possible_targets.push(idx);
}
}
}
}

spawn_region(map, rng, &possible_targets, map_depth, spawn_list);


}

/// Fills a region with stuff!


pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area :
&[usize], map_depth: i32, spawn_list : &mut Vec<(usize, String)>) {
let spawn_table = room_table(map_depth);
let mut spawn_points : HashMap<usize, String> = HashMap::new();
let mut areas : Vec<usize> = Vec::from(area);

// Scope to keep the borrow checker happy


{
let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1,
MAX_MONSTERS + 3) + (map_depth - 1) - 3);
if num_spawns == 0 { return; }

for _i in 0 .. num_spawns {
let array_index = if areas.len() == 1 { 0usize } else {
(rng.roll_dice(1, areas.len() as i32)-1) as usize };

let map_idx = areas[array_index];


spawn_points.insert(map_idx, spawn_table.roll(rng));
areas.remove(array_index);
}
}

// Actually spawn the monsters


for spawn in spawn_points.iter() {
spawn_list.push((*spawn.0, spawn.1.to_string()));
}

535 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You'll notice that the biggest change is taking a mutable reference to the spawn_list
in each function, and instead of actually spawning the entity - we defer the operation
by pushing the spawn information into the spawn_list vector at the end. Instead of
passing in the ECS, we're passing in the Map and RandomNumberGenerator .

Going back to simple_map.rs , we move the spawning code into the end of build :

...
self.starting_position = Position{ x: start_pos.0, y: start_pos.1 };

// Spawn some entities


for room in self.rooms.iter().skip(1) {
spawner::spawn_room(&self.map, &mut rng, room, self.depth, &mut
self.spawn_list);
}

We can now delete SimpleMapBuilder 's implementation of spawn_entities - the


default will work �ne.

The same changes can be made to all of the builders that rely on room spawning; for
brevity, I won't spell them all out here - you can �nd them in the source code. The
various builders that use Voronoi diagrams are similarly simple to update. For
example, Cellular Automata. Add the spawn_list to the builder structure, and add a
spawn_list : Vec::new() into the constructor. Move the monster spawning from
spawn_entities into the end of build and delete the function. Copy the
get_spawn_list from the other implementations. We changed the region spawning
code a little, so here's the implementation from cellular_automota.rs :

// Now we build a noise map for use in spawning entities later


self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng);

// Spawn the entities


for area in self.noise_areas.iter() {
spawner::spawn_region(&self.map, &mut rng, area.1, self.depth, &mut
self.spawn_list);
}

Once again, it's rinse and repeat on the other Voronoi spawn algorithms. I've done the

536 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

work in the source code for you, if you'd like to take a peek.

Jump to here if refactoring is boring!

SO - now that we've refactored our spawn system, how do we use it inside our
PrefabBuilder ? We can add one line to our apply_sectional function and get all of
the entities from the previous map. You could simply copy it, but that's probably not
what you want; you need to �lter out entities inside the new prefab, both to make
room for new ones and to ensure that the spawning makes sense. We'll also need to
rearrange a little to keep the borrow checker happy. Here's the function now:

537 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn apply_sectional(&mut self, section :


&prefab_sections::PrefabSection) {
use prefab_sections::*;

let string_vec = PrefabBuilder::read_ascii_to_vec(section.template);

// Place the new section


let chunk_x;
match section.placement.0 {
HorizontalPlacement::Left => chunk_x = 0,
HorizontalPlacement::Center => chunk_x = (self.map.width / 2) -
(section.width as i32 / 2),
HorizontalPlacement::Right => chunk_x = (self.map.width-1) -
section.width as i32
}

let chunk_y;
match section.placement.1 {
VerticalPlacement::Top => chunk_y = 0,
VerticalPlacement::Center => chunk_y = (self.map.height / 2) -
(section.height as i32 / 2),
VerticalPlacement::Bottom => chunk_y = (self.map.height-1) -
section.height as i32
}

// Build the map


let prev_builder = self.previous_builder.as_mut().unwrap();
prev_builder.build_map();
self.starting_position = prev_builder.get_starting_position();
self.map = prev_builder.get_map().clone();
for e in prev_builder.get_spawn_list().iter() {
let idx = e.0;
let x = idx as i32 % self.map.width;
let y = idx as i32 / self.map.width;
if x < chunk_x || x > (chunk_x + section.width as i32) ||
y < chunk_y || y > (chunk_y + section.height as i32) {
self.spawn_list.push(
(idx, e.1.to_string())
)
}
}
self.take_snapshot();

let mut i = 0;
for ty in 0..section.height {

538 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for tx in 0..section.width {
if tx > 0 && tx < self.map.width as usize -1 && ty <
self.map.height as usize -1 && ty > 0 {
let idx = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 +
chunk_y);
self.char_to_map(string_vec[i], idx);
}
i += 1;
}
}
self.take_snapshot();
}

If you cargo run now, you'll face enemies in both sections of the map.

Wrap Up
In this chapter, we've covered quite a bit of ground:

We can load Rex Paint levels, complete with hand-placed entities and play them.

539 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We can de�ne ASCII premade maps in our game, and play them (removing the
requirement to use Rex Paint).
We can load level sectionals, and apply them to the level.
We can adjust the spawns from previous levels in the builder chain.

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Room Vaults

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

The last chapter was getting overly long, so it was broken into two. In the previous
chapter, we learned how to load prefabricated maps and map sections, modi�ed the
spawn system so that meta-builders could a�ect the spawn patterns from the previous
builder, and demonstrated integration of whole map chunks into levels. In this
chapter, we'll explode room vaults - prefabricated content that integrates itself into
your level. So you might hand-craft some rooms, and have them seamlessly �t into
your existing map.

Designing a room: Totally Not A Trap

540 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The life of a roguelike developer is part programmer, part interior decorator (in a
weirdly Gnome Mad Scientist fashion). We've already designed whole levels and level
sections, so it isn't a huge leap to designing rooms. Lets go ahead and build a few pre-
designed rooms.

We'll make a new �le in map_builders/prefab_builders called prefab_rooms.rs .


We'll insert a relatively iconic map feature into it:

#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub struct PrefabRoom {
pub template : &'static str,
pub width : usize,
pub height: usize,
pub first_depth: i32,
pub last_depth: i32
}

#[allow(dead_code)]
pub const TOTALLY_NOT_A_TRAP : PrefabRoom = PrefabRoom{
template : TOTALLY_NOT_A_TRAP_MAP,
width: 5,
height: 5,
first_depth: 0,
last_depth: 100
};

#[allow(dead_code)]
const TOTALLY_NOT_A_TRAP_MAP : &str = "

^^^
^!^
^^^

";

If you look at the ASCII, you'll see a classic piece of map design: a health potion
completely surrounded by traps. Since the traps are hidden by default, we're relying
on the player to think "well, that doesn't look suspicious at all"! Not that there are
spaces all around the content - there's a 1-tile gutter all around it. This ensures that
any 5x5 room into which the vault is placed will still be traversable. We're also
introducing first_depth and last_depth - these are the levels at which the vault
might be applied; for the sake of introduction, we'll pick 0..100 - which should be every

541 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

level, unless you are a really dedicated play-tester!

Placing the not-a-trap room


We'll start by adding another mode to the PrefabBuiler system:

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum PrefabMode {
RexLevel{ template : &'static str },
Constant{ level : prefab_levels::PrefabLevel },
Sectional{ section : prefab_sections::PrefabSection },
RoomVaults
}

We're not going to add any parameters yet - by the end of the chapter, we'll have it
integrated into a broader system for placing vaults. We'll update our constructor to
use this type of placement:

impl PrefabBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32, previous_builder : Option<Box<dyn
MapBuilder>>) -> PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RoomVaults,
previous_builder,
spawn_list : Vec::new()
}
}
...

And we'll teach our match function in build to use it:

542 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template),
PrefabMode::Constant{level} => self.load_ascii_map(&level),
PrefabMode::Sectional{section} => self.apply_sectional(&section),
PrefabMode::RoomVaults => self.apply_room_vaults()
}
self.take_snapshot();
...

That leaves the next logical step being to write apply_room_vaults . Our objective is
to scan the incoming map (from a di�erent builder, even a previous iteration of this
one!) for appropriate places into which we can place a vault, and add it to the map.
We'll also want to remove any spawned creatures from the vault area - so the vaults
remain hand-crafted and aren't interfered with by random spawning.

We'll be re-using our "create previous iteration" code from apply_sectional - so lets
rewrite it into a more generic form:

fn apply_previous_iteration<F>(&mut self, mut filter: F)


where F : FnMut(i32, i32, &(usize, String)) -> bool
{
// Build the map
let prev_builder = self.previous_builder.as_mut().unwrap();
prev_builder.build_map();
self.starting_position = prev_builder.get_starting_position();
self.map = prev_builder.get_map().clone();
for e in prev_builder.get_spawn_list().iter() {
let idx = e.0;
let x = idx as i32 % self.map.width;
let y = idx as i32 / self.map.width;
if filter(x, y, e) {
self.spawn_list.push(
(idx, e.1.to_string())
)
}
}
self.take_snapshot();
}

There's a lot of new Rust here! Lets walk through it:

543 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. You'll notice that we've added a template type to the function.


fn apply_previous_iteration<F> . This speci�es that we don't know exactly
what F is when we write the function.
2. The second parameter ( mut filter: F ) is also of type F . So we're telling the
function signature to accept the template type as the parameter.
3. Before the opening curly bracket, we've added a where clause. This type of
clause can be used to limit what it accepted by the generic type. In this case,
we're saying that F must be an FnMut . An FnMut is a function pointer that is
allowed to change state (mutable; if it were immutable it'd be an Fn ). We then
specify the parameters of the function, and its return type. Inside the function,
we can now treat filter like a function - even though we haven't actually
written one. We're requiring that function accept two i32 (integers), and a
tuple of (usize, String) . The latter should look familiar - its our spawn list
format. The �rst two are the x and y coordinates of the spawn - we're passing
that to save the caller from doing the math each time.
4. We then run the prev_builder code we wrote in the previous chapter - it builds
the map and obtains the map data itself, along with the spawn_list from the
previous algorithm.
5. We then iterate through the spawn list, and calculate the x/y coordinates and
map index for each entity. We call filter with this information, and if it returns
true we add it to our spawn_list .
6. Lastly, we take a snapshot of the map so you can see the step in action.

That sounds really complicated, but most of what it as done is allow us to replace the
following code in apply_sectional :

544 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Build the map


let prev_builder = self.previous_builder.as_mut().unwrap();
prev_builder.build_map();
self.starting_position = prev_builder.get_starting_position();
self.map = prev_builder.get_map().clone();
for e in prev_builder.get_spawn_list().iter() {
let idx = e.0;
let x = idx as i32 % self.map.width;
let y = idx as i32 / self.map.width;
if x < chunk_x || x > (chunk_x + section.width as i32) ||
y < chunk_y || y > (chunk_y + section.height as i32) {
self.spawn_list.push(
(idx, e.1.to_string())
)
}
}
self.take_snapshot();

We can replace it with a more generic call:

// Build the map


self.apply_previous_iteration(|x,y,e| {
x < chunk_x || x > (chunk_x + section.width as i32) || y < chunk_y ||
y > (chunk_y + section.height as i32)
});

This is interesting: we're passing in a closure - a lambda function to the filter . It


receives x , y , and e from the previous map's spawn_list for each entity. In this
case, we're checking against chunk_x , chunk_y , section.width and
section.height to see if the entity is inside our sectional. You've probably noticed
that we didn't declare these anywhere in the lambda function; we are relying on
capture - you can call a lambda and reference other variables that are in its scope - and
it can reference them as if they were its own. This is a very powerful feature, and you
can learn about it here.

Room Vaults
Let's start building apply_room_vaults . We'll take it step-by-step, and work our way

545 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

through. We'll start with the function signature:

fn apply_room_vaults(&mut self) {
use prefab_rooms::*;
let mut rng = RandomNumberGenerator::new();

Simple enough: no parameters other than mutable membership of the builder. It is


going to be referring to types in prefab_rooms , so rather than type that every time an
in-function using statement imports the names to the local namespace to save your
�ngers. We'll also need a random number generator, so we make one as we have
before. Next up:

// Apply the previous builder, and keep all entities it spawns (for now)
self.apply_previous_iteration(|_x,_y,_e| true);

We use the code we just wrote to apply the previous map. The filter we're passing
in this time always returns true: keep all the entities for now. Next:

// Note that this is a place-holder and will be moved out of this function
let master_vault_list = vec![TOTALLY_NOT_A_TRAP];

// Filter the vault list down to ones that are applicable to the current
depth
let possible_vaults : Vec<&PrefabRoom> = master_vault_list
.iter()
.filter(|v| { self.depth >= v.first_depth && self.depth <=
v.last_depth })
.collect();

if possible_vaults.is_empty() { return; } // Bail out if there's nothing


to build

let vault_index = if possible_vaults.len() == 1 { 0 } else {


(rng.roll_dice(1, possible_vaults.len() as i32)-1) as usize };
let vault = possible_vaults[vault_index];

We make a vector of all possible vault types - there's currently only one, but when we
have more they go in here. This isn't really ideal, but we'll worry about making it a
global resource in a future chapter. We then make a possible_vaults list by taking
the master_vault_list and �ltering it to only include those whose first_depth and

546 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

last_depth line up with the requested dungeon depth. The


iter().filter(...).collect() pattern has been described before, and it's a very
powerful way to quickly extract what you need from a vector. If there are no possible
vaults, we return out of the function - nothing to do here! Finally, we use another
pattern we've used before: we pick a vault to create by selecting a random member of
the possible_vaults vector.

Next up:

547 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// We'll make a list of places in which the vault could fit


let mut vault_positions : Vec<Position> = Vec::new();

let mut idx = 0usize;


loop {
let x = (idx % self.map.width as usize) as i32;
let y = (idx / self.map.width as usize) as i32;

// Check that we won't overflow the map


if x > 1
&& (x+vault.width as i32) < self.map.width-2
&& y > 1
&& (y+vault.height as i32) < self.map.height-2
{

let mut possible = true;


for ty in 0..vault.height as i32 {
for tx in 0..vault.width as i32 {

let idx = self.map.xy_idx(tx + x, ty + y);


if self.map.tiles[idx] != TileType::Floor {
possible = false;
}
}
}

if possible {
vault_positions.push(Position{ x,y });
break;
}

idx += 1;
if idx >= self.map.tiles.len()-1 { break; }
}

There's quite a bit of code in this section (which determines all the places a the vault
might �t). Lets walk through it:

1. We make a new vector of Position s. This will contain all the possible places in
which we could spawn our vault.
2. We set idx to 0 - we plan to iterate through the whole map.
3. We start a loop - the Rust loop type that doesn't exit until you call break .

548 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. We calculate x and y to know where we are on the map.


2. We do an over�ow check; x needs to be greater than 1, and x+1 needs to
be less than the map width. We check the same with y and the map
height. If we're within the bounds:
1. We set possible to true.
2. We iterate every tile on the map in the range
(x .. x+vault width), (y .. y + vault height) - if any tile isn't a
�oor, we set possible to false .
3. If it is possible to place the vault here, we add the position to our
vault_positions vector from step 1.
3. We increment idx by 1.
4. If we've run out of map, we break out of the loop.

In other words, we quickly scan the whole map for everywhere we could put the vault -
and make a list of possible placements. We then:

if !vault_positions.is_empty() {
let pos_idx = if vault_positions.len()==1 { 0 } else {
(rng.roll_dice(1, vault_positions.len() as i32)-1) as usize };
let pos = &vault_positions[pos_idx];

let chunk_x = pos.x;


let chunk_y = pos.y;

let string_vec = PrefabBuilder::read_ascii_to_vec(vault.template);


let mut i = 0;
for ty in 0..vault.height {
for tx in 0..vault.width {
let idx = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 +
chunk_y);
self.char_to_map(string_vec[i], idx);
i += 1;
}
}
self.take_snapshot();
}

So if there are any valid positions for the vault, we:

1. Pick a random entry in the vault_positions vector - this is where we will place
the vault.

549 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

2. Use read_ascii_to_vec to read in the ASCII, just like we did in prefabs and
sectionals.
3. Iterate the vault data and use char_to_map to place it - just like we did before.

Putting it all together, you have the following function:

550 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn apply_room_vaults(&mut self) {
use prefab_rooms::*;
let mut rng = RandomNumberGenerator::new();

// Apply the previous builder, and keep all entities it spawns (for
now)
self.apply_previous_iteration(|_x,_y,_e| true);

// Note that this is a place-holder and will be moved out of this


function
let master_vault_list = vec![TOTALLY_NOT_A_TRAP];

// Filter the vault list down to ones that are applicable to the
current depth
let possible_vaults : Vec<&PrefabRoom> = master_vault_list
.iter()
.filter(|v| { self.depth >= v.first_depth && self.depth <=
v.last_depth })
.collect();

if possible_vaults.is_empty() { return; } // Bail out if there's


nothing to build

let vault_index = if possible_vaults.len() == 1 { 0 } else {


(rng.roll_dice(1, possible_vaults.len() as i32)-1) as usize };
let vault = possible_vaults[vault_index];

// We'll make a list of places in which the vault could fit


let mut vault_positions : Vec<Position> = Vec::new();

let mut idx = 0usize;


loop {
let x = (idx % self.map.width as usize) as i32;
let y = (idx / self.map.width as usize) as i32;

// Check that we won't overflow the map


if x > 1
&& (x+vault.width as i32) < self.map.width-2
&& y > 1
&& (y+vault.height as i32) < self.map.height-2
{

let mut possible = true;


for ty in 0..vault.height as i32 {
for tx in 0..vault.width as i32 {

551 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let idx = self.map.xy_idx(tx + x, ty + y);


if self.map.tiles[idx] != TileType::Floor {
possible = false;
}
}
}

if possible {
vault_positions.push(Position{ x,y });
break;
}

idx += 1;
if idx >= self.map.tiles.len()-1 { break; }
}

if !vault_positions.is_empty() {
let pos_idx = if vault_positions.len()==1 { 0 } else {
(rng.roll_dice(1, vault_positions.len() as i32)-1) as usize };
let pos = &vault_positions[pos_idx];

let chunk_x = pos.x;


let chunk_y = pos.y;

let string_vec = PrefabBuilder::read_ascii_to_vec(vault.template);


let mut i = 0;
for ty in 0..vault.height {
for tx in 0..vault.width {
let idx = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 +
chunk_y);
self.char_to_map(string_vec[i], idx);
i += 1;
}
}
self.take_snapshot();
}
}

It's more likely that a square vault will �t in rectangular rooms, so we'll pop over to
map_builders/mod.rs and slightly adjust the random_builder to use the original
simple map algorithm for the base map:

552 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Box::new(
PrefabBuilder::new(
new_depth,
Some(
Box::new(
SimpleMapBuilder::new(new_depth)
)
)
)
)

If you cargo run now, the vault will probably be placed on your map. Here's a
screenshot of a run in which I found it:

Filtering out entities

553 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We probably don't want to keep entities that are inside our new vault from the
previous map iteration. You might have a cunningly placed trap and spawn a goblin
on top of it! (While fun, probably not what you had in mind). So we'll extend
apply_room_vaults to do some �ltering when it places the vault. We want to �lter
before we spawn new stu�, and then spawn more stu� with the room. Enter the
retain feature:

...
let chunk_y = pos.y;

let width = self.map.width; // The borrow checker really doesn't like it


let height = self.map.height; // when we access `self` inside the `retain`
self.spawn_list.retain(|e| {
let idx = e.0 as i32;
let x = idx % width;
let y = idx / height;
x < chunk_x || x > chunk_x + vault.width as i32 || y < chunk_y || y >
chunk_y + vault.height as i32
});
...

Calling retain on a vector iterates through every entry, and calls the passed
closure/lambda function. If it returns true , then the element is retained (kept) -
otherwise it is removed. So here we're catching width and height (to avoid
borrowing self ), and then calculate the location for each entry. If it is outside of the
new vault - we keep it.

I want more than one vault!


Having only one vault is pretty dull - albeit a good start in terms of proving the
functionality works. In prefab_rooms.rs we'll go ahead and write a couple more.
These aren't intended to be seminal examples of level design, but they illustrate the
process. We'll add some more room prefabs:

554 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub struct PrefabRoom {
pub template : &'static str,
pub width : usize,
pub height: usize,
pub first_depth: i32,
pub last_depth: i32
}

#[allow(dead_code)]
pub const TOTALLY_NOT_A_TRAP : PrefabRoom = PrefabRoom{
template : TOTALLY_NOT_A_TRAP_MAP,
width: 5,
height: 5,
first_depth: 0,
last_depth: 100
};

#[allow(dead_code)]
const TOTALLY_NOT_A_TRAP_MAP : &str = "

^^^
^!^
^^^

";

#[allow(dead_code)]
pub const SILLY_SMILE : PrefabRoom = PrefabRoom{
template : SILLY_SMILE_MAP,
width: 6,
height: 6,
first_depth: 0,
last_depth: 100
};

#[allow(dead_code)]
const SILLY_SMILE_MAP : &str = "

^ ^
#

###

555 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

";

#[allow(dead_code)]
pub const CHECKERBOARD : PrefabRoom = PrefabRoom{
template : CHECKERBOARD_MAP,
width: 6,
height: 6,
first_depth: 0,
last_depth: 100
};

#[allow(dead_code)]
const CHECKERBOARD_MAP : &str = "

g#%#
^# #

";

We've added CHECKERBOARD (a grid of walls and spaces with traps, a goblin and
goodies in it), and SILLY_SMILE which just looks like a silly wall feature. Now open up
apply_room_vaults in map_builders/prefab_builder/mod.rs and add these to the
master vector:

// Note that this is a place-holder and will be moved out of this function
let master_vault_list = vec![TOTALLY_NOT_A_TRAP, CHECKERBOARD,
SILLY_SMILE];

If you cargo run now, you'll most likely encounter one of the three vaults. Each time
you advance a depth, you will probably encounter one of the three. My test ran into
the checkerboard almost immediately:

556 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

That's a great start, and gives a bit of �air to maps as you descend - but it may not be
quite what you were asking for when you said you wanted more than one vault! How
about more than one vault on a level? Back to apply_room_vaults ! It's easy enough to
come up with a number of vaults to spawn:

let n_vaults = i32::min(rng.roll_dice(1, 3), possible_vaults.len() as


i32);

This sets n_vaults to the minimum value of a dice roll ( 1d3 ) and the number of
possible vaults - so it'll never exceed the number of options, but can vary a bit. It's also
pretty easy to wrap the creation function in a for loop:

557 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if possible_vaults.is_empty() { return; } // Bail out if there's nothing


to build

let n_vaults = i32::min(rng.roll_dice(1, 3), possible_vaults.len()


as i32);

for _i in 0..n_vaults {

let vault_index = if possible_vaults.len() == 1 { 0 } else {


(rng.roll_dice(1, possible_vaults.len() as i32)-1) as usize };
let vault = possible_vaults[vault_index];

...

self.take_snapshot();

possible_vaults.remove(vault_index);
}
}

Notice that at the end of the loop, we're removing the vault we added from
possible_vaults . We have to change the declaration to be able to do that:
let mut possible_vaults : Vec<&PrefabRoom> = ... - we add the mut to allow us
to change the vector. This way, we won't keep adding the same vault - they only get
spawned once.

Now for the more di�cult part: making sure that our new vaults don't overlap the
previously spawned ones. We'll create a new HashSet of tiles we've consumed:

let mut used_tiles : HashSet<usize> = HashSet::new();

Hash sets have the advantage of o�ering a quick way to say if they contain a value, so
they are ideal for what we need. We'll insert the tile idx into the set when we add a
tile:

558 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for ty in 0..vault.height {
for tx in 0..vault.width {
let idx = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 +
chunk_y);
self.char_to_map(string_vec[i], idx);
used_tiles.insert(idx);
i += 1;
}
}

Lastly, in our possibility checking we want to do a check against used_tiles to ensure


we aren't overlapping:

let idx = self.map.xy_idx(tx + x, ty + y);


if self.map.tiles[idx] != TileType::Floor {
possible = false;
}
if used_tiles.contains(&idx) {
possible = false;
}

Now if you cargo run your project, you might encounter several vaults. Here's a case
where we encountered two vaults:

559 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I don't always want a vault!


If you o�er all of your vaults on every level, the game will be a bit more predictable
than you probably want (unless you make a lot of vaults!). We'll modify
apply_room_vaults to only sometimes have any vaults, with an increasing probability
as you descend into the dungeon:

// Apply the previous builder, and keep all entities it spawns (for now)
self.apply_previous_iteration(|_x,_y,_e| true);

// Do we want a vault at all?


let vault_roll = rng.roll_dice(1, 6) + self.depth;
if vault_roll < 4 { return; }

This is very simple: we roll a six-sided dice and add the current depth. If we rolled less

560 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

than 4 , we bail out and just provide the previously generated map. If you cargo run
your project now, you'll sometimes encounter vaults - and sometimes you won't.

Finishing up: o�ering some constructors other than


just new
We should o�er some more friendly ways to build our PrefabBuilder , so it's obvious
what we're doing when we construct our builder chain. Add the following constructors
to prefab_builder/mod.rs :

561 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
pub fn rex_level(new_depth : i32, template : &'static str) ->
PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RexLevel{ template },
previous_builder : None,
spawn_list : Vec::new()
}
}

#[allow(dead_code)]
pub fn constant(new_depth : i32, level : prefab_levels::PrefabLevel) ->
PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::Constant{ level },
previous_builder : None,
spawn_list : Vec::new()
}
}

#[allow(dead_code)]
pub fn sectional(new_depth : i32, section :
prefab_sections::PrefabSection, previous_builder : Box<dyn MapBuilder>) ->
PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::Sectional{ section },
previous_builder : Some(previous_builder),
spawn_list : Vec::new()
}
}

#[allow(dead_code)]
pub fn vaults(new_depth : i32, previous_builder : Box<dyn MapBuilder>) ->

562 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RoomVaults,
previous_builder : Some(previous_builder),
spawn_list : Vec::new()
}
}

We now have a decent interface for creating our meta-builder!

It's Turtles (Or Meta-Builders) All The Way Down


The last few chapters have all created meta builders - they aren't really builders in that
they don't create an entirely new map, they modify the results of another algorithm.
The really interesting thing here is that you can keep chaining them together to
achieve the results you want. For example, lets make a map by starting with a Cellular
Automata map, feeding it through Waveform Collapse, possibly adding a castle wall,
and then searching for vaults!

The syntax for this is currently quite ugly (that will be a future chapter topic). In
map_builders/mod.rs :

Box::new(
PrefabBuilder::vaults(
new_depth,
Box::new(PrefabBuilder::sectional(
new_depth,
prefab_builder::prefab_sections::UNDERGROUND_FORT,
Box::new(WaveformCollapseBuilder::derived_map(
new_depth,
Box::new(CellularAutomotaBuilder::new(new_depth))
))
))
)
)

563 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Also in map_builders/prefab_builder/mod.rs make sure that you are publicly


sharing the map modules:

pub mod prefab_levels;


pub mod prefab_sections;
pub mod prefab_rooms;

If you cargo run this, you get to watch it cycle through the layered building:

Restoring Randomness
Now that we've completed a two-chapter marathon of prefabricated, layered map
building - it's time to restore the random_builder function to provide randomness
once more. Here's the new function from map_builders/mod.rs :

564 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {


let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 17);
let mut result : Box<dyn MapBuilder>;
match builder {
1 => { result = Box::new(BspDungeonBuilder::new(new_depth)); }
2 => { result = Box::new(BspInteriorBuilder::new(new_depth)); }
3 => { result = Box::new(CellularAutomotaBuilder::new(new_depth));
}
4 => { result =
Box::new(DrunkardsWalkBuilder::open_area(new_depth)); }
5 => { result =
Box::new(DrunkardsWalkBuilder::open_halls(new_depth)); }
6 => { result =
Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)); }
7 => { result =
Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)); }
8 => { result =
Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)); }
9 => { result = Box::new(MazeBuilder::new(new_depth)); }
10 => { result = Box::new(DLABuilder::walk_inwards(new_depth)); }
11 => { result = Box::new(DLABuilder::walk_outwards(new_depth)); }
12 => { result =
Box::new(DLABuilder::central_attractor(new_depth)); }
13 => { result = Box::new(DLABuilder::insectoid(new_depth)); }
14 => { result =
Box::new(VoronoiCellBuilder::pythagoras(new_depth)); }
15 => { result =
Box::new(VoronoiCellBuilder::manhattan(new_depth)); }
16 => { result = Box::new(PrefabBuilder::constant(new_depth,
prefab_builder::prefab_levels::WFC_POPULATED)) },
_ => { result = Box::new(SimpleMapBuilder::new(new_depth)); }
}

if rng.roll_dice(1, 3)==1 {
result = Box::new(WaveformCollapseBuilder::derived_map(new_depth,
result));
}

if rng.roll_dice(1, 20)==1 {
result = Box::new(PrefabBuilder::sectional(new_depth,
prefab_builder::prefab_sections::UNDERGROUND_FORT ,result));
}

result = Box::new(PrefabBuilder::vaults(new_depth, result));

565 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

result
}

We're taking full advantage of the composability of our layers system now! Our
random builder now:

1. In the �rst layer, we roll 1d17 and pick a map type; we've included our pre-made
level as one of the options.
2. Next, we roll 1d3 - and on a 1, we run the WaveformCollapse algorithm on that
builder.
3. We roll 1d20 , and on a 1 - we apply a PrefabBuilder sectional, and add our
fortress. That way, you'll only occasionally run into it.
4. We run whatever builder we came up with against our PrefabBuilder 's Room
Vault system (the focus of this chapter!), to add premade rooms to the mix.

Wrap-Up
In this chapter, we've gained the ability to prefabricate rooms and include them if they
�t into our level design. We've also explored the ability to add algorithms together,
giving even more layers of randomness.

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Layering/Builder Chaining

566 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

The last few chapters have introduced an important concept in procedural


generation: chained builders. We're happily building a map, calling Waveform
Collapse to mutate the map, calling our PrefabBuilder to change it again, and so on.
This chapter will formalize this process a bit, expand upon it, and leave you with a
framework that lets you clearly build new maps by chaining concepts together.

A builder-based interface
Builder chaining is a pretty profound approach to procedurally generating maps, and
gives us an opportunity to clean up a lot of the code we've built thus far. We want an
interface similar to the way we build entities with Specs : a builder, onto which we can
keep chaining builders and return it as an "executor" - ready to build the maps. We
also want to stop builders from doing more than one thing - they should do one thing,
and do it well (that's a good principle of design; it makes debugging easier, and
reduces duplication).

There are two major types of builders: those that just make a map (and only make
sense to run once), and those that modify an existing map. We'll name those
InitialMapBuilder and MetaMapBuilder respectively.

This gives us an idea of the syntax we want to employ:

Our Builder should have:


ONE Initial Builder.
n Meta Builders, that run in order.

It makes sense then that the builder should have a start_with method that accepts
the �rst map, and additional with methods to chain builders. The builders should be
stored in a container that preserves the order in which they were added - a vector
being the obvious choice.

567 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

It would also make sense to no longer make individual builders responsible for setting
up their predecessors; ideally, a builder shouldn't have to know anything about the
process beyond what it does. So we need to abstract the process, and support
snapshotting (so you can view your procedural generation progress) along the way.

Shared map state - the BuilderMap


Rather than each builder de�ning their own copies of shared data, it would make
sense to put the shared data in one place - and pass it around the chain as needed. So
we'll start by de�ning some new structures and interfaces. First of all, we'll make
BuilderMap in map_builders/mod.rs :

pub struct BuilderMap {


pub spawn_list : Vec<(usize, String)>,
pub map : Map,
pub starting_position : Option<Position>,
pub rooms: Option<Vec<Rect>>,
pub history : Vec<Map>
}

You'll notice that this has all of the data we've been building into each map builder -
and nothing else. It's intentionally generic - we'll be passing it to builders, and letting
them work on it. Notice that all the �elds are public - that's because we're passing it
around, and there's a good chance that anything that touches it will need to access
any/all of its contents.

The BuilderMap also needs to facilitate the task of taking snapshots for debugger
viewing of maps as we work on algorithms. We're going to put one function into
BuilderMap - to handle snapshotting development:

568 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl BuilderMap {
fn take_snapshot(&mut self) {
if SHOW_MAPGEN_VISUALIZER {
let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() {
*v = true;
}
self.history.push(snapshot);
}
}
}

This is the same as the take_snapshot code we've been mixing into our builders.
Since we're using a central repository of map building knowledge, we can promote it
to apply to all our builders.

The BuilderChain - master builder to manage map


creation
Previously, we've passed MapBuilder classes around, each capable of building
previous maps. Since we've concluded that this is a poor idea, and de�ned the syntax
we want, we'll make a replacement. The BuilderChain is a master builder - it controls
the whole build process. To this end, we'll add the BuilderChain type:

pub struct BuilderChain {


starter: Option<Box<dyn InitialMapBuilder>>,
builders: Vec<Box<dyn MetaMapBuilder>>,
pub build_data : BuilderMap
}

This is a more complicated structure, so let's go through it:

starter is an Option , so we know if there is one. Not having a �rst step (a map
that doesn't refer to other maps) would be an error condition, so we'll track it.
We're referencing a new trait, InitialMapBuilder ; we'll get to that in a moment.
builders is a vector of MetaMapBuilders , another new trait (and again - we'll
get to it in a moment). These are builders that operate on the results of previous

569 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

maps.
build_data is a public variable (anyone can read/write it), containing the
BuilderMap we just made.

We'll implement some functions to support it. First up, a constructor:

impl BuilderChain {
pub fn new(new_depth : i32) -> BuilderChain {
BuilderChain{
starter: None,
builders: Vec::new(),
build_data : BuilderMap {
spawn_list: Vec::new(),
map: Map::new(new_depth),
starting_position: None,
rooms: None,
history : Vec::new()
}
}
}
...

This is pretty simple: it makes a new BuilderChain with default values for everything.
Now, lets permit our users to add a starting map to the chain. (A starting map is a �rst
step that doesn't require a previous map as input, and results in a usable map
structure which we may then modify):

...
pub fn start_with(&mut self, starter : Box<dyn InitialMapBuilder>) {
match self.starter {
None => self.starter = Some(starter),
Some(_) => panic!("You can only have one starting builder.")
};
}
...

There's one new concept in here: panic! . If the user tries to add a second starting
builder, we'll crash - because that doesn't make any sense. You'd simply be
overwriting your previous steps, which is a giant waste of time! We'll also permit the
user to add meta-builders:

570 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
pub fn with(&mut self, metabuilder : Box<dyn MetaMapBuilder>) {
self.builders.push(metabuilder);
}
...

This is very simple: we simply add the meta-builder to the builder vector. Since vectors
remain in the order in which you add to them, your operations will remain sorted
appropriately. Finally, we'll implement a function to actually construct the map:

pub fn build_map(&mut self, rng : &mut rltk::RandomNumberGenerator) {


match &mut self.starter {
None => panic!("Cannot run a map builder chain without a starting
build system"),
Some(starter) => {
// Build the starting map
starter.build_map(rng, &mut self.build_data);
}
}

// Build additional layers in turn


for metabuilder in self.builders.iter_mut() {
metabuilder.build_map(rng, &mut self.build_data);
}
}

Let's walk through the steps here:

1. We match on our starting map. If there isn't one, we panic - and crash the
program with a message that you have to set a starting builder.
2. We call build_map on the starting map.
3. For each meta-builder, we call build_map on it - in the order speci�ed.

That's not a bad syntax! It should enable us to chain builders together, and provide
the required overview for constructing complicated, layered maps.

New Traits - InitialMapBuilder and MetaMapBuilder

571 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Lets look at the two trait interfaces we've de�ned, InitialMapBuilder and
MetaMapBuilder . We made them separate types to force the user to only pick one
starting builder, and not try to put any starting builders in the list of modi�cation
layers. The implementation for them is the same:

pub trait InitialMapBuilder {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap);
}

pub trait MetaMapBuilder {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap);
}

build_map takes a random-number generator (so we stop creating new ones


everywhere!), and a mutable reference to the BuilderMap we are working on. So
instead of each builder optionally calling the previous one, we're passing along state
as we work on it.

Spawn Function
We'll also want to implement our spawning system:

pub fn spawn_entities(&mut self, ecs : &mut World) {


for entity in self.build_data.spawn_list.iter() {
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
}
}

This is almost exactly the same code as our previous spawner in MapBuilder , but
instead we're spawning from the spawn_list in our build_data structure.
Otherwise, it's identical.

Random Builder - Take 1

572 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Finally, we'll modify random_builder to use our SimpleMapBuilder with some new
types to break out the creation steps:

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(SimpleMapBuilder::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStartingPosition::new());
builder.with(RoomBasedStairs::new());
builder
}

Notice that we're now taking a RandomNumberGenerator parameter. That's because


we'd like to use the global RNG, rather than keep making new ones. This way, if the
caller sets a "seed" - it will apply to world generation. This is intended to be the topic
of a future chapter. We're also now returning a BuilderChain rather than a boxed
trait - we're hiding the messy boxing/dynamic dispatch inside the implementation, so
the caller doesn't have to worry about it. There's also two new types here:
RoomBasedSpawner and RoomBasedStartingPosition - as well as a changed
constructor for SimpleMapBuilder (it no longer accepts a depth parameter). We'll be
looking at that in a second - but �rst, lets deal with the changes to the main program
resulting from the new interface.

Nice looking interface - but you broke stu�!


We now have the interface we want - a good map of how the system interacts with the
world. Unfortunately, the world is still expecting the setup we had before - so we need
to �x it. In main.rs , we need to update our generate_world_map function to use the
new interface:

573 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn generate_world_map(&mut self, new_depth : i32) {


self.mapgen_index = 0;
self.mapgen_timer = 0.0;
self.mapgen_history.clear();
let mut rng = self.ecs.write_resource::
<rltk::RandomNumberGenerator>();
let mut builder = map_builders::random_builder(new_depth, &mut rng);
builder.build_map(&mut rng);
std::mem::drop(rng);
self.mapgen_history = builder.build_data.history.clone();
let player_start;
{
let mut worldmap_resource = self.ecs.write_resource::<Map>();
*worldmap_resource = builder.build_data.map.clone();
player_start =
builder.build_data.starting_position.as_mut().unwrap().clone();
}

// Spawn bad guys


builder.spawn_entities(&mut self.ecs);

1. We reset mapgen_index , mapgen_timer and the mapgen_history so that the


progress viewer will run from the beginning.
2. We obtain the RNG from the ECS World .
3. We create a new random_builder with the new interface, passing along the
random number generator.
4. We tell it to build the new maps from the chain, also utilizing the RNG.
5. We call std::mem::drop on the RNG. This stops the "borrow" on it - so we're no
longer borrowing self either. This prevents borrow-checker errors on the next
phases of code.
6. We clone the map builder history into our own copy of the world's history. We
copy it so we aren't destroying the builder, yet.
7. We set player_start to a clone of the builder's determined starting position.
Note that we are calling unwrap - so the Option for a starting position must
have a value at this point, or we'll crash. That's deliberate: we'd rather crash
knowing that we forgot to set a starting point than have the program run in an
unknown/confusing state.
8. We call spawn_entities to populate the map.

574 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Modifying SimpleMapBuilder
We can simplify SimpleMapBuilder (making it worthy of the name!) quite a bit. Here's
the new code:

575 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, Rect, apply_room_to_map,


apply_horizontal_tunnel, apply_vertical_tunnel };
use rltk::RandomNumberGenerator;

pub struct SimpleMapBuilder {}

impl InitialMapBuilder for SimpleMapBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.rooms_and_corridors(rng, build_data);
}
}

impl SimpleMapBuilder {
#[allow(dead_code)]
pub fn new() -> Box<SimpleMapBuilder> {
Box::new(SimpleMapBuilder{})
}

fn rooms_and_corridors(&mut self, rng : &mut RandomNumberGenerator,


build_data : &mut BuilderMap) {
const MAX_ROOMS : i32 = 30;
const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;
let mut rooms : Vec<Rect> = Vec::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, build_data.map.width - w - 1) - 1;
let y = rng.roll_dice(1, build_data.map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
apply_room_to_map(&mut build_data.map, &new_room);
build_data.take_snapshot();

if !rooms.is_empty() {
let (new_x, new_y) = new_room.center();
let (prev_x, prev_y) = rooms[rooms.len()-1].center();
if rng.range(0,1) == 1 {

576 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

apply_horizontal_tunnel(&mut build_data.map,
prev_x, new_x, prev_y);
apply_vertical_tunnel(&mut build_data.map, prev_y,
new_y, new_x);
} else {
apply_vertical_tunnel(&mut build_data.map, prev_y,
new_y, prev_x);
apply_horizontal_tunnel(&mut build_data.map,
prev_x, new_x, new_y);
}
}

rooms.push(new_room);
build_data.take_snapshot();
}
}
build_data.rooms = Some(rooms);
}
}

This is basically the same as the old SimpleMapBuilder , but there's a number of
changes:

Notice that we're only applying the InitialMapBuilder trait - MapBuilder is no


more.
We're also not setting a starting position, or spawning entities - those are now
the purview of other builders in the chain. We've basically distilled it down to just
the room building algorithm.
We set build_data.rooms to Some(rooms) . Not all algorithms support rooms -
so our trait leaves the Option set to None until we �ll it. Since the
SimpleMapBuilder is all about rooms - we �ll it in.

Room-based spawning
Create a new �le, room_based_spawner.rs in the map_builders directory. We're
going to apply just the room populating system from the old SimpleMapBuilder here:

577 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, spawner};


use rltk::RandomNumberGenerator;

pub struct RoomBasedSpawner {}

impl MetaMapBuilder for RoomBasedSpawner {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl RoomBasedSpawner {
#[allow(dead_code)]
pub fn new() -> Box<RoomBasedSpawner> {
Box::new(RoomBasedSpawner{})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
if let Some(rooms) = &build_data.rooms {
for room in rooms.iter().skip(1) {
spawner::spawn_room(&build_data.map, rng, room,
build_data.map.depth, &mut build_data.spawn_list);
}
} else {
panic!("Room Based Spawning only works after rooms have been
created");
}
}
}

In this sub-module, we're implementing MetaMapBuilder : this builder requires that


you already have a map. In build , we've copied the old room-based spawning code
from SimpleMapBuilder , and modi�ed it to operate on the builder's rooms structure.
To that end, if we if let to obtain the inner value of the Option ; if there isn't one,
then we panic! and the program quits stating that room-based spawning is only
going to work if you have rooms.

We've reduced the functionality to just one task: if there are rooms, we spawn
monsters in them.

578 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Room-based starting position


This is very similar to room-based spawning, but places the player in the �rst room -
just like it used to in SimpleMapBuilder . Create a new �le,
room_based_starting_position in map_builders :

use super::{MetaMapBuilder, BuilderMap, Position};


use rltk::RandomNumberGenerator;

pub struct RoomBasedStartingPosition {}

impl MetaMapBuilder for RoomBasedStartingPosition {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl RoomBasedStartingPosition {
#[allow(dead_code)]
pub fn new() -> Box<RoomBasedStartingPosition> {
Box::new(RoomBasedStartingPosition{})
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
if let Some(rooms) = &build_data.rooms {
let start_pos = rooms[0].center();
build_data.starting_position = Some(Position{ x: start_pos.0,
y: start_pos.1 });
} else {
panic!("Room Based Staring Position only works after rooms
have been created");
}
}
}

Room-based stairs
This is also very similar to how we generated exit stairs in SimpleMapBuilder . Make a

579 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

new �le, room_based_stairs.rs :

use super::{MetaMapBuilder, BuilderMap, TileType};


use rltk::RandomNumberGenerator;

pub struct RoomBasedStairs {}

impl MetaMapBuilder for RoomBasedStairs {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl RoomBasedStairs {
#[allow(dead_code)]
pub fn new() -> Box<RoomBasedStairs> {
Box::new(RoomBasedStairs{})
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
if let Some(rooms) = &build_data.rooms {
let stairs_position = rooms[rooms.len()-1].center();
let stairs_idx = build_data.map.xy_idx(stairs_position.0,
stairs_position.1);
build_data.map.tiles[stairs_idx] = TileType::DownStairs;
build_data.take_snapshot();
} else {
panic!("Room Based Stairs only works after rooms have been
created");
}
}
}

Putting it together to make a simple map with the


new framework
Let's take another look at random_builder :

580 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStartingPosition::new());
builder.with(RoomBasedStairs::new());
builder

Now that we've made all of the steps, this should make sense:

1. We start with a map generated with the SimpleMapBuilder generator.


2. We modify the map with the meta-builder RoomBasedSpawner to spawn entities in
rooms.
3. We again modify the map with the meta-builder RoomBasedStartingPosition to
start in the �rst room.
4. Once again, we modify the map with the meta-builder RoomBasedStairs to place
a down staircase in the last room.

If you cargo run the project now, you'll let lots of warnings about unused code - but
the game should play with just the simple map from our �rst section. You may be
wondering why we've taken so much e�ort to keep things the same; hopefully, it will
become clear as we clean up more builders!

Cleaning up the BSP Dungeon Builder


Once again, we can seriously clean-up a map builder! Here's the new version of
bsp_dungeon.rs :

581 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, Map, Rect, apply_room_to_map,


TileType, draw_corridor};
use rltk::RandomNumberGenerator;

pub struct BspDungeonBuilder {


rects: Vec<Rect>,
}

impl InitialMapBuilder for BspDungeonBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl BspDungeonBuilder {
#[allow(dead_code)]
pub fn new() -> Box<BspDungeonBuilder> {
Box::new(BspDungeonBuilder{
rects: Vec::new(),
})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let mut rooms : Vec<Rect> = Vec::new();
self.rects.clear();
self.rects.push( Rect::new(2, 2, build_data.map.width-5,
build_data.map.height-5) ); // Start with a single map-sized rectangle
let first_room = self.rects[0];
self.add_subrects(first_room); // Divide the first room

// Up to 240 times, we get a random rectangle and divide it. If


its possible to squeeze a
// room in there, we place it and add it to the rooms list.
let mut n_rooms = 0;
while n_rooms < 240 {
let rect = self.get_random_rect(rng);
let candidate = self.get_random_sub_rect(rect, rng);

if self.is_possible(candidate, &build_data.map) {
apply_room_to_map(&mut build_data.map, &candidate);
rooms.push(candidate);
self.add_subrects(rect);

582 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

build_data.take_snapshot();
}

n_rooms += 1;
}

// Now we sort the rooms


rooms.sort_by(|a,b| a.x1.cmp(&b.x1) );

// Now we want corridors


for i in 0..rooms.len()-1 {
let room = rooms[i];
let next_room = rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 -
room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 -
room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1,
i32::abs(next_room.x1 - next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1,
i32::abs(next_room.y1 - next_room.y2))-1);
draw_corridor(&mut build_data.map, start_x, start_y, end_x,
end_y);
build_data.take_snapshot();
}
build_data.rooms = Some(rooms);
}

fn add_subrects(&mut self, rect : Rect) {


let width = i32::abs(rect.x1 - rect.x2);
let height = i32::abs(rect.y1 - rect.y2);
let half_width = i32::max(width / 2, 1);
let half_height = i32::max(height / 2, 1);

self.rects.push(Rect::new( rect.x1, rect.y1, half_width,


half_height ));
self.rects.push(Rect::new( rect.x1, rect.y1 + half_height,
half_width, half_height ));
self.rects.push(Rect::new( rect.x1 + half_width, rect.y1,
half_width, half_height ));
self.rects.push(Rect::new( rect.x1 + half_width, rect.y1 +
half_height, half_width, half_height ));
}

fn get_random_rect(&mut self, rng : &mut RandomNumberGenerator) ->


Rect {

583 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if self.rects.len() == 1 { return self.rects[0]; }


let idx = (rng.roll_dice(1, self.rects.len() as i32)-1) as usize;
self.rects[idx]
}

fn get_random_sub_rect(&self, rect : Rect, rng : &mut


RandomNumberGenerator) -> Rect {
let mut result = rect;
let rect_width = i32::abs(rect.x1 - rect.x2);
let rect_height = i32::abs(rect.y1 - rect.y2);

let w = i32::max(3, rng.roll_dice(1, i32::min(rect_width, 10))-1)


+ 1;
let h = i32::max(3, rng.roll_dice(1, i32::min(rect_height, 10))-1)
+ 1;

result.x1 += rng.roll_dice(1, 6)-1;


result.y1 += rng.roll_dice(1, 6)-1;
result.x2 = result.x1 + w;
result.y2 = result.y1 + h;

result
}

fn is_possible(&self, rect : Rect, map : &Map) -> bool {


let mut expanded = rect;
expanded.x1 -= 2;
expanded.x2 += 2;
expanded.y1 -= 2;
expanded.y2 += 2;

let mut can_build = true;

for y in expanded.y1 ..= expanded.y2 {


for x in expanded.x1 ..= expanded.x2 {
if x > map.width-2 { can_build = false; }
if y > map.height-2 { can_build = false; }
if x < 1 { can_build = false; }
if y < 1 { can_build = false; }
if can_build {
let idx = map.xy_idx(x, y);
if map.tiles[idx] != TileType::Wall {
can_build = false;
}
}
}

584 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

can_build
}
}

Just like SimpleMapBuilder , we've stripped out all the non-room building code for a
much cleaner piece of code. We're referencing the build_data struct from the
builder, rather than making our own copies of everything - and the meat of the code is
largely the same.

Now you can modify random_builder to make this map type:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspDungeonBuilder::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStartingPosition::new());
builder.with(RoomBasedStairs::new());
builder

If you cargo run now, you'll get a dungeon based on the BspDungeonBuilder . See
how you are reusing the spawner, starting position and stairs code? That's de�nitely
an improvement over the older versions - if you change one, it can now help on
multiple builders!

Same again for BSP Interior


Yet again, we can greatly clean up a builder - this time the BspInteriorBuilder .
Here's the code for bsp_interior.rs :

585 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, Rect, TileType, draw_corridor};


use rltk::RandomNumberGenerator;

const MIN_ROOM_SIZE : i32 = 8;

pub struct BspInteriorBuilder {


rects: Vec<Rect>
}

impl InitialMapBuilder for BspInteriorBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl BspInteriorBuilder {
#[allow(dead_code)]
pub fn new() -> Box<BspInteriorBuilder> {
Box::new(BspInteriorBuilder{
rects: Vec::new()
})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let mut rooms : Vec<Rect> = Vec::new();
self.rects.clear();
self.rects.push( Rect::new(1, 1, build_data.map.width-2,
build_data.map.height-2) ); // Start with a single map-sized rectangle
let first_room = self.rects[0];
self.add_subrects(first_room, rng); // Divide the first room

let rooms_copy = self.rects.clone();


for r in rooms_copy.iter() {
let room = *r;
//room.x2 -= 1;
//room.y2 -= 1;
rooms.push(room);
for y in room.y1 .. room.y2 {
for x in room.x1 .. room.x2 {
let idx = build_data.map.xy_idx(x, y);
if idx > 0 && idx < ((build_data.map.width *
build_data.map.height)-1) as usize {

586 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

build_data.map.tiles[idx] = TileType::Floor;
}
}
}
build_data.take_snapshot();
}

// Now we want corridors


for i in 0..rooms.len()-1 {
let room = rooms[i];
let next_room = rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 -
room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 -
room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1,
i32::abs(next_room.x1 - next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1,
i32::abs(next_room.y1 - next_room.y2))-1);
draw_corridor(&mut build_data.map, start_x, start_y, end_x,
end_y);
build_data.take_snapshot();
}

build_data.rooms = Some(rooms);
}

fn add_subrects(&mut self, rect : Rect, rng : &mut


RandomNumberGenerator) {
// Remove the last rect from the list
if !self.rects.is_empty() {
self.rects.remove(self.rects.len() - 1);
}

// Calculate boundaries
let width = rect.x2 - rect.x1;
let height = rect.y2 - rect.y1;
let half_width = width / 2;
let half_height = height / 2;

let split = rng.roll_dice(1, 4);

if split <= 2 {
// Horizontal split
let h1 = Rect::new( rect.x1, rect.y1, half_width-1, height );
self.rects.push( h1 );

587 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if half_width > MIN_ROOM_SIZE { self.add_subrects(h1, rng); }


let h2 = Rect::new( rect.x1 + half_width, rect.y1, half_width,
height );
self.rects.push( h2 );
if half_width > MIN_ROOM_SIZE { self.add_subrects(h2, rng); }
} else {
// Vertical split
let v1 = Rect::new( rect.x1, rect.y1, width, half_height-1 );
self.rects.push(v1);
if half_height > MIN_ROOM_SIZE { self.add_subrects(v1, rng); }
let v2 = Rect::new( rect.x1, rect.y1 + half_height, width,
half_height );
self.rects.push(v2);
if half_height > MIN_ROOM_SIZE { self.add_subrects(v2, rng); }
}
}
}

You may test it by modifying random_builder :

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspInteriorBuilder::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStartingPosition::new());
builder.with(RoomBasedStairs::new());
builder

cargo run will now take you around an interior builder.

Cellular Automata
You should understand the basic idea here, now - we're breaking up builders into
small chunks, and implementing the appropriate traits for the map type. Looking at
Cellular Automata maps, you'll see that we do things a little di�erently:

We make a map as usual. This obviously belongs in CellularAutomotaBuilder .


We search for a starting point close to the middle. This looks like it should be a
separate step.
We search the map for unreachable areas and cull them. This also looks like a
separate step.

588 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We place the exit far from the starting position. That's also a di�erent algorithm
step.

The good news is that the last three of those are used in lots of other builders - so
implementing them will let us reuse the code, and not keep repeating ourselves. The
bad news is that if we run our cellular automata builder with the existing room-based
steps, it will crash - we don't have rooms!

So we'll start by constructing the basic map builder. Like the others, this is mostly just
rearranging code to �t with the new trait scheme. Here's the new
cellular_automota.rs �le:

589 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, TileType};


use rltk::RandomNumberGenerator;

pub struct CellularAutomotaBuilder {}

impl InitialMapBuilder for CellularAutomotaBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl CellularAutomotaBuilder {
#[allow(dead_code)]
pub fn new() -> Box<CellularAutomotaBuilder> {
Box::new(CellularAutomotaBuilder{})
}

#[allow(clippy::map_entry)]
fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :
&mut BuilderMap) {
// First we completely randomize the map, setting 55% of it to be
floor.
for y in 1..build_data.map.height-1 {
for x in 1..build_data.map.width-1 {
let roll = rng.roll_dice(1, 100);
let idx = build_data.map.xy_idx(x, y);
if roll > 55 { build_data.map.tiles[idx] = TileType::Floor
}
else { build_data.map.tiles[idx] = TileType::Wall }
}
}
build_data.take_snapshot();

// Now we iteratively apply cellular automota rules


for _i in 0..15 {
let mut newtiles = build_data.map.tiles.clone();

for y in 1..build_data.map.height-1 {
for x in 1..build_data.map.width-1 {
let idx = build_data.map.xy_idx(x, y);
let mut neighbors = 0;
if build_data.map.tiles[idx - 1] == TileType::Wall {
neighbors += 1; }

590 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if build_data.map.tiles[idx + 1] == TileType::Wall {
neighbors += 1; }
if build_data.map.tiles[idx - build_data.map.width as
usize] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx + build_data.map.width as
usize] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx - (build_data.map.width as
usize - 1)] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx - (build_data.map.width as
usize + 1)] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx + (build_data.map.width as
usize - 1)] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx + (build_data.map.width as
usize + 1)] == TileType::Wall { neighbors += 1; }

if neighbors > 4 || neighbors == 0 {


newtiles[idx] = TileType::Wall;
}
else {
newtiles[idx] = TileType::Floor;
}
}
}

build_data.map.tiles = newtiles.clone();
build_data.take_snapshot();
}
}
}

Non-Room Starting Points

It's entirely possible that we don't actually want to start in the middle of the map.
Doing so presents lots of opportunities (and helps ensure connectivity), but maybe
you would rather the player trudge through lots of map with less opportunity to pick
the wrong direction. Maybe your story makes more sense if the player arrives at one
end of the map and leaves via another. Lets implement a starting position system that
takes a preferred starting point, and picks the closest valid tile. Create
area_starting_points.rs :

591 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, Position, TileType};


use rltk::RandomNumberGenerator;

#[allow(dead_code)]
pub enum XStart { LEFT, CENTER, RIGHT }

#[allow(dead_code)]
pub enum YStart { TOP, CENTER, BOTTOM }

pub struct AreaStartingPosition {


x : XStart,
y : YStart
}

impl MetaMapBuilder for AreaStartingPosition {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl AreaStartingPosition {
#[allow(dead_code)]
pub fn new(x : XStart, y : YStart) -> Box<AreaStartingPosition> {
Box::new(AreaStartingPosition{
x, y
})
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let seed_x;
let seed_y;

match self.x {
XStart::LEFT => seed_x = 1,
XStart::CENTER => seed_x = build_data.map.width / 2,
XStart::RIGHT => seed_x = build_data.map.width - 2
}

match self.y {
YStart::TOP => seed_y = 1,
YStart::CENTER => seed_y = build_data.map.height / 2,
YStart::BOTTOM => seed_y = build_data.map.height - 2
}

592 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut available_floors : Vec<(usize, f32)> = Vec::new();


for (idx, tiletype) in build_data.map.tiles.iter().enumerate() {
if *tiletype == TileType::Floor {
available_floors.push(
(
idx,
rltk::DistanceAlg::PythagorasSquared.distance2d(
rltk::Point::new(idx as i32 %
build_data.map.width, idx as i32 / build_data.map.width),
rltk::Point::new(seed_x, seed_y)
)
)
);
}
}
if available_floors.is_empty() {
panic!("No valid floors to start on");
}

available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());

let start_x = available_floors[0].0 as i32 % build_data.map.width;


let start_y = available_floors[0].0 as i32 / build_data.map.width;

build_data.starting_position = Some(Position{x : start_x, y:


start_y});
}
}

We've covered the boilerplate enough to not need to go over it again - so lets step
through the build function:

1. We are taking in a couple of enum types: preferred position on the X and Y axes.
2. So we set seed_x and seed_y to a point closest to the speci�ed locations.
3. We iterate through the whole map, adding �oor tiles to available_floors - and
calculating the distance to the preferred starting point.
4. We sort the available tile list, so the lower distances are �rst.
5. We pick the �rst one on the list.

Note that we also panic! if there are no �oors at all.

The great part here is that this will work for any map type - it searches for �oors to
stand on, and tries to �nd the closest starting point.

593 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Culling Unreachable Areas

We've previously had good luck with culling areas that can't be reached from the
starting point. So lets formalize that into its own meta-builder. Create
cull_unreachable.rs :

594 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, TileType};


use rltk::RandomNumberGenerator;

pub struct CullUnreachable {}

impl MetaMapBuilder for CullUnreachable {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl CullUnreachable {
#[allow(dead_code)]
pub fn new() -> Box<CullUnreachable> {
Box::new(CullUnreachable{})
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let starting_pos =
build_data.starting_position.as_ref().unwrap().clone();
let start_idx = build_data.map.xy_idx(
starting_pos.x,
starting_pos.y
);
build_data.map.populate_blocked();
let map_starts : Vec<i32> = vec![start_idx as i32];
let dijkstra_map = rltk::DijkstraMap::new(build_data.map.width,
build_data.map.height, &map_starts , &build_data.map, 1000.0);
for (i, tile) in build_data.map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor {
let distance_to_start = dijkstra_map.map[i];
// We can't get to this tile - so we'll make it a wall
if distance_to_start == std::f32::MAX {
*tile = TileType::Wall;
}
}
}
}
}

You'll notice this is almost exactly the same as


remove_unreachable_areas_returning_most_distant from common.rs , but without

595 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

returning a Dijkstra map. That's the intent: we remove areas the player can't get to,
and only do that.

Voronoi-based spawning

We also need to replicate the functionality of Voronoi-based spawning. Create


voronoi_spawning.rs :

596 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, TileType, spawner};


use rltk::RandomNumberGenerator;
use std::collections::HashMap;

pub struct VoronoiSpawning {}

impl MetaMapBuilder for VoronoiSpawning {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl VoronoiSpawning {
#[allow(dead_code)]
pub fn new() -> Box<VoronoiSpawning> {
Box::new(VoronoiSpawning{})
}

#[allow(clippy::map_entry)]
fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :
&mut BuilderMap) {
let mut noise_areas : HashMap<i32, Vec<usize>> = HashMap::new();
let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as
u64);
noise.set_noise_type(rltk::NoiseType::Cellular);
noise.set_frequency(0.08);

noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan);

for y in 1 .. build_data.map.height-1 {
for x in 1 .. build_data.map.width-1 {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::Floor {
let cell_value_f = noise.get_noise(x as f32, y as f32)
* 10240.0;
let cell_value = cell_value_f as i32;

if noise_areas.contains_key(&cell_value) {

noise_areas.get_mut(&cell_value).unwrap().push(idx);
} else {
noise_areas.insert(cell_value, vec![idx]);
}
}

597 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}

// Spawn the entities


for area in noise_areas.iter() {
spawner::spawn_region(&build_data.map, rng, area.1,
build_data.map.depth, &mut build_data.spawn_list);
}
}
}

This is almost exactly the same as the code from common.rs we were calling in
various builders, just modi�ed to work within the builder chaining/builder map
framework.

Spawning a distant exit

Another commonly used piece of code generated a Dijkstra map of the level, starting
at the player's entry point - and used that map to place the exit at the most distant
location from the player. This was in common.rs , and we called it a lot. We'll turn this
into a map building step; create map_builders/distant_exit.rs :

598 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, TileType};


use rltk::RandomNumberGenerator;

pub struct DistantExit {}

impl MetaMapBuilder for DistantExit {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl DistantExit {
#[allow(dead_code)]
pub fn new() -> Box<DistantExit> {
Box::new(DistantExit{})
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let starting_pos =
build_data.starting_position.as_ref().unwrap().clone();
let start_idx = build_data.map.xy_idx(
starting_pos.x,
starting_pos.y
);
build_data.map.populate_blocked();
let map_starts : Vec<i32> = vec![start_idx as i32];
let dijkstra_map = rltk::DijkstraMap::new(build_data.map.width,
build_data.map.height, &map_starts , &build_data.map, 1000.0);
let mut exit_tile = (0, 0.0f32);
for (i, tile) in build_data.map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor {
let distance_to_start = dijkstra_map.map[i];
if distance_to_start != std::f32::MAX {
// If it is further away than our current exit
candidate, move the exit
if distance_to_start > exit_tile.1 {
exit_tile.0 = i;
exit_tile.1 = distance_to_start;
}
}
}
}

599 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Place a staircase
let stairs_idx = exit_tile.0;
build_data.map.tiles[stairs_idx] = TileType::DownStairs;
build_data.take_snapshot();
}
}

Again, this is the same code we've used previously - just tweaked to match the new
interface, so we won't go over it in detail.

Testing Cellular Automata

We've �nally got all the pieces together, so lets give it a test. In random_builder , we'll
use the new builder chains:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(CellularAutomotaBuilder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

If you cargo run now, you'll get to play in a Cellular Automata generated map.

Updating Drunkard's Walk


You should have a pretty good picture of what we're doing now, so we'll gloss over the
changes to drunkard.rs :

600 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, TileType, Position, paint,


Symmetry};
use rltk::RandomNumberGenerator;

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum DrunkSpawnMode { StartingPoint, Random }

pub struct DrunkardSettings {


pub spawn_mode : DrunkSpawnMode,
pub drunken_lifetime : i32,
pub floor_percent: f32,
pub brush_size: i32,
pub symmetry: Symmetry
}

pub struct DrunkardsWalkBuilder {


settings : DrunkardSettings
}

impl InitialMapBuilder for DrunkardsWalkBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl DrunkardsWalkBuilder {
#[allow(dead_code)]
pub fn new(settings: DrunkardSettings) -> DrunkardsWalkBuilder {
DrunkardsWalkBuilder{
settings
}
}

#[allow(dead_code)]
pub fn open_area() -> Box<DrunkardsWalkBuilder> {
Box::new(DrunkardsWalkBuilder{
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::StartingPoint,
drunken_lifetime: 400,
floor_percent: 0.5,
brush_size: 1,
symmetry: Symmetry::None

601 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
})
}

#[allow(dead_code)]
pub fn open_halls() -> Box<DrunkardsWalkBuilder> {
Box::new(DrunkardsWalkBuilder{
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 400,
floor_percent: 0.5,
brush_size: 1,
symmetry: Symmetry::None
},
})
}

#[allow(dead_code)]
pub fn winding_passages() -> Box<DrunkardsWalkBuilder> {
Box::new(DrunkardsWalkBuilder{
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100,
floor_percent: 0.4,
brush_size: 1,
symmetry: Symmetry::None
},
})
}

#[allow(dead_code)]
pub fn fat_passages() -> Box<DrunkardsWalkBuilder> {
Box::new(DrunkardsWalkBuilder{
settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100,
floor_percent: 0.4,
brush_size: 2,
symmetry: Symmetry::None
},
})
}

#[allow(dead_code)]
pub fn fearful_symmetry() -> Box<DrunkardsWalkBuilder> {
Box::new(DrunkardsWalkBuilder{

602 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

settings : DrunkardSettings{
spawn_mode: DrunkSpawnMode::Random,
drunken_lifetime: 100,
floor_percent: 0.4,
brush_size: 1,
symmetry: Symmetry::Both
},
})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
// Set a central starting point
let starting_position = Position{ x: build_data.map.width / 2, y:
build_data.map.height / 2 };
let start_idx = build_data.map.xy_idx(starting_position.x,
starting_position.y);
build_data.map.tiles[start_idx] = TileType::Floor;

let total_tiles = build_data.map.width * build_data.map.height;


let desired_floor_tiles = (self.settings.floor_percent *
total_tiles as f32) as usize;
let mut floor_tile_count = build_data.map.tiles.iter().filter(|a|
**a == TileType::Floor).count();
let mut digger_count = 0;
while floor_tile_count < desired_floor_tiles {
let mut did_something = false;
let mut drunk_x;
let mut drunk_y;
match self.settings.spawn_mode {
DrunkSpawnMode::StartingPoint => {
drunk_x = starting_position.x;
drunk_y = starting_position.y;
}
DrunkSpawnMode::Random => {
if digger_count == 0 {
drunk_x = starting_position.x;
drunk_y = starting_position.y;
} else {
drunk_x = rng.roll_dice(1, build_data.map.width -
3) + 1;
drunk_y = rng.roll_dice(1, build_data.map.height -
3) + 1;
}
}
}

603 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut drunk_life = self.settings.drunken_lifetime;

while drunk_life > 0 {


let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y);
if build_data.map.tiles[drunk_idx] == TileType::Wall {
did_something = true;
}
paint(&mut build_data.map, self.settings.symmetry,
self.settings.brush_size, drunk_x, drunk_y);
build_data.map.tiles[drunk_idx] = TileType::DownStairs;

let stagger_direction = rng.roll_dice(1, 4);


match stagger_direction {
1 => { if drunk_x > 2 { drunk_x -= 1; } }
2 => { if drunk_x < build_data.map.width-2 { drunk_x
+= 1; } }
3 => { if drunk_y > 2 { drunk_y -=1; } }
_ => { if drunk_y < build_data.map.height-2 { drunk_y
+= 1; } }
}

drunk_life -= 1;
}
if did_something {
build_data.take_snapshot();
}

digger_count += 1;
for t in build_data.map.tiles.iter_mut() {
if *t == TileType::DownStairs {
*t = TileType::Floor;
}
}
floor_tile_count = build_data.map.tiles.iter().filter(|a| **a
== TileType::Floor).count();
}
}
}

Once again, you can test it by adjusting random_builder to:

604 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(DrunkardsWalkBuilder::fearful_symmetry());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

You can cargo run and see it in action.

Update Di�usion-Limited Aggregation


This is more of the same, so we'll again just provide the code for dla.rs :

605 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, TileType, Position, Symmetry,


paint};
use rltk::RandomNumberGenerator;

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum DLAAlgorithm { WalkInwards, WalkOutwards, CentralAttractor }

pub struct DLABuilder {


algorithm : DLAAlgorithm,
brush_size: i32,
symmetry: Symmetry,
floor_percent: f32,
}

impl InitialMapBuilder for DLABuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl DLABuilder {
#[allow(dead_code)]
pub fn new() -> Box<DLABuilder> {
Box::new(DLABuilder{
algorithm: DLAAlgorithm::WalkInwards,
brush_size: 2,
symmetry: Symmetry::None,
floor_percent: 0.25,
})
}

#[allow(dead_code)]
pub fn walk_inwards() -> Box<DLABuilder> {
Box::new(DLABuilder{
algorithm: DLAAlgorithm::WalkInwards,
brush_size: 1,
symmetry: Symmetry::None,
floor_percent: 0.25,
})
}

606 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
pub fn walk_outwards() -> Box<DLABuilder> {
Box::new(DLABuilder{
algorithm: DLAAlgorithm::WalkOutwards,
brush_size: 2,
symmetry: Symmetry::None,
floor_percent: 0.25,
})
}

#[allow(dead_code)]
pub fn central_attractor() -> Box<DLABuilder> {
Box::new(DLABuilder{
algorithm: DLAAlgorithm::CentralAttractor,
brush_size: 2,
symmetry: Symmetry::None,
floor_percent: 0.25,
})
}

#[allow(dead_code)]
pub fn insectoid() -> Box<DLABuilder> {
Box::new(DLABuilder{
algorithm: DLAAlgorithm::CentralAttractor,
brush_size: 2,
symmetry: Symmetry::Horizontal,
floor_percent: 0.25,
})
}

#[allow(clippy::map_entry)]
fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :
&mut BuilderMap) {
// Carve a starting seed
let starting_position = Position{ x: build_data.map.width/2, y :
build_data.map.height/2 };
let start_idx = build_data.map.xy_idx(starting_position.x,
starting_position.y);
build_data.take_snapshot();
build_data.map.tiles[start_idx] = TileType::Floor;
build_data.map.tiles[start_idx-1] = TileType::Floor;
build_data.map.tiles[start_idx+1] = TileType::Floor;
build_data.map.tiles[start_idx-build_data.map.width as usize] =
TileType::Floor;
build_data.map.tiles[start_idx+build_data.map.width as usize] =
TileType::Floor;

607 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Random walker
let total_tiles = build_data.map.width * build_data.map.height;
let desired_floor_tiles = (self.floor_percent * total_tiles as
f32) as usize;
let mut floor_tile_count = build_data.map.tiles.iter().filter(|a|
**a == TileType::Floor).count();
while floor_tile_count < desired_floor_tiles {

match self.algorithm {
DLAAlgorithm::WalkInwards => {
let mut digger_x = rng.roll_dice(1,
build_data.map.width - 3) + 1;
let mut digger_y = rng.roll_dice(1,
build_data.map.height - 3) + 1;
let mut prev_x = digger_x;
let mut prev_y = digger_y;
let mut digger_idx = build_data.map.xy_idx(digger_x,
digger_y);
while build_data.map.tiles[digger_idx] ==
TileType::Wall {
prev_x = digger_x;
prev_y = digger_y;
let stagger_direction = rng.roll_dice(1, 4);
match stagger_direction {
1 => { if digger_x > 2 { digger_x -= 1; } }
2 => { if digger_x < build_data.map.width-2 {
digger_x += 1; } }
3 => { if digger_y > 2 { digger_y -=1; } }
_ => { if digger_y < build_data.map.height-2 {
digger_y += 1; } }
}
digger_idx = build_data.map.xy_idx(digger_x,
digger_y);
}
paint(&mut build_data.map, self.symmetry,
self.brush_size, prev_x, prev_y);
}

DLAAlgorithm::WalkOutwards => {
let mut digger_x = starting_position.x;
let mut digger_y = starting_position.y;
let mut digger_idx = build_data.map.xy_idx(digger_x,
digger_y);
while build_data.map.tiles[digger_idx] ==
TileType::Floor {

608 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let stagger_direction = rng.roll_dice(1, 4);


match stagger_direction {
1 => { if digger_x > 2 { digger_x -= 1; } }
2 => { if digger_x < build_data.map.width-2 {
digger_x += 1; } }
3 => { if digger_y > 2 { digger_y -=1; } }
_ => { if digger_y < build_data.map.height-2 {
digger_y += 1; } }
}
digger_idx = build_data.map.xy_idx(digger_x,
digger_y);
}
paint(&mut build_data.map, self.symmetry,
self.brush_size, digger_x, digger_y);
}

DLAAlgorithm::CentralAttractor => {
let mut digger_x = rng.roll_dice(1,
build_data.map.width - 3) + 1;
let mut digger_y = rng.roll_dice(1,
build_data.map.height - 3) + 1;
let mut prev_x = digger_x;
let mut prev_y = digger_y;
let mut digger_idx = build_data.map.xy_idx(digger_x,
digger_y);

let mut path = rltk::line2d(


rltk::LineAlg::Bresenham,
rltk::Point::new( digger_x, digger_y ),
rltk::Point::new( starting_position.x,
starting_position.y )
);

while build_data.map.tiles[digger_idx] ==
TileType::Wall && !path.is_empty() {
prev_x = digger_x;
prev_y = digger_y;
digger_x = path[0].x;
digger_y = path[0].y;
path.remove(0);
digger_idx = build_data.map.xy_idx(digger_x,
digger_y);
}
paint(&mut build_data.map, self.symmetry,
self.brush_size, prev_x, prev_y);
}

609 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

build_data.take_snapshot();

floor_tile_count = build_data.map.tiles.iter().filter(|a| **a


== TileType::Floor).count();
}
}
}

Updating the Maze Builder


Once again, here's the code for maze.rs :

610 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{Map, InitialMapBuilder, BuilderMap, TileType};


use rltk::RandomNumberGenerator;

pub struct MazeBuilder {}

impl InitialMapBuilder for MazeBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl MazeBuilder {
#[allow(dead_code)]
pub fn new() -> Box<MazeBuilder> {
Box::new(MazeBuilder{})
}

#[allow(clippy::map_entry)]
fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :
&mut BuilderMap) {
// Maze gen
let mut maze = Grid::new((build_data.map.width / 2)-2,
(build_data.map.height / 2)-2, rng);
maze.generate_maze(build_data);
}
}

/* Maze code taken under MIT from https://github.com/cyucelen


/mazeGenerator/ */

const TOP : usize = 0;


const RIGHT : usize = 1;
const BOTTOM : usize = 2;
const LEFT : usize = 3;

#[derive(Copy, Clone)]
struct Cell {
row: i32,
column: i32,
walls: [bool; 4],
visited: bool,
}

611 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl Cell {
fn new(row: i32, column: i32) -> Cell {
Cell{
row,
column,
walls: [true, true, true, true],
visited: false
}
}

unsafe fn remove_walls(&mut self, next : *mut Cell) {


let x = self.column - (*(next)).column;
let y = self.row - (*(next)).row;

if x == 1 {
self.walls[LEFT] = false;
(*(next)).walls[RIGHT] = false;
}
else if x == -1 {
self.walls[RIGHT] = false;
(*(next)).walls[LEFT] = false;
}
else if y == 1 {
self.walls[TOP] = false;
(*(next)).walls[BOTTOM] = false;
}
else if y == -1 {
self.walls[BOTTOM] = false;
(*(next)).walls[TOP] = false;
}
}
}

struct Grid<'a> {
width: i32,
height: i32,
cells: Vec<Cell>,
backtrace: Vec<usize>,
current: usize,
rng : &'a mut RandomNumberGenerator
}

impl<'a> Grid<'a> {
fn new(width: i32, height:i32, rng: &mut RandomNumberGenerator) ->
Grid {
let mut grid = Grid{

612 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

width,
height,
cells: Vec::new(),
backtrace: Vec::new(),
current: 0,
rng
};

for row in 0..height {


for column in 0..width {
grid.cells.push(Cell::new(row, column));
}
}

grid
}

fn calculate_index(&self, row: i32, column: i32) -> i32 {


if row < 0 || column < 0 || column > self.width-1 || row >
self.height-1 {
-1
} else {
column + (row * self.width)
}
}

fn get_available_neighbors(&self) -> Vec<usize> {


let mut neighbors : Vec<usize> = Vec::new();

let current_row = self.cells[self.current].row;


let current_column = self.cells[self.current].column;

let neighbor_indices : [i32; 4] = [


self.calculate_index(current_row -1, current_column),
self.calculate_index(current_row, current_column + 1),
self.calculate_index(current_row + 1, current_column),
self.calculate_index(current_row, current_column - 1)
];

for i in neighbor_indices.iter() {
if *i != -1 && !self.cells[*i as usize].visited {
neighbors.push(*i as usize);
}
}

neighbors

613 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn find_next_cell(&mut self) -> Option<usize> {


let neighbors = self.get_available_neighbors();
if !neighbors.is_empty() {
if neighbors.len() == 1 {
return Some(neighbors[0]);
} else {
return Some(neighbors[(self.rng.roll_dice(1,
neighbors.len() as i32)-1) as usize]);
}
}
None
}

fn generate_maze(&mut self, build_data : &mut BuilderMap) {


let mut i = 0;
loop {
self.cells[self.current].visited = true;
let next = self.find_next_cell();

match next {
Some(next) => {
self.cells[next].visited = true;
self.backtrace.insert(0, self.current);
unsafe {
let next_cell : *mut Cell = &mut self.cells[next];
let current_cell = &mut self.cells[self.current];
current_cell.remove_walls(next_cell);
}
self.current = next;
}
None => {
if !self.backtrace.is_empty() {
self.current = self.backtrace[0];
self.backtrace.remove(0);
} else {
break;
}
}
}

if i % 50 == 0 {
self.copy_to_map(&mut build_data.map);
build_data.take_snapshot();
}

614 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

i += 1;
}
}

fn copy_to_map(&self, map : &mut Map) {


// Clear the map
for i in map.tiles.iter_mut() { *i = TileType::Wall; }

for cell in self.cells.iter() {


let x = cell.column + 1;
let y = cell.row + 1;
let idx = map.xy_idx(x * 2, y * 2);

map.tiles[idx] = TileType::Floor;
if !cell.walls[TOP] { map.tiles[idx - map.width as usize] =
TileType::Floor }
if !cell.walls[RIGHT] { map.tiles[idx + 1] = TileType::Floor }
if !cell.walls[BOTTOM] { map.tiles[idx + map.width as usize] =
TileType::Floor }
if !cell.walls[LEFT] { map.tiles[idx - 1] = TileType::Floor }
}
}
}

Updating Voronoi Maps


Here's the updated code for the Voronoi builder (in voronoi.rs ):

615 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, TileType};


use rltk::RandomNumberGenerator;

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum DistanceAlgorithm { Pythagoras, Manhattan, Chebyshev }

pub struct VoronoiCellBuilder {


n_seeds: usize,
distance_algorithm: DistanceAlgorithm
}

impl InitialMapBuilder for VoronoiCellBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl VoronoiCellBuilder {
#[allow(dead_code)]
pub fn new() -> Box<VoronoiCellBuilder> {
Box::new(VoronoiCellBuilder{
n_seeds: 64,
distance_algorithm: DistanceAlgorithm::Pythagoras,
})
}

#[allow(dead_code)]
pub fn pythagoras() -> Box<VoronoiCellBuilder> {
Box::new(VoronoiCellBuilder{
n_seeds: 64,
distance_algorithm: DistanceAlgorithm::Pythagoras,
})
}

#[allow(dead_code)]
pub fn manhattan() -> Box<VoronoiCellBuilder> {
Box::new(VoronoiCellBuilder{
n_seeds: 64,
distance_algorithm: DistanceAlgorithm::Manhattan,
})
}

616 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(clippy::map_entry)]
fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :
&mut BuilderMap) {
// Make a Voronoi diagram. We'll do this the hard way to learn
about the technique!
let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new();

while voronoi_seeds.len() < self.n_seeds {


let vx = rng.roll_dice(1, build_data.map.width-1);
let vy = rng.roll_dice(1, build_data.map.height-1);
let vidx = build_data.map.xy_idx(vx, vy);
let candidate = (vidx, rltk::Point::new(vx, vy));
if !voronoi_seeds.contains(&candidate) {
voronoi_seeds.push(candidate);
}
}

let mut voroni_distance = vec![(0, 0.0f32) ; self.n_seeds];


let mut voronoi_membership : Vec<i32> = vec![0 ;
build_data.map.width as usize * build_data.map.height as usize];
for (i, vid) in voronoi_membership.iter_mut().enumerate() {
let x = i as i32 % build_data.map.width;
let y = i as i32 / build_data.map.width;

for (seed, pos) in voronoi_seeds.iter().enumerate() {


let distance;
match self.distance_algorithm {
DistanceAlgorithm::Pythagoras => {
distance =
rltk::DistanceAlg::PythagorasSquared.distance2d(
rltk::Point::new(x, y),
pos.1
);
}
DistanceAlgorithm::Manhattan => {
distance =
rltk::DistanceAlg::Manhattan.distance2d(
rltk::Point::new(x, y),
pos.1
);
}
DistanceAlgorithm::Chebyshev => {
distance =
rltk::DistanceAlg::Chebyshev.distance2d(
rltk::Point::new(x, y),

617 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pos.1
);
}
}
voroni_distance[seed] = (seed, distance);
}

voroni_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());

*vid = voroni_distance[0].0 as i32;


}

for y in 1..build_data.map.height-1 {
for x in 1..build_data.map.width-1 {
let mut neighbors = 0;
let my_idx = build_data.map.xy_idx(x, y);
let my_seed = voronoi_membership[my_idx];
if voronoi_membership[build_data.map.xy_idx(x-1, y)] !=
my_seed { neighbors += 1; }
if voronoi_membership[build_data.map.xy_idx(x+1, y)] !=
my_seed { neighbors += 1; }
if voronoi_membership[build_data.map.xy_idx(x, y-1)] !=
my_seed { neighbors += 1; }
if voronoi_membership[build_data.map.xy_idx(x, y+1)] !=
my_seed { neighbors += 1; }

if neighbors < 2 {
build_data.map.tiles[my_idx] = TileType::Floor;
}
}
build_data.take_snapshot();
}
}
}

Updating Waveform Collapse


Waveform Collapse is a slightly di�erent one to port, because it already had a concept
of a "previous builder". That's gone now (chaining is automatic), so there's a bit more
to update. Waveform Collapse is a meta-builder, so it implements that trait, rather
than the initial map builder. Overall, these changes make it a lot more simple! The
changes all take place in waveform_collapse/mod.rs :

618 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, Map, TileType};


use rltk::RandomNumberGenerator;
mod common;
use common::*;
mod constraints;
use constraints::*;
mod solver;
use solver::*;

/// Provides a map builder using the Waveform Collapse algorithm.


pub struct WaveformCollapseBuilder {}

impl MetaMapBuilder for WaveformCollapseBuilder {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl WaveformCollapseBuilder {
/// Generic constructor for waveform collapse.
/// # Arguments
/// * new_depth - the new map depth
/// * derive_from - either None, or a boxed MapBuilder, as output by
`random_builder`
#[allow(dead_code)]
pub fn new() -> Box<WaveformCollapseBuilder> {
Box::new(WaveformCollapseBuilder{})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
const CHUNK_SIZE :i32 = 8;
build_data.take_snapshot();

let patterns = build_patterns(&build_data.map, CHUNK_SIZE, true,


true);
let constraints = patterns_to_constaints(patterns, CHUNK_SIZE);
self.render_tile_gallery(&constraints, CHUNK_SIZE, build_data);

build_data.map = Map::new(build_data.map.depth);
loop {
let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE,
&build_data.map);
while !solver.iteration(&mut build_data.map, rng) {

619 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

build_data.take_snapshot();
}
build_data.take_snapshot();
if solver.possible { break; } // If it has hit an impossible
condition, try again
}
build_data.spawn_list.clear();
}

fn render_tile_gallery(&mut self, constraints: &[MapChunk],


chunk_size: i32, build_data : &mut BuilderMap) {
build_data.map = Map::new(0);
let mut counter = 0;
let mut x = 1;
let mut y = 1;
while counter < constraints.len() {
render_pattern_to_map(&mut build_data.map,
&constraints[counter], chunk_size, x, y);

x += chunk_size + 1;
if x + chunk_size > build_data.map.width {
// Move to the next row
x = 1;
y += chunk_size + 1;

if y + chunk_size > build_data.map.height {


// Move to the next page
build_data.take_snapshot();
build_data.map = Map::new(0);

x = 1;
y = 1;
}
}

counter += 1;
}
build_data.take_snapshot();
}
}

TODO: Text!

Test:

620 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(VoronoiCellBuilder::pythagoras());
builder.with(WaveformCollapseBuilder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

Updating the Prefab Builder


So here's a fun one. The PrefabBuilder is both an InitialMapBuilder and a
MetaMapBuilder - with shared code between the two. Fortunately, the traits are
identical - so we can implement them both and call into the main build function
from each! Rust is smart enough to �gure out which one we're calling based on the
trait we are storing - so PrefabBuilder can be placed in either an initial or a meta
map builder.

The changes all take place in prefab_builder/mod.rs :

621 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, MetaMapBuilder, BuilderMap, TileType,


Position};
use rltk::RandomNumberGenerator;
pub mod prefab_levels;
pub mod prefab_sections;
pub mod prefab_rooms;
use std::collections::HashSet;

#[derive(PartialEq, Copy, Clone)]


#[allow(dead_code)]
pub enum PrefabMode {
RexLevel{ template : &'static str },
Constant{ level : prefab_levels::PrefabLevel },
Sectional{ section : prefab_sections::PrefabSection },
RoomVaults
}

#[allow(dead_code)]
pub struct PrefabBuilder {
mode: PrefabMode
}

impl MetaMapBuilder for PrefabBuilder {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl InitialMapBuilder for PrefabBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl PrefabBuilder {
#[allow(dead_code)]
pub fn new() -> Box<PrefabBuilder> {
Box::new(PrefabBuilder{
mode : PrefabMode::RoomVaults,
})
}

622 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
pub fn rex_level(template : &'static str) -> Box<PrefabBuilder> {
Box::new(PrefabBuilder{
mode : PrefabMode::RexLevel{ template },
})
}

#[allow(dead_code)]
pub fn constant(level : prefab_levels::PrefabLevel) ->
Box<PrefabBuilder> {
Box::new(PrefabBuilder{
mode : PrefabMode::Constant{ level },
})
}

#[allow(dead_code)]
pub fn sectional(section : prefab_sections::PrefabSection) ->
Box<PrefabBuilder> {
Box::new(PrefabBuilder{
mode : PrefabMode::Sectional{ section },
})
}

#[allow(dead_code)]
pub fn vaults() -> Box<PrefabBuilder> {
Box::new(PrefabBuilder{
mode : PrefabMode::RoomVaults,
})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template,
build_data),
PrefabMode::Constant{level} => self.load_ascii_map(&level,
build_data),
PrefabMode::Sectional{section} =>
self.apply_sectional(&section, rng, build_data),
PrefabMode::RoomVaults => self.apply_room_vaults(rng,
build_data)
}
build_data.take_snapshot();
}

fn char_to_map(&mut self, ch : char, idx: usize, build_data : &mut

623 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

BuilderMap) {
match ch {
' ' => build_data.map.tiles[idx] = TileType::Floor,
'#' => build_data.map.tiles[idx] = TileType::Wall,
'@' => {
let x = idx as i32 % build_data.map.width;
let y = idx as i32 / build_data.map.width;
build_data.map.tiles[idx] = TileType::Floor;
build_data.starting_position = Some(Position{ x:x as i32,
y:y as i32 });
}
'>' => build_data.map.tiles[idx] = TileType::DownStairs,
'g' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, "Goblin".to_string()));
}
'o' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, "Orc".to_string()));
}
'^' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, "Bear
Trap".to_string()));
}
'%' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, "Rations".to_string()));
}
'!' => {
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, "Health
Potion".to_string()));
}
_ => {
println!("Unknown glyph loading map: {}", (ch as u8) as
char);
}
}
}

#[allow(dead_code)]
fn load_rex_map(&mut self, path: &str, build_data : &mut BuilderMap) {
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap();

for layer in &xp_file.layers {

624 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

for y in 0..layer.height {
for x in 0..layer.width {
let cell = layer.get(x, y).unwrap();
if x < build_data.map.width as usize && y <
build_data.map.height as usize {
let idx = build_data.map.xy_idx(x as i32, y as
i32);
// We're doing some nasty casting to make it
easier to type things like '#' in the match
self.char_to_map(cell.ch as u8 as char, idx,
build_data);
}
}
}
}
}

fn read_ascii_to_vec(template : &str) -> Vec<char> {


let mut string_vec : Vec<char> = template.chars().filter(|a| *a !=
'\r' && *a !='\n').collect();
for c in string_vec.iter_mut() { if *c as u8 == 160u8 { *c = ' ';
} }
string_vec
}

#[allow(dead_code)]
fn load_ascii_map(&mut self, level: &prefab_levels::PrefabLevel,
build_data : &mut BuilderMap) {
let string_vec = PrefabBuilder::read_ascii_to_vec(level.template);

let mut i = 0;
for ty in 0..level.height {
for tx in 0..level.width {
if tx < build_data.map.width as usize && ty <
build_data.map.height as usize {
let idx = build_data.map.xy_idx(tx as i32, ty as i32);
self.char_to_map(string_vec[i], idx, build_data);
}
i += 1;
}
}
}

fn apply_previous_iteration<F>(&mut self, mut filter: F, _rng: &mut


RandomNumberGenerator, build_data : &mut BuilderMap)
where F : FnMut(i32, i32, &(usize, String)) -> bool

625 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
let spawn_clone = build_data.spawn_list.clone();
for e in spawn_clone.iter() {
let idx = e.0;
let x = idx as i32 % build_data.map.width;
let y = idx as i32 / build_data.map.width;
if filter(x, y, e) {
build_data.spawn_list.push(
(idx, e.1.to_string())
)
}
}
build_data.take_snapshot();
}

#[allow(dead_code)]
fn apply_sectional(&mut self, section :
&prefab_sections::PrefabSection, rng: &mut RandomNumberGenerator,
build_data : &mut BuilderMap) {
use prefab_sections::*;

let string_vec =
PrefabBuilder::read_ascii_to_vec(section.template);

// Place the new section


let chunk_x;
match section.placement.0 {
HorizontalPlacement::Left => chunk_x = 0,
HorizontalPlacement::Center => chunk_x = (build_data.map.width
/ 2) - (section.width as i32 / 2),
HorizontalPlacement::Right => chunk_x =
(build_data.map.width-1) - section.width as i32
}

let chunk_y;
match section.placement.1 {
VerticalPlacement::Top => chunk_y = 0,
VerticalPlacement::Center => chunk_y = (build_data.map.height
/ 2) - (section.height as i32 / 2),
VerticalPlacement::Bottom => chunk_y =
(build_data.map.height-1) - section.height as i32
}

// Build the map


self.apply_previous_iteration(|x,y,_e| {
x < chunk_x || x > (chunk_x + section.width as i32) || y <

626 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

chunk_y || y > (chunk_y + section.height as i32)


}, rng, build_data);

let mut i = 0;
for ty in 0..section.height {
for tx in 0..section.width {
if tx > 0 && tx < build_data.map.width as usize -1 && ty <
build_data.map.height as usize -1 && ty > 0 {
let idx = build_data.map.xy_idx(tx as i32 + chunk_x,
ty as i32 + chunk_y);
self.char_to_map(string_vec[i], idx, build_data);
}
i += 1;
}
}
build_data.take_snapshot();
}

fn apply_room_vaults(&mut self, rng : &mut RandomNumberGenerator,


build_data : &mut BuilderMap) {
use prefab_rooms::*;

// Apply the previous builder, and keep all entities it spawns


(for now)
self.apply_previous_iteration(|_x,_y,_e| true, rng, build_data);

// Do we want a vault at all?


let vault_roll = rng.roll_dice(1, 6) + build_data.map.depth;
if vault_roll < 4 { return; }

// Note that this is a place-holder and will be moved out of this


function
let master_vault_list = vec![TOTALLY_NOT_A_TRAP, CHECKERBOARD,
SILLY_SMILE];

// Filter the vault list down to ones that are applicable to the
current depth
let mut possible_vaults : Vec<&PrefabRoom> = master_vault_list
.iter()
.filter(|v| { build_data.map.depth >= v.first_depth &&
build_data.map.depth <= v.last_depth })
.collect();

if possible_vaults.is_empty() { return; } // Bail out if there's


nothing to build

627 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let n_vaults = i32::min(rng.roll_dice(1, 3), possible_vaults.len()


as i32);
let mut used_tiles : HashSet<usize> = HashSet::new();

for _i in 0..n_vaults {

let vault_index = if possible_vaults.len() == 1 { 0 } else {


(rng.roll_dice(1, possible_vaults.len() as i32)-1) as usize };
let vault = possible_vaults[vault_index];

// We'll make a list of places in which the vault could fit


let mut vault_positions : Vec<Position> = Vec::new();

let mut idx = 0usize;


loop {
let x = (idx % build_data.map.width as usize) as i32;
let y = (idx / build_data.map.width as usize) as i32;

// Check that we won't overflow the map


if x > 1
&& (x+vault.width as i32) < build_data.map.width-2
&& y > 1
&& (y+vault.height as i32) < build_data.map.height-2
{

let mut possible = true;


for ty in 0..vault.height as i32 {
for tx in 0..vault.width as i32 {

let idx = build_data.map.xy_idx(tx + x, ty +


y);
if build_data.map.tiles[idx] !=
TileType::Floor {
possible = false;
}
if used_tiles.contains(&idx) {
possible = false;
}
}
}

if possible {
vault_positions.push(Position{ x,y });
break;
}

628 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

idx += 1;
if idx >= build_data.map.tiles.len()-1 { break; }
}

if !vault_positions.is_empty() {
let pos_idx = if vault_positions.len()==1 { 0 } else {
(rng.roll_dice(1, vault_positions.len() as i32)-1) as usize };
let pos = &vault_positions[pos_idx];

let chunk_x = pos.x;


let chunk_y = pos.y;

let width = build_data.map.width; // The borrow checker


really doesn't like it
let height = build_data.map.height; // when we access
`self` inside the `retain`
build_data.spawn_list.retain(|e| {
let idx = e.0 as i32;
let x = idx % width;
let y = idx / height;
x < chunk_x || x > chunk_x + vault.width as i32 || y <
chunk_y || y > chunk_y + vault.height as i32
});

let string_vec =
PrefabBuilder::read_ascii_to_vec(vault.template);
let mut i = 0;
for ty in 0..vault.height {
for tx in 0..vault.width {
let idx = build_data.map.xy_idx(tx as i32 +
chunk_x, ty as i32 + chunk_y);
self.char_to_map(string_vec[i], idx, build_data);
used_tiles.insert(idx);
i += 1;
}
}
build_data.take_snapshot();

possible_vaults.remove(vault_index);
}
}
}
}

629 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You can test our recent changes with the following code in random_builder (in
map_builders/mod.rs ):

let mut builder = BuilderChain::new(new_depth);


builder.start_with(VoronoiCellBuilder::pythagoras());
builder.with(WaveformCollapseBuilder::new());
builder.with(PrefabBuilder::vaults());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
builder.with(DistantExit::new());
builder

This demonstrates the power of our approach - we're putting a lot of functionality
together from small building blocks. In this example we are:

1. Starting with a map generated with the VoronoiBuilder in Pythagoras mode.


2. Modifying the map with a WaveformCollapseBuilder run, which will rearrange
the map like a jigsaw puzzle.
3. Modifying the map by placing vaults, via the PrefabBuilder (in Vaults mode).
4. Modifying the map with AreaStartingPositions indicating that we'd like to start
near the middle of the map.
5. Modifying the map to cull unreachable areas.
6. Modifying the map to spawn entities using a Voronoi spawning method.
7. Modifying the map to add an underground fortress, again using the
PrefabBuilder .
8. Modifying the map to add an exit staircase, in the most distant location.

Delete the MapBuilder Trait and bits from common


Now that we have the builder mechanism in place, there's some old code we can
delete. From common.rs , we can delete
remove_unreachable_areas_returning_most_distant and
generate_voronoi_spawn_regions ; we've replaced them with builder steps.

We can also open map_builders/mod.rs and delete the MapBuilder trait and its

630 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

implementation: we've completely replaced it now.

Randomize
As usual, we'd like to go back to having map generation be random. We're going to
break the process up into two steps. We'll make a new function,
random_initial_builder that rolls a dice and picks the starting builder. It also returns
a bool , indicating whether or not we picked an algorithm that provides room data.
The basic function should look familiar, but we've got rid of all the Box::new calls -
the constructors make boxes for us, now:

fn random_initial_builder(rng: &mut rltk::RandomNumberGenerator) ->


(Box<dyn InitialMapBuilder>, bool) {
let builder = rng.roll_dice(1, 17);
let result : (Box<dyn InitialMapBuilder>, bool);
match builder {
1 => result = (BspDungeonBuilder::new(), true),
2 => result = (BspInteriorBuilder::new(), true),
3 => result = (CellularAutomotaBuilder::new(), false),
4 => result = (DrunkardsWalkBuilder::open_area(), false),
5 => result = (DrunkardsWalkBuilder::open_halls(), false),
6 => result = (DrunkardsWalkBuilder::winding_passages(), false),
7 => result = (DrunkardsWalkBuilder::fat_passages(), false),
8 => result = (DrunkardsWalkBuilder::fearful_symmetry(), false),
9 => result = (MazeBuilder::new(), false),
10 => result = (DLABuilder::walk_inwards(), false),
11 => result = (DLABuilder::walk_outwards(), false),
12 => result = (DLABuilder::central_attractor(), false),
13 => result = (DLABuilder::insectoid(), false),
14 => result = (VoronoiCellBuilder::pythagoras(), false),
15 => result = (VoronoiCellBuilder::manhattan(), false),
16 => result =
(PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED),
false),
_ => result = (SimpleMapBuilder::new(), true)
}
result
}

This is a pretty straightforward function - we roll a dice, match on the result table and

631 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

return the builder and room information we picked. Now we'll modify our
random_builder function to use it:

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
let (random_starter, has_rooms) = random_initial_builder(rng);
builder.start_with(random_starter);
if has_rooms {
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
} else {
builder.with(AreaStartingPosition::new(XStart::CENTER,
YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(PrefabBuilder::vaults());

builder
}

This should look familiar. This function:

1. Selects a random room using the function we just created.


2. If the builder provides room data, we chain RoomBasedSpawner ,
RoomBasedStairs and RoomBasedStartingPositiosn - the three important
steps required for room data.
3. If the builder doesn't provide room information, we chain
AreaStartingPosition , CullUnreachable , VoronoiSpawning and DistantExit
- the defaults we used to apply inside each builder.

632 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

4. We roll a 3-sided die; if it comes up 1 - we apply a WaveformCollapseBuilder to


rearrange the map.
5. We roll a 20-sided die; if it comes up 1 - we apply our Underground Fort prefab.
6. We apply vault creation to the �nal map, giving a chance for pre-made rooms to
appear.

Wrap-Up
This has been an enormous chapter, but we've accomplished a lot:

We now have a consistent builder interface for chaining as many meta-map


modi�ers as we want to our build chain. This should let us build the maps we
want.
Each builder now does just one task - so it's much more obvious where to go if
you need to �x/debug them.
Builders are no longer responsible for making other builders - so we've culled a
swathe of code and moved the opportunity for bugs to creep in to just one
(simple) control �ow.

This sets the stage for the next chapter, which will look at more ways to use �lters to
modify your map.

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Fun With Layers

633 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Now that we have a nice, clean layering system we'll take the opportunity to play with
it a bit. This chapter is a collection of fun things you can do with layers, and will
introduce a few new layer types. It's meant to whet your appetite to write more: the
sky really is the limit!

Existing Algorithms as Meta-Builders


Let's start by adjusting some of our existing algorithms to be useful as �lters.

Applying Cellular Automata as a meta-builder

When we wrote the Cellular Automata system, we aimed for a generic cavern builder.
The algorithm is capable of quite a bit more than that - each iteration is basically a
"meta builder" running on the previous iteration. A simple tweak allows it to also be a
meta-builder that only runs a single iteration.

We'll start by moving the code for a single iteration into its own function:

634 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn apply_iteration(&mut self, build_data : &mut BuilderMap) {


let mut newtiles = build_data.map.tiles.clone();

for y in 1..build_data.map.height-1 {
for x in 1..build_data.map.width-1 {
let idx = build_data.map.xy_idx(x, y);
let mut neighbors = 0;
if build_data.map.tiles[idx - 1] == TileType::Wall { neighbors
+= 1; }
if build_data.map.tiles[idx + 1] == TileType::Wall { neighbors
+= 1; }
if build_data.map.tiles[idx - build_data.map.width as usize]
== TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx + build_data.map.width as usize]
== TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx - (build_data.map.width as usize -
1)] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx - (build_data.map.width as usize +
1)] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx + (build_data.map.width as usize -
1)] == TileType::Wall { neighbors += 1; }
if build_data.map.tiles[idx + (build_data.map.width as usize +
1)] == TileType::Wall { neighbors += 1; }

if neighbors > 4 || neighbors == 0 {


newtiles[idx] = TileType::Wall;
}
else {
newtiles[idx] = TileType::Floor;
}
}
}

build_data.map.tiles = newtiles.clone();
build_data.take_snapshot();
}

The build function is easily modi�ed to call this on each iteration:

// Now we iteratively apply cellular automota rules


for _i in 0..15 {
self.apply_iteration(build_data);
}

635 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Finally, we'll add an implementation of MetaMapBuilder to the mix:

impl MetaMapBuilder for CellularAutomotaBuilder {


#[allow(dead_code)]
fn build_map(&mut self, _rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.apply_iteration(build_data);
}
}

See how we're calling a single iteration, instead of replacing the whole map? This
shows how we can apply the cellular automata rules to the map - and change the
resultant character quite a bit.

Now lets modify map_builders/mod.rs 's random_builder to force it to use this as an


example:

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(VoronoiCellBuilder::pythagoras());
builder.with(CellularAutomotaBuilder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER,
YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder
}

If you cargo run the project now, you'll see something like this:

636 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Eroding a boxy map with drunken dwarves

The Drunken Walk algorithm can also make a nice post-processing e�ect, with very
minimal modi�cation. In drunkard.rs , simply add the following:

impl MetaMapBuilder for DrunkardsWalkBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

You can test it by once again modifying random_builder :

637 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(DrunkardsWalkBuilder::winding_passages());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

If you cargo run the project, you'll see something like this:

Notice how the initial boxy design now looks a bit more natural, because drunken
dwarves have carved out sections of the map!

Attacking your boxy map with Di�usion-Limited Aggregation

DLA can also be modi�ed to erode an existing, boxy map. Simply add the
MetaBuilder trait to dla.rs :

638 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl MetaMapBuilder for DLABuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

We'll also add a new mode, heavy_erosion - it's the same as "walk inwards", but
wants a greater percentage of �oor space:

#[allow(dead_code)]
pub fn heavy_erosion() -> Box<DLABuilder> {
Box::new(DLABuilder{
algorithm: DLAAlgorithm::WalkInwards,
brush_size: 2,
symmetry: Symmetry::None,
floor_percent: 0.35,
})
}

And modify your random_builder test harness:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(DLABuilder::heavy_erosion());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

If you cargo run the project, you'll see something like this:

639 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Some New Meta-Builders


There's also plenty of scope to write new map �lters. We'll explore a few of the more
interesting ones in this section. Pretty much anything you might use as an image �lter
in a program like Photoshop (or the GIMP!) could be adapted for this purpose. How
useful a given �lter is remains an open/interesting question!

Eroding rooms

Nethack-style boxy rooms make for very early-D&D type play, but people often
remark that they aren't all that visually pleasing or interesting. One way to keep the
basic room style, but get a more organic look, is to run drunkard's walk inside each
room. I like to call this "exploding the room" - because it looks a bit like you set o�
dynamite in each room. In map_builders/ , make a new �le room_exploder.rs :

640 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, TileType, paint, Symmetry, Rect};


use rltk::RandomNumberGenerator;

pub struct RoomExploder {}

impl MetaMapBuilder for RoomExploder {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl RoomExploder {
#[allow(dead_code)]
pub fn new() -> Box<RoomExploder> {
Box::new(RoomExploder{})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Room Explosions require a builder with room
structures");
}

for room in rooms.iter() {


let start = room.center();
let n_diggers = rng.roll_dice(1, 20)-5;
if n_diggers > 0 {
for _i in 0..n_diggers {
let mut drunk_x = start.0;
let mut drunk_y = start.1;

let mut drunk_life = 20;


let mut did_something = false;

while drunk_life > 0 {


let drunk_idx = build_data.map.xy_idx(drunk_x,
drunk_y);
if build_data.map.tiles[drunk_idx] ==
TileType::Wall {
did_something = true;

641 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
paint(&mut build_data.map, Symmetry::None, 1,
drunk_x, drunk_y);
build_data.map.tiles[drunk_idx] =
TileType::DownStairs;

let stagger_direction = rng.roll_dice(1, 4);


match stagger_direction {
1 => { if drunk_x > 2 { drunk_x -= 1; } }
2 => { if drunk_x < build_data.map.width-2 {
drunk_x += 1; } }
3 => { if drunk_y > 2 { drunk_y -=1; } }
_ => { if drunk_y < build_data.map.height-2 {
drunk_y += 1; } }
}

drunk_life -= 1;
}
if did_something {
build_data.take_snapshot();

for t in build_data.map.tiles.iter_mut() {
if *t == TileType::DownStairs {
*t = TileType::Floor;
}
}
}
}
}
}
}
}

There's nothing too surprising in this code: it takes the rooms list from the parent
build data, and then iterates each room. A random number (which can be zero) of
drunkards is then run from the center of each room, with a short lifespan, carving out
the edges of each room. You can test this with the following random_builder code:

642 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspDungeonBuilder::new());
builder.with(RoomExploder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

Rounding Room Corners

Another quick and easy way to make a boxy map look less rectangular is to smooth
the corners a bit. Add room_corner_rounding.rs to map_builders/ :

643 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, TileType, Rect};


use rltk::RandomNumberGenerator;

pub struct RoomCornerRounder {}

impl MetaMapBuilder for RoomCornerRounder {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl RoomCornerRounder {
#[allow(dead_code)]
pub fn new() -> Box<RoomCornerRounder> {
Box::new(RoomCornerRounder{})
}

fn fill_if_corner(&mut self, x: i32, y: i32, build_data : &mut


BuilderMap) {
let w = build_data.map.width;
let h = build_data.map.height;
let idx = build_data.map.xy_idx(x, y);
let mut neighbor_walls = 0;
if x > 0 && build_data.map.tiles[idx-1] == TileType::Wall {
neighbor_walls += 1; }
if y > 0 && build_data.map.tiles[idx-w as usize] == TileType::Wall
{ neighbor_walls += 1; }
if x < w-2 && build_data.map.tiles[idx+1] == TileType::Wall {
neighbor_walls += 1; }
if y < h-2 && build_data.map.tiles[idx+w as usize] ==
TileType::Wall { neighbor_walls += 1; }

if neighbor_walls == 2 {
build_data.map.tiles[idx] = TileType::Wall;
}
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Room Rounding require a builder with room

644 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

structures");
}

for room in rooms.iter() {


self.fill_if_corner(room.x1+1, room.y1+1, build_data);
self.fill_if_corner(room.x2, room.y1+1, build_data);
self.fill_if_corner(room.x1+1, room.y2, build_data);
self.fill_if_corner(room.x2, room.y2, build_data);

build_data.take_snapshot();
}
}
}

The boilerplate (repeated code) should look familiar by now, so we'll focus on the
algorithm in build :

1. We obtain a list of rooms, and panic! if there aren't any.


2. For each of the 4 corners of the room, we call a new function fill_if_corner .
3. fill_if_corner counts each of the neighboring tiles to see if it is a wall. If there
are exactly 2 walls, then this tile is eligible to become a corner - so we �ll in a
wall.

You can try it out with the following random_builder code:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspDungeonBuilder::new());
builder.with(RoomCornerRounder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder

The result (if you cargo run ) should be something like this:

645 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Decoupling Rooms and Corridors


There's a fair amount of shared code between BSP room placement and "simple map"
room placement - but with di�erent corridor decision-making. What if we were to de-
couple the stages - so the room algorithms decide where the rooms go, another
algorithm draws them (possibly changing how they are drawn), and a third algorithm
places corridors? Our improved framework supports this with just a bit of algorithm
tweaking.

Here's simple_map.rs with the corridor code removed:

646 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{InitialMapBuilder, BuilderMap, Rect, apply_room_to_map,


apply_horizontal_tunnel, apply_vertical_tunnel };
use rltk::RandomNumberGenerator;

pub struct SimpleMapBuilder {}

impl InitialMapBuilder for SimpleMapBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build_rooms(rng, build_data);
}
}

impl SimpleMapBuilder {
#[allow(dead_code)]
pub fn new() -> Box<SimpleMapBuilder> {
Box::new(SimpleMapBuilder{})
}

fn build_rooms(&mut self, rng : &mut RandomNumberGenerator, build_data


: &mut BuilderMap) {
const MAX_ROOMS : i32 = 30;
const MIN_SIZE : i32 = 6;
const MAX_SIZE : i32 = 10;
let mut rooms : Vec<Rect> = Vec::new();

for _i in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, build_data.map.width - w - 1) - 1;
let y = rng.roll_dice(1, build_data.map.height - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in rooms.iter() {
if new_room.intersect(other_room) { ok = false }
}
if ok {
apply_room_to_map(&mut build_data.map, &new_room);
build_data.take_snapshot();

rooms.push(new_room);
build_data.take_snapshot();
}
}

647 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

build_data.rooms = Some(rooms);
}
}

Other than renaming rooms_and_corridors to just build_rooms , the only change is


removing the dice roll to place corridors.

Lets make a new �le, map_builders/rooms_corridors_dogleg.rs . This is where we


place the corridors. For now, we'll use the same algorithm we just removed from
SimpleMapBuilder :

648 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, Rect, apply_horizontal_tunnel,


apply_vertical_tunnel };
use rltk::RandomNumberGenerator;

pub struct DoglegCorridors {}

impl MetaMapBuilder for DoglegCorridors {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.corridors(rng, build_data);
}
}

impl DoglegCorridors {
#[allow(dead_code)]
pub fn new() -> Box<DoglegCorridors> {
Box::new(DoglegCorridors{})
}

fn corridors(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Dogleg Corridors require a builder with room
structures");
}

for (i,room) in rooms.iter().enumerate() {


if i > 0 {
let (new_x, new_y) = room.center();
let (prev_x, prev_y) = rooms[rooms.len()-1].center();
if rng.range(0,1) == 1 {
apply_horizontal_tunnel(&mut build_data.map, prev_x,
new_x, prev_y);
apply_vertical_tunnel(&mut build_data.map, prev_y,
new_y, new_x);
} else {
apply_vertical_tunnel(&mut build_data.map, prev_y,
new_y, prev_x);
apply_horizontal_tunnel(&mut build_data.map, prev_x,
new_x, new_y);
}

649 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

build_data.take_snapshot();
}
}
}
}

Again - this is the code we just removed, but placed into a new builder by itself. So
there's really nothing new. We can adjust random_builder to test this code:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(DoglegCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

Testing it with cargo run should show you that rooms are built, and then corridors:

Same again with BSP Dungeons

650 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

It's easy to do the same to our BSPDungeonBuilder . In bsp_dungeon.rs , we also trim


out the corridor code. We'll just include the build function for brevity:

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut


BuilderMap) {
let mut rooms : Vec<Rect> = Vec::new();
self.rects.clear();
self.rects.push( Rect::new(2, 2, build_data.map.width-5,
build_data.map.height-5) ); // Start with a single map-sized rectangle
let first_room = self.rects[0];
self.add_subrects(first_room); // Divide the first room

// Up to 240 times, we get a random rectangle and divide it. If


its possible to squeeze a
// room in there, we place it and add it to the rooms list.
let mut n_rooms = 0;
while n_rooms < 240 {
let rect = self.get_random_rect(rng);
let candidate = self.get_random_sub_rect(rect, rng);

if self.is_possible(candidate, &build_data.map) {
apply_room_to_map(&mut build_data.map, &candidate);
rooms.push(candidate);
self.add_subrects(rect);
build_data.take_snapshot();
}

n_rooms += 1;
}

build_data.rooms = Some(rooms);
}

We'll also move our BSP corridor code into a new builder, without the room sorting
(we'll be touching on sorting in the next heading!). Create the new �le
map_builders/rooms_corridors_bsp.rs :

651 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, Rect, draw_corridor };


use rltk::RandomNumberGenerator;

pub struct BspCorridors {}

impl MetaMapBuilder for BspCorridors {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.corridors(rng, build_data);
}
}

impl BspCorridors {
#[allow(dead_code)]
pub fn new() -> Box<BspCorridors> {
Box::new(BspCorridors{})
}

fn corridors(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("BSP Corridors require a builder with room
structures");
}

for i in 0..rooms.len()-1 {
let room = rooms[i];
let next_room = rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 -
room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 -
room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1,
i32::abs(next_room.x1 - next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1,
i32::abs(next_room.y1 - next_room.y2))-1);
draw_corridor(&mut build_data.map, start_x, start_y, end_x,
end_y);
build_data.take_snapshot();
}
}

652 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Again, this is the corridor code from BspDungeonBuilder - just �tted into its own
builder stage. You can prove that it works by modifying random_builder once again:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspDungeonBuilder::new());
builder.with(BspCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

If you cargo run it, you'll see something like this:

That looks like it works - but if you pay close attention, you'll see why we sorted the
rooms in the original algorithm: there's lots of overlap between rooms/corridors, and
corridors don't trend towards the shortest path. This was deliberate - we need to
make a RoomSorter builder, to give us some more map-building options. Lets create
map_builders/room_sorter.rs :

653 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap };


use rltk::RandomNumberGenerator;

pub struct RoomSorter {}

impl MetaMapBuilder for RoomSorter {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.sorter(rng, build_data);
}
}

impl RoomSorter {
#[allow(dead_code)]
pub fn new() -> Box<RoomSorter> {
Box::new(RoomSorter{})
}

fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1)
);
}
}

This is exactly the same sorting we used before, and we can test it by inserting it into
our builder sequence:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspDungeonBuilder::new());
builder.with(RoomSorter::new());
builder.with(BspCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

If you cargo run it, you'll see something like this:

654 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

That's better - we've restored the look and feel of our BSP Dungeon Builder!

More Room Sorting Options


Breaking the sorter into its own step is only really useful if we're going to come up
with some di�erent ways to sort the rooms! We're currently sorting by the left-most
entry - giving a map that gradually works its way East, but jumps around.

Lets add an enum to give us more sorting options:

655 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap };


use rltk::RandomNumberGenerator;

pub enum RoomSort { LEFTMOST }

pub struct RoomSorter {


sort_by : RoomSort
}

impl MetaMapBuilder for RoomSorter {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.sorter(rng, build_data);
}
}

impl RoomSorter {
#[allow(dead_code)]
pub fn new(sort_by : RoomSort) -> Box<RoomSorter> {
Box::new(RoomSorter{ sort_by })
}

fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
match self.sort_by {
RoomSort::LEFTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) )
}
}
}

Simple enough: we store the sorting algorithm we wish to use in the structure, and
match on it when it comes time to execute.

Lets add RIGHTMOST - which will simply reverse the sort:

656 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub enum RoomSort { LEFTMOST, RIGHTMOST }


...
fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut
BuilderMap) {
match self.sort_by {
RoomSort::LEFTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ),
RoomSort::RIGHTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) )
}
}

That's so simple it's basically cheating! Lets add TOPMOST and BOTTOMMOST as well,
for completeness of this type of sort:

#[allow(dead_code)]
pub enum RoomSort { LEFTMOST, RIGHTMOST, TOPMOST, BOTTOMMOST }
...
fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut
BuilderMap) {
match self.sort_by {
RoomSort::LEFTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ),
RoomSort::RIGHTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) ),
RoomSort::TOPMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.y1.cmp(&b.y1) ),
RoomSort::BOTTOMMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.y2.cmp(&a.y2) )
}
}

Here's BOTTOMMOST in action:

657 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

See how that changes the character of the map without really changing the structure?
It's amazing what you can do with little tweaks!

We'll add another sort, CENTRAL. This time, we're sorting by distance from the map
center:

658 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[allow(dead_code)]
pub enum RoomSort { LEFTMOST, RIGHTMOST, TOPMOST, BOTTOMMOST, CENTRAL }
...
fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut
BuilderMap) {
match self.sort_by {
RoomSort::LEFTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ),
RoomSort::RIGHTMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) ),
RoomSort::TOPMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.y1.cmp(&b.y1) ),
RoomSort::BOTTOMMOST =>
build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.y2.cmp(&a.y2) ),
RoomSort::CENTRAL => {
let map_center = rltk::Point::new( build_data.map.width / 2,
build_data.map.height / 2 );
let center_sort = |a : &Rect, b : &Rect| {
let a_center = a.center();
let a_center_pt = rltk::Point::new(a_center.0,
a_center.1);
let b_center = b.center();
let b_center_pt = rltk::Point::new(b_center.0,
b_center.1);
let distance_a =
rltk::DistanceAlg::Pythagoras.distance2d(a_center_pt, map_center);
let distance_b =
rltk::DistanceAlg::Pythagoras.distance2d(b_center_pt, map_center);
distance_a.partial_cmp(&distance_b).unwrap()
};

build_data.rooms.as_mut().unwrap().sort_by(center_sort);
}
}
}

You can modify your random_builder function to use this:

659 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspDungeonBuilder::new());
builder.with(RoomSorter::new(RoomSort::CENTRAL));
builder.with(BspCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

And cargo run will give you something like this:

Notice how all roads now lead to the middle - for a very connected map!

Cleaning up our random builder


Now that we're getting towards the end of this section (not there yet!), lets take the
time to really take advantage of what we've built so far. We're going to completely
restructure the way we're selecting a random build pattern.

660 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Room-based spawning isn't as embarrassingly predictable as it used to be, now. So


lets make a function that exposes all of the room variety we've built so far:

661 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn random_room_builder(rng: &mut rltk::RandomNumberGenerator, builder :


&mut BuilderChain) {
let build_roll = rng.roll_dice(1, 3);
match build_roll {
1 => builder.start_with(SimpleMapBuilder::new()),
2 => builder.start_with(BspDungeonBuilder::new()),
_ => builder.start_with(BspInteriorBuilder::new())
}

// BSP Interior still makes holes in the walls


if build_roll != 3 {
// Sort by one of the 5 available algorithms
let sort_roll = rng.roll_dice(1, 5);
match sort_roll {
1 => builder.with(RoomSorter::new(RoomSort::LEFTMOST)),
2 => builder.with(RoomSorter::new(RoomSort::RIGHTMOST)),
3 => builder.with(RoomSorter::new(RoomSort::TOPMOST)),
4 => builder.with(RoomSorter::new(RoomSort::BOTTOMMOST)),
_ => builder.with(RoomSorter::new(RoomSort::CENTRAL)),
}

let corridor_roll = rng.roll_dice(1, 2);


match corridor_roll {
1 => builder.with(DoglegCorridors::new()),
_ => builder.with(BspCorridors::new())
}

let modifier_roll = rng.roll_dice(1, 6);


match modifier_roll {
1 => builder.with(RoomExploder::new()),
2 => builder.with(RoomCornerRounder::new()),
_ => {}
}
}

let start_roll = rng.roll_dice(1, 2);


match start_roll {
1 => builder.with(RoomBasedStartingPosition::new()),
_ => {
let (start_x, start_y) = random_start_position(rng);
builder.with(AreaStartingPosition::new(start_x, start_y));
}
}

let exit_roll = rng.roll_dice(1, 2);

662 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

match exit_roll {
1 => builder.with(RoomBasedStairs::new()),
_ => builder.with(DistantExit::new())
}

let spawn_roll = rng.roll_dice(1, 2);


match spawn_roll {
1 => builder.with(RoomBasedSpawner::new()),
_ => builder.with(VoronoiSpawning::new())
}
}

That's a big function, so we'll step through it. It's quite simple, just really spread out
and full of branches:

1. We roll 1d3, and pick from BSP Interior, Simple and BSP Dungeon map builders.
2. If we didn't pick BSP Interior (which does a lot of stu� itself), we:
1. Randomly pick a room sorting algorithm.
2. Randomly pick one of the two corridor algorithms we now have.
3. Randomly pick (or ignore) a room exploder or corner-rounder.
3. We randomly choose between a Room-based starting position, and an area-
based starting position. For the latter, call random_start_position to pick
between 3 X-axis and 3 Y-axis starting positions to favor.
4. We randomly choose between a Room-based stairs placement and a "most
distant from the start" exit.
5. We randomly choose between Voronoi-area spawning and room-based
spawning.

So that function is all about rolling dice, and making a map! It's a lot of combinations,
even ignoring the thousands of possible layouts that can come from each starting
builder. There are:

2 <starting rooms with options> * 5 <sort> * 2 <corridor> * 3 <modifier> =


60 basic room options.
+1 for BSP Interior Dungeons = 61 room options.
*2 <starting position options> = 122 room options.
*2 <exit placements> = 244 room options.
*2 <spawn options> = 488 room options!

So this function is o�ering 488 possible builder combinations!.

Now we'll create a function for the non-room spawners:

663 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn random_shape_builder(rng: &mut rltk::RandomNumberGenerator, builder :


&mut BuilderChain) {
let builder_roll = rng.roll_dice(1, 16);
match builder_roll {
1 => builder.start_with(CellularAutomotaBuilder::new()),
2 => builder.start_with(DrunkardsWalkBuilder::open_area()),
3 => builder.start_with(DrunkardsWalkBuilder::open_halls()),
4 => builder.start_with(DrunkardsWalkBuilder::winding_passages()),
5 => builder.start_with(DrunkardsWalkBuilder::fat_passages()),
6 => builder.start_with(DrunkardsWalkBuilder::fearful_symmetry()),
7 => builder.start_with(MazeBuilder::new()),
8 => builder.start_with(DLABuilder::walk_inwards()),
9 => builder.start_with(DLABuilder::walk_outwards()),
10 => builder.start_with(DLABuilder::central_attractor()),
11 => builder.start_with(DLABuilder::insectoid()),
12 => builder.start_with(VoronoiCellBuilder::pythagoras()),
13 => builder.start_with(VoronoiCellBuilder::manhattan()),
_ =>
builder.start_with(PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATE
}

// Set the start to the center and cull


builder.with(AreaStartingPosition::new(XStart::CENTER,
YStart::CENTER));
builder.with(CullUnreachable::new());

// Now set the start to a random starting area


let (start_x, start_y) = random_start_position(rng);
builder.with(AreaStartingPosition::new(start_x, start_y));

// Setup an exit and spawn mobs


builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
}

This is similar to what we've done before, but with a twist: we now place the player
centrally, cull unreachable areas, and then place the player in a random location. It's
likely that the middle of a generated map is quite connected - so this gets rid of dead
space, and minimizes the likelihood of starting in an "orphaned" section and culling
the map down to just a few tiles.

This also provides a lot of combinations, but not quite as many.

664 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

14 basic room options


*1 Spawn option
*1 Exit option
*6 Starting options
= 84 options.

So this function is o�ering 84 room builder combinations.

Finally, we pull it all together in random_builder :

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
let type_roll = rng.roll_dice(1, 2);
match type_roll {
1 => random_room_builder(rng, &mut builder),
_ => random_shape_builder(rng, &mut builder)
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(PrefabBuilder::vaults());

builder
}

This is relatively straightforward. We randomly pick either a room or a shape builder,


as de�ned above. There's a 1 in 3 chance we'll then run Waveform Collapse on it, and
a 1 in 20 chance that we'll add a sectional to it. Finally, we try to spawn any vaults we
might want to use.

So how does our total combinatorial explosion look? Pretty good at this point:

665 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

488 possible room builders +


84 possible shape builders =
572 builder combinations.

We might run Waveform Collapse, giving another 2 options:


*2 = 1,144

We might add a sectional:


*2 = 2,288

So we now have 2,288 possible builder combinations, just from the last few
chapters. Combine that with a random seed, and it's increasingly unlikely that a player
will see the exact same combination of maps on a run twice.

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Improved room building

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the last chapter, we abstracted out room layout - but kept the actual placement of
the rooms the same: they are always rectangles, although this can be mitigated with
room explosion and corner rounding. This chapter will add the ability to use rooms of

666 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

di�erent shapes.

Rectangle Room Builder


First, we'll make a builder that accepts a set of rooms as input, and outputs those
rooms as rectangles on the map - exactly like the previous editions. We'll also modify
SimpleMapBuilder and BspDungeonBuilder to not duplicate the functionality.

We'll make a new �le, map_builders/room_draw.rs :

667 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, TileType, Rect};


use rltk::RandomNumberGenerator;

pub struct RoomDrawer {}

impl MetaMapBuilder for RoomDrawer {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl RoomDrawer {
#[allow(dead_code)]
pub fn new() -> Box<RoomDrawer> {
Box::new(RoomDrawer{})
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Room Rounding require a builder with room
structures");
}

for room in rooms.iter() {


for y in room.y1 +1 ..= room.y2 {
for x in room.x1 + 1 ..= room.x2 {
let idx = build_data.map.xy_idx(x, y);
if idx > 0 && idx < ((build_data.map.width *
build_data.map.height)-1) as usize {
build_data.map.tiles[idx] = TileType::Floor;
}
}
}
build_data.take_snapshot();
}
}
}

This is the same drawing functionality found in common.rs 's apply_room_to_map -


wrapped in the same meta-builder functionality we've used in the last couple of

668 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

chapters. Nothing too surprising here!

In bsp_dungeon.rs , simply remove the line referencing apply_room_to_map . You can


also remove take_snapshot - since we aren't applying anything to the map yet:

if self.is_possible(candidate, &build_data.map, &rooms) {


rooms.push(candidate);
self.add_subrects(rect);
}

We'll also have to update is_possible to check the rooms list rather than reading
the live map (to which we haven't written anything):

669 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn is_possible(&self, rect : Rect, build_data : &BuilderMap, rooms:


&Vec<Rect>) -> bool {
let mut expanded = rect;
expanded.x1 -= 2;
expanded.x2 += 2;
expanded.y1 -= 2;
expanded.y2 += 2;

let mut can_build = true;

for r in rooms.iter() {
if r.intersect(&rect) { can_build = false; }
}

for y in expanded.y1 ..= expanded.y2 {


for x in expanded.x1 ..= expanded.x2 {
if x > build_data.map.width-2 { can_build = false; }
if y > build_data.map.height-2 { can_build = false; }
if x < 1 { can_build = false; }
if y < 1 { can_build = false; }
if can_build {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] != TileType::Wall {
can_build = false;
}
}
}
}

can_build
}

Likewise, in simple_map.rs - just remove the apply_room_to_map and


take_snapshot calls:

if ok {
rooms.push(new_room);
}

Nothing is using apply_room_to_map in common.rs anymore - so we can delete that


too!

Lastly, modify random_builder in map_builders/mod.rs to test our code:

670 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
/*let mut builder = BuilderChain::new(new_depth);
let type_roll = rng.roll_dice(1, 2);
match type_roll {
1 => random_room_builder(rng, &mut builder),
_ => random_shape_builder(rng, &mut builder)
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(PrefabBuilder::vaults());

builder*/

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(BspCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder
}

If you cargo run the project, you'll see our simple map builder run - just like before.

Circular Rooms
Simply moving the draw code out of the algorithm cleans things up, but doesn't gain
us anything new. So we'll look at adding a few shape options for rooms. We'll start by
moving the draw code out of the main loop and into its own function. Modify
room_draw.rs as follows:

671 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn rectangle(&mut self, build_data : &mut BuilderMap, room : &Rect) {


for y in room.y1 +1 ..= room.y2 {
for x in room.x1 + 1 ..= room.x2 {
let idx = build_data.map.xy_idx(x, y);
if idx > 0 && idx < ((build_data.map.width *
build_data.map.height)-1) as usize {
build_data.map.tiles[idx] = TileType::Floor;
}
}
}
}

fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut


BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Room Drawing require a builder with room structures");
}

for room in rooms.iter() {


self.rectangle(build_data, room);
build_data.take_snapshot();
}
}

Once again, if you feel like testing it - cargo run will give you similar results to last
time. Lets add a second room shape - circular rooms:

672 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn circle(&mut self, build_data : &mut BuilderMap, room : &Rect) {


let radius = i32::min(room.x2 - room.x1, room.y2 - room.y1) as f32 /
2.0;
let center = room.center();
let center_pt = rltk::Point::new(center.0, center.1);
for y in room.y1 ..= room.y2 {
for x in room.x1 ..= room.x2 {
let idx = build_data.map.xy_idx(x, y);
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(center_pt, rltk::Point::new(x,
y));
if idx > 0
&& idx < ((build_data.map.width *
build_data.map.height)-1) as usize
&& distance <= radius
{
build_data.map.tiles[idx] = TileType::Floor;
}
}
}
}

Now replace your call to rectangle with circle , type cargo run and enjoy the new
room type:

673 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Picking a shape at random


It would be nice for round rooms to be an occasional feature. So we'll modify our
build function to make roughly one quarter of rooms round:

674 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut


BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Room Drawing require a builder with room structures");
}

for room in rooms.iter() {


let room_type = rng.roll_dice(1,4);
match room_type {
1 => self.circle(build_data, room),
_ => self.rectangle(build_data, room)
}
build_data.take_snapshot();
}
}

If you cargo run the project now, you'll see something like this:

675 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Restoring randomness
In map_builders/mod.rs uncomment the code and remove the test harness:

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
let type_roll = rng.roll_dice(1, 2);
match type_roll {
1 => random_room_builder(rng, &mut builder),
_ => random_shape_builder(rng, &mut builder)
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(PrefabBuilder::vaults());

builder
}

In random_room_builder , we add in the room drawing:

676 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
let sort_roll = rng.roll_dice(1, 5);
match sort_roll {
1 => builder.with(RoomSorter::new(RoomSort::LEFTMOST)),
2 => builder.with(RoomSorter::new(RoomSort::RIGHTMOST)),
3 => builder.with(RoomSorter::new(RoomSort::TOPMOST)),
4 => builder.with(RoomSorter::new(RoomSort::BOTTOMMOST)),
_ => builder.with(RoomSorter::new(RoomSort::CENTRAL)),
}

builder.with(RoomDrawer::new());

let corridor_roll = rng.roll_dice(1, 2);


match corridor_roll {
1 => builder.with(DoglegCorridors::new()),
_ => builder.with(BspCorridors::new())
}
...

You can now get the full gamut of random room creation - but with the occasional
round instead of rectangular room. That adds a bit more variety to the mix.

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Improved corridors

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to

677 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Our corridor generation so far has been quite primitive, featuring overlaps - and
unless you use Voronoi spawning, nothing in them. This chapter will try to o�er a few
more generation strategies (in turn providing even more map variety), and allow
hallways to contain entities.

New corridor strategy: nearest neighbor


One way to make a map feel more natural is to build hallways between near
neighbors. This reduces (but doesn't eliminate) overlaps, and looks more like
something that someone might actually build. We'll make a new �le,
map_builders/rooms_corridors_nearest.rs :

678 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, Rect, draw_corridor };


use rltk::RandomNumberGenerator;
use std::collections::HashSet;

pub struct NearestCorridors {}

impl MetaMapBuilder for NearestCorridors {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.corridors(rng, build_data);
}
}

impl NearestCorridors {
#[allow(dead_code)]
pub fn new() -> Box<NearestCorridors> {
Box::new(NearestCorridors{})
}

fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data


: &mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Nearest Corridors require a builder with room
structures");
}

let mut connected : HashSet<usize> = HashSet::new();


for (i,room) in rooms.iter().enumerate() {
let mut room_distance : Vec<(usize, f32)> = Vec::new();
let room_center = room.center();
let room_center_pt = rltk::Point::new(room_center.0,
room_center.1);
for (j,other_room) in rooms.iter().enumerate() {
if i != j && !connected.contains(&j) {
let other_center = other_room.center();
let other_center_pt = rltk::Point::new(other_center.0,
other_center.1);
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(
room_center_pt,
other_center_pt

679 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

);
room_distance.push((j, distance));
}
}

if !room_distance.is_empty() {
room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()
);
let dest_center = rooms[room_distance[0].0].center();
draw_corridor(
&mut build_data.map,
room_center.0, room_center.1,
dest_center.0, dest_center.1
);
connected.insert(i);
build_data.take_snapshot();
}
}
}
}

There's some boilerplate with which you should be familiar by now, so lets walk
through the corridors function:

1. We start by obtaining the rooms list, and panic! if there isn't one.
2. We make a new HashSet named connected . We'll add rooms to this as they
gain exits, so as to avoid linking repeatedly to the same room.
3. For each room, we retrieve an "enumeration" called i (the index number in the
vector) and the room :
1. We create a new vector called room_distance . It stores tuples containing
the room being considered's index and a �oating point number that will
store its distance to the current room.
2. We calculate the center of the room, and store it in a Point from RLTK (for
compatibility with the distance algorithms).
3. For every room, we retrieve an enumeration called j (it's customary to use
i and j for counters, presumably dating back to the days in which longer
variable names were expensive!), and the other_room .
1. If i and j are equal, we are looking at a corridor to/from the same
room. We don't want to do that, so we skip it!
2. Likewise, if the other_room 's index ( j ) is in our connected set, then
we don't want to evaluate it either - so we skip that.
3. We calculate the distance from the outer room ( room / i ) to the room

680 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

we are evaluating ( other_room / j ).


4. We push the distance and the j index into room_distance .
4. If the list for room_distance is empty, we skip ahead. Otherwise:
5. We use sort_by to sort the room_distance vector, with the shortest
distance being closest.
6. Then we use the draw_corridor function to draw a corridor from the
center of the current room to the closest room (index 0 in room_distance
)

Lastly, we'll modify random_builder in map_builders/mod.rs to use this algorithm:

681 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
/*
let mut builder = BuilderChain::new(new_depth);
let type_roll = rng.roll_dice(1, 2);
match type_roll {
1 => random_room_builder(rng, &mut builder),
_ => random_shape_builder(rng, &mut builder)
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(PrefabBuilder::vaults());

builder*/

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(NearestCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder
}

This gives nicely connected maps, with sensibly short corridor distances. If you
cargo run the project, you should see something like this:

682 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Overlapping corridors can still happen, but it is now quite unlikely.

Corridors with Bresenham Lines


Instead of dog-legging around a corner, we can draw corridors as a straight line. This
is a little more irritating for the player to navigate (more corners to navigate), but can
give a pleasing e�ect. We'll create a new �le,
map_builders/rooms_corridors_lines.rs :

683 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use super::{MetaMapBuilder, BuilderMap, Rect, TileType };


use rltk::RandomNumberGenerator;
use std::collections::HashSet;

pub struct StraightLineCorridors {}

impl MetaMapBuilder for StraightLineCorridors {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.corridors(rng, build_data);
}
}

impl StraightLineCorridors {
#[allow(dead_code)]
pub fn new() -> Box<StraightLineCorridors> {
Box::new(StraightLineCorridors{})
}

fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data


: &mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Straight Line Corridors require a builder with room
structures");
}

let mut connected : HashSet<usize> = HashSet::new();


for (i,room) in rooms.iter().enumerate() {
let mut room_distance : Vec<(usize, f32)> = Vec::new();
let room_center = room.center();
let room_center_pt = rltk::Point::new(room_center.0,
room_center.1);
for (j,other_room) in rooms.iter().enumerate() {
if i != j && !connected.contains(&j) {
let other_center = other_room.center();
let other_center_pt = rltk::Point::new(other_center.0,
other_center.1);
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(
room_center_pt,
other_center_pt

684 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

);
room_distance.push((j, distance));
}
}

if !room_distance.is_empty() {
room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()
);
let dest_center = rooms[room_distance[0].0].center();
let line = rltk::line2d(
rltk::LineAlg::Bresenham,
room_center_pt,
rltk::Point::new(dest_center.0, dest_center.1)
);
for cell in line.iter() {
let idx = build_data.map.xy_idx(cell.x, cell.y);
build_data.map.tiles[idx] = TileType::Floor;
}
connected.insert(i);
build_data.take_snapshot();
}
}
}
}

This is almost the same as the previous one, but instead of calling draw_corridor we
use RLTK's line function to plot a line from the center of the source and destination
rooms. We then mark each tile along the line as a �oor. If you modify your
random_builder to use this:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(StraightLineCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

Then cargo run your project, you will see something like this:

685 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Storing corridor locations


We might want to do something with our corridor locations in the future, so lets store
them. In map_builders/mod.rs , lets add a container to store our corridor locations.
We'll make it an Option , so as to preserve compatibility with map types that don't use
the concept:

pub struct BuilderMap {


pub spawn_list : Vec<(usize, String)>,
pub map : Map,
pub starting_position : Option<Position>,
pub rooms: Option<Vec<Rect>>,
pub corridors: Option<Vec<Vec<usize>>>,
pub history : Vec<Map>
}

We also need to adjust the constructor to ensure that corridors isn't forgotten:

686 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl BuilderChain {
pub fn new(new_depth : i32) -> BuilderChain {
BuilderChain{
starter: None,
builders: Vec::new(),
build_data : BuilderMap {
spawn_list: Vec::new(),
map: Map::new(new_depth),
starting_position: None,
rooms: None,
corridors: None,
history : Vec::new()
}
}
}
...

Now in common.rs , lets modify our corridor functions to return corridor placement
information:

687 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) ->


Vec<usize> {
let mut corridor = Vec::new();
for x in min(x1,x2) ..= max(x1,x2) {
let idx = map.xy_idx(x, y);
if idx > 0 && idx < map.width as usize * map.height as usize &&
map.tiles[idx as usize] != TileType::Floor {
map.tiles[idx as usize] = TileType::Floor;
corridor.push(idx as usize);
}
}
corridor
}

pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) ->


Vec<usize> {
let mut corridor = Vec::new();
for y in min(y1,y2) ..= max(y1,y2) {
let idx = map.xy_idx(x, y);
if idx > 0 && idx < map.width as usize * map.height as usize &&
map.tiles[idx as usize] != TileType::Floor {
corridor.push(idx);
map.tiles[idx as usize] = TileType::Floor;
}
}
corridor
}

pub fn draw_corridor(map: &mut Map, x1:i32, y1:i32, x2:i32, y2:i32) ->


Vec<usize> {
let mut corridor = Vec::new();
let mut x = x1;
let mut y = y1;

while x != x2 || y != y2 {
if x < x2 {
x += 1;
} else if x > x2 {
x -= 1;
} else if y < y2 {
y += 1;
} else if y > y2 {
y -= 1;
}

688 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let idx = map.xy_idx(x, y);


if map.tiles[idx] != TileType::Floor {
corridor.push(idx);
map.tiles[idx] = TileType::Floor;
}
}

corridor
}

Notice that they are essentially unchanged, but now return a vector of tile indices -
and only add to them if the tile being modi�ed is a �oor? That will give us de�nitions
for each leg of a corridor. Now we need to modify the corridor drawing algorithms to
store this information. In rooms_corridors_bsp.rs , modify the corridors function
to do this:

...
let mut corridors : Vec<Vec<usize>> = Vec::new();
for i in 0..rooms.len()-1 {
let room = rooms[i];
let next_room = rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 -
room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 -
room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 -
next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 -
next_room.y2))-1);
let corridor = draw_corridor(&mut build_data.map, start_x, start_y,
end_x, end_y);
corridors.push(corridor);
build_data.take_snapshot();
}
build_data.corridors = Some(corridors);
...

We do the same again in rooms_corridors_dogleg.rs :

689 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
let mut corridors : Vec<Vec<usize>> = Vec::new();
for (i,room) in rooms.iter().enumerate() {
if i > 0 {
let (new_x, new_y) = room.center();
let (prev_x, prev_y) = rooms[rooms.len()-1].center();
if rng.range(0,1) == 1 {
let mut c1 = apply_horizontal_tunnel(&mut build_data.map,
prev_x, new_x, prev_y);
let mut c2 = apply_vertical_tunnel(&mut build_data.map,
prev_y, new_y, new_x);
c1.append(&mut c2);
corridors.push(c1);
} else {
let mut c1 = apply_vertical_tunnel(&mut build_data.map,
prev_y, new_y, prev_x);
let mut c2 = apply_horizontal_tunnel(&mut build_data.map,
prev_x, new_x, new_y);
c1.append(&mut c2);
corridors.push(c1);
}
build_data.take_snapshot();
}
}
build_data.corridors = Some(corridors);
...

You'll notice that we append the second leg of the corridor to the �rst, so we treat it
as one long corridor rather than two hallways. We need to apply the same change to
our newly minted rooms_corridors_lines.rs :

690 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Straight Line Corridors require a builder with room
structures");
}

let mut connected : HashSet<usize> = HashSet::new();


let mut corridors : Vec<Vec<usize>> = Vec::new();
for (i,room) in rooms.iter().enumerate() {
let mut room_distance : Vec<(usize, f32)> = Vec::new();
let room_center = room.center();
let room_center_pt = rltk::Point::new(room_center.0,
room_center.1);
for (j,other_room) in rooms.iter().enumerate() {
if i != j && !connected.contains(&j) {
let other_center = other_room.center();
let other_center_pt = rltk::Point::new(other_center.0,
other_center.1);
let distance = rltk::DistanceAlg::Pythagoras.distance2d(
room_center_pt,
other_center_pt
);
room_distance.push((j, distance));
}
}

if !room_distance.is_empty() {
room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() );
let dest_center = rooms[room_distance[0].0].center();
let line = rltk::line2d(
rltk::LineAlg::Bresenham,
room_center_pt,
rltk::Point::new(dest_center.0, dest_center.1)
);
let mut corridor = Vec::new();
for cell in line.iter() {
let idx = build_data.map.xy_idx(cell.x, cell.y);
if build_data.map.tiles[idx] != TileType::Floor {
build_data.map.tiles[idx] = TileType::Floor;
corridor.push(idx);
}

691 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
corridors.push(corridor);
connected.insert(i);
build_data.take_snapshot();
}
}
build_data.corridors = Some(corridors);
}

We'll also do the same in rooms_corridors_nearest.rs :

692 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
let rooms : Vec<Rect>;
if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone();
} else {
panic!("Nearest Corridors require a builder with room
structures");
}

let mut connected : HashSet<usize> = HashSet::new();


let mut corridors : Vec<Vec<usize>> = Vec::new();
for (i,room) in rooms.iter().enumerate() {
let mut room_distance : Vec<(usize, f32)> = Vec::new();
let room_center = room.center();
let room_center_pt = rltk::Point::new(room_center.0,
room_center.1);
for (j,other_room) in rooms.iter().enumerate() {
if i != j && !connected.contains(&j) {
let other_center = other_room.center();
let other_center_pt = rltk::Point::new(other_center.0,
other_center.1);
let distance = rltk::DistanceAlg::Pythagoras.distance2d(
room_center_pt,
other_center_pt
);
room_distance.push((j, distance));
}
}

if !room_distance.is_empty() {
room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() );
let dest_center = rooms[room_distance[0].0].center();
let corridor = draw_corridor(
&mut build_data.map,
room_center.0, room_center.1,
dest_center.0, dest_center.1
);
connected.insert(i);
build_data.take_snapshot();
corridors.push(corridor);
}
}
build_data.corridors = Some(corridors);
}

693 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Ok, we have corridor data - now what?


One obvious use is to be able to spawn entities inside corridors. We'll make the new
room_corridor_spawner.rs to do just that:

use super::{MetaMapBuilder, BuilderMap, spawner};


use rltk::RandomNumberGenerator;

pub struct CorridorSpawner {}

impl MetaMapBuilder for CorridorSpawner {


fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build(rng, build_data);
}
}

impl CorridorSpawner {
#[allow(dead_code)]
pub fn new() -> Box<CorridorSpawner> {
Box::new(CorridorSpawner{})
}

fn build(&mut self, rng : &mut RandomNumberGenerator, build_data :


&mut BuilderMap) {
if let Some(corridors) = &build_data.corridors {
for c in corridors.iter() {
let depth = build_data.map.depth;
spawner::spawn_region(&build_data.map,
rng,
&c,
depth,
&mut build_data.spawn_list);
}
} else {
panic!("Corridor Based Spawning only works after corridors
have been created");
}
}
}

This was based o� of room_based_spawner.rs - copy/pasted and changed the names!


Then the if let for rooms was replaced with corridors and instead of spawning

694 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

per room - we pass the corridor to spawn_region . Entities now spawn in the hallways.

You can test this by adding the spawner to your random_builder :

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(StraightLineCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(CorridorSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

Once you are playing, you can now �nd entities inside your corridors:

Restoring Randomness
Once again, it's the end of a sub-section - so we'll make random_builder random
once more, but utilizing our new stu�!

Start by uncommenting the code in random_builder , and removing the test harness:

695 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
let type_roll = rng.roll_dice(1, 2);
match type_roll {
1 => random_room_builder(rng, &mut builder),
_ => random_shape_builder(rng, &mut builder)
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(PrefabBuilder::vaults());

builder
}

Since everything we've worked on here has been room based, we'll also modify
random_room_builder to include it. We'll expand the corridor related section:

let corridor_roll = rng.roll_dice(1, 4);


match corridor_roll {
1 => builder.with(DoglegCorridors::new()),
2 => builder.with(NearestCorridors::new()),
3 => builder.with(StraightLineCorridors::new()),
_ => builder.with(BspCorridors::new())
}

let cspawn_roll = rng.roll_dice(1, 2);


if cspawn_roll == 1 {
builder.with(CorridorSpawner::new());
}

So we've added an equal chance of straight-line corridors and nearest-neighbor


corridors, and 50% of the time it will spawn entities in hallways.

...

696 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Doors

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Doors and corners, that's where they get you. If we're ever going to make Miller's (from
The Expanse - probably my favorite sci-� novel series of the moment) warning come
true - it would be a good idea to have doors in the game. Doors are a staple of
dungeon-bashing! We've waited this long to implement them so as to ensure that we
have good places to put them.

Doors are an entity, too


We'll start with simple, cosmetic doors that don't do anything at all. This will let us
work on placing them appropriately, and then we can implement some door-related
functionality. It's been a while since we added an entity type; fortunately, we have
everything we need for cosmetic doors in the existing components . Open up
spawner.rs , and refamiliarize yourself with it! Then we'll add a door spawner
function:

697 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn door(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('+'),
fg: RGB::named(rltk::CHOCOLATE),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Door".to_string() })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

So our cosmetic-only door is pretty simple: it has a glyph ( + is traditional in many


roguelikes), is brown, and it has a Name and a Position . That's really all we need to
make them appear on the map! We'll also modify spawn_entity to know what to do
when given a Door to spawn:

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Health Potion" => health_potion(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Rations" => rations(ecs, x, y),
"Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
"Bear Trap" => bear_trap(ecs, x, y),
"Door" => door(ecs, x, y),
_ => {}
}

We won't add doors to the spawn tables; it wouldn't make a lot of sense for them to
randomly appear in rooms!

698 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Placing doors
We'll create a new builder (we're still in the map section, after all!) that can place
doors. So in map_builders , make a new �le: door_placement.rs :

use super::{MetaMapBuilder, BuilderMap };


use rltk::RandomNumberGenerator;

pub struct DoorPlacement {}

impl MetaMapBuilder for DoorPlacement {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.doors(rng, build_data);
}
}

impl DoorPlacement {
#[allow(dead_code)]
pub fn new() -> Box<DoorPlacement> {
Box::new(DoorPlacement{ })
}

fn doors(&mut self, _rng : &mut RandomNumberGenerator, _build_data :


&mut BuilderMap) {
}
}

This is an empty skeleton of a meta-builder. Let's deal with the easiest case �rst: when
we have corridor data, that provides something of a blueprint as to where doors
might �t. We'll start with a new function, door_possible :

699 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn door_possible(&self, build_data : &mut BuilderMap, idx : usize) -> bool


{
let x = idx % build_data.map.width as usize;
let y = idx / build_data.map.width as usize;

// Check for east-west door possibility


if build_data.map.tiles[idx] == TileType::Floor &&
(x > 1 && build_data.map.tiles[idx-1] == TileType::Floor) &&
(x < build_data.map.width-2 && build_data.map.tiles[idx+1] ==
TileType::Floor) &&
(y > 1 && build_data.map.tiles[idx - build_data.map.width as
usize] == TileType::Wall) &&
(y < build_data.map.height-2 && build_data.map.tiles[idx +
build_data.map.width as usize] == TileType::Wall)
{
return true;
}

// Check for north-south door possibility


if build_data.map.tiles[idx] == TileType::Floor &&
(x > 1 && build_data.map.tiles[idx-1] == TileType::Wall) &&
(x < build_data.map.width-2 && build_data.map.tiles[idx+1] ==
TileType::Wall) &&
(y > 1 && build_data.map.tiles[idx - build_data.map.width as
usize] == TileType::Floor) &&
(y < build_data.map.height-2 && build_data.map.tiles[idx +
build_data.map.width as usize] == TileType::Floor)
{
return true;
}

false
}

There really are only two places in which a door makes sense: with east-west open
and north-south blocked, and vice versa. We don't want doors to appear in open
areas. So this function checks for those conditions, and returns true if a door is
possible - and false otherwise. Now we expand the doors function to scan
corridors and put doors at their beginning:

700 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn doors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut


BuilderMap) {
if let Some(halls_original) = &build_data.corridors {
let halls = halls_original.clone(); // To avoid nested borrowing
for hall in halls.iter() {
if hall.len() > 2 { // We aren't interested in tiny corridors
if self.door_possible(build_data, hall[0]) {
build_data.spawn_list.push((hall[0],
"Door".to_string()));
}
}
}
}
}

We start by checking that there is corridor information to use. If there is, we take a
copy (to make the borrow checker happy - otherwise we're borrowing twice into
halls ) and iterate it. Each entry is a hallway - a vector of tiles that make up that hall.
We're only interested in halls with more than 2 entries - to avoid really short corridors
with doors attached. So, if its long enough - we check to see if a door makes sense at
index 0 of the hall; if it does, we add it to the spawn list.

We'll quickly modify random_builder again to create a case in which there are
probably doors to spawn:

let mut builder = BuilderChain::new(new_depth);


builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(StraightLineCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(CorridorSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder.with(DoorPlacement::new());
builder

We cargo run the project, and lo and behold - doors:

701 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

What about other designs?


It's certainly possible to scan other maps tile-by-tile looking to see if there is a
possibility of a door appearing. Lets do that:

if let Some(halls_original) = &build_data.corridors {


let halls = halls_original.clone(); // To avoid nested borrowing
for hall in halls.iter() {
if hall.len() > 2 { // We aren't interested in tiny corridors
if self.door_possible(build_data, hall[0]) {
build_data.spawn_list.push((hall[0],
"Door".to_string()));
}
}
}
} else {
// There are no corridors - scan for possible places
let tiles = build_data.map.tiles.clone();
for (i, tile) in tiles.iter().enumerate() {
if *tile == TileType::Floor && self.door_possible(build_data,
i) {
build_data.spawn_list.push((i, "Door".to_string()));
}
}
}
}

Modify your random_builder to use a map without hallways:

702 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let mut builder = BuilderChain::new(new_depth);


builder.start_with(BspInteriorBuilder::new());
builder.with(DoorPlacement::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder

You can cargo run the project and see doors:

That worked rather well!

Restore our random function


We'll but random_builder back to how it was, with one change: we'll add a door
spawner as the �nal step:

703 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth);
let type_roll = rng.roll_dice(1, 2);
match type_roll {
1 => random_room_builder(rng, &mut builder),
_ => random_shape_builder(rng, &mut builder)
}

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());
}

if rng.roll_dice(1, 20)==1 {

builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FOR
}

builder.with(DoorPlacement::new());
builder.with(PrefabBuilder::vaults());

builder
}

Notice that we added it before we add vaults; that's deliberate - the vault gets the
chance to spawn and remove any doors that would interfere with it.

Making Doors Do Something


Doors have a few properties: when closed, they block movement and visibility. They
can be opened (optionally requiring unlocking, but we're not going there yet), at which
point you can see through them just �ne.

Let's start by "blocking out" (suggesting!) some new components. In spawner.rs :

704 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn door(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('+'),
fg: RGB::named(rltk::CHOCOLATE),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Door".to_string() })
.with(BlocksTile{})
.with(BlocksVisibility{})
.with(Door{open: false})
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

There are two new component types here!

BlocksVisibility will do what it says - prevent you (and monsters) from seeing
through it. It's nice to have this as a component rather than a special-case,
because now you can make anything block visibility. A really big treasure chest, a
giant or even a moving wall - it makes sense to be able to prevent seeing
through them.
Door - which denotes that it is a door, and will need its own handling.

Open up components.rs and we'll make these new components:

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct BlocksVisibility {}

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct Door {
pub open: bool
}

As with all components, don't forget to register them both in main and in
saveload_system.rs .

705 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Extending The Visibility System to Handle Entities Blocking Your


View

Since �eld of view is handled by RLTK, which relies upon a Map trait - we need to
extend our map class to handle the concept. Add a new �eld:

#[derive(Default, Serialize, Deserialize, Clone)]


pub struct Map {
pub tiles : Vec<TileType>,
pub width : i32,
pub height : i32,
pub revealed_tiles : Vec<bool>,
pub visible_tiles : Vec<bool>,
pub blocked : Vec<bool>,
pub depth : i32,
pub bloodstains : HashSet<usize>,
pub view_blocked : HashSet<usize>,

#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content : Vec<Vec<Entity>>
}

And update the constructor so it can't be forgotten:

pub fn new(new_depth : i32) -> Map {


Map{
tiles : vec![TileType::Wall; MAPCOUNT],
width : MAPWIDTH as i32,
height: MAPHEIGHT as i32,
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
tile_content : vec![Vec::new(); MAPCOUNT],
depth: new_depth,
bloodstains: HashSet::new(),
view_blocked : HashSet::new()
}
}

Now we'll update the is_opaque function (used by �eld-of-view) to include a check
against it:

706 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn is_opaque(&self, idx:i32) -> bool {


let idx_u = idx as usize;
self.tiles[idx_u] == TileType::Wall ||
self.view_blocked.contains(&idx_u)
}

We'll also have to visit visibility_system.rs to populate this data. We'll need to
extend the system's data to retrieve a little more:

type SystemData = ( WriteExpect<'a, Map>,


Entities<'a>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Position>,
ReadStorage<'a, Player>,
WriteStorage<'a, Hidden>,
WriteExpect<'a, rltk::RandomNumberGenerator>,
WriteExpect<'a, GameLog>,
ReadStorage<'a, Name>,
ReadStorage<'a, BlocksVisibility>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, entities, mut viewshed, pos, player,
mut hidden, mut rng, mut log, names, blocks_visibility) =
data;
...

Right after that, we'll loop through all entities that block visibility and set their index in
the view_blocked HashSet :

map.view_blocked.clear();
for (block_pos, _block) in (&pos, &blocks_visibility).join() {
let idx = map.xy_idx(block_pos.x, block_pos.y);
map.view_blocked.insert(idx);
}

If you cargo run the project now, you'll see that doors now block line-of-sight:

707 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Handling Doors

Moving against a closed door should open it, and then you can pass freely through
(we could add an open and close command - maybe we will later - but for now lets
keep it simple). Open up player.rs , and we'll add the functionality to
try_move_player :

708 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
let mut doors = ecs.write_storage::<Door>();
let mut blocks_visibility = ecs.write_storage::<BlocksVisibility>();
let mut blocks_movement = ecs.write_storage::<BlocksTile>();
let mut renderables = ecs.write_storage::<Renderable>();

for (entity, _player, pos, viewshed) in (&entities, &players, &mut


positions, &mut viewsheds).join() {
let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);

for potential_target in map.tile_content[destination_idx].iter() {


let target = combat_stats.get(*potential_target);
if let Some(_target) = target {
wants_to_melee.insert(entity, WantsToMelee{ target:
*potential_target }).expect("Add target failed");
return;
}
let door = doors.get_mut(*potential_target);
if let Some(door) = door {
door.open = true;
blocks_visibility.remove(*potential_target);
blocks_movement.remove(*potential_target);
let glyph = renderables.get_mut(*potential_target).unwrap();
glyph.glyph = rltk::to_cp437('/');
viewshed.dirty = true;
}
}
...

Let's walk through it:

1. We obtain write access to the storages for Door , BlocksVisibility ,


BlocksTile and Renderable .
2. We iterate potential targets in the movement tile, handling melee as before.
3. We also check if potential targets are a door. If they are:
1. Set the door open variable to true .
2. Remove the BlocksVisibility entry - you can see through it, now (and so
can monsters!).
3. Remove the BlocksTile entry - you can move through it, now (and so can
everyone else!)
4. Update the glyph to show an open doorway.
5. We mark the viewshed as dirty, to now reveal what you can see through

709 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

the door.

If you cargo run the project now, you get the desired functionality:

Too many doors!


On the non-corridor maps, there is a slight problem when play-testing the door
placement: there are doors everywhere. Lets reduce the frequency of door placement.
We'll just add a little randomness:

710 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn doors(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut


BuilderMap) {
if let Some(halls_original) = &build_data.corridors {
let halls = halls_original.clone(); // To avoid nested borrowing
for hall in halls.iter() {
if hall.len() > 2 { // We aren't interested in tiny corridors
if self.door_possible(build_data, hall[0]) {
build_data.spawn_list.push((hall[0],
"Door".to_string()));
}
}
}
} else {
// There are no corridors - scan for possible places
let tiles = build_data.map.tiles.clone();
for (i, tile) in tiles.iter().enumerate() {
if *tile == TileType::Floor && self.door_possible(build_data,
i) && rng.roll_dice(1,3)==1 {
build_data.spawn_list.push((i, "Door".to_string()));
}
}
}
}

This gives a 1 in 3 chance of any possible door placement yielding a door. From playing
the game, this feels about right. It may not work for you - so you can change it! You
may even want to make it a parameter.

Doors on top of other entities


Sometimes, a door spawns on top of another entity. It's rare, but it can happen. Lets
prevent that issue from occurring. We can �x this with a quick scan of the spawn list in
door_possible :

711 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn door_possible(&self, build_data : &mut BuilderMap, idx : usize) -> bool


{
let mut blocked = false;
for spawn in build_data.spawn_list.iter() {
if spawn.0 == idx { blocked = true; }
}
if blocked { return false; }
...

If speed becomes a concern, this would be easy to speed up (make a quick HashSet
of occupied tiles, and query that instead of the whole list) - but we haven't really had
any performance issues, and map building runs outside of the main loop (so it's once
per level, not every frame) - so chances are that you don't need it.

Addendum: Fixing WFC


In our random_builder , we've made a mistake! Waveform Collapse changes the
nature of maps, and should adjust spawn, entry and exit points. Here's the correct
code:

if rng.roll_dice(1, 3)==1 {
builder.with(WaveformCollapseBuilder::new());

// Now set the start to a random starting area


let (start_x, start_y) = random_start_position(rng);
builder.with(AreaStartingPosition::new(start_x, start_y));

// Setup an exit and spawn mobs


builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
}

Wrap-Up
That's it for doors! There's de�nitely room for improvement in the future - but the

712 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

feature is working. You can approach a door, and it blocks both movement and line-
of-sight (so the occupants of the room won't bother you). Open it, and you can see
through - and the occupants can see you back. Now it's open, you can travel through
it. That's pretty close to the de�nition of a door!

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Decoupling map size from terminal size

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

So far, we've �rmly tied map size to terminal resolution. You have an 80x50 screen,
and use a few lines for the user interface - so everything we've made is 80 tiles wide
and 43 tiles high. As you've seen in previous chapters, you can do a lot with 3,440 tiles
- but sometimes you want more (and sometimes you want less). You may also want a
big, open world setting - but we're not going to go there yet! This chapter will start by
decoupling the camera from the map, and then enable map size and screen size to
di�er. The di�cult topic of resizing the user interface will be left for future
development.

713 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Introducing a Camera
A common abstraction in games is to separate what you are viewing (the map and
entities) from how you are viewing it - the camera. The camera typically follows your
brave adventurer around the map, showing you the world from their point of view. In
3D games, the camera can be pretty complicated; in top-down roguelikes (viewing the
map from above), it typically centers the view on the player's @ .

Predictably enough, we'll start by making a new �le: camera.rs . To enable it, add
pub mod camera towards the top of main.rs (with the other module access).

We'll start out by making a function, render_camera , and doing some calculations
we'll need:

use specs::prelude::*;
use super::{Map,TileType,Position,Renderable,Hidden};
use rltk::{Point, Rltk, Console, RGB};

const SHOW_BOUNDARIES : bool = true;

pub fn render_camera(ecs: &World, ctx : &mut Rltk) {


let map = ecs.fetch::<Map>();
let player_pos = ecs.fetch::<Point>();
let (x_chars, y_chars) = ctx.get_char_size();

let center_x = (x_chars / 2) as i32;


let center_y = (y_chars / 2) as i32;

let min_x = player_pos.x - center_x;


let max_x = min_x + x_chars as i32;
let min_y = player_pos.y - center_y;
let max_y = min_.y + y_chars as i32;
...

I've broken this down into steps to make it clear what's going on:

1. We create a constant, SHOW_BOUNDARIES . If true, we'll render a marker for out-of-


bounds tiles so we know where the edges of the map are. Most of the time, this
will be false (no need for the player to get that information), but it's very handy
for debugging.
2. We start by retrieving the map from the ECS World.

714 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

3. We then retrieve the player's position from the ECS World.


4. We ask RLTK for the current console dimensions, in character space (so with an
8x8 font, 80x50 ).
5. We calculate the center of the console.
6. We set min_x to the left-most tile, relative to the player. So the player's x
position, minus the center of the console. This will center the x axis on the
player.
7. We set max_x to the be min_x plus the console width - again, ensuring that the
player is centered.
8. We do the same for min_y and max_y .

So we've established where the camera is in world space - that is, coordinates on the
map itself. We've also established that with our camera view, that should be the center
of the rendered area.

Now we'll render the actual map:

let map_width = map.width-1;


let map_height = map.height-1;

let mut y = 0;
for ty in min_y .. max_y {
let mut x = 0;
for tx in min_x .. max_x {
if tx > 0 && tx < map_width && ty > 0 && ty < map_height {
let idx = map.xy_idx(tx, ty);
if map.revealed_tiles[idx] {
let (glyph, fg, bg) = get_tile_glyph(idx, &*map);
ctx.set(x, y, fg, bg, glyph);
}
} else if SHOW_BOUNDARIES {
ctx.set(x, y, RGB::named(rltk::GRAY), RGB::named(rltk::BLACK),
rltk::to_cp437('·'));
}
x += 1;
}
y += 1;
}

This is similar to our old draw_map code, but a little more complicated. Lets walk
through it:

715 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. We set y to 0; we're using x and y to represent actual screen coordinates.


2. We loop ty from min_y to max_y . We're using tx and ty for map coordinates
- or "tile space" coordinates (hence the t ).
1. We set x to zero, because we're starting a new row on the screen.
2. We loop from min_x to max_x in the variable tx - so we're covering the
visible tile space in tx .
1. We do a clipping check. We check that tx and ty are actually inside
the map boundaries. It's quite likely that the player will visit the edge
of the map, and you don't want to crash because they can see tiles
that aren't in the map area!
2. We calculate the idx (index) of the tx/ty position, telling us where
on the map this screen location is.
3. If it is revealed, we call the mysterious get_tile_glyph function for
this index (more on that in a moment), and set the results on the
screen.
4. If the tile is o� the map and SHOW_BOUNDARIES is true - we draw a
dot.
5. Regardless of clipping, we add 1 to x - we're moving to the next
column.
3. We add one to y , since we're now moving down the screen.
3. We've rendered a map!

That's actually quite simple - we're rendering what is e�ectively a window looking into
part of the map, rather than the whole map - and centering the window on the player.

Next, we need to render our entities:

716 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let positions = ecs.read_storage::<Position>();


let renderables = ecs.read_storage::<Renderable>();
let hidden = ecs.read_storage::<Hidden>();
let map = ecs.fetch::<Map>();

let mut data = (&positions, &renderables, !&hidden).join().collect::


<Vec<_>>();
data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
for (pos, render, _hidden) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] {
let entity_screen_x = pos.x - min_x;
let entity_screen_y = pos.y - min_y;
if entity_screen_x > 0 && entity_screen_x < map_width &&
entity_screen_y > 0 && entity_screen_y < map_height {
ctx.set(entity_screen_x, entity_screen_y, render.fg,
render.bg, render.glyph);
}
}
}

If this looks familiar, it's because it's the same as the render code that used to live in
main.rs . There are two major di�erences: we subtract min_x and min_y from the
x and y coordinates, to line the entities up with our camera view. We also perform
clipping on the coordinates - we won't try and render anything that isn't on the screen.

We previously referred to get_tile_glyph , so here it is:

717 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn get_tile_glyph(idx: usize, map : &Map) -> (u8, RGB, RGB) {


let glyph;
let mut fg;
let mut bg = RGB::from_f32(0., 0., 0.);

match map.tiles[idx] {
TileType::Floor => {
glyph = rltk::to_cp437('.');
fg = RGB::from_f32(0.0, 0.5, 0.5);
}
TileType::Wall => {
let x = idx as i32 % map.width;
let y = idx as i32 / map.width;
glyph = wall_glyph(&*map, x, y);
fg = RGB::from_f32(0., 1.0, 0.);
}
TileType::DownStairs => {
glyph = rltk::to_cp437('>');
fg = RGB::from_f32(0., 1.0, 1.0);
}
}
if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.);
}
if !map.visible_tiles[idx] {
fg = fg.to_greyscale();
bg = RGB::from_f32(0., 0., 0.); // Don't show stains out of visual
range
}

(glyph, fg, bg)


}

This is very similar to the code from draw_map we wrote ages ago, but instead of
drawing to the map it returns a glyph, foreground and background colors. It still
handles bloodstains, greying out areas that you can't see, and calls wall_glyph for
nice walls. We've simply copied wall_glyph over from map.rs :

718 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {


if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 {
return 35; }
let mut mask : u8 = 0;

if is_revealed_and_wall(map, x, y - 1) { mask +=1; }


if is_revealed_and_wall(map, x, y + 1) { mask +=2; }
if is_revealed_and_wall(map, x - 1, y) { mask +=4; }
if is_revealed_and_wall(map, x + 1, y) { mask +=8; }

match mask {
0 => { 9 } // Pillar because we can't see neighbors
1 => { 186 } // Wall only to the north
2 => { 186 } // Wall only to the south
3 => { 186 } // Wall to the north and south
4 => { 205 } // Wall only to the west
5 => { 188 } // Wall to the north and west
6 => { 187 } // Wall to the south and west
7 => { 185 } // Wall to the north, south and west
8 => { 205 } // Wall only to the east
9 => { 200 } // Wall to the north and east
10 => { 201 } // Wall to the south and east
11 => { 204 } // Wall to the north, south and east
12 => { 205 } // Wall to the east and west
13 => { 202 } // Wall to the east, west, and south
14 => { 203 } // Wall to the east, west, and north
_ => { 35 } // We missed one?
}
}

fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {


let idx = map.xy_idx(x, y);
map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx]
}

Finally, in main.rs �nd the following code:

719 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
RunState::GameOver{..} => {}
_ => {
draw_map(&self.ecs.fetch::<Map>(), ctx);
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();
let hidden = self.ecs.read_storage::<Hidden>();
let map = self.ecs.fetch::<Map>();

let mut data = (&positions, &renderables, !&hidden).join().collect::


<Vec<_>>();
data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
for (pos, render, _hidden) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg,
render.bg, render.glyph) }
}
gui::draw_ui(&self.ecs, ctx);
}
...

We can now replace that with a much shorter piece of code:

RunState::GameOver{..} => {}
_ => {
camera::render_camera(&self.ecs, ctx);
gui::draw_ui(&self.ecs, ctx);
}

If you cargo run the project now, you'll see that we can still play - and the camera is
centered on the player:

720 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Oops - we didn't move the tooltips or targeting!

If you play for a bit, you'll probably notice that tool-tips aren't working (they are still
bound to the map coordinates). We should �x that! First of all, it's becoming obvious
that the screen boundaries are something we'll need in more than just the drawing
code, so lets break it into a separate function in camera.rs :

721 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn get_screen_bounds(ecs: &World, ctx : &mut Rltk) -> (i32, i32, i32,
i32) {
let player_pos = ecs.fetch::<Point>();
let (x_chars, y_chars) = ctx.get_char_size();

let center_x = (x_chars / 2) as i32;


let center_y = (y_chars / 2) as i32;

let min_x = player_pos.x - center_x;


let max_x = min_x + x_chars as i32;
let min_y = player_pos.y - center_y;
let max_y = min_y + y_chars as i32;

(min_x, max_x, min_y, max_y)


}

pub fn render_camera(ecs: &World, ctx : &mut Rltk) {


let map = ecs.fetch::<Map>();
let (min_x, max_x, min_y, max_y) = get_screen_bounds(ecs, ctx);

It's the same code from render_camera - just moved into a function. We've also
extended render_camera to use the function, rather than repeating ourselves. Now
we can go into gui.rs and edit draw_tooltips to use the camera position quite
easily:

722 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {


let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs,
ctx);
let map = ecs.fetch::<Map>();
let names = ecs.read_storage::<Name>();
let positions = ecs.read_storage::<Position>();
let hidden = ecs.read_storage::<Hidden>();

let mouse_pos = ctx.mouse_pos();


let mut mouse_map_pos = mouse_pos;
mouse_map_pos.0 += min_x;
mouse_map_pos.1 += min_y;
if mouse_map_pos.0 >= map.width-1 || mouse_map_pos.1 >= map.height-1
|| mouse_map_pos.0 < 1 || mouse_map_pos.1 < 1
{
return;
}
let mut tooltip : Vec<String> = Vec::new();
for (name, position, _hidden) in (&names, &positions, !&hidden).join()
{
if position.x == mouse_map_pos.0 && position.y == mouse_map_pos.1
{
tooltip.push(name.name.to_string());
}
}
...

So our changes are:

1. At the beginning, we retrieve the screen boundaries with


camera::get_screen_bounds . We aren't going to use the max variables, so we
put an underscore before them to let Rust know that we're intentionally ignoring
them.
2. After getting the mouse_pos , we make a new mouse_map_pos variable. It is equal
to mouse_pos , but we add the min_x and min_y values - o�setting it to match
the visible coordinates.
3. We extended our clipping to check all directions, so tooltips don't crash the
game when you look at an area outside of the actual map because the viewport
is at an extreme end of the map.
4. Our comparison for position now compares with mouse_map_pos rather than
mouse_pos .
5. That's it - the rest can be unchanged.

723 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you cargo run now, tooltips will work:

Fixing Targeting
If you play for a bit, you'll also notice if you try and use a �reball or similar e�ect - the
targeting system is completely out of whack. It's still referencing the screen/map
positions from when they were directly linked. So you see the available tiles, but they
are in completely the wrong place! We should �x that, too.

In gui.rs , we'll edit the function ranged_target :

724 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) ->
(ItemMenuResult, Option<Point>) {
let (min_x, max_x, min_y, max_y) = camera::get_screen_bounds(&gs.ecs,
ctx);
let player_entity = gs.ecs.fetch::<Entity>();
let player_pos = gs.ecs.fetch::<Point>();
let viewsheds = gs.ecs.read_storage::<Viewshed>();

ctx.print_color(5, 0, RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK), "Select Target:");

// Highlight available target cells


let mut available_cells = Vec::new();
let visible = viewsheds.get(*player_entity);
if let Some(visible) = visible {
// We have a viewshed
for idx in visible.visible_tiles.iter() {
let distance =
rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx);
if distance <= range as f32 {
let screen_x = idx.x - min_x;
let screen_y = idx.y - min_y;
if screen_x > min_x && screen_x < max_x && screen_y >
min_y && screen_y < max_y {
ctx.set_bg(screen_x, screen_y,
RGB::named(rltk::BLUE));
available_cells.push(idx);
}
}
}
} else {
return (ItemMenuResult::Cancel, None);
}

// Draw mouse cursor


let mouse_pos = ctx.mouse_pos();
let mut mouse_map_pos = mouse_pos;
mouse_map_pos.0 += min_x;
mouse_map_pos.1 += min_y;
let mut valid_target = false;
for idx in available_cells.iter() { if idx.x == mouse_map_pos.0 &&
idx.y == mouse_map_pos.1 { valid_target = true; } }
if valid_target {
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN));
if ctx.left_click {

725 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

return (ItemMenuResult::Selected,
Some(Point::new(mouse_map_pos.0, mouse_map_pos.1)));
}
} else {
ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED));
if ctx.left_click {
return (ItemMenuResult::Cancel, None);
}
}

(ItemMenuResult::NoResponse, None)
}

This is fundamentally what we had before, with some changes:

1. We obtain the boundaries at the beginning, once again with


camera::get_screen_bounds .
2. In our visible target tiles section, we're calculating screen_x and screen_y by
taking the map index and adding our min_x and min_y values. We then check
to see if it is on the screen, before drawing the targeting highlight at those
locations.
3. We use the same mouse_map_pos calculation after calculating mouse_pos .
4. We then reference the mouse_map_pos when checking if a target is under the
mouse, or selected.

If you cargo run now, targeting will work:

Addendum 1: Fixing a bug in the visibility calculator

726 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

You may encounter a crash when boundaries haven't properly been applied to the
edge of the map. In visibility_system.rs , the following �x takes care of it:

for vis in viewshed.visible_tiles.iter() {


if vis.x > 0 && vis.x < map.width-1 && vis.y > 0 && vis.y <
map.height-1 {
let idx = map.xy_idx(vis.x, vis.y);
map.revealed_tiles[idx] = true;
map.visible_tiles[idx] = true;

// Chance to reveal hidden things


for e in map.tile_content[idx].iter() {
let maybe_hidden = hidden.get(*e);
if let Some(_maybe_hidden) = maybe_hidden {
if rng.roll_dice(1,24)==1 {
let name = names.get(*e);
if let Some(name) = name {
log.entries.insert(0, format!("You spotted a
{}.", &name.name));
}
hidden.remove(*e);
}
}
}
}
}

This will be merged back into the tutorial soon - this addendum is to let you know we
�xed it.

Variable map sizes


Now that our map isn't directly linked to our screen, we can have maps of any size we
want! A word of caution: if you go with a huge map, it will take your player a really long
time to explore it all - and it becomes more and more challenging to ensure that all of
the map is interesting enough to want to visit it.

An easy start

727 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Let's start with the simplest possible case: changing the size of the map globally. Go to
map.rs , and �nd the constants MAPWIDTH , MAPHEIGHT and MAPCOUNT . Lets change
them to a square map:

pub const MAPWIDTH : usize = 64;


pub const MAPHEIGHT : usize = 64;
pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;

If you cargo run the project, it should work - we've been pretty good about using
either map.width / map.height or these constants throughout the program. The
algorithms run, and try to make a map for your use. Here's our player wandering a
64x64 map - note how the sides of the map are displayed as out-of-bounds:

Harder: removing the constants

Now delete the three constants from map.rs , and watch your IDE paint the world red.
Before we start �xing things, we'll add a bit more red:

728 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

/// Generates an empty map, consisting entirely of solid walls


pub fn new(new_depth : i32, width: i32, height: i32) -> Map {
Map{
tiles : vec![TileType::Wall; MAPCOUNT],
width,
height,
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
tile_content : vec![Vec::new(); MAPCOUNT],
depth: new_depth,
bloodstains: HashSet::new(),
view_blocked : HashSet::new()
}
}

Now creating a map requires that you specify a size as well as depth. We can make a
start on �xing some errors by changing the constructor once more to use the speci�ed
size in creating the various vectors:

pub fn new(new_depth : i32, width: i32, height: i32) -> Map {


let map_tile_count = (width*height) as usize;
Map{
tiles : vec![TileType::Wall; map_tile_count],
width,
height,
revealed_tiles : vec![false; map_tile_count],
visible_tiles : vec![false; map_tile_count],
blocked : vec![false; map_tile_count],
tile_content : vec![Vec::new(); map_tile_count],
depth: new_depth,
bloodstains: HashSet::new(),
view_blocked : HashSet::new()
}
}

map.rs also has an error in draw_map . Fortunately, it's an easy �x:

729 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
// Move the coordinates
x += 1;
if x > (map.width * map.height) as i32-1 {
x = 0;
y += 1;
}
...

spawner.rs is an equally easy �x. Remove map::MAPWIDTH from the list of use
imports at the beginning, and �nd the spawn_entity function. We can obtain the
map width from the ECS directly:

pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {


let map = ecs.fetch::<Map>();
let width = map.width as usize;
let x = (*spawn.0 % width) as i32;
let y = (*spawn.0 / width) as i32;
std::mem::drop(map);
...

The issue in saveload_system.rs is also easy to �x. Around line 102, you can replace
MAPCOUNT with (worldmap.width * worldmap.height) as usize :

...
let mut deleteme : Option<Entity> = None;
{
let entities = ecs.entities();
let helper = ecs.read_storage::<SerializationHelper>();
let player = ecs.read_storage::<Player>();
let position = ecs.read_storage::<Position>();
for (e,h) in (&entities, &helper).join() {
let mut worldmap = ecs.write_resource::<super::map::Map>();
*worldmap = h.map.clone();
worldmap.tile_content = vec![Vec::new(); (worldmap.height *
worldmap.width) as usize];
deleteme = Some(e);
}
...

main.rs also needs some help. In tick , the MagicMapReveal code is a simple �x:

730 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

RunState::MagicMapReveal{row} => {
let mut map = self.ecs.fetch_mut::<Map>();
for x in 0..map.width {
let idx = map.xy_idx(x as i32,row);
map.revealed_tiles[idx] = true;
}
if row == map.height-1 {
newrunstate = RunState::MonsterTurn;
} else {
newrunstate = RunState::MagicMapReveal{ row: row+1 };
}
}

Down around line 451, we're also making a map with map::new(1) . We want to
introduce a map size here, so we go with map::new(1, 64, 64) (the size doesn't
really matter since we'll be replacing it with a map from a builder anyway).

Open up player.rs and you'll �nd that we've committed a real programming sin.
We've hard-coded 79 and 49 as map boundaries for player movement! Let's �x that:

if !map.blocked[destination_idx] {
pos.x = min(map.width-1 , max(0, pos.x + delta_x));
pos.y = min(map.height-1, max(0, pos.y + delta_y));

Finally, expanding our map_builders folder reveals a few errors. We're going to
introduce a couple more before we �x them! In map_builders/mod.rs we'll store the
requested map size:

pub struct BuilderMap {


pub spawn_list : Vec<(usize, String)>,
pub map : Map,
pub starting_position : Option<Position>,
pub rooms: Option<Vec<Rect>>,
pub corridors: Option<Vec<Vec<usize>>>,
pub history : Vec<Map>,
pub width: i32,
pub height: i32
}

We'll then update the constructor to use it:

731 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl BuilderChain {
pub fn new(new_depth : i32, width: i32, height: i32) -> BuilderChain {
BuilderChain{
starter: None,
builders: Vec::new(),
build_data : BuilderMap {
spawn_list: Vec::new(),
map: Map::new(new_depth, width, height),
starting_position: None,
rooms: None,
corridors: None,
history : Vec::new(),
width,
height
}
}
}

We also need to adjust the signature for random_builder to accept a map size:

pub fn random_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
let mut builder = BuilderChain::new(new_depth, width, height);
...

We'll also visit map_builders/waveform_collapse/mod.rs and make some �xes.


Basically, all our references to Map::new need to include the new size.

Finally, go back to main.rs and around line 370 you'll �nd our call to
random_builder . We need to add a width and height to it; for now, we'll use 64x64:

let mut builder = map_builders::random_builder(new_depth, &mut rng, 64,


64);

And that's it! If you cargo run the project now, you can roam a 64x64 map:

732 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

If you change that line to di�erent sizes, you can roam a huge map:

let mut builder = map_builders::random_builder(new_depth, &mut rng, 128,


128);

Voila - you are roaming a huge map! A de�nite downside of a huge map, and rolling a
largely open area is that sometimes it can be really di�cult to survive:

733 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Revisiting draw_map for progressive map rendering.


If you keep the huge map, open main.rs and set
const SHOW_MAPGEN_VISUALIZER : bool = false; to true - congratulations, you just
crashed the game! That's because we never adjusted the draw_map function that we
are using to verify map creation to handle maps of any size other than the original.
Oops. This does bring up a problem: on an ASCII terminal we can't simply render the
whole map and scale it down to �t. So we'll settle for rendering a portion of the map.

We'll add a new function to camera.rs :

734 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn render_debug_map(map : &Map, ctx : &mut Rltk) {


let player_pos = Point::new(map.width / 2, map.height / 2);
let (x_chars, y_chars) = ctx.get_char_size();

let center_x = (x_chars / 2) as i32;


let center_y = (y_chars / 2) as i32;

let min_x = player_pos.x - center_x;


let max_x = min_x + x_chars as i32;
let min_y = player_pos.y - center_y;
let max_y = min_y + y_chars as i32;

let map_width = map.width-1;


let map_height = map.height-1;

let mut y = 0;
for ty in min_y .. max_y {
let mut x = 0;
for tx in min_x .. max_x {
if tx > 0 && tx < map_width && ty > 0 && ty < map_height {
let idx = map.xy_idx(tx, ty);
if map.revealed_tiles[idx] {
let (glyph, fg, bg) = get_tile_glyph(idx, &*map);
ctx.set(x, y, fg, bg, glyph);
}
} else if SHOW_BOUNDARIES {
ctx.set(x, y, RGB::named(rltk::GRAY),
RGB::named(rltk::BLACK), rltk::to_cp437('·'));
}
x += 1;
}
y += 1;
}
}

This is a lot like our regular map drawing, but we lock the camera to the middle of the
map - and don't render entities.

In main.rs , replace the call to draw_map with:

camera::render_debug_map(&self.mapgen_history[self.mapgen_index], ctx);

Now you can go into map.rs and remove draw_map , wall_glyph and

735 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

is_revealed_and_wall completely.

Wrap-Up
We'll set the map size back to something reasonable in main.rs :

let mut builder = map_builders::random_builder(new_depth, &mut rng, 80,


50);

And - we're done! In this chapter, we've made it possible to have any size of map you
like. We've reverted to a "normal" size at the end - but we'll �nd this feature very
useful in the future. We can scale maps up or down - and the system won't mind at all.

...

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Section 3 - Wrap Up

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

736 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

And that wraps up section 3 - map building! We've covered a lot of ground in this
section, learning many techniques for map building. I hope it has inspired you to
search for your own interesting combinations, and make fun games! Procedurally
generating maps is a huge part of making a roguelike, hence it being such a large part
of this tutorial.

Section 4 will cover actually making a game.

...

Copyright (C) 2019, Herbert Wolverson.

Let's Make a Game!

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

So far, the tutorial has followed three sections:

1. Make a skeletal game, showing how to make a very minimalistic roguelike.


2. Add some essential genre features to the game, making it more fun to play.
3. Building lots of maps, a very important part of making fun roguelikes.

Now we're going to start a series of articles that actually makes a cohesive game from
our framework. It won't be huge, and it's unlikely to challenge for "best Roguelike
ever!" status - but it will explore the trials and tribulations that go with turning a tech
demo into a cohesive game.

The Berlin Interpretation


This game will stick closely to the genre, with very little ground-breaking innovation.

737 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

So we'll have a fantasy setting, dungeon diving, and limited progression. If you're
familiar with the Berlin Interpretation (an attempt at codifying what counts as a
roguelike in a world of games using the name!), we'll try to stick closely to the
important aspects:

High-value targets

Random Environment Generation is essential, and we've already covered a lot of


interesting ways to do it!
Permadeath de�nes the genre, so we'll go with it. We'll probably sneak in game
saving/loading, and look at how to handle non-permadeath if that's what you
want - but we'll stick to the principle, and its implication that you should be able
to beat a roguelike without dying.
Turn-based - we'll de�nitely stick to a turn-based setup, but will introduce varying
speeds and initiative.
Grid-based - we'll de�nitely stick to a grid-based system.
Non-modal - we'll probably break this one, by having systems that take you out
of the regular "all on one screen" play system.
Complexity - we'll strive for complexity, but try to keep the game playable without
being a Master's thesis topic!
Resource management - we've already got some of that with the hunger clock and
consumable items, but we'll de�nitely want to retain this as a de�ning trait.
Hack'n'slash - de�nitely!
Exploration and discovery - absolutely!

Low-value targets

Single player character - we're unlikely to introduce groups in this section, but we
might introduce friendly NPCs.
Monsters are similar to players - the ECS helps with this, since we're simulating the
player in the same way as NPCs. We'll stick to the basic principle.
Tactical challenge - always something to strive for; what good is a game without
challenge?
ASCII Display - we'll be sticking with this, but may �nd time to introduce graphical
tiles later.
Dungeons - of course! They don't have to be rooms and corridors, but we've
worked hard to have good rooms and corridors!
Numbers - this one is a little more controversial; not everyone wants to see a
giant wall of math every time they punch a goblin. We'll try for some balance - so
there are plenty of numbers, mostly visible, but they aren't essential to playing

738 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

the game.

So it seems pretty likely that with this constraints we will be making a real roguelike -
one that checks almost all of the boxes!

Setting
We've already decided on a fantasy-faux-medieval setting, but that doesn't mean it
has to be just like D&D or Tolkien! We'll try and introduce some fun and unique
elements in our setting.

Narrative
In the next chapter, we'll work on outlining our overall objective in a design document.
This will necessarily include some narrative, although roguelikes aren't really known
for deep stories!

...

Copyright (C) 2019, Herbert Wolverson.

Design Document

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

If you plan to �nish a game, it's important to set out your objectives ahead of time!
Traditionally, this has taken the form of a design document - a master document

739 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

outlining the game, and smaller sections detailing what you want to accomplish. In
this case, it also forms the skeleton of writing the section. There are thousands of
online references to writing game design documents. The format really doesn't matter
so long as it acts as a guiding light for development and gives you criteria for which
you can say "this is done!"

Because this is a tutorial, we're going to make the game design document a skeleton
for now, and �esh it out as we progress. That leaves some �exibility in writing the
guide on my end! So until this section is approaching complete, consider this to be a
living document - a perpetual work in progress, being expanded as we go. That's really
not how one should write a design document, but I have two luxuries that most teams
don't: no time limit, and no team members to direct!

Rusty Roguelike
Rusty Roguelike is a 2D traditional roguelike that attempts to capture the essentials of
the genre as it has developed since Rogue's release in 1980. Turn-based, tile-based
and centered on an adventurer's descent into a dungeon to retrieve the Amulet of
Yala (Yet Another Lost Amulet). The adventurer battles through numerous
procedurally generated levels to retrieve the amulet, and then must �ght their way
back to town to win the game.

Characters

The player controls one major character, Hero Protagonist as he/she/it battles through
the dungeon. Human NPCs will range from shop-keepers to fantasy RPG staples such
as bandits, brigands, sorcerers, etc. Other characters in the game will largely be
fantasy RPG staples: elves, dwarves, gnomes, hal�ings, orcs, goblins, trolls, ogres,
dragons, etc.

(Description of all NPCs should go here)

A stretch goal is to have NPCs belong to factions, and allow the clever player to
"faction farm" and adjust loyalties.

Ideally, NPC AI should be more intelligent than a rock.

740 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Story

This is not a story heavy game (Roguelikes are frequently shorter in story than
traditional RPGs, because you die and restart a lot and won't generally spend a lot of
time reading story/lore).

In the dark ages of yore, the sorcerer kings crafted the Amulet of Yala to bind the demons
of the Abyss - and end their reign of terror. A Golden Age followed, and the good races
�ourished. Now dark times have fallen upon the land once more, demons stir, and the
forces of darkness once again ravage the land. The Amulet of Yala may be the good folk's
last hope. After a long night in the pub, you realize that maybe it is your destiny to recover
it and restore tranquility to the land. Only slightly hungover, you set forth into the
dungeons beneath your home town - sure that you can be the one to set things right.

Theme

We'll aim for a traditional D&D style dungeon bash, with traps, monsters, the
occasional puzzle and "replayability". The game should be di�erent every time. A light-
hearted approach is preferred, with humor sprinkled liberally (another staple of the
genre). A "kitchen sink" approach is preferred to strictly focused realism - this is a
tutorial project, and it's better to have lots of themes (from which to learn) than a
single cohesive one in this case.

Story Progression

There is no horizontal progression - you don't keep any bene�ts from previous runs
through the game. So you always start in the same place as a new character, and gain
bene�ts for a single run only. You can go both up and down in the dungeon, returning
to town to sell items and goods. Progression on levels is preserved until you �nd the
Amulet of Yala - at which point the universe truly is out to get you until you return
home.

As a starting guide, consider the following progression. It will evolve and become
more random as we work on the game.

1. The game starts in town. In town, there are only minimal enemies (pickpockets,
thugs). You start in the to-be-named pub (tavern), armed only with a meager

741 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

purse, minimal starting equipment, a stein of beer, a dried sausage, a backpack


and a hangover. Town lets you visit various vendors.
2. You spelunk into the caves next to town, and �ght your way through natural
limestone caverns.
3. The limestone caverns give way to a ruined dwarven fortress, now occupied by
vile beasts - and a black dragon (thanks Mr. Sveller!).
4. Beneath the dwarven fortress lies a vast mushroom forest.
5. The mushroom forest gives way to a dark elven city.
6. The depths contain a citadel with a portal to the Abyss.
7. The Abyss is a nasty �ght against high-level demonic monsters. Here you �nd the
Amulet of Yala.
8. You �ght your way back up to town.

Travel should be facilitated with an equivalent of Town Portal scrolls from Diablo.

Gameplay

Gameplay should be a very traditional turn-based dungeon crawl, but with an


emphasis on making mechanics easy to use. At the base level, this is the "murder
hobo experience": you start with very little, subsist o� of what you �nd, kill (or evade)
monsters you encounter, and take their stu�! This should be sprinkled with staples of
the genre: item identi�cation, interesting magical items, stats and plenty of ways to
modify them, and multiple "valid" ways to play and beat the game. The game should
be di�cult but not impossible. Nothing that requires quick re�exes is permitted!

In a real game design document, we'd painstakingly describe each element here. For
the purposes of the tutorial, we'll add to the list as we write more.

Goals

Overall: The ultimate goal is to retrieve the Amulet of Yala - and return to town
(town portal spells stop working once you have it).
Short-term: Defeat enemies on each level.
Navigate each level of the dungeon, avoiding traps and reaching the exit.
Obtain lots of cool loot.
Earn bragging rights for your score.

742 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

User Skills

Navigating di�erent dungeons.


Tactical combat, learning AI behavior and terrain to maximize the chances of
survival.
Item identi�cation should be more than just "identify spell" - there should be
some hints/system that the user can use to better understand the odds.
Stat management - equip to improve your chances of survival for di�erent
threats.
Long and short-term resource management.
Ideally we want enough depth to spur "build" discussions.

Game Mechanics

We'll go with the tried and tested "sort of D&D" mechanics used by so many games
(and licensed under the Open Gaming License), but without being tied to a D&D-like
game. We'll expand upon this as we develop the tutorial.

Items and Power-Ups

The game should include a good variety of items. Broadly, items are divided as:

Wearables (armor, clothes, etc.)


Wearable specials (amulets, rings, etc.)
Defense items (shields and similar)
Melee weapons
Ranged weapons
Consumables (potions, scrolls, anything consumed by use)
Charged items (items that can only be used x times unless recharged)
Loot/junk to sell/scrap.
Food.

Other notes:

Eventually, items should have weight and inventory management becomes a


skill. Until then, it can be quite loose/ready.
Magical items shouldn't immediately reveal what they do, beyond being magical.

743 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Items should be drawn from loot tables that at least sort-of make sense.
"Props" are a special form of item that doesn't move, but can be interacted with.

Progression and challenge

As you defeat enemies, you earn experience points and can level up. This
improves your general abilities and grants access to better ways to defeat more
enemies!
The levels should increase in di�culty as you descend. "Out of level" enemies
are possible but very rare - to keep it fair.
Try to avoid capriciously killing the player with no hope of circumventing it.
Once the Amulet of Yala has been claimed, di�culty ramps up on all levels as you
�ght your way back up to town. Certain perks (like town portal) no longer work.
There is no progression between runs - it's entirely self-contained.

Losing

Losing is fun! In fact, a fair portion of the appeal of traditional roguelikes is that you
have one life - and it's "game over" when you succumb to your wounds/traps/being
turned into a banana. The game will feature permadeath - once you've died, your run
is over and you start afresh.

As a stretch goal, we may introduce some ways to mitigate/soften this.

Art Style

We'll aim for beautiful ASCII, and may introduce tiles.

Music and Sound

None! It would be nice to have once tiles are done, but fully voicing a modern RPG is
far beyond my resources.

744 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Technical Description

The game will be written in Rust, using RLTK_RS for its back-end. It will support all the
platforms on which Rust can compile and link to OpenGL, including Web Assembly for
browser-based play.

Marketing and Funding

This is a free tutorial, so the budget is approximately $0. If anyone wants to donate to
my Patreon I can promise eternal gratitude, a monster in your honor, and not a lot
else!

Localization

I'm hopeless at languages, so English it is.

Other Ideas

Anyone who has great ideas should send them to me. :-)

Wrap-Up
So there we have it: a very skeletal design document, with lots of holes in it. It's a good
idea to write one of these, especially when making a time-constrained game such as a
"7-day roguelike challenge". This chapter will keep improving in quality as more
features are implemented. For now, it's intended to serve as a baseline.

...

Copyright (C) 2019, Herbert Wolverson.

745 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Data-Driven Design: Raw Files

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

If you've ever played Dwarf Fortress, one of its de�ning characteristics (under the
hood) is the raw �le system. Huge amounts of the game are detailed in the raws , and
you can completely "mod" the game into something else. Other games, such as Tome
4 take this to the extent of de�ning scripting engine �les for everything - you can
customize the game to your heart's content. Once implemented, raws turn your
game into more of an engine - displaying/managing interactions with content written
in the raw �les. That isn't to say the engine is simple: it has to support everything that
one speci�es in the raw �les!

This is called data-driven design: your game is de�ned by the data describing it, more
than the actual engine mechanics. It has a few advantages:

It makes it very easy to make changes; you don't have to dig through
spawner.rs every time you want to change a goblin, or make a new variant such
as a cowardly goblin . Instead, you edit the raws to include your new monster,
add him/her/it to spawn, loot and faction tables, and the monster is now in your
game! (Unless of course being cowardly requires new support code - in which
case you write that, too).
Data-driven design meshes beautifully with Entity Component Systems (ECS).
The raws serve as a template, from which you build your entities by composing
components until it matches your raw description.
Data-driven design makes it easy for people to change the game you've created.
For a tutorial such as this, this is pretty essential: I'd much rather you come out
of this tutorial able to go forth and make your own game, rather than just re-
hashing this one!

746 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

A downside of web assembly


Web assembly doesn't make it easy to read �les from your computer. That's why we
started using the embedding system for assets; otherwise you have to make a bunch
of hooks to read game data with JavaScript calls to download resources, obtain them
as arrays of data, and pass the arrays into the Web Assembly module. There are
probably better ways to do it than embedding everything, but until I �nd a good one
(that also works in native code), we'll stick to embedding.

That gets rid of one advantage of data-driven design: you still have to recompile the
game. So we'll make the embedding optional; if we can read a �le from disk, we'll do
so. In practice, this will mean that when you ship your game, you have to include the
executable and the raw �les - or embed them in the �nal build.

Deciding upon a format for our Raw �les


In some projects, I've used the scripting language Lua for this sort of thing. It's a great
language, and having executable con�guration is surprisingly useful (the con�guration
can include functions and helpers to build itself). That's overkill for this project. We
already support JSON in our saving/loading of the game, so we'll use it for Raws also.

Taking a look at spawner.rs in the current game should give us some clues as to
what to put into these �les. Thanks to our use of components, there's already a lot of
shared functionality we can build upon. For example, the de�nition for a health potion
looks like this:

747 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn health_potion(ecs: &mut World, x: i32, y: i32) {


ecs.create_entity()
.with(Position{ x, y })
.with(Renderable{
glyph: rltk::to_cp437('¡'),
fg: RGB::named(rltk::MAGENTA),
bg: RGB::named(rltk::BLACK),
render_order: 2
})
.with(Name{ name : "Health Potion".to_string() })
.with(Item{})
.with(Consumable{})
.with(ProvidesHealing{ heal_amount: 8 })
.marked::<SimpleMarker<SerializeMe>>()
.build();
}

In JSON, we might go for a representation like this (just an example):

{
"name" : "Healing Potion",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000"
},
"consumable" : {
"effects" : { "provides_healing" : "8" }
}
}

Making a raw �les


Your package should be laid out like this:

| Root folder
\ - src (your source files)

At the root level, we'll make a new directory/folder called raws . So your tree should
look like this:

748 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

| Root folder
\ - src (your source files)
\ - raws

In this directory, create a new �le: spawns.json . We'll temporarily put all of our
de�nitions into one �le; this will change later, but we want to get support for our data-
driven ambitions bootstrapped. In this �le, we'll put de�nitions for some of the
entities we currently support in spawner.rs . We'll start with just a couple of items:

{
{
"items" : [
{
"name" : "Health Potion",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : { "provides_healing" : "8" }
}
},

{
"name" : "Magic Missile Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20"
}
}
}
]
}

If you aren't familiar with the JSON format, it's basically a JavaScript dump of data:

749 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We wrap the �le in { and } to denote the object we are loading. This will be our
Raws object, eventually.
Then we have an array called Items - which will hold our items.
Each Item has a name - this maps directly to the Name component.
Items may have a renderable structure, listing glyph, foreground and
background colors.
These items are consumable , and we list their e�ects in a "key/value map" -
basically a HashMap like we've used before, a Dictionary in other languages.

We'll be adding a lot more to the spawns list eventually, but lets start by making these
work.

Embedding the Raw Files


In your project src directory, make a new directory: src/raws . We can reasonably
expect this module to become quite large, so we'll support breaking it into smaller
pieces from the beginning. To comply with Rust's requirements for building modules,
make a new �le called mod.rs in the new folder:

rltk::embedded_resource!(RAW_FILE, "../../raws/spawns.json");

pub fn load_raws() {
rltk::link_resource!(RAW_FILE, "../../raws/spawns.json");
}

And at the top of main.rs add it to the list of modules we use:

pub mod raws;

In our initialization, add a call to load_raws after component initialization and before
you start adding to World :

750 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
gs.ecs.register::<Door>();
gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());

raws::load_raws();

gs.ecs.insert(Map::new(1, 64, 64));


...

The spawns.json �le will now be embedded into your executable, courtesy of RLTK's
embedding system.

Parsing the raw �les


This is the hard part: we need a way to read the JSON �le we've created, and to turn it
into a format we can use within Rust. Going back to mod.rs , we can expand the
function to load our embedded data as a string:

// Retrieve the raw data as an array of u8 (8-bit unsigned chars)


let raw_data = rltk::embedding::EMBED
.lock()
.unwrap()
.get_resource("../../raws/spawns.json".to_string())
.unwrap();
let raw_string = std::str::from_utf8(&raw_data).expect("Unable to convert
to a valid UTF-8 string.");

This will panic (crash) if it isn't able to �nd the resource, or if it is unable to parse it as
a regular string (Rust likes UTF-8 Unicode encoding, so we'll go with it. It lets us include
extended glyphs, which we can parse via RLTK's to_cp437 function - so it works out
nicely!).

Now we need to actually parse the JSON into something usable. Just like our
saveload.rs system, we can do this with Serde. For now, we'll just dump the results
to the console so we can see that it did something:

751 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let decoder : Raws = serde_json::from_str(&raw_string).expect("Unable to


parse JSON");
println!("{:?}", decoder);

(See the cryptic {:?} ? That's a way to print debug information about a structure). This
will fail to compile, because we haven't actually implemented Raws - the type it is
looking for.

For clarity, we'll put the classes that actually handle the data in their own �le,
raws/item_structs.rs . Here's the �le:

use serde::{Deserialize};
use std::collections::HashMap;

#[derive(Deserialize, Debug)]
pub struct Raws {
pub items : Vec<Item>
}

#[derive(Deserialize, Debug)]
pub struct Item {
pub name : String,
pub renderable : Option<Renderable>,
pub consumable : Option<Consumable>
}

#[derive(Deserialize, Debug)]
pub struct Renderable {
pub glyph: String,
pub fg : String,
pub bg : String,
pub order: i32
}

#[derive(Deserialize, Debug)]
pub struct Consumable {
pub effects : HashMap<String, String>
}

At the top of the �le, make sure to include use serde::{Deserialize}; and
use std::collections::HashMap; to include the types we need. Also notice that we
have included Debug in the derived types list. This allows Rust to print a debug copy

752 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

of the struct, so we can see what the code did. Notice also that a lot of things are an
Option . This way, the parsing will work if an item doesn't have that entry. It will make
reading them a little more complicated later on, but we can live with that!

If you cargo run the project now, ignore the game window - watch the console. You'll
see the following:

Raws { items: [Item { name: "Healing Potion", renderable: Some(Renderable


{ glyph: "!", fg: "#FF00FF", bg: "#000000" }), consumable: Some(Consumable
{ effects: {"provides_healing": "8"} }) }, Item { name: "Magic Missile
Scroll", renderable: Some(Renderable { glyph: ")", fg: "#00FFFF", bg:
"#000000"
}), consumable: Some(Consumable { effects: {"damage": "20", "ranged": "6"}
}) }] }

That's super ugly and horribly formatted, but you can see that it contains the data we
entered!

Storing and indexing our raw item data


Having this (largely text) data is great, but it doesn't really help us until it can directly
relate to spawning entities. We're also discarding the data as soon as we've loaded it!

We want to create a structure to hold all of our raw data, and provide useful services
such as spawning an object entirely from the data in the raws . We'll make a new �le,
raws/rawmaster.rs :

753 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use std::collections::HashMap;
use specs::prelude::*;
use crate::components::*;
use super::{Raws};

pub struct RawMaster {


raws : Raws,
item_index : HashMap<String, usize>
}

impl RawMaster {
pub fn empty() -> RawMaster {
RawMaster {
raws : Raws{ items: Vec::new() },
item_index : HashMap::new()
}
}

pub fn load(&mut self, raws : Raws) {


self.raws = raws;
self.item_index = HashMap::new();
for (i,item) in self.raws.items.iter().enumerate() {
self.item_index.insert(item.name.clone(), i);
}
}
}

That's very straightforward, and well within what we've learned of Rust so far: we
make a structure called RawMaster , it gets a private copy of the Raws data and a
HashMap storing item names and their index inside Raws.items . The empty
constructor does just that: it makes a completely empty version of the RawMaster
structure. load takes the de-serialized Raws structure, stores it, and indexes the
items by name and location in the items array.

Accessing Raw Data From Anywhere


This is one of those times that it would be nice if Rust didn't make global variables
di�cult to use; we want exactly one copy of the RawMaster data, and we'd like to be
able to read it from anywhere. You can accomplish that with a bunch of unsafe code,

754 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

but we'll be good "Rustaceans" and use a popular method: the lazy_static . This
functionality isn't part of the language itself, so we need to add a crate to cargo.toml
. Add the following line to your [dependencies] in the �le:

lazy_static = "1.4.0"

Now we do a bit of a dance to make the global safely available from everywhere. At
the end of main.rs 's import section, add:

#[macro_use]
extern crate lazy_static;

This is similar to what we've done for other macros: it tells Rust that we'd like to
import the macros from the crate lazy_static . In mod.rs , declare the following:

mod rawmaster;
pub use rawmaster::*;
use std::sync::Mutex;

Also:

lazy_static! {
pub static ref RAWS : Mutex<RawMaster> =
Mutex::new(RawMaster::empty());
}

The lazy_static! macro does a bunch of hard work for us to make this safe. The
interesting part is that we still have to use a Mutex . Mutexes are a construct that
ensure that no more than one thread at a time can write to a structure. You access a
Mutex by calling lock - it is now yours until the lock goes out of scope. So in our
load_raws function, we need to populate it:

755 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Retrieve the raw data as an array of u8 (8-bit unsigned chars)


let raw_data = rltk::embedding::EMBED
.lock()
.unwrap()
.get_resource("../../raws/spawns.json".to_string())
.unwrap();
let raw_string = std::str::from_utf8(&raw_data).expect("Unable to
convert to a valid UTF-8 string.");
let decoder : Raws = serde_json::from_str(&raw_string).expect("Unable
to parse JSON");

RAWS.lock().unwrap().load(decoder);

You'll notice that RLTK's embedding system is quietly using a lazy_static itself -
that's what the lock and unwrap code is for: it manages the Mutex. So for our RAWS
global, we lock it (retrieving a scoped lock), unwrap that lock (to allow us to access
the contents), and call the load function we wrote earlier. Quite a mouthful, but now
we can safely share the RAWS data without having to worry about threading
problems. Once loaded, we'll probably never write to it again - and Mutex locks for
reading are pretty much instantaneous when you don't have lots of threads running.

Spawning items from the RAWS


In rawmaster.rs , we'll make a new function:

756 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn spawn_named_item(raws: &RawMaster, new_entity : EntityBuilder, key


: &str, pos : SpawnType) -> Option<Entity> {
if raws.item_index.contains_key(key) {
let item_template = &raws.raws.items[raws.item_index[key]];

let mut eb = new_entity;

// Spawn in the specified location


match pos {
SpawnType::AtPosition{x,y} => {
eb = eb.with(Position{ x, y });
}
}

// Renderable
if let Some(renderable) = &item_template.renderable {
eb = eb.with(crate::components::Renderable{
glyph:
rltk::to_cp437(renderable.glyph.chars().next().unwrap()),
fg : rltk::RGB::from_hex(&renderable.fg).expect("Invalid
RGB"),
bg : rltk::RGB::from_hex(&renderable.bg).expect("Invalid
RGB"),
render_order : renderable.order
});
}

eb = eb.with(Name{ name : item_template.name.clone() });

eb = eb.with(crate::components::Item{});

if let Some(consumable) = &item_template.consumable {


eb = eb.with(crate::components::Consumable{});
for effect in consumable.effects.iter() {
let effect_name = effect.0.as_str();
match effect_name {
"provides_healing" => {
eb = eb.with(ProvidesHealing{ heal_amount:
effect.1.parse::<i32>().unwrap() })
}
"ranged" => { eb = eb.with(Ranged{ range:
effect.1.parse::<i32>().unwrap() }) },
"damage" => { eb = eb.with(InflictsDamage{ damage :
effect.1.parse::<i32>().unwrap() }) }
_ => {

757 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

println!("Warning: consumable effect {} not


implemented.", effect_name);
}
}
}
}

return Some(eb.build());
}
None
}

It's a long function, but it's actually very straightforward - and uses patterns we've
encountered plenty of times before. It does the following:

1. It looks to see if the key we've passed exists in the item_index . If it doesn't, it
returns None - it didn't do anything.
2. If the key does exist, then it adds a Name component to the entity - with the
name from the raw �le.
3. If Renderable exists in the item de�nition, it creates a component of type
Renderable .
4. If Consumable exists in the item de�nition, it makes a new consumable. It
iterates through all of the keys/values inside the effect dictionary, adding
e�ect components as needed.

Now you can open spawner.rs and modify spawn_entity :

758 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {


let map = ecs.fetch::<Map>();
let width = map.width as usize;
let x = (*spawn.0 % width) as i32;
let y = (*spawn.0 / width) as i32;
std::mem::drop(map);

let item_result = spawn_named_item(&RAWS.lock().unwrap(),


ecs.create_entity(), &spawn.1, SpawnType::AtPosition{ x, y});
if item_result.is_some() {
return;
}

match spawn.1.as_ref() {
"Goblin" => goblin(ecs, x, y),
"Orc" => orc(ecs, x, y),
"Fireball Scroll" => fireball_scroll(ecs, x, y),
"Confusion Scroll" => confusion_scroll(ecs, x, y),
"Dagger" => dagger(ecs, x, y),
"Shield" => shield(ecs, x, y),
"Longsword" => longsword(ecs, x, y),
"Tower Shield" => tower_shield(ecs, x, y),
"Rations" => rations(ecs, x, y),
"Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
"Bear Trap" => bear_trap(ecs, x, y),
"Door" => door(ecs, x, y),
_ => {}
}
}

Note that we've deleted the items we've added into spawns.json . We can also delete
the associated functions. spawner.rs will be really small when we're done! So the
magic here is that it calls spawn_named_item , using a rather ugly
&RAWS.lock().unwrap() to obtain safe access to our RAWS global variable. If it
matched a key, it will return Some(Entity) - otherwise, we get None . So we check if
item_result.is_some() and return if we succeeded in spawning something from the
data. Otherwise, we use the new code.

You'll also want to add a raws::* to the list of items imported from super .

If you cargo run now, the game runs as before - including health potions and magic
missile scrolls.

759 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding the rest of the consumables


We'll go ahead and get the rest of the consumables into spawns.json :

760 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
{
"name" : "Fireball Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#FFA500",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"area_of_effect" : "3"
}
}
},

{
"name" : "Confusion Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"confusion" : "4"
}
}
},

{
"name" : "Magic Mapping Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#AAAAFF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"magic_mapping" : ""

761 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

}
}
},

{
"name" : "Rations",
"renderable": {
"glyph" : "%",
"fg" : "#00FF00",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"food" : ""
}
}
}
]
}

We'll put their e�ects into rawmaster.rs 's spawn_named_item function:

762 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if let Some(consumable) = &item_template.consumable {


eb = eb.with(crate::components::Consumable{});
for effect in consumable.effects.iter() {
let effect_name = effect.0.as_str();
match effect_name {
"provides_healing" => {
eb = eb.with(ProvidesHealing{ heal_amount:
effect.1.parse::<i32>().unwrap() })
}
"ranged" => { eb = eb.with(Ranged{ range: effect.1.parse::
<i32>().unwrap() }) },
"damage" => { eb = eb.with(InflictsDamage{ damage :
effect.1.parse::<i32>().unwrap() }) }
"area_of_effect" => { eb = eb.with(AreaOfEffect{ radius:
effect.1.parse::<i32>().unwrap() }) }
"confusion" => { eb = eb.with(Confusion{ turns:
effect.1.parse::<i32>().unwrap() }) }
"magic_mapping" => { eb = eb.with(MagicMapper{}) }
"food" => { eb = eb.with(ProvidesFood{}) }
_ => {
println!("Warning: consumable effect {} not implemented.",
effect_name);
}
}
}
}

You can now delete the �reball, magic mapping and confusion scrolls from
spawner.rs ! Run the game, and you have access to these items. Hopefully, this is
starting to illustrate the power of linking a data �le to your component creation.

Adding the remaining items


We'll make a few more JSON entries in spawns.json to cover the various other items
we have remaining:

763 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Dagger",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 2
}
},

{
"name" : "Longsword",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAFF",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 4
}
},

{
"name" : "Shield",
"renderable": {
"glyph" : "[",
"fg" : "#00AAFF",
"bg" : "#000000",
"order" : 2
},
"shield" : {
"defense_bonus" : 1
}
},

{
"name" : "Tower Shield",
"renderable": {
"glyph" : "[",
"fg" : "#00FFFF",

764 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"bg" : "#000000",
"order" : 2
},
"shield" : {
"defense_bonus" : 3
}
}

There are two new �elds here! shield and weapon . We need to expand our
item_structs.rs to handle them:

#[derive(Deserialize, Debug)]
pub struct Item {
pub name : String,
pub renderable : Option<Renderable>,
pub consumable : Option<Consumable>,
pub weapon : Option<Weapon>,
pub shield : Option<Shield>
}

...

#[derive(Deserialize, Debug)]
pub struct Weapon {
pub range: String,
pub power_bonus: i32
}

#[derive(Deserialize, Debug)]
pub struct Shield {
pub defense_bonus: i32
}

We'll also have to teach our spawn_named_item function (in rawmaster.rs ) to use this
data:

765 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

if let Some(weapon) = &item_template.weapon {


eb = eb.with(Equippable{ slot: EquipmentSlot::Melee });
eb = eb.with(MeleePowerBonus{ power : weapon.power_bonus });
}

if let Some(shield) = &item_template.shield {


eb = eb.with(Equippable{ slot: EquipmentSlot::Shield });
eb = eb.with(DefenseBonus{ defense: shield.defense_bonus });
}

You can now delete these items from spawner.rs as well, and they still spawn in
game - as before.

Now for the monsters!


We'll add a new array to spawns.json to handle monsters. We're calling it "mobs" -
this is slang from many games for "movable object", but it has come to mean things
that move around and �ght you in common parlance:

766 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"mobs" : [
{
"name" : "Orc",
"renderable": {
"glyph" : "o",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 8
},

{
"name" : "Goblin",
"renderable": {
"glyph" : "g",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 8,
"hp" : 8,
"defense" : 1,
"power" : 3
},
"vision_range" : 8
}
]

You'll notice that we're �xing a minor issue from before: orcs and goblins are no
longer identical in stats! Otherwise, this should make sense: the stats we set in
spawner.rs are instead set in the JSON �le. We need to create a new �le,
raws/mob_structs.rs :

767 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use serde::{Deserialize};
use super::{Renderable};

#[derive(Deserialize, Debug)]
pub struct Mob {
pub name : String,
pub renderable : Option<Renderable>,
pub blocks_tile : bool,
pub stats : MobStats,
pub vision_range : i32
}

#[derive(Deserialize, Debug)]
pub struct MobStats {
pub max_hp : i32,
pub hp : i32,
pub power : i32,
pub defense : i32
}

We'll also modify Raws (currently in item_structs.rs ). We'll move it to mod.rs , since
it is shared with other modules and edit it:

#[derive(Deserialize, Debug)]
pub struct Raws {
pub items : Vec<Item>,
pub mobs : Vec<Mob>
}

We also need to modify rawmaster.rs to add an empty mobs list to the constructor:

impl RawMaster {
pub fn empty() -> RawMaster {
RawMaster {
raws : Raws{ items: Vec::new(), mobs: Vec::new() },
item_index : HashMap::new()
}
}
...

We'll also modify RawMaster to index our mobs:

768 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct RawMaster {


raws : Raws,
item_index : HashMap<String, usize>,
mob_index : HashMap<String, usize>
}

impl RawMaster {
pub fn empty() -> RawMaster {
RawMaster {
raws : Raws{ items: Vec::new(), mobs: Vec::new() },
item_index : HashMap::new(),
mob_index : HashMap::new()
}
}

pub fn load(&mut self, raws : Raws) {


self.raws = raws;
self.item_index = HashMap::new();
for (i,item) in self.raws.items.iter().enumerate() {
self.item_index.insert(item.name.clone(), i);
}
for (i,mob) in self.raws.mobs.iter().enumerate() {
self.mob_index.insert(mob.name.clone(), i);
}
}
}

We're going to want to build a spawn_named_mob function, but �rst lets create some
helpers so we're sharing functionality with spawn_named_item - avoid repeating
ourselves. The �rst is pretty straightforward:

769 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn spawn_position(pos : SpawnType, new_entity : EntityBuilder) ->


EntityBuilder {
let mut eb = new_entity;

// Spawn in the specified location


match pos {
SpawnType::AtPosition{x,y} => {
eb = eb.with(Position{ x, y });
}
}

eb
}

When we add more SpawnType entries, this function will necessarily expand to
include them - so it's great that it's a function. We can replace the same code in
spawn_named_item with a single call to this function:

// Spawn in the specified location


eb = spawn_position(pos, eb);

Let's also break out handling of Renderable data. This was more di�cult; I had a
terrible time getting Rust's lifetime checker to work with a system that actually added
it to the EntityBuilder . I �nally settled on a function that returns the component for
the caller to add:

fn get_renderable_component(renderable : &super::item_structs::Renderable)
-> crate::components::Renderable {
crate::components::Renderable{
glyph: rltk::to_cp437(renderable.glyph.chars().next().unwrap()),
fg : rltk::RGB::from_hex(&renderable.fg).expect("Invalid RGB"),
bg : rltk::RGB::from_hex(&renderable.bg).expect("Invalid RGB"),
render_order : renderable.order
}
}

That still cleans up the call in spawn_named_item :

770 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Renderable
if let Some(renderable) = &item_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}

Alright - so with that in hand, we can go ahead and make spawn_named_mob :

pub fn spawn_named_mob(raws: &RawMaster, new_entity : EntityBuilder, key :


&str, pos : SpawnType) -> Option<Entity> {
if raws.mob_index.contains_key(key) {
let mob_template = &raws.raws.mobs[raws.mob_index[key]];

let mut eb = new_entity;

// Spawn in the specified location


eb = spawn_position(pos, eb);

// Renderable
if let Some(renderable) = &mob_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}

eb = eb.with(Name{ name : mob_template.name.clone() });

eb = eb.with(Monster{});
if mob_template.blocks_tile {
eb = eb.with(BlocksTile{});
}
eb = eb.with(CombatStats{
max_hp : mob_template.stats.max_hp,
hp : mob_template.stats.hp,
power : mob_template.stats.power,
defense : mob_template.stats.defense
});
eb = eb.with(Viewshed{ visible_tiles : Vec::new(), range:
mob_template.vision_range, dirty: true });

return Some(eb.build());
}
None
}

There's really nothing we haven't already covered in this function: we simply apply a

771 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

renderable, position, name using the same code as before - and then check
blocks_tile to see if we should add a BlocksTile component, and copy the stats
into a CombatStats component. We also setup a Viewshed component with
vision_range range.

Before we update spawner.rs again, lets introduce a master spawning method -


spawn_named_entity . The reasoning behind this is that the spawn system doesn't
actually know (or care) if an entity is an item, mob, or anything else. Rather than push
a lot of if checks into it, we'll provide a single interface:

pub fn spawn_named_entity(raws: &RawMaster, new_entity : EntityBuilder,


key : &str, pos : SpawnType) -> Option<Entity> {
if raws.item_index.contains_key(key) {
return spawn_named_item(raws, new_entity, key, pos);
} else if raws.mob_index.contains_key(key) {
return spawn_named_mob(raws, new_entity, key, pos);
}

None
}

So over in spawner.rs we can use the generic spawner now:

let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(),


ecs.create_entity(), &spawn.1, SpawnType::AtPosition{ x, y});
if spawn_result.is_some() {
return;
}

We can also go ahead and delete the references to Orcs, Goblins and Monsters! We're
nearly there - you can get your data-driven monsters now.

Doors and Traps


There are two remaining hard-coded entities. These have been left separate because
they aren't really the same as the other types: they are what I call "props" - level
features. You can't pick them up, but they are an integral part of the level. So in

772 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

spawns.json , we'll go ahead and de�ne some props:

"props" : [
{
"name" : "Bear Trap",
"renderable": {
"glyph" : "^",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 2
},
"hidden" : true,
"entry_trigger" : {
"effects" : {
"damage" : "6",
"single_activation" : "1"
}
}
},
{
"name" : "Door",
"renderable": {
"glyph" : "+",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false,
"blocks_tile" : true,
"blocks_visibility" : true,
"door_open" : true
}
]

The problem with props is that they can be really quite varied, so we end up with a lot
of optional stu� in the de�nition. I'd rather have a complex de�nition on the Rust side
than on the JSON side, to reduce the sheer volume of typing when we have a lot of
props. So we wind up making something reasonably expressive in JSON, and do a lot
of work to make it function in Rust! We'll make a new �le, prop_structs.rs and put
our serialization classes into it:

773 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use serde::{Deserialize};
use super::{Renderable};
use std::collections::HashMap;

#[derive(Deserialize, Debug)]
pub struct Prop {
pub name : String,
pub renderable : Option<Renderable>,
pub hidden : Option<bool>,
pub blocks_tile : Option<bool>,
pub blocks_visibility : Option<bool>,
pub door_open : Option<bool>,
pub entry_trigger : Option<EntryTrigger>
}

#[derive(Deserialize, Debug)]
pub struct EntryTrigger {
pub effects : HashMap<String, String>
}

We have to tell raws/mod.rs to use it:

mod prop_structs;
use prop_structs::*;

We also need to extend Raws to hold them:

#[derive(Deserialize, Debug)]
pub struct Raws {
pub items : Vec<Item>,
pub mobs : Vec<Mob>,
pub props : Vec<Prop>
}

That takes us into rawmaster.rs , where we need to extend the constructor and
reader to include the new types:

774 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct RawMaster {


raws : Raws,
item_index : HashMap<String, usize>,
mob_index : HashMap<String, usize>,
prop_index : HashMap<String, usize>
}

impl RawMaster {
pub fn empty() -> RawMaster {
RawMaster {
raws : Raws{ items: Vec::new(), mobs: Vec::new(), props:
Vec::new() },
item_index : HashMap::new(),
mob_index : HashMap::new(),
prop_index : HashMap::new()
}
}

pub fn load(&mut self, raws : Raws) {


self.raws = raws;
self.item_index = HashMap::new();
for (i,item) in self.raws.items.iter().enumerate() {
self.item_index.insert(item.name.clone(), i);
}
for (i,mob) in self.raws.mobs.iter().enumerate() {
self.mob_index.insert(mob.name.clone(), i);
}
for (i,prop) in self.raws.props.iter().enumerate() {
self.prop_index.insert(prop.name.clone(), i);
}
}
}

We also make a new function, spawn_named_prop :

775 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn spawn_named_prop(raws: &RawMaster, new_entity : EntityBuilder, key


: &str, pos : SpawnType) -> Option<Entity> {
if raws.prop_index.contains_key(key) {
let prop_template = &raws.raws.props[raws.prop_index[key]];

let mut eb = new_entity;

// Spawn in the specified location


eb = spawn_position(pos, eb);

// Renderable
if let Some(renderable) = &prop_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}

eb = eb.with(Name{ name : prop_template.name.clone() });

if let Some(hidden) = prop_template.hidden {


if hidden { eb = eb.with(Hidden{}) };
}
if let Some(blocks_tile) = prop_template.blocks_tile {
if blocks_tile { eb = eb.with(BlocksTile{}) };
}
if let Some(blocks_visibility) = prop_template.blocks_visibility {
if blocks_visibility { eb = eb.with(BlocksVisibility{}) };
}
if let Some(door_open) = prop_template.door_open {
eb = eb.with(Door{ open: door_open });
}
if let Some(entry_trigger) = &prop_template.entry_trigger {
eb = eb.with(EntryTrigger{});
for effect in entry_trigger.effects.iter() {
match effect.0.as_str() {
"damage" => { eb = eb.with(InflictsDamage{ damage :
effect.1.parse::<i32>().unwrap() }) }
"single_activation" => { eb =
eb.with(SingleActivation{}) }
_ => {}
}
}
}

return Some(eb.build());
}

776 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

None
}

We'll gloss over the contents because this is basically the same as what we've done
before. We need to extend spawn_named_entity to include props:

pub fn spawn_named_entity(raws: &RawMaster, new_entity : EntityBuilder,


key : &str, pos : SpawnType) -> Option<Entity> {
if raws.item_index.contains_key(key) {
return spawn_named_item(raws, new_entity, key, pos);
} else if raws.mob_index.contains_key(key) {
return spawn_named_mob(raws, new_entity, key, pos);
} else if raws.prop_index.contains_key(key) {
return spawn_named_prop(raws, new_entity, key, pos);
}

None
}

Finally, we can go into spawner.rs and remove the door and bear trap functions. We
can �nish cleaning up the spawn_entity function. We're also going to add a warning
in case you try to spawn something we don't know about:

/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)


pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
let map = ecs.fetch::<Map>();
let width = map.width as usize;
let x = (*spawn.0 % width) as i32;
let y = (*spawn.0 / width) as i32;
std::mem::drop(map);

let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(),


ecs.create_entity(), &spawn.1, SpawnType::AtPosition{ x, y});
if spawn_result.is_some() {
return;
}

println!("WARNING: We don't know how to spawn [{}]!", spawn.1);


}

If you cargo run now, you'll see doors and traps working as before.

777 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrap-Up
This chapter has given us the ability to easily change the items, mobs and props that
adorn our levels. We haven't touched adding more yet (or adjusting the spawn tables) -
that'll be the next chapter. You can quickly change the character of the game now;
want Goblins to be weaker? Lower their stats! Want them to have better eyesight than
Orcs? Adjust their vision range! That's the primary bene�t of a data-driven approach:
you can quickly make changes without having to dive into source code. The engine
becomes responsible for simulating the world - and the data becomes responsible for
describing the world.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Data-Driven Spawn Tables

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the previous chapter, we moved spawning to be data-driven: you de�ne your


monsters, items and props in a JSON data �le - and the spawn function becomes a
parser that builds components based on your de�nitions. That gets you half-way to a
data-driven world.

If you look at the ever-shrinking spawner.rs �le, we have a hard-coded table for

778 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

handling spawning:

fn room_table(map_depth: i32) -> RandomTable {


RandomTable::new()
.add("Goblin", 10)
.add("Orc", 1 + map_depth)
.add("Health Potion", 7)
.add("Fireball Scroll", 2 + map_depth)
.add("Confusion Scroll", 2 + map_depth)
.add("Magic Missile Scroll", 4)
.add("Dagger", 3)
.add("Shield", 3)
.add("Longsword", map_depth - 1)
.add("Tower Shield", map_depth - 1)
.add("Rations", 10)
.add("Magic Mapping Scroll", 2)
.add("Bear Trap", 5)
}

It's served us well for all these chapters, but sadly it's time to put it out to pasture.
We'd like to be able to specify the spawn table in our JSON data - that way, we can add
new entities to the data �le and spawn list, and they appear in the game with no
additional Rust coding (unless they need new features, in which case it's time to
extend the engine).

A JSON-based spawn table


Here's an example of how I'm envisioning our spawn table:

"spawn_table" : [
{ "name" : "Goblin", "weight" : 10, "min_depth" : 0, "max_depth" : 100
}
],

So the spawn_table is an array, with each entry containing something that can be
spawned. We're storing the name of the spawnable. We give it a weight, which
corresponds to the same �eld in our current RandomTable structure. We've added a
min_depth and max_depth - so this spawn line will only apply to a speci�ed depth
range of the dungeon.

779 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

That looks pretty good, so lets put all of our entities in:

"spawn_table" : [
{ "name" : "Goblin", "weight" : 10, "min_depth" : 0, "max_depth" : 100
},
{ "name" : "Orc", "weight" : 1, "min_depth" : 0, "max_depth" : 100,
"add_map_depth_to_weight" : true },
{ "name" : "Health Potion", "weight" : 7, "min_depth" : 0, "max_depth"
: 100 },
{ "name" : "Fireball Scroll", "weight" : 2, "min_depth" : 0,
"max_depth" : 100, "add_map_depth_to_weight" : true },
{ "name" : "Confusion Scroll", "weight" : 2, "min_depth" : 0,
"max_depth" : 100, "add_map_depth_to_weight" : true },
{ "name" : "Magic Missile Scroll", "weight" : 4, "min_depth" : 0,
"max_depth" : 100 },
{ "name" : "Dagger", "weight" : 3, "min_depth" : 0, "max_depth" : 100
},
{ "name" : "Shield", "weight" : 3, "min_depth" : 0, "max_depth" : 100
},
{ "name" : "Longsword", "weight" : 1, "min_depth" : 1, "max_depth" :
100 },
{ "name" : "Tower Shield", "weight" : 1, "min_depth" : 1, "max_depth"
: 100 },
{ "name" : "Rations", "weight" : 10, "min_depth" : 0, "max_depth" :
100 },
{ "name" : "Magic Mapping Scroll", "weight" : 2, "min_depth" : 0,
"max_depth" : 100 },
{ "name" : "Bear Trap", "weight" : 5, "min_depth" : 0, "max_depth" :
100 }
],

Notice that we've added add_map_depth_to_weight to allow us to indicate that things


become more probable later on in the game. That lets us keep the variable weighting
capability. We've also put longsword and tower shield only occurring after the �rst level.

That's pretty comprehensive (covers everything we have so far, and adds some
capability), so lets make a new �le spawn_table_structs in raws and de�ne the
classes required to read this data:

780 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use serde::{Deserialize};
use super::{Renderable};

#[derive(Deserialize, Debug)]
pub struct SpawnTableEntry {
pub name : String,
pub weight : i32,
pub min_depth: i32,
pub max_depth: i32,
pub add_map_depth_to_weight : Option<bool>
}

Open up raws/mod.rs and we'll add it to the Raws structure:

mod spawn_table_structs;
use spawn_table_structs::*;
...
#[derive(Deserialize, Debug)]
pub struct Raws {
pub items : Vec<Item>,
pub mobs : Vec<Mob>,
pub props : Vec<Prop>,
pub spawn_table : Vec<SpawnTableEntry>
}

We also need to add it to the constructor in rawmaster.rs :

pub fn empty() -> RawMaster {


RawMaster {
raws : Raws{ items: Vec::new(), mobs: Vec::new(), props:
Vec::new(), spawn_table: Vec::new() },
item_index : HashMap::new(),
mob_index : HashMap::new(),
prop_index : HashMap::new(),
}
}

It's worth doing a quick cargo run now, just to be sure that the spawn table is
loading without errors. It won't do anything yet, but it's always good to know that the
data loads properly.

781 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Using the new spawn table


In rawmaster.rs , we're going to add a new function to build a random spawn table
from our JSON data:

pub fn get_spawn_table_for_depth(raws: &RawMaster, depth: i32) ->


RandomTable {
use super::SpawnTableEntry;

let available_options : Vec<&SpawnTableEntry> = raws.raws.spawn_table


.iter()
.filter(|a| depth >= a.min_depth && depth <= a.max_depth)
.collect();

let mut rt = RandomTable::new();


for e in available_options.iter() {
let mut weight = e.weight;
if e.add_map_depth_to_weight.is_some() {
weight += depth;
}
rt = rt.add(e.name.clone(), weight);
}

rt
}

This function is quite simple:

1. We obtain raws.raws.spawn_table - which is the master spawn table list.


2. We obtain an iterator with iter() .
3. We use filter to only include items that are within the requested map depth's
range.
4. We collect() it into a vector of references to SpawnTableEntry lines.
5. We iterate all of the collected available options:
1. We grab the weight.
2. If the entry has an "add map depth to weight" requirement, we add that
depth to that entry's weight.
3. We add it to our RandomTable .

That's pretty straightforward! We can open up spawner.rs and modify our


RoomTable function to use it:

782 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn room_table(map_depth: i32) -> RandomTable {


get_spawn_table_for_depth(&RAWS.lock().unwrap(), map_depth)
}

Wow, that's a short function! It does the job, however. If you cargo run now, you'll be
playing the game like before.

Adding some sanity checks


We've now got the ability to add entities without touching our Rust code! Before we
explore that, lets look at adding some "sanity checking" to the system to help avoid
mistakes. We simply change the load function in rawmaster.rs :

783 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn load(&mut self, raws : Raws) {


self.raws = raws;
self.item_index = HashMap::new();
let mut used_names : HashSet<String> = HashSet::new();
for (i,item) in self.raws.items.iter().enumerate() {
if used_names.contains(&item.name) {
println!("WARNING - duplicate item name in raws [{}]",
item.name);
}
self.item_index.insert(item.name.clone(), i);
used_names.insert(item.name.clone());
}
for (i,mob) in self.raws.mobs.iter().enumerate() {
if used_names.contains(&mob.name) {
println!("WARNING - duplicate mob name in raws [{}]",
mob.name);
}
self.mob_index.insert(mob.name.clone(), i);
used_names.insert(mob.name.clone());
}
for (i,prop) in self.raws.props.iter().enumerate() {
if used_names.contains(&prop.name) {
println!("WARNING - duplicate prop name in raws [{}]",
prop.name);
}
self.prop_index.insert(prop.name.clone(), i);
used_names.insert(prop.name.clone());
}

for spawn in self.raws.spawn_table.iter() {


if !used_names.contains(&spawn.name) {
println!("WARNING - Spawn tables references unspecified entity
{}", spawn.name);
}
}
}

What are we doing here? We create used_names as a HashSet . Whenever we load


something, we add it to the set. If it already exists? Then we've made a duplicate and
bad things will happen - so we warn the user. Then we iterate the spawn table, and if
we've references an entity name that hasn't been de�ned - we again warn the user.

These types of data-entry bugs are common, and won't actually crash the program.
This sanity check ensures that we are at least warned about it before we proceed

784 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

thinking that all is well. If you're paranoid (when programming, that's actually a good
trait; there are plenty of people who are out to get you!), you could replace the
println! with panic! and crash instead of just reminding the user. You may not
want to do that if you like to cargo run often to see how you are doing!

Bene�tting from our data-driven architecture


Lets quickly add a new weapon and a new monster to the game. We can do this
without touching the Rust code other than to recompile (embedding the changed
�le). In spawns.json , lets add a Battleaxe to the weapons list:

{
"name" : "Battleaxe",
"renderable": {
"glyph" : "¶",
"fg" : "#FF55FF",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 5
}
},

We'll also add it into the spawn table:

{ "name" : "Battleaxe", "weight" : 1, "min_depth" : 2, "max_depth" : 100 }

Let's also add a humble kobold. It's basically an even weaker goblin. We like kobolds,
lets have lots of them!

785 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Kobold",
"renderable": {
"glyph" : "k",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 4,
"hp" : 4,
"defense" : 0,
"power" : 2
},
"vision_range" : 4
}

So we'll also add this little critter to the spawn list:

{ "name" : "Kobold", "weight" : 15, "min_depth" : 0, "max_depth" : 3 }

Notice that we make them really common - and stop harassing the player with them
after level 3.

If you cargo run the project now, you'll �nd the new entities in the game:

786 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrap-Up
That's it for spawn tables! You've gained considerable power in these last two
chapters - use it wisely. You can add in all manner of entities without having to write a
line of Rust now, and could easily start to shape the game to what you want. In the
next chapter, we'll begin doing just that.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

787 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Making the starting town

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

What is the town for?


Back in the Design Document we decided: The game starts in town. In town, there are
only minimal enemies (pickpockets, thugs). You start in the to-be-named pub (tavern),
armed only with a meager purse, minimal starting equipment, a stein of beer, a dried
sausage, a backpack and a hangover. Town lets you visit various vendors.

From a development point of view, this tells us a few things:

The town has a story aspect, in that you start there and it ground the story -
giving a starting point, a destiny (in this case a drunken promise to save the
world). So the town implies a certain cozy starting point, implies some
communication to help you understand why you are embarking on the life of an
adventurer, and so on.
The town has vendors. That won't make sense at this point, because we don't
have a value/currency system - but we know that we need somewhere to put
them.
The town has a tavern/inn/pub - it's a starting location, but it's obviously
important enough that it needs to do something!
Elsewhere in the design document, we mention that you can town portal back to
the settlement. This again implies a certain coziness/safety, and also implies that
doing so is useful - so the services o�ered by the town need to retain their utility
throughout the game.
Finally, the town is the winning condition: once you've grabbed the Amulet of
Yala - getting back to town lets you save the world. That implies that the town
should have some sort of holy structure to which you have to return the amulet.

788 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The town is the �rst thing that new players will encounter - so it has to look alive
and somewhat slick, or players will just close the window and try something else.
It may also serve as a location for some tutorials.

This sort of discussion is essential to game design; you don't want to implement
something just because you can (in most cases; big open world games relax that a
bit). The town has a purpose, and that purpose guides its design.

So what do we have to include in the town?


So that discussion lets us determine that the town must include:

One or more merchants. We're not implementing the sale of goods yet, but they
need a place to operate.
Some friendly/neutral NPCs for color.
A temple.
A tavern.
A place that town portals arrive.
A way out to begin your adventure.

We can also think a little bit about what makes a town:

There's generally a communication route (land or sea), otherwise the town won't
prosper.
Frequently, there's a market (surrounding villages use towns for commerce).
There's almost certainly either a river or a deep natural water source.
Towns typically have authority �gures, visible at least as Guards or Watch.
Towns also generally have a shady side.

How do we want to generate our town?


We could go for a prefabricated town. This has the upside that the town can be
tweaked until it's just right, and plays smoothly. It has the downside that getting out of
the town becomes a purely mechanical step after the �rst couple of play-throughs
("runs"); look at Joppa in Caves of Qud - it became little more than a "grab the chest

789 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

content, talk to these guys, and o� you go" speed-bump start to an amazing game.

So - we want a procedurally generated town, but we want to keep it functional - and


make it pretty. Not much to ask!

Making some new tile types


From the above, it sounds like we are going to need some new tiles. The ones that
spring to mind for a town are roads, grass, water (both deep and shallow), bridge,
wooden �oors, and building walls. One thing we can count on: we're going to add lots
of new tile types as we progress, so we better take the time to make it a seamless
experience up-front!

The map.rs could get quite complicated if we're not careful, so lets make it into its
own module with a directory. We'll start by making a directory, map/ . Then we'll move
map.rs into it, and rename it mod.rs . Now, we'll take TileType out of mod.rs and
put it into a new �le - tiletype.rs :

use serde::{Serialize, Deserialize};

#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]


pub enum TileType {
Wall, Floor, DownStairs
}

And in mod.rs we'll accept the module and share the public types it exposes:

mod tiletype;
pub use tiletype::TileType;

This hasn't gained us much yet... but now we can start supporting the various tile
types. As we add functionality, you'll hopefully see why using a separate �le makes it
easier to �nd the relevant code:

790 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]


pub enum TileType {
Wall,
Floor,
DownStairs,
Road,
Grass,
ShallowWater,
DeepWater,
WoodFloor,
Bridge
}

This is only part of the picture, because now we need to handle a bunch of grunt-
work: can you enter tiles of that type, do they block visibility, do they have a di�erent
cost for path-�nding, and so on. We've also done a lot of "spawn if its a �oor" code in
our map builders; maybe that wasn't such a good idea if you can have multiple �oor
types? Anyway, the current map.rs provides some of what we need in order to satisfy
the BaseMap trait for RLTK.

We'll make a few functions to help satisfy this requirement, while keeping our tile
functionality in one place:

pub fn tile_walkable(tt : TileType) -> bool {


match tt {
TileType::Floor | TileType::DownStairs | TileType::Road |
TileType::Grass |
TileType::ShallowWater | TileType::WoodFloor | TileType::Bridge
=> true,
_ => false
}
}

pub fn tile_opaque(tt : TileType) -> bool {


match tt {
TileType::Wall => true,
_ => false
}
}

Now we'll go back into mod.rs , and import these - and make them public to anyone
who wants them:

791 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

mod tiletype;
pub use tiletype::{TileType, tile_walkable, tile_opaque};

We also need to update some of our functions to use this functionality. We determine
a lot of path-�nding with the blocked system, so we need to update
populate_blocked to handle the various types using the functions we just made:

pub fn populate_blocked(&mut self) {


for (i,tile) in self.tiles.iter_mut().enumerate() {
self.blocked[i] = !tile_walkable(*tile);
}
}

We also need to update our visibility determination code:

impl BaseMap for Map {


fn is_opaque(&self, idx:i32) -> bool {
let idx_u = idx as usize;
if idx_u > 0 && idx_u < self.tiles.len() {
tile_opaque(self.tiles[idx_u]) ||
self.view_blocked.contains(&idx_u)
} else {
true
}
}
...

Lastly, lets look at get_available_exits . This uses the blocked system to determine
if an exit is possible, but so far we've hard-coded all of our costs. When there is just a
�oor and a wall to choose from, it is a pretty easy choice after all! Once we start
o�ering choices, we might want to encourage certain behaviors. It would certainly
look more realistic if people preferred to travel on the road than the grass, and
de�nitely more realistic if they avoid standing in shallow water unless they need to. So
we'll build a cost function (in tiletype.rs ):

792 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub fn tile_cost(tt : TileType) -> f32 {


match tt {
TileType::Road => 0.8,
TileType::Grass => 1.1,
TileType::ShallowWater => 1.2,
_ => 1.0
}
}

Then we update our get_available_exits to use it:

fn get_available_exits(&self, idx:i32) -> Vec<(i32, f32)> {


let mut exits : Vec<(i32, f32)> = Vec::new();
let x = idx % self.width;
let y = idx / self.width;
let tt = self.tiles[idx as usize];

// Cardinal directions
if self.is_exit_valid(x-1, y) { exits.push((idx-1, tile_cost(tt))) };
if self.is_exit_valid(x+1, y) { exits.push((idx+1, tile_cost(tt))) };
if self.is_exit_valid(x, y-1) { exits.push((idx-self.width,
tile_cost(tt))) };
if self.is_exit_valid(x, y+1) { exits.push((idx+self.width,
tile_cost(tt))) };

// Diagonals
if self.is_exit_valid(x-1, y-1) { exits.push(((idx-self.width)-1,
tile_cost(tt) * 1.45)); }
if self.is_exit_valid(x+1, y-1) { exits.push(((idx-self.width)+1,
tile_cost(tt) * 1.45)); }
if self.is_exit_valid(x-1, y+1) { exits.push(((idx+self.width)-1,
tile_cost(tt) * 1.45)); }
if self.is_exit_valid(x+1, y+1) { exits.push(((idx+self.width)+1,
tile_cost(tt) * 1.45)); }

exits
}

We've replaced all the costs of 1.0 with a call to our tile_cost function, and
multiplied diagonals by 1.45 to encourage more natural looking movement.

793 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Fixing our camera


We also need to be able to render these tile types, so we open up camera.rs and add
them to the match statement in get_tile_glyph :

fn get_tile_glyph(idx: usize, map : &Map) -> (u8, RGB, RGB) {


let glyph;
let mut fg;
let mut bg = RGB::from_f32(0., 0., 0.);

match map.tiles[idx] {
TileType::Floor => { glyph = rltk::to_cp437('.'); fg =
RGB::from_f32(0.0, 0.5, 0.5); }
TileType::WoodFloor => { glyph = rltk::to_cp437('.'); fg =
RGB::named(rltk::CHOCOLATE); }
TileType::Wall => {
let x = idx as i32 % map.width;
let y = idx as i32 / map.width;
glyph = wall_glyph(&*map, x, y);
fg = RGB::from_f32(0., 1.0, 0.);
}
TileType::DownStairs => { glyph = rltk::to_cp437('>'); fg =
RGB::from_f32(0., 1.0, 1.0); }
TileType::Bridge => { glyph = rltk::to_cp437('.'); fg =
RGB::named(rltk::CHOCOLATE); }
TileType::Road => { glyph = rltk::to_cp437('~'); fg =
RGB::named(rltk::GRAY); }
TileType::Grass => { glyph = rltk::to_cp437('"'); fg =
RGB::named(rltk::GREEN); }
TileType::ShallowWater => { glyph = rltk::to_cp437('≈'); fg =
RGB::named(rltk::CYAN); }
TileType::DeepWater => { glyph = rltk::to_cp437('≈'); fg =
RGB::named(rltk::NAVY_BLUE); }
}
if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.);
}
if !map.visible_tiles[idx] {
fg = fg.to_greyscale();
bg = RGB::from_f32(0., 0., 0.); // Don't show stains out of visual
range
}

(glyph, fg, bg)


}

794 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Starting to build our town


We want to stop making maps randomly, and instead start being a bit predictable in
what we make. So when you start depth 1, you always get a town. In
map_builders/mod.rs , we'll make a new function. For now, it'll just fall back to being
random:

pub fn level_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
random_builder(new_depth, rng, width, height)
}

Pop over to main.rs and change the builder function call to use our new function:

fn generate_world_map(&mut self, new_depth : i32) {


self.mapgen_index = 0;
self.mapgen_timer = 0.0;
self.mapgen_history.clear();
let mut rng = self.ecs.write_resource::
<rltk::RandomNumberGenerator>();
let mut builder = map_builders::level_builder(new_depth, &mut rng, 80,
50);
...

Now, we'll start �eshing out our level_builder ; we want depth 1 to generate a town
map - otherwise, we'll stick with random for now. We also want it to be obvious via a
match statement how we're routing each level's procedural generation:

pub fn level_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
println!("Depth: {}", new_depth);
match new_depth {
1 => town_builder(new_depth, rng, width, height),
_ => random_builder(new_depth, rng, width, height)
}
}

At the top of the mod.rs �le, add:

795 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

mod town;
use town::town_builder;

And in a new �le, map_builders/town.rs we'll begin our function:

use super::BuilderChain;

pub fn level_builder(new_depth: i32, rng: &mut


rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
let mut chain = BuilderChain::new(new_depth, width, height);
chain.start_with(TownBuilder::new());
let (start_x, start_y) = super::random_start_position(rng);
chain.with(AreaStartingPosition::new(start_x, start_y));
chain.with(DistantExit::new());
chain
}

The AreaStartingPosition and DistantExit are temporary to get us valid start/end


points. The meat is the call to TownBuilder . We haven't written that yet, so we'll work
through step-by-step until we have a town we like!

Here's an empty skeleton to start with:

796 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

pub struct TownBuilder {}

impl InitialMapBuilder for TownBuilder {


#[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap) {
self.build_rooms(rng, build_data);
}
}

impl TownBuilder {
pub fn new() -> Box<TownBuilder> {
Box::new(TownBuilder{})
}

pub fn build_rooms(&mut self, rng: &mut rltk::RandomNumberGenerator,


build_data : &mut BuilderMap) {
}
}

Let's make a �shing town


Let's start by adding grass, water and piers to the region. We'll write the skeleton �rst:

pub fn build_rooms(&mut self, rng: &mut rltk::RandomNumberGenerator,


build_data : &mut BuilderMap) {
self.grass_layer(build_data);
self.water_and_piers(rng, build_data);

// Make visible for screenshot


for t in build_data.map.visible_tiles.iter_mut() {
*t = true;
}
build_data.take_snapshot();
}

The function grass_layer is really simple: we replace everything with grass:

797 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn grass_layer(&mut self, build_data : &mut BuilderMap) {


// We'll start with a nice layer of grass
for t in build_data.map.tiles.iter_mut() {
*t = TileType::Grass;
}
build_data.take_snapshot();
}

Adding water is more interesting. We don't want it to be the same each time, but we
want to keep the same basic structure. Here's the code:

fn water_and_piers(&mut self, rng: &mut rltk::RandomNumberGenerator,


build_data : &mut BuilderMap) {
let mut n = (rng.roll_dice(1, 65535) as f32) / 65535f32;
let mut water_width : Vec<i32> = Vec::new();
for y in 0..build_data.height {
let n_water = (f32::sin(n) * 10.0) as i32 + 14 + rng.roll_dice(1,
6);
water_width.push(n_water);
n += 0.1;
for x in 0..n_water {
let idx = build_data.map.xy_idx(x, y);
build_data.map.tiles[idx] = TileType::DeepWater;
}
for x in n_water .. n_water+3 {
let idx = build_data.map.xy_idx(x, y);
build_data.map.tiles[idx] = TileType::ShallowWater;
}
}
build_data.take_snapshot();

// Add piers
for _i in 0..rng.roll_dice(1, 4)+6 {
let y = rng.roll_dice(1, build_data.height)-1;
for x in 2 + rng.roll_dice(1, 6) .. water_width[y as usize] + 4 {
let idx = build_data.map.xy_idx(x, y);
build_data.map.tiles[idx] = TileType::WoodFloor;
}
}
build_data.take_snapshot();
}

There's quite a bit going on here, so we'll step through:

798 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

1. We make n equal to a random �oating point number between 0.0 and 1.0 by
rolling a 65,535 sided dice (wouldn't it be nice if one of those existed?) and
dividing by the maximum number.
2. We make a new vector called water_width . We'll store the number of water tiles
on each row in here as we generate them.
3. For each y row down the map:
1. We make n_water . This is the number of water tiles present. We start by
taking the sin (Sine) of n (we randomized it to give a random gradient).
Sin waves are great, they give a nice predictable curve and you can read
anywhere along them to determine where the curve is. Since sin gives a
number from -1 to 1, we multiply by 10 to give -10 to +10. We then add 14,
guaranteeing between 4 and 24 tiles of water. To make it look jagged, we
add a little bit of randomness also.
2. We push this into the water_width vector, storing it for later.
3. We add 0.1 to n , progressing along the sine wave.
4. Then we iterate from 0 to n_water (as x ) and write DeepWater tiles to the
position of each water tile.
5. We go from n_water to n_water+3 to add some shallow water at the
edge.
4. We take a snapshot so you can watch the map progression.
5. We iterate from 0 to 1d4+6 to generate between 10 and 14 piers.
1. We pick y at random.
2. We look up the water placement for that y value, and draw wooden �oors
starting at 2+1d6 to water_width[y]+4 - giving a pier that extends out into
the water for some way, and ends squarely on land.

If you cargo run , you'll see a map like this now:

799 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding town walls, gravel and a road


Now that we have some terrain, we should add some initial outline to the town.
Extend the build function with another function call:

let (mut available_building_tiles, wall_gap_y) = self.town_walls(rng,


build_data);

The function looks like this:

800 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn town_walls(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data


: &mut BuilderMap)
-> (HashSet<usize>, i32)
{
let mut available_building_tiles : HashSet<usize> = HashSet::new();
let wall_gap_y = rng.roll_dice(1, build_data.height - 8) + 5;
for y in 1 .. build_data.height-2 {
if !(y > wall_gap_y-4 && y < wall_gap_y+4) {
let idx = build_data.map.xy_idx(30, y);
build_data.map.tiles[idx] = TileType::Wall;
build_data.map.tiles[idx-1] = TileType::Floor;
let idx_right = build_data.map.xy_idx(build_data.width - 2,
y);
build_data.map.tiles[idx_right] = TileType::Wall;
for x in 31 .. build_data.width-2 {
let gravel_idx = build_data.map.xy_idx(x, y);
build_data.map.tiles[gravel_idx] = TileType::Gravel;
if y > 2 && y < build_data.height-1 {
available_building_tiles.insert(gravel_idx);
}
}
} else {
for x in 30 .. build_data.width {
let road_idx = build_data.map.xy_idx(x, y);
build_data.map.tiles[road_idx] = TileType::Road;
}
}
}
build_data.take_snapshot();

for x in 30 .. build_data.width-1 {
let idx_top = build_data.map.xy_idx(x, 1);
build_data.map.tiles[idx_top] = TileType::Wall;
let idx_bot = build_data.map.xy_idx(x, build_data.height-2);
build_data.map.tiles[idx_bot] = TileType::Wall;
}
build_data.take_snapshot();

(available_building_tiles, wall_gap_y)
}

Again, let's step through how this works:

1. We make a new HashSet called available_building_tiles . We'll be returning


this so that other functions can use it later.

801 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

2. We set wall_gap_y to be a random y location on the map, between 6 and


map.height - 8 . We'll use this for the location of the road that runs through the
town, and gates in the city walls.
3. We iterate the y axis on the map, skipping the very top and bottom-most tiles.
1. If y is outside of the "wall gap" (8 tiles centered on wall_gap_y ):
1. We draw a wall tile at location 30,y and a road at 29,y . This gives a
wall after the shore, and a clear gap in front of it (clearly they have
lawn management employees!)
2. We also draw a wall at the far east of the map.
3. We �ll the intervening area with gravel.
4. For tiles that gained gravel, we add them to the
available_building_tiles set.
2. If it is in the gap, we draw a road.
4. Lastly we �ll rows 1 and height-2 with walls between 30 and width-2 .

If you cargo run now, you have the outline of a town:

Adding some buildings

802 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

A town without buildings is both rather pointless and rather unusual! So let's add
some. We'll add another call to the builder function, this time passing the
available_building_tiles structure we created:

let mut buildings = self.buildings(rng, build_data, &mut


available_building_tiles);

The meat of the buildings code looks like this:

803 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn buildings(&mut self,
rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap,
available_building_tiles : &mut HashSet<usize>)
-> Vec<(i32, i32, i32, i32)>
{
let mut buildings : Vec<(i32, i32, i32, i32)> = Vec::new();
let mut n_buildings = 0;
while n_buildings < 12 {
let bx = rng.roll_dice(1, build_data.map.width - 32) + 30;
let by = rng.roll_dice(1, build_data.map.height)-2;
let bw = rng.roll_dice(1, 8)+4;
let bh = rng.roll_dice(1, 8)+4;
let mut possible = true;
for y in by .. by+bh {
for x in bx .. bx+bw {
if x < 0 || x > build_data.width-1 || y < 0 || y >
build_data.height-1 {
possible = false;
} else {
let idx = build_data.map.xy_idx(x, y);
if !available_building_tiles.contains(&idx) { possible
= false; }
}
}
}
if possible {
n_buildings += 1;
buildings.push((bx, by, bw, bh));
for y in by .. by+bh {
for x in bx .. bx+bw {
let idx = build_data.map.xy_idx(x, y);
build_data.map.tiles[idx] = TileType::WoodFloor;
available_building_tiles.remove(&idx);
available_building_tiles.remove(&(idx+1));
available_building_tiles.remove(&(idx+build_data.width
as usize));
available_building_tiles.remove(&(idx-1));
available_building_tiles.remove(&(idx-build_data.width
as usize));
}
}
build_data.take_snapshot();
}
}

804 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Outline buildings
let mut mapclone = build_data.map.clone();
for y in 2..build_data.height-2 {
for x in 32..build_data.width-2 {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::WoodFloor {
let mut neighbors = 0;
if build_data.map.tiles[idx - 1] != TileType::WoodFloor {
neighbors +=1; }
if build_data.map.tiles[idx + 1] != TileType::WoodFloor {
neighbors +=1; }
if build_data.map.tiles[idx-build_data.width as usize] !=
TileType::WoodFloor { neighbors +=1; }
if build_data.map.tiles[idx+build_data.width as usize] !=
TileType::WoodFloor { neighbors +=1; }
if neighbors > 0 {
mapclone.tiles[idx] = TileType::Wall;
}
}
}
}
build_data.map = mapclone;
build_data.take_snapshot();
buildings
}

Once again, lets walk through this algorithm:

1. We make a vector of tuples, each containing 4 integers. These are the building's
x and y coordinates, along with its size in each dimension.
2. We make a variable n_buildings to store how many we've placed, and loop
until we have 12. For each building:
1. We pick a random x and y position, and a random width and height
for the building.
2. We set possible to true - and then loop over every tile in the candidate
building location. If it isn't in the available_building_tiles set, we set
possible to false .
3. If possible is still true, we again loop over every tile - setting to be a
WoodenFloor . We then remove that tile, and all four surrounding tiles from
the available_building_tiles list - ensuring a gap between buildings. We
also increment n_buildings , and add the building to a list of completed
buildings.

805 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

3. Now we have 12 buildings, we take a copy of the map.


4. We loop over every tile on in the "town" part of the map.
1. For each tile, we count the number of neighboring tiles that aren't a
WoodenFloor (in all four directions).
2. If the neighboring tile count is greater than zero, then we can place a wall
here (because it must be the edge of a building). We write to our copy of
the map - so as not to in�uence the check on subsequent tiles (otherwise,
you'll have buildings replaced with walls).
5. We put the copy back into our map.
6. We return out list of placed buildings.

If you cargo run now, you'll see that we have buildings!

Adding some doors


The buildings are great, but there are no doors. So you can't ever enter or exit them.
We should �x that. Extend the builder function with another call:

806 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let doors = self.add_doors(rng, build_data, &mut buildings, wall_gap_y);

The add_doors function looks like this:

fn add_doors(&mut self,
rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap,
buildings: &mut Vec<(i32, i32, i32, i32)>,
wall_gap_y : i32)
-> Vec<usize>
{
let mut doors = Vec::new();
for building in buildings.iter() {
let door_x = building.0 + 1 + rng.roll_dice(1, building.2 - 3);
let cy = building.1 + (building.3 / 2);
let idx = if cy > wall_gap_y {
// Door on the north wall
build_data.map.xy_idx(door_x, building.1)
} else {
build_data.map.xy_idx(door_x, building.1 + building.3 - 1)
};
build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, "Door".to_string()));
doors.push(idx);
}
build_data.take_snapshot();
doors
}

This function is quite simple, but we'll step through it:

1. We make a new vector of door locations; we'll need it later.


2. For each building in our buildings list:
1. Set door_x to a random point along the building's horizontal side, not
including the corners.
2. Calculate cy to be the center of the building.
3. If cy > wall_gap_y (remember that one? Where the road is!), we place the
door's y coordinate on the North side - so building.1 . Otherwise, we
place it on the south side - building.1 + building.3 - 1 ( y location plus
height, minus one).
4. We set the door tile to be a Floor .

807 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

5. We add a Door to the spawn list.


6. We add the door to the doors vector.
3. We return the doors vector.

If you cargo run now, you'll see doors appear for each building:

Paths to doors
It would be nice to decorate the gravel with some paths to the various doors in the
town. It makes sense - even wear and tear from walking to/from the buildings will
erode a path. So we add another call to the builder function:

self.add_paths(build_data, &doors);

The add_paths function is a little long, but quite simple:

808 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn add_paths(&mut self,
build_data : &mut BuilderMap,
doors : &[usize])
{
let mut roads = Vec::new();
for y in 0..build_data.height {
for x in 0..build_data.width {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::Road {
roads.push(idx);
}
}
}

build_data.map.populate_blocked();
for door_idx in doors.iter() {
let mut nearest_roads : Vec<(usize, f32)> = Vec::new();
let door_pt = rltk::Point::new( *door_idx as i32 %
build_data.map.width as i32, *door_idx as i32 / build_data.map.width as
i32 );
for r in roads.iter() {
nearest_roads.push((
*r,
rltk::DistanceAlg::PythagorasSquared.distance2d(
door_pt,
rltk::Point::new( *r as i32 % build_data.map.width, *r
as i32 / build_data.map.width )
)
));
}
nearest_roads.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());

let destination = nearest_roads[0].0;


let path = rltk::a_star_search(*door_idx as i32, destination as
i32, &mut build_data.map);
if path.success {
for step in path.steps.iter() {
let idx = *step as usize;
build_data.map.tiles[idx] = TileType::Road;
roads.push(idx);
}
}
build_data.take_snapshot();
}
}

809 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Let's walk through this:

1. We start by making a roads vector, storing the map indices of every road tile on
the map. We gather this by quickly scanning the map and adding matching tiles
to our list.
2. Then we iterate through all the doors we've placed:
1. We make another vector ( nearest_roads ) containing an index and a �oat.
2. We add each road, with its index and the calculated distance to the door.
3. We sort the nearest_roads vector by the distances, ensuring that element
0 will be the closest road position. Not that we're doing this for each door:
if the nearest road is one we've added to another door, it will choose that
one.
4. We call RLTK's a star pathing to �nd a route from the door to the nearest
road.
5. We iterate the path, writing a road tile at each location on the route. We
also add it to the roads vector, so it will in�uence future paths.

If you cargo run now, you'll see a pretty decent start for a town:

810 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Start position and exit


We don't really want completely random start positions, nor an exit that is
deliberately far away on this map. So we'll edit our TownBuilder constructor to
remove the additional meta-builders that provide this:

pub fn town_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator,


width: i32, height: i32) -> BuilderChain {
let mut chain = BuilderChain::new(new_depth, width, height);
chain.start_with(TownBuilder::new());
chain
}

Now we have to modify our build function to provide these instead. Placing the exit
is easy - we want it to be to the East, on the road:

let exit_idx = build_data.map.xy_idx(build_data.width-5, wall_gap_y);


build_data.map.tiles[exit_idx] = TileType::DownStairs;

Placing the entrance is more di�cult. We want the player to start their journey in the
pub - but we haven't decided which building is the pub! We'll make the pub the largest
building on the map. After all, it's the most important for the game! The following
code will sort the buildings by size (in a building_size vector, with the �rst tuple
element being the building's index and the second being it's "square tileage"):

let mut building_size : Vec<(usize, i32)> = Vec::new();


for (i,building) in buildings.iter().enumerate() {
building_size.push((
i,
building.2 * building.3
));
}
building_size.sort_by(|a,b| b.1.cmp(&a.1));

Not that we sorted in descending order (by doing b.cmp(&a) rather than the other
way around) - so the largest building is building 0 .

Now we can set the player's starting location:

811 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

// Start in the pub


let the_pub = &buildings[building_size[0].0];
build_data.starting_position = Some(Position{
x : the_pub.0 + (the_pub.2 / 2),
y : the_pub.1 + (the_pub.3 / 2)
});

If you cargo run now, you'll start in the pub - and be able to navigate an empty town
to the exit:

Wrap-Up
This chapter has walked through how to use what we know about map generation to
make a targeted procedural generation project - a �shing town. There's a river to the
west, a road, town walls, buildings, and paths. It doesn't look bad at all for a starting
point!

It is completely devoid of NPCs, props and anything to do. We'll rectify that in the next

812 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

chapter.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Populating the starting town

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

In the previous chapter, we built the layout of our town. In this chapter, we'll populate
it with NPCs and Props. We'll introduce some new AI types to handle friendly or
neutral NPCs, and begin placing merchants, townsfolk and other residents to make
the town come alive. We'll also begin placing furniture and items to make the place
feel less barren.

Identifying the buildings


We're not making a real, full-sized down. There would be potentially hundreds of
buildings, and the player would quickly grow bored trying to �nd the exit. Instead - we
have 12 buildings. Looking at our design document, two of them are important:

The Pub.
The Temple.

813 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

That leaves 10 other locations that aren't really relevant, but we've implied that they
will include vendors. Brainstorming a few vendors, it would make sense to have:

A Blacksmith (for your weapon/armor needs).


A clothier (for clothes, leather, and similar).
An alchemist for potions, magical items and item identi�cation.

So we're down to 5 more locations to �ll! Lets make three of them into regular homes
with residents, one into your house - complete with a nagging mother, and one into
an abandoned house with a rodent issue. Rodent problems are a staple of fantasy
games, and it might make for a good tutorial when we get that far.

You'll remember that we sorted our buildings by size, and decided that the largest is
the pub. Let's extend that to tag each building. In map_builders/town.rs , look at the
build function and we'll expand the building sorter. First, lets make an enum for our
building types:

enum BuildingTag {
Pub, Temple, Blacksmith, Clothier, Alchemist, PlayerHouse, Hovel,
Abandoned, Unassigned
}

Next, we'll move our building sorter code into its own function (as part of
TownBuilder ):

814 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn sort_buildings(&mut self, buildings: &[(i32, i32, i32, i32)]) ->


Vec<(usize, i32, BuildingTag)>
{
let mut building_size : Vec<(usize, i32, BuildingTag)> = Vec::new();
for (i,building) in buildings.iter().enumerate() {
building_size.push((
i,
building.2 * building.3,
BuildingTag::Unassigned
));
}
building_size.sort_by(|a,b| b.1.cmp(&a.1));
building_size[0].2 = BuildingTag::Pub;
building_size[1].2 = BuildingTag::Temple;
building_size[2].2 = BuildingTag::Blacksmith;
building_size[3].2 = BuildingTag::Clothier;
building_size[4].2 = BuildingTag::Alchemist;
building_size[5].2 = BuildingTag::PlayerHouse;
for b in building_size.iter_mut().skip(6) {
b.2 = BuildingTag::Hovel;
}
let last_index = building_size.len()-1;
building_size[last_index].2 = BuildingTag::Abandoned;
building_size
}

This is the code we had before, with added BuildingTag entries. Once we've sorted
by size, we assign the various building types - with the last one always being the
abandoned house. This will ensure that we have all of our building types, and they are
sorted in descending size order.

In the build function, replace your sort code with a call to the function - and a call to
building_factory , which we'll write in a moment:

let building_size = self.sort_buildings(&buildings);


self.building_factory(rng, build_data, &buildings, &building_size);

Now we'll build a skeletal factory:

815 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn building_factory(&mut self,
rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap,
buildings: &[(i32, i32, i32, i32)],
building_index : &[(usize, i32, BuildingTag)])
{
for (i,building) in buildings.iter().enumerate() {
let build_type = &building_index[i].2;
match build_type {
_ => {}
}
}
}

The Pub
So what would you expect to �nd in a pub early in the morning, when you awaken
hung-over and surprised to discover that you've promised to save the world? A few
ideas spring to mind:

Other hung-over patrons, possibly asleep.


A shady-as-can-be "lost" goods salesperson.
A Barkeep, who probably wants you to go home.
Tables, chairs, barrels.

We'll extend our factory function to have a match line to build the pub:

816 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn building_factory(&mut self,
rng: &mut rltk::RandomNumberGenerator,
build_data : &mut BuilderMap,
buildings: &[(i32, i32, i32, i32)],
building_index : &[(usize, i32, BuildingTag)])
{
for (i,building) in buildings.iter().enumerate() {
let build_type = &building_index[i].2;
match build_type {
BuildingTag::Pub => self.build_pub(&building, build_data,
rng),
_ => {}
}
}
}

And we'll start on the new function build_pub :

817 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build_pub(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place the player
build_data.starting_position = Some(Position{
x : building.0 + (building.2 / 2),
y : building.1 + (building.3 / 2)
});
let player_idx = build_data.map.xy_idx(building.0 + (building.2 / 2),
building.1 + (building.3 / 2));

// Place other items


let mut to_place : Vec<&str> = vec!["Barkeep", "Shady Salesman",
"Patron", "Patron", "Keg",
"Table", "Chair", "Table", "Chair"];
for y in building.1 .. building.1 + building.3 {
for x in building.0 .. building.0 + building.2 {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::WoodFloor && idx !=
player_idx && rng.roll_dice(1, 3)==1 && !to_place.is_empty() {
let entity_tag = to_place[0];
to_place.remove(0);
build_data.spawn_list.push((idx, entity_tag.to_string()));
}
}
}
}

Let's walk through this:

1. The function takes our building data, map information and random number
generator as parameters.
2. Since we always start the player in the pub, we do that here. We can remove it
from the build function.
3. We store the player_idx - we don't want to spawn anything on top of the
player.
4. We make to_place - a list of string tags that we want in the bar. We'll worry
about writing these in a bit.
5. We iterate x and y across the whole building.
1. We calculate the map index of the building tile.

818 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

2. If the building tile is a wooden �oor, the map index is not the player map
index, and a 1d3 roll comes up 1, we:
1. Take the �rst tag from the to_place list, and remove it from the list
(no duplicates unless we put it in twice).
2. Add that tag to the spawn_list for the map, using the current tile
tag.

That's pretty simple, and also parts are de�nitely generic enough to help with future
buildings. If you were to run the project now, you'll see error messages such as:
WARNING: We don't know how to spawn [Barkeep]! . That's because we haven't
written them, yet. We need spawns.json to include all of the tags we're trying to
spawn.

Making non-hostile NPCs

Let's add an entry into spawns.json for our Barkeep. We'll introduce one new
element - the ai :

"mobs" : [
{
"name" : "Barkeep",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

To support the AI element, we need to open up raws/mob_structs.rs and edit Mob :

819 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Deserialize, Debug)]
pub struct Mob {
pub name : String,
pub renderable : Option<Renderable>,
pub blocks_tile : bool,
pub stats : MobStats,
pub vision_range : i32,
pub ai : String
}

We'll also need to add "ai" : "melee" to each other mob. Now open
raws/rawmaster.rs , and we'll edit spawn_named_mob to support it. Replace the line
eb = eb.with(Monster{}); with:

match mob_template.ai.as_ref() {
"melee" => eb = eb.with(Monster{}),
"bystander" => eb = eb.with(Bystander{}),
_ => {}
}

Bystander is a new component - so we need to open up components.rs and add it:

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct Bystander {}

Then don't forget to register it in main.rs and saveload_system.rs !

If you cargo run now, you should see a smiling barkeep. He's resplendent in Purple
(RGB #EE82EE from the JSON). Why purple? We're going to make vendors purple
eventually (vendors are for a future chapter):

He won't react to you or do anything, but he's there. We'll add some behavior later in

820 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

the chapter. For now, lets go ahead and add some other entities to spawns.json now
that we support innocent bystanders (pro-tip: copy an existing entry and edit it; much
easier than typing it all out again):

{
"name" : "Shady Salesman",
"renderable": {
"glyph" : "h",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

{
"name" : "Patron",
"renderable": {
"glyph" : "☺",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

If you cargo run now, the bar comes to life a bit more:

821 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Adding props
A pub with people and nothing for them to drink, sit on or eat at is a pretty shabby
pub. I suppose we could argue that it's a real dive and the budget won't stretch to
that, but that argument wears thin when you start adding other buildings. So we'll add
some props to spawns.json :

822 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Keg",
"renderable": {
"glyph" : "φ",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Table",
"renderable": {
"glyph" : "╦",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Chair",
"renderable": {
"glyph" : "└",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
}

If you cargo run now, you'll see some inert props littering the pub:

That's not amazing, but it already feels more alive!

823 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Making the temple


The temple will be similar to the pub in terms of spawning code. So similar, in fact,
that we're going to break out the part of the build_pub function that spawns entities
and make a generic function out of it. Here's the new function:

fn random_building_spawn(
&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator,
to_place : &mut Vec<&str>,
player_idx : usize)
{
for y in building.1 .. building.1 + building.3 {
for x in building.0 .. building.0 + building.2 {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::WoodFloor && idx !=
player_idx && rng.roll_dice(1, 3)==1 && !to_place.is_empty() {
let entity_tag = to_place[0];
to_place.remove(0);
build_data.spawn_list.push((idx, entity_tag.to_string()));
}
}
}
}

We'll replace our call to that code in build_pub with:

// Place other items


let mut to_place : Vec<&str> = vec!["Barkeep", "Shady Salesman", "Patron",
"Patron", "Keg",
"Table", "Chair", "Table", "Chair"];
self.random_building_spawn(building, build_data, rng, &mut to_place,
player_idx);

With that in place, let's think about what you might �nd in a temple:

Priests
Parishioners
Chairs

824 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Candles

Now we'll extend our factory to include temples:

match build_type {
BuildingTag::Pub => self.build_pub(&building, build_data, rng),
BuildingTag::Temple => self.build_temple(&building, build_data, rng),
_ => {}
}

And our build_temple function can be very simple:

fn build_temple(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place items
let mut to_place : Vec<&str> = vec!["Priest", "Parishioner",
"Parishioner", "Chair", "Chair", "Candle", "Candle"];
self.random_building_spawn(building, build_data, rng, &mut to_place,
0);
}

So, with that in place - we still have to add Priests, Parishioners, and Candles to the
spawns.json list. The Priest and Parishioner go in the mobs section, and are basically
the same as the Barkeep:

825 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Priest",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

{
"name" : "Parishioner",
"renderable": {
"glyph" : "☺",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

Likewise, for now at least - candles are just another prop:

826 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Candle",
"renderable": {
"glyph" : "Ä",
"fg" : "#FFA500",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
}

If you cargo run now, you can run around and �nd a temple:

Build other buildings


We've done most of the hard work now, so we are just �lling in the blanks. Lets
expand our match in our builder to include the various types other than the
Abandoned House:

827 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let build_type = &building_index[i].2;


match build_type {
BuildingTag::Pub => self.build_pub(&building, build_data, rng),
BuildingTag::Temple => self.build_temple(&building, build_data, rng),
BuildingTag::Blacksmith => self.build_smith(&building, build_data,
rng),
BuildingTag::Clothier => self.build_clothier(&building, build_data,
rng),
BuildingTag::Alchemist => self.build_alchemist(&building, build_data,
rng),
BuildingTag::PlayerHouse => self.build_my_house(&building, build_data,
rng),
BuildingTag::Hovel => self.build_hovel(&building, build_data, rng),
_ => {}
}

We're lumping these in together because they are basically the same function! Here's
the body of each of them:

828 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build_smith(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place items
let mut to_place : Vec<&str> = vec!["Blacksmith", "Anvil", "Water
Trough", "Weapon Rack", "Armor Stand"];
self.random_building_spawn(building, build_data, rng, &mut to_place,
0);
}

fn build_clothier(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place items
let mut to_place : Vec<&str> = vec!["Clothier", "Cabinet", "Table",
"Loom", "Hide Rack"];
self.random_building_spawn(building, build_data, rng, &mut to_place,
0);
}

fn build_alchemist(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place items
let mut to_place : Vec<&str> = vec!["Alchemist", "Chemistry Set",
"Dead Thing", "Chair", "Table"];
self.random_building_spawn(building, build_data, rng, &mut to_place,
0);
}

fn build_my_house(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place items
let mut to_place : Vec<&str> = vec!["Mom", "Bed", "Cabinet", "Chair",
"Table"];
self.random_building_spawn(building, build_data, rng, &mut to_place,

829 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

0);
}

fn build_hovel(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
// Place items
let mut to_place : Vec<&str> = vec!["Peasant", "Bed", "Chair",
"Table"];
self.random_building_spawn(building, build_data, rng, &mut to_place,
0);
}

As you can see - these are basically passing spawn lists to the building spawner, rather
than doing anything too fancy. We've created quite a lot of new entities here! I tried to
come up with things you might �nd in each location:

The smith has of course got a Blacksmith. He likes to be around Anvils, Water
Troughs, Weapon Racks, and Armor Stands.
The clothier has a Clothier, and a Cabinet, a Table, a Loom and a Hide Rack.
The alchemist has an Alchemist, a Chemistry Set, a Dead Thing (why not, right?), a
Chair and a Table.
My House features Mom (the characters mother!), a bed, a cabinet, a chair and a
table.
Hovels feature a Peasant, a bed, a chair and a table.

So we'll need to support these in spawns.json :

830 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Blacksmith",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

{
"name" : "Clothier",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

{
"name" : "Alchemist",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},

831 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

{
"name" : "Mom",
"renderable": {
"glyph" : "☺",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},

{
"name" : "Peasant",
"renderable": {
"glyph" : "☺",
"fg" : "#999999",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,

832 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"ai" : "bystander"
},

And in the props section:

833 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Anvil",
"renderable": {
"glyph" : "╔",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Water Trough",
"renderable": {
"glyph" : "•",
"fg" : "#5555FF",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Weapon Rack",
"renderable": {
"glyph" : "π",
"fg" : "#FFD700",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Armor Stand",
"renderable": {
"glyph" : "⌠",
"fg" : "#FFFFFF",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Chemistry Set",

834 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"renderable": {
"glyph" : "δ",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Dead Thing",
"renderable": {
"glyph" : "☻",
"fg" : "#AA0000",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Cabinet",
"renderable": {
"glyph" : "∩",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Bed",
"renderable": {
"glyph" : "8",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Loom",
"renderable": {
"glyph" : "≡",

835 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},

{
"name" : "Hide Rack",
"renderable": {
"glyph" : "π",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
}

If you cargo run now, you can run around and �nd largely populated rooms:

Hopefully, you also spot the bug: the player beat his/her Mom (and the alchemist)! We
don't really want to encourage that type of behavior! So in the next segment, we'll
work on some neutral AI and player movement behavior with NPCs.

836 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Neutral AI/Movement
There are two issues present with our current "bystander" handling: bystanders just
stand there like lumps (blocking your movement, even!), and there is no way to get
around them without slaughtering them. I'd like to think our hero won't start his/her
adventure by murdering their Mom - so lets rectify the situation!

Trading Places

Currently, when you "bump" into a tile containing anything with combat stats - you
launch an attack. This is provided in player.rs , the try_move_player function:

let target = combat_stats.get(*potential_target);


if let Some(_target) = target {
wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target
}).expect("Add target failed");
return;
}

We need to extend this to not only attack, but swap places with the NPC when we
bump into them. This way, they can't block your movement - but you also can't
murder your mother! So �rst, we need to gain access to the Bystanders component
store, and make a vector in which we will store our intent to move NPCs (we can't just
access them in-loop; the borrow checker will throw a �t, unfortunately):

let bystanders = ecs.read_storage::<Bystander>();

let mut swap_entities : Vec<(Entity, i32, i32)> = Vec::new();

So in swap_entities , we're storing the entity to move and their x/y destination
coordinates. Now we adjust our main loop to check to see if a target is a bystander,
add them to the swap list and move anyway if they are. We also make attacking
conditional upon them not being a bystander:

837 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

let bystander = bystanders.get(*potential_target);


if bystander.is_some() {
// Note that we want to move the bystander
swap_entities.push((*potential_target, pos.x, pos.y));

// Move the player


pos.x = min(map.width-1 , max(0, pos.x + delta_x));
pos.y = min(map.height-1, max(0, pos.y + delta_y));
entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert
marker");

viewshed.dirty = true;
let mut ppos = ecs.write_resource::<Point>();
ppos.x = pos.x;
ppos.y = pos.y;
} else {
let target = combat_stats.get(*potential_target);
if let Some(_target) = target {
wants_to_melee.insert(entity, WantsToMelee{ target:
*potential_target }).expect("Add target failed");
return;
}
}

Finally, at the very end of the function we iterate through swap_entities and apply
the movement:

for m in swap_entities.iter() {
let their_pos = positions.get_mut(m.0);
if let Some(their_pos) = their_pos {
their_pos.x = m.1;
their_pos.y = m.2;
}
}

If you cargo run now, you can no longer murder all of the NPCs; bumping into them
swaps your positions:

838 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

The Abandoned House


Lastly (for this chapter), we need to populate the abandoned house. We decided that
it was going to contain a massive rodent problem, since rodents of unusual size are a
signi�cant problem for low-level adventurers! We'll add another match line to our
building factory matcher:

BuildingTag::Abandoned => self.build_abandoned_house(&building,


build_data, rng),

And here's the function to about half-�ll the house with rodents:

839 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn build_abandoned_house(&mut self,
building: &(i32, i32, i32, i32),
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator)
{
for y in building.1 .. building.1 + building.3 {
for x in building.0 .. building.0 + building.2 {
let idx = build_data.map.xy_idx(x, y);
if build_data.map.tiles[idx] == TileType::WoodFloor && idx !=
0 && rng.roll_dice(1, 2)==1 {
build_data.spawn_list.push((idx, "Rat".to_string()));
}
}
}
}

Lastly, we need to add Rat to the mob list in spawns.json :

{
"name" : "Rat",
"renderable": {
"glyph" : "r",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 2,
"hp" : 2,
"defense" : 1,
"power" : 3
},
"vision_range" : 8,
"ai" : "melee"
},

If you cargo run now, and hunt around for the abandoned house - you'll �nd it full of
hostile rats:

840 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrap-Up
In this chapter, we've added a bunch of props and bystanders to the town - as well as
a house full of angry rats. That makes it feel a lot more alive. It's by no means done
yet, but it's already starting to feel like the opening scene of a fantasy game. In the
next chapter, we're going to make some AI adjustments to make it feel more alive -
and add some bystanders who aren't conveniently hanging around inside buildings.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Bringing NPCs to Life

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

841 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

I'd like to suggest dark incantations and candles to breathe life into NPCs, but in
reality - it's more code. We don't want our bystanders to stand around, dumb as rocks
anymore. They don't have to behave particularly sensibly, but it would be good if they
at least roam around a bit (other than vendors, that gets annoying - "where did the
blacksmith go?") and tell you about their day.

New components - dividing vendors from bystanders


First, we're going to make a new component - the Vendor . In components.rs , add the
following component type:

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct Vendor {}

Don't forget to register it in main.rs and saveload_system.rs !

Now we'll adjust our raw �les ( spawns.json ); all of the merchants who feature
"ai" : "bystander" need to be changed to "ai" : "vendor" . So we'll change it for
our Barkeep, Alchemist, Clothier, Blacksmith and Shady Salesman.

Next, we adjust our raws/rawmaster.rs 's function spawn_named_mob to also spawn


vendors:

match mob_template.ai.as_ref() {
"melee" => eb = eb.with(Monster{}),
"bystander" => eb = eb.with(Bystander{}),
"vendor" => eb = eb.with(Vendor{}),
_ => {}
}

Finally, we'll adjust the try_move_player function in player.rs to also not attack
vendors:

842 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
let vendors = ecs.read_storage::<Vendor>();

let mut swap_entities : Vec<(Entity, i32, i32)> = Vec::new();

for (entity, _player, pos, viewshed) in (&entities, &players, &mut


positions, &mut viewsheds).join() {
let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);

for potential_target in map.tile_content[destination_idx].iter() {


let bystander = bystanders.get(*potential_target);
let vendor = vendors.get(*potential_target);
if bystander.is_some() || vendor.is_some() {
...

A System for Moving Bystanders


We want bystanders to wander around the town. We won't have them open doors, to
keep things consistent (so when you enter the pub, you can expect patrons - and they
won't have wandered o� to �ght rats!). Make a new �le, bystander_ai_system.rs
and paste the following into it:

843 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

use specs::prelude::*;
use super::{Viewshed, Bystander, Map, Position, RunState, EntityMoved};

pub struct BystanderAI {}

impl<'a> System<'a> for BystanderAI {


#[allow(clippy::type_complexity)]
type SystemData = ( WriteExpect<'a, Map>,
ReadExpect<'a, RunState>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
ReadStorage<'a, Bystander>,
WriteStorage<'a, Position>,
WriteStorage<'a, EntityMoved>,
WriteExpect<'a, rltk::RandomNumberGenerator>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, runstate, entities, mut viewshed, bystander, mut
position,
mut entity_moved, mut rng) = data;

if *runstate != RunState::MonsterTurn { return; }

for (entity, mut viewshed, _bystander, mut pos) in (&entities,


&mut viewshed, &bystander, &mut position).join() {
// Try to move randomly
let mut x = pos.x;
let mut y = pos.y;
let move_roll = rng.roll_dice(1, 5);
match move_roll {
1 => x -= 1,
2 => x += 1,
3 => y -= 1,
4 => y += 1,
_ => {}
}

if x > 0 && x < map.width-1 && y > 0 && y < map.height-1 {


let dest_idx = map.xy_idx(x, y);
if !map.blocked[dest_idx] {
let idx = map.xy_idx(pos.x, pos.y);
map.blocked[idx] = false;
pos.x = x;
pos.y = y;
entity_moved.insert(entity,

844 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

EntityMoved{}).expect("Unable to insert marker");


map.blocked[dest_idx] = true;
viewshed.dirty = true;
}
}
}
}
}

If you remember from the systems we've made before, the �rst part is boilerplate
telling the ECS what resources we want to access. We check to see if it is the monster's
turn (really, NPCs are monsters in this setup); if it isn't, we bail out. Then we roll a dice
for a random direction, see if we can go that way - and move if we can. It's pretty
simple!

In main.rs , we need to tell it to use the new module:

pub mod bystander_ai_system;

We also need to add the system to our list of systems to run:

845 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

impl State {
fn run_systems(&mut self) {
let mut mapindex = MapIndexingSystem{};
mapindex.run_now(&self.ecs);
let mut vis = VisibilitySystem{};
vis.run_now(&self.ecs);
let mut mob = MonsterAI{};
mob.run_now(&self.ecs);
let mut bystander = bystander_ai_system::BystanderAI{};
bystander.run_now(&self.ecs);
let mut triggers = trigger_system::TriggerSystem{};
triggers.run_now(&self.ecs);
let mut melee = MeleeCombatSystem{};
melee.run_now(&self.ecs);
let mut damage = DamageSystem{};
damage.run_now(&self.ecs);
let mut pickup = ItemCollectionSystem{};
pickup.run_now(&self.ecs);
let mut itemuse = ItemUseSystem{};
itemuse.run_now(&self.ecs);
let mut drop_items = ItemDropSystem{};
drop_items.run_now(&self.ecs);
let mut item_remove = ItemRemoveSystem{};
item_remove.run_now(&self.ecs);
let mut hunger = hunger_system::HungerSystem{};
hunger.run_now(&self.ecs);
let mut particles = particle_system::ParticleSpawnSystem{};
particles.run_now(&self.ecs);

self.ecs.maintain();
}
}

If you cargo run the project now, you can watch NPCs bumbling around randomly.
Having them move goes a long way to not making it feel like a town of statues!

846 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Quipping NPCs
To further brings things to life, lets allow NPCs to "quip" when they spot you. In
spawns.json , lets add some quips to the Patron (bar patron):

{
"name" : "Patron",
"renderable": {
"glyph" : "☺",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander",
"quips" : [ "Quiet down, it's too early!", "Oh my, I drank too much.",
"Still saving the world, eh?" ]
},

We need to modify raws/mob_structs.rs to handle loading this data:

#[derive(Deserialize, Debug)]
pub struct Mob {
pub name : String,
pub renderable : Option<Renderable>,
pub blocks_tile : bool,
pub stats : MobStats,
pub vision_range : i32,
pub ai : String,
pub quips : Option<Vec<String>>
}

We also need to create a component to hold available quips. In components.rs :

847 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

#[derive(Component, Debug, Serialize, Deserialize, Clone)]


pub struct Quips {
pub available : Vec<String>
}

Don't forget to register it in main.rs and saveload_system.rs !

We need to update rawmaster.rs 's function spawn_named_mob to be able to add this


component:

if let Some(quips) = &mob_template.quips {


eb = eb.with(Quips{
available: quips.clone()
});
}

Lastly, we'll add the ability to enter these quips into the game log when they spot you.
In bystander_ai_system.rs . First, extend the available set of data for the system as
follows:

...
WriteExpect<'a, rltk::RandomNumberGenerator>,
ReadExpect<'a, Point>,
WriteExpect<'a, GameLog>,
WriteStorage<'a, Quips>,
ReadStorage<'a, Name>);

fn run(&mut self, data : Self::SystemData) {


let (mut map, runstate, entities, mut viewshed, bystander, mut
position,
mut entity_moved, mut rng, player_pos, mut gamelog, mut quips,
names) = data;
...

You may remember this: it gets read-only access the the Point resource we store
containing the player's location, write access to the GameLog , and access to the
component stores for Quips and Name . Now, we add the quipping to the function
body:

848 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

...
for (entity, mut viewshed,_bystander,mut pos) in (&entities, &mut
viewshed, &bystander, &mut position).join() {
// Possibly quip
let quip = quips.get_mut(entity);
if let Some(quip) = quip {
if !quip.available.is_empty() &&
viewshed.visible_tiles.contains(&player_pos) && rng.roll_dice(1,6)==1 {
let name = names.get(entity);
let quip_index = if quip.available.len() == 1 { 0 } else {
(rng.roll_dice(1, quip.available.len() as i32)-1) as usize };
gamelog.entries.insert(0,
format!("{} says \"{}\"", name.unwrap().name,
quip.available[quip_index])
);
quip.available.remove(quip_index);
}
}

// Try to move randomly


...

We can step through what it's doing:

1. It asks for a component from the quips store. This will be an Option - either
None (nothing to say) or Some - containing the quips.
2. If there are some quips...
3. If the list of available quips isn't empty, the viewshed contains the player's tile,
and 1d6 roll comes up 1...
4. We look up the entity's name,
5. Randomly pick an entry in the available list from quip .
6. Log a string as Name says Quip .
7. Remove the quip from that entity's available quip list - they won't keep repeating
themselves.

If you run the game now, you'll �nd that patrons are willing to comment on life in
general:

849 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

We'll �nd that this can be used in other parts of the game, such as having guards
shouting alerts, or goblins saying appropriately "Goblinesque" things. For brevity, we
won't list every quip in the game here. Check out the source to see what we've added.

This sort of "�u�" goes a long way towards making a world feel alive, even if it doesn't
really add to gameplay in a meaningful fashion. Since the town is the �rst area the
player sees, it's good to have �u�.

Outdoor NPCs
All of the NPCs in the town so far have been conveniently located inside buildings. It
isn't very realistic, even in terrible weather (which we don't have!); so we should look
at spawning a few outdoor NPCs.

Open up map_builders/town.rs and we'll make two new functions; here's the calls to
them in the main build function:

850 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

self.spawn_dockers(build_data, rng);
self.spawn_townsfolk(build_data, rng, &mut available_building_tiles);

The spawn_dockers function looks for bridge tiles, and places various people on
them:

fn spawn_dockers(&mut self, build_data : &mut BuilderMap, rng: &mut


rltk::RandomNumberGenerator) {
for (idx, tt) in build_data.map.tiles.iter().enumerate() {
if *tt == TileType::Bridge && rng.roll_dice(1, 6)==1 {
let roll = rng.roll_dice(1, 3);
match roll {
1 => build_data.spawn_list.push((idx, "Dock
Worker".to_string())),
2 => build_data.spawn_list.push((idx, "Wannabe
Pirate".to_string())),
_ => build_data.spawn_list.push((idx,
"Fisher".to_string())),
}
}
}
}

This is simple enough: for each tile on the map, retrieve its index and type. If its a
bridge, and a 1d6 comes up a 1 - spawn someone. We randomly pick between Dock
Workers, Wannabe Pirates and Fisherfolk.

spawn_townsfolk is pretty simple, too:

851 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

fn spawn_townsfolk(&mut self,
build_data : &mut BuilderMap,
rng: &mut rltk::RandomNumberGenerator,
available_building_tiles : &mut HashSet<usize>)
{
for idx in available_building_tiles.iter() {
if rng.roll_dice(1, 10)==1 {
let roll = rng.roll_dice(1, 4);
match roll {
1 => build_data.spawn_list.push((*idx,
"Peasant".to_string())),
2 => build_data.spawn_list.push((*idx,
"Drunk".to_string())),
3 => build_data.spawn_list.push((*idx, "Dock
Worker".to_string())),
_ => build_data.spawn_list.push((*idx,
"Fisher".to_string())),
}
}
}
}

This iterates all the remaining availble_building_tiles ; these are tiles we know
won't be inside of a building, because we removed them when we placed buildings! So
each spot is guaranteed to be outdoors, and in town. For each tile, we roll 1d10 - and
if its a 1, we spawn one of a Peasant, a Drunk, a Dock Worker or a Fisher.

Lastly, we add these folk to our spawns.json �le:

852 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

{
"name" : "Dock Worker",
"renderable": {
"glyph" : "☺",
"fg" : "#999999",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander",
"quips" : [ "Lovely day, eh?", "Nice weather", "Hello" ]
},

{
"name" : "Fisher",
"renderable": {
"glyph" : "☺",
"fg" : "#999999",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander",
"quips" : [ "They're biting today!", "I caught something, but it
wasn't a fish!", "Looks like rain" ]
},

{
"name" : "Wannabe Pirate",
"renderable": {
"glyph" : "☺",
"fg" : "#aa9999",

853 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander",
"quips" : [ "Arrr", "Grog!", "Booze!" ]
},

{
"name" : "Drunk",
"renderable": {
"glyph" : "☺",
"fg" : "#aa9999",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander",
"quips" : [ "Hic", "Need... more... booze!", "Spare a copper?" ]
},

If you cargo run now, you'll see a town teeming with life:

854 of 856 2019-11-02, 3:59 p.m.


Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

Wrap-Up
This chapter has really brought our town to life. There's always room for
improvement, but it's good enough for a starting map! The next chapter will change
gear, and start adding stats to the game.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in


your browser (WebGL2 required)
Copyright (C) 2019, Herbert Wolverson.

Contributors
855 of 856 2019-11-02, 3:59 p.m.
Roguelike Tutorial - In Rust https://bfnightly.bracketproductions.com/rustbook...

About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to
do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

The following people have contributed to this project:

Herbert Wolverson, the primary author.


Marius Gedminas provided some �xes to the visibility system, and chapter 5.
Corrected a typo in chapter 7, �xed a Markdown error in chapter 7, �xed
indentation in chapter 13, and a typo in chapter 18. He also �xed some code in
my hunger clock to let monsters get hungry too, if we so desire.
Ben Morrison �xed an issue in chapter 4, correctly matching room passing-by-
reference with the accompanying source code.
Tommi Jalkanen helped me remove some Dispatcher code that I'd accidentally
left in place. He also found an issue with bounds checking on movement, for
which I am eternally grateful!
Gabriel Martinez helped me �x the Vi keys.
Dominic D found some issues with chapter 7.

Supporters
I'd also like to take a moment to thank everyone who has sent me kind words,
contributed with issue reports, and the following Patrons (from patreon.com):

Ben Gamble
Noah
Ryan Orlando
Shane Sveller
Tom Leys

Copyright (C) 2019, Herbert Wolverson.

856 of 856 2019-11-02, 3:59 p.m.

You might also like