Morgemil Game Update #5

I’ve been working off and on making a video game in F#. I thought I’d drop an update saying my progress.

Github

I’ve done 20 commits since the beginning of the year according to GitHub. Although progress technically started on Mar 15, 2015.

This number doesn’t mean anything to anyone. I just use it so I can look at the last progress update I did and be motivated because I am writing code, even if not all code directly becomes a very visual output.

Github progress

Progress

Updating to .NET 6

All F# projects were in netcoreapp3.1 or netstandard2.1 before being updated to net6.0 which has been smooth.

No trouble was experienced with the framework version.

Updating packages had a few breaking changes related to XNA vs SadConsole types and a few namespace changes. On the whole, it was remarkably painless. I’ve kept this project fairly clear of dependencies, and most of the dependencies are about testing which is acceptable.

NuGet Package Versions

Builds with FAKE and on GitHub.

The F# ecosystem has a build tool called FAKE. I’ve defined the build script in FAKE and then calling it from GitHub Workflows. For reference, I recommend this blog post which supplied the starting point.

> dotnet fake build
The last restore is still up to date. Nothing left to do.
run All
Building project with version: LocalBuild
Shortened DependencyGraph for Target All:
<== All
   <== Report
      <== Test
         <== Build
            <== Clean

The running order is:
Group - 1
  - Clean
Group - 2
  - Build
Group - 3
  - Test
Group - 4
  - Report
Group - 5
  - All
Starting target 'Clean'
Finished (Success) 'Clean' in 00:00:01.7544636
Starting target 'Build'
...
...
...
---------------------------------------------------------------------
Build Time Report
---------------------------------------------------------------------
Target     Duration
------     --------
Clean      00:00:01.7453083
Build      00:01:55.9501941
Test       00:03:05.8054016
Report     00:00:18.8476461
All        00:00:00.0001567
Total:     00:05:22.5606801
Status:    Ok
---------------------------------------------------------------------
...
...
...

Constructing scenario data

In update 2, I had mentioned data validation being an important piece. I feel that I should elaborate on that.

The game engine has two classifications of data: immutable scenario data and then also tracked entity data.

Immutable scenario data is loaded once at game initialization. This data is loaded from JSON files and represents the seed data for an experience. The overriding command is that anything should be able to reference this data with full expectations that the reference will never expire.

An example of a file is that of “floorgeneration.json” which currently only has one entry.

[
	{	"id": "0",
		"DefaultTile": 1,
		"tiles": [ 0, 1 ],
		"sizerange": {
			"position": { "x": 8, "y": 8 },
			"size": { "x": 10, "y": 10 }
		},
		"strategy": "openfloor"
	}
]

This floor generation data file is a fantastic example because it references IDs from other files, specifically “tiles.json”.

[<RequireQualifiedAccess>]
type FloorGenerationStrategy =
  | OpenFloor

type FloorGenerationParameter =
  { ID: int64
    /// Default Tile
    DefaultTile: int64
    ///Tiles used
    Tiles: int64 list
    ///Size generation
    SizeRange: Rectangle
    ///Generation Strategy
    Strategy: Morgemil.Models.FloorGenerationStrategy
  }

The model definition above that the JSON will be read from uses Int64 to reference identifiers. This isn’t preferred because this scenario data is immutable and anytime that this FloorGenerationParameter is used, there’s extra work to go find all Tiles with those IDs.

The final model being used by the game engine is below and includes direct references to the Tile type as well as making all Int64s be formal ID types. Not to mention the ID is surfaced as an interface implementation for use in entity lookup tables.

[<Record>]
type FloorGenerationParameter =
  { [<RecordId>] ID : FloorGenerationParameterID
    /// Default Tile
    DefaultTile : Tile
    ///Tiles used
    Tiles : Tile list
    ///Size generation
    SizeRange : Morgemil.Math.Rectangle
    ///Generation Strategy
    Strategy : FloorGenerationStrategy
  }
  interface Relational.IRow with
        [<JsonIgnore()>]
        member this.Key = this.ID.Key

To reach the final model from the JSON files, all this data goes through several stages of transformation and validation. An intermediate step of transformation looks like this for FloorGenerationParameter.

let FloorGenerationParameterFromDto (getTileByID: TileID -> Tile) (floorGenerationParameter: DTO.FloorGenerationParameter) : FloorGenerationParameter =
    {
        FloorGenerationParameter.ID = FloorGenerationParameterID floorGenerationParameter.ID
        DefaultTile = floorGenerationParameter.DefaultTile |> TileID |> getTileByID
        Tiles = floorGenerationParameter.Tiles |> Seq.map (TileID >> getTileByID) |> Seq.toList
        SizeRange = floorGenerationParameter.SizeRange |> RectangleFromDto
        Strategy = floorGenerationParameter.Strategy
    }

Floor Generation Parameters validation

As a whole for all the scenario data, the intermediate phases look like these types below. These phases show where there is arrays of data and then also multiple layers of validation that can occur.

type RawDtoPhase0 =
    {    Tiles: DtoValidResult<Tile[]>
         TileFeatures: DtoValidResult<TileFeature[]>
         Races: DtoValidResult<Race[]>
         RaceModifiers: DtoValidResult<RaceModifier[]>
         MonsterGenerationParameters: DtoValidResult<MonsterGenerationParameter[]>
         Items: DtoValidResult<Item[]>
         FloorGenerationParameters: DtoValidResult<FloorGenerationParameter[]>
         Aspects: DtoValidResult<Aspect[]>
    }
type RawDtoPhase1 =
    {    Tiles: DtoValidResult<DtoValidResult<Tile>[]>
         TileFeatures: DtoValidResult<DtoValidResult<TileFeature>[]>
         Races: DtoValidResult<DtoValidResult<Race>[]>
         RaceModifiers: DtoValidResult<DtoValidResult<RaceModifier>[]>
         MonsterGenerationParameters: DtoValidResult<DtoValidResult<MonsterGenerationParameter>[]>
         Items: DtoValidResult<DtoValidResult<Item>[]>
         FloorGenerationParameters: DtoValidResult<DtoValidResult<FloorGenerationParameter>[]>
         Aspects: DtoValidResult<DtoValidResult<Aspect>[]>
    }
type RawDtoPhase2 =
    {    Tiles: Morgemil.Models.Tile []
         TileFeatures: Morgemil.Models.TileFeature []
         Races: Morgemil.Models.Race []
         RaceModifiers: Morgemil.Models.RaceModifier []
         MonsterGenerationParameters: Morgemil.Models.MonsterGenerationParameter []
         Items: Morgemil.Models.Item []
         FloorGenerationParameters: Morgemil.Models.FloorGenerationParameter []
         Aspects: Morgemil.Models.Aspect []
    }

The final ScenarioData model output is this where all the scenario data is addressable by its ID in a readonly table.

type ScenarioData =
    { Races: IReadonlyTable<Race, RaceID>
      Tiles: IReadonlyTable<Tile, TileID>
      TileFeatures: IReadonlyTable<TileFeature, TileFeatureID>
      Items: IReadonlyTable<Item, ItemID>
      RaceModifiers: IReadonlyTable<RaceModifier, RaceModifierID>
      MonsterGenerationParameters: IReadonlyTable<MonsterGenerationParameter, MonsterGenerationParameterID>
      FloorGenerationParameters: IReadonlyTable<FloorGenerationParameter, FloorGenerationParameterID>
      Aspects: IReadonlyTable<Aspect, AspectID> }

That’s a lot of steps to say that some of these tables of data are linked to other tables and that I should put together an entity-relationship-diagram (ERD) like for a database.

Scenario Data being constructed from DTOs

The game engine has two classifications of data: immutable scenario data and then also tracked entity data. The tracked entity data is a topic for another day.

Game Loop Processing Requests

The game engine operates via a mailbox receiving requests. All interaction from the player is via a request to do an action. For example, a player may request to move in a direction from their current location.

The current logic happening is to

  1. Sanity check the character even exists.
  2. Get the current position
  3. Calculate the new position that would be.
  4. If new position blocks movement, then refuse to move.
  5. If new position is ok, then move and also increment the next time this character is allowed to move.

Process direction move request

Summary

My code is here. I may not be doing a lot of things in a functional way of programming, but my code is always in a workable state and I’m constantly refactoring to make it better.