Building a roguelike in F# from scratch - Repo and Initial Types
Introduction
This is the first of my development blogs for my roguelike started in Building a roguelike in F# from scratch - Introduction
But first some house keeping. The project repo is now up on GitHub as Woz.FSharpeRoguelike, this will always be in sync or slightly ahead of the development blog.
I will pick a suitable article picture once the project is named.
In this post I am exploring the structure used to store a game world level. I am using this post as a form of Rubber Duck Debugging as talking about the structures make me think about them in more detail :)
Level Structure
We will start with a means to hold a coordinate pair. I could have used a tuple for this but the record type gives us named values hence I am using the type. I will layer the functionality from my C# Vector object later later on, you can see that code in Roguelike line of sight calculation
type Vector = {x: int; y: int}
Time to build the structure to hold a level tile map. For now a map will be a 2D array of Tiles and a width and height held in a vector. I do not plan on this being mutated hence the 2D array, that would be too costly to constantly mutate on a large map.
type Tile = Void | Floor | Wall | Water
type Map = {tiles: Tile[][]; mapSize: Vector}
You can open, close and unlock door meaning they mutate. This is the reason they are not contained in Map, they will be in a dictionary keyed by their location in the level, a cheaper update solution. For now just the door states, you will see the map in the level. The string held against the locked door is the name of the key that opens it.
type Door =
| Open
| Closed
| Locked of string // Key name
The item definition is just a basic place holder for the time being. The ItemId type is just to ensure we use the correct ID when talking about items, and the name should be self evident.
As with doors, there will be a dictionary to say where items are in the map, when not held by an actor.
type ItemId = int
type Item = {id: ItemId; name: string}
Actors are used to describe the Player and NPCs. By now you should be able to figure out the structure from the following code, this has been designed for best balance between flexibility, performance and update speed. It is simple to add more stats if and when needed.
type Stats =
| Health
| Strength
| Intelligence
| Stamina
| Dexterity
type Stat = {current: int; max: int}
type ActorId = int
type Actor =
{
id: int
isNpc: bool
name: string
stats: Map<Stats, Stat>
location: Vector
backpack: List<ItemId>
}
The level ties all of the above together. The only real surprise here is probably mapItems, I could have just stuffed the location in Item but that would not have been a viable field when held by an actor. So the choice was an Optional/Maybe Monad or a dictionary lookup. The extra dictionary trades lookup speed against update costs and complexity but for now it felt the more flexible choice.
type Level =
{
playerId: ActorId
map: Map;
doors: Map<Vector, Door>;
actors: Map<ActorId, Actor>
items: Map<ItemId, Item>
mapItems: Map<Vector, List<ItemId>>
}
That is actually a fair chunk of storage structure for a small amount of code.
If you were to build this as classes in an OO language would be far more verbose, even more so if you forced immutability into the classes.
Here is the full code from above unbroken by comments, not even a page :)
type Vector = {x: int; y: int}
type Tile = Void | Floor | Wall | Water
type Map = {tiles: Tile[][]; mapSize: Vector}
type Door =
| Open
| Closed
| Locked of string // Key name
type ItemId = int
type Item = {id: ItemId; name: string; }
type Stats =
| Health
| Strength
| Intelligence
| Stamina
| Dexterity
type Stat = {current: int; max: int}
type ActorId = int
type Actor =
{
id: int
isNpc: bool
name: string
stats: Map<Stats, Stat>
location: Vector
backpack: List<ItemId>
}
type Level =
{
playerId: ActorId
map: Map;
doors: Map<Vector, Door>;
actors: Map<ActorId, Actor>
items: Map<ItemId, Item>
mapItems: Map<Vector, List<ItemId>>
}
Ask if you have any questions on my choices and I will try to explain them
Happy Coding
Woz
Hey - you have a typo in the title... Should be ROGUE not ROUGE. 😀
Other than that, keep the series coming. This is great.
Thanks for that. Can't believe I didn't spot that. And as I copy pasted the title from the into they were both the same.
I can mask the mistake in the text by editing but forever logged in the URL rotf
Makes me wonder what a rougelike would be now. A makeup sim? How did you know this was not a makeup sim though :)
Haha yes, I'm no coder so clearly just didn't read that it's instructions on how to program the most blushing red cheeks in all of gaming 😀
Very interesting
I know the point of rougelikes is to start over when you die. But I still wish more of them had some persistence in progression like keeping your level, gear, gold etc so you at least feel some sort of accomplishment even if you die.
I will take that on board, I have played a few like that and makes death less soul crushing :)