Building a roguelike in F# from scratch - Repo and Initial Types

in #programming7 years ago (edited)

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

Sort:  

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 😀

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 :)

Coin Marketplace

STEEM 0.17
TRX 0.15
JST 0.028
BTC 60336.09
ETH 2333.36
USDT 1.00
SBD 2.53