I now have the game up to the point where the player has a random dungeon to explore. It's time to make it more interesting by adding some threats - namely, some monsters.
These new monsters are all going to be added as entities to the entities array. First though, I converted the array to an entities vector, since Rust arrays need to have known sizes. I still kept the player in slot 0 since I want my rendering code to be able to iterate uniformly over the entities list.
Since I'm going to be adding these monsters as part of the random map generation, I also needed to pass in the entities vector to the map code. I'm not really thrilled about this design, as I can see that one likely eventual outcome is having pretty much all the code in the program depend on the entities list. I did consider some alternatives, but they all involved some version of the map keeping track of the rooms or monsters privately, and then having my main function retrieve the monsters from the map and adding them to the entities vector. That actually seemed quite a bit worse, so I discarded that plan. As development progresses, I'll want to keep an eye on all the dependencies on the entities vector to see if it gets out of hand.
So immediately, this dependency means I had to rearrange some of the main code. The player gets created first, and gets added to the entities in slot 0. However, until I create the map, I won't know where to place the player. It's easy enough to work around, but still a minor annoyance. Since I didn't want to borrow the player for the entire main function, I created a new scope to borrow the player just to set its position. This is unique to Rust since its borrow checker requires that I tell the compiler that this variable won't be used for sure after these statements.
A couple of small side notes: I think there's a bug in the Python tutorial, with the way the monsters are generated. The tutorial uses a range that can potentially generate a monster on the walls of the room. That doesn't actually cause a problem, since there's code later to drop the monster if it's on a wall, but I went ahead and just fixed the range in my program. Also, I needed to pick some colors for the monsters. I took a look at the tcod source code to see what how it generates the color variants, saw some linear interpolation code, and decided that's just way too tedious for me to port. So I went online and picked some colors instead.
This first version of the monster generator gets a random (x,y) value from the dimensions of each room, and then generates a random monster. There currently isn't any overlap checking, so if I happen to get the same (x,y) value again, I'll have two monsters on the same cell. I fixed that by iterating through all the existing entities to see if there's an overlapping one. (The Python version actually adds a "blocking" flag to the entities, since there will eventually be entities like potions that can overlap monsters and players. I generally discourage adding things until I'm directly building a feature with it, but in this case it's small enough that I throw it in here as well.)
Fortunately the Rust borrow checker doesn't complain. I didn't think it should, but there can always be surprises. At least the surprises are getting fewer in number as I get more experience.
Given the randomized nature of the monster generation, there's no simple direct way to test the overlap check. I added in a temporary print statement whenever an overlap was detected, and launched the game a few times. I saw it get printed out occasionally, and I considered that good enough testing for this code.
Managing the game state
Now that there are monsters, the game has to manage the player and monster taking turns. The turns are mutually exclusive, so I think the right construct here is an enum. I'll also add in a state for "exiting" when the user presses escape; that way, I don't have the handle_keys function return a weird boolean telling the main program to quit. So there are three states: PlayerTurn, EnemyTurn, and Exiting.
What happens if we reach the handle_keys code and it's not the player's turn?
In this game design, that shouldn't be possible. I added an assert just to make sure.
What happens if the player tries to walk into a wall? Does the player lose their turn, or should we let the player try again?
Now I'm in the realm of not just code design, but game design. I didn't think I'd be able to reason a solution to this one, so I just made my best guess - let the player try again - and I'll tweak it after playtesting if it "doesn't feel right."
Finally I updated my main loop. The game starts in the player's turn, and it handles keypresses. If the player doesn't walk into a wall, update the player's position, and set the state to the enemy's turn. Then, back in the main loop after handling keypresses, if it's the enemy's turn, let the enemies do some action (for now, just logging to the console). It's also at this point that I discover a bug in the field of vision code. While handling key presses, I computed the new position of the player, but I neglected to check if it was a move action before calling the FOV code. In other words, every time through the keypress handler, for non movement keys, I was setting the player back to the same location, then forcing an FOV recomputation. The game doesn't support any other keys right now... but still, this was a pretty serious bug. After fixing this, it's straightforward to test the turn code. When the player moves, the enemy prints an action. When the player tries to move into a wall, I called that no action, and as expected, the enemy doesn't take a turn.
The last thing to handle for the game state is when the player makes contact with the enemy. To keep this phase manageable, I'll just log when the player is moving onto an enemy, and defer the combat engine until later.
To check for player combat, I need to iterate through all the entities, and see if any entity's location overlaps with the player's new position.
I now have enough experience with Rust to know that the compiler will almost certainly complain. Since the player is part of the entities vector, I need to borrow a reference to it. I'll need to modify the player's position later, so I'll need a mutable reference. That's going to pin the entire vector... meaning I won't be able to iterate through the monsters to compare their positions.
However, since I'm learning the language, I implemented this anyway, just to see if my understanding was correct. It was, and yes, the code fails to compile.
The most direct fix is to borrow twice, the first time for the comparisons, and the second time to update the player position. This is a reasonable solution, and I don't think there's anything wrong with it. However, I've been reading up on the standard library during some of my downtime, and I found a more elegant solution. Rust vectors have a method to split the vector into two slices, each of which can be independently borrowed. So:
let (player_array,enemies) = game.entities.split_at_mut(1); let player = &mut player_array;
Now I can borrow the player and the entities array independently. I also think this expresses much better the meaning I'm trying to convey. Yes, in the rendering contexts, treat all the entities as uniform. But in the movement context, treat the player as a completely separate entity from the enemy entities.
For UI purposes, I also needed to add a name to the entities, so that I could print what kind of enemy is being targeted. That's simple enough. Just add another ivar to the Entity struct, of type &str.
Of course, the borrow checker managed to get me one last time.
Somehow, after all this development, I hadn't needed to add a reference as an element to a struct. Of course, the problem is fairly obvious with the explicit compiler error: if the struct has a reference to an object, the object has to live longer than the struct does. Otherwise, the struct is left with a dangling pointer. In the reference book, there's an entire section dealing with struct reference lifetimes (which itself is a part of an entire subsection of a chapter dealing only with annotating lifetimes).
And here I thought I was finally getting the hang of the borrow checker...
Learning all of this lifetime syntax is going to take me a while, and I didn't want to completely block further progress on this game in the meantime. So I took the easy way out and just marked all the name references as static - meaning that the names have to be string literals. That suffices for now, and the best part is, the compiler will tell me if I accidentally try to use a dynamic string, so I don't have to worry about having to debug weird memory corruption later!
On the development side, I don't think there's much new in the process. It continues to be a cycle of decide feature, design, implement, refactor - and with the code base continuing to expand, each change is becoming smaller and smaller relative to the rest of the code base.
On the Rust side, I feel like I've finally reached the point where I can anticipate the borrow checker's compiler errors. I might not always have a good fix, but I can see when it's wrong. That may sound kind of minor, but I consider that a pretty big milestone... the closest analogue would be learning pointers in C - when I finally understood pointers and could pick the right syntax instead of trying to guess how many *'s or &'s go in front of a variable.