Morgemil Game Update #7: Notes since 2021

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

Progress

Most noticeable is that some semblance of menus and display info is starting to form.

Update

Notable code changes

  • Updated to SadConsole v9
  • Replaced primitives from namespace Morgemil.Math like Rectangle and Vector2i with SadRogue.Primitives.
    • After deleting tests and everything, reduced the code footprint by about 600 lines total and adds new functionality.
    • Also that primitives library is mostly shared by SadConsole so interoptability without conversion is nice.
    • Only problem is that I still have to do some serialization logic for storage as I want it.

Data Entry

Data entry is tedious and time consuming and has little tolerance for mistakes without a high level of rework. When creating data for what is hoped to be a large game, creating patterns that can be combined in interersting ways is more fun.

Towards creating unusual and unplanned scenarios, a system of tags is being added in. Broadly, these tags are defined like this.

[<RequireQualifiedAccess>]
type MorTagMatches =
    /// Checks whether the match has this tag. Doesn't care about values.
    | Has
    /// Checks whether the match lacks this tag. Doesn't care about values.
    | Not
    /// Checks whether the match has this RequireTag also. Expects this to be unique.
    | Unique

/// Conventions:
/// * Usage of these in data should be as sparse as possible. Assume use of these to be the rare case.
/// * If prefix "No" is used, then assume that's the more likely unusual case.
[<RequireQualifiedAccess>]
type MorTags =
    | Custom
    | Placeholder of Any: string
    | Playable
    | Undead
    | NoSkeleton
    | NoBlood
    | Humanoid
    | Modifier of Stat: int

An example of use is when looking at the ancestory and heritage data.

/// Every inhabitant has a single ancestry that describes common and base attributes.
/// Carnivorous Mold is one example of an ancestry.
[<Record>]
type Ancestry =
    {
        /// ... other fields emitted for brevity ...
        ///Tags this ancestry has
        Tags: Map<string, MorTags>
        ///Required tags for procedural matching.
        RequireTags: Map<string, MorTagMatches>
    }

And an example data record. Do notice the tags.

  {
    "ID": "4",
    "Noun": "Carnivorous Mold",
    "Adjective": "Carnivorous Moldy",
    "Description": "A carnivorous mold that's not exceptionally mobile or intelligent or threatening or even aware of your presence.",
    "Tags": {
      "NoSkeleton": "",
      "NoBlood": ""
    }
  }

Then some example heritage definitions

/// Every inhabitant may have zero or more pieces of heritage.
/// A heritage is a collection of attributes that modify and affect a creature for good or for ill.
[<Record>]
type Heritage =
    {
        /// ... other fields emitted for brevity ...
        ///Tags this heritage has
        Tags: Map<string, MorTags>
        ///Required tags for procedural matching.
        RequireTags: Map<string, MorTagMatches>
    }

and some interesting related heritages

[
  {
    "id": "0",
    "noun": "Skeleton",
    "adjective": "Skeletal",
    "description": "Skeletons are a basic form of undead.  Stupid and resilient to many mortal afflictions, but rather brittle",
    "tags": {
      "undead": "",
      "NoBlood": ""
    },
    "RequireTags": {
      "NoSkeleton": "not"
    }
  },
  {
    "id": "1",
    "noun": "Specter",
    "adjective": "Spectral",
    "description": "Specters are weak in this world and easily banished with physicial assult, but do not underestimate the single minded obsession of the departed.",
    "Tags": {
      "NoBlood": ""
    }
  },
  {
    "id": "2",
    "noun": "Vampire",
    "adjective": "Vampiric",
    "description": "Blood calls to them. Do you have blood? If you do, they want it. Keep your blood away from them.",
    "RequireTags": {
      "NoBlood": "not"
    }
  }
]

When looking to see which of those heritages that a “Carnivorous Mold” can be combined with, the important thing to look at is the RequireTags property which shows Vampire and Skeleton being unavailable because a mold doesn’t have blood or a skeleton. However, the Specter is available, and when applied, the tag NoBlood would be added if not already present.

That was only one example, but the point is that now each combination of ancestory and heritage can be procedurally combined in hopefully interesting ways with as little explicit data entry as possible.

Game Server

The entire game’s state has been abstracted behind an API. Whether or not that game instance is running on localhost or remotely, the game’s internal logic and the game’s client are separate and do not share mutable objects directly.

I don’t plan to support multiplayer anytime soon, but having game data clearly independent of any user interface is so convenient.

This particular component below shows the overall server state, whether scenarios are being built, whether data is being loaded, and all that. Then if a game is built, returns a second more specific API context to use.

[<RequireQualifiedAccess>]
type GameServerWorkflow = | ScenarioSelection

[<RequireQualifiedAccess>]
type GameServerRequest =
    | AddCurrentPlayer of AncestryID: AncestryID
    | SelectScenario of ScenarioID: string
    | Workflow of Workflow: GameServerWorkflow

/// The steps and state of a game that's being built.
[<RequireQualifiedAccess>]
type GameServerState =
    | WaitingForCurrentPlayer
    | GameBuilt of GameEngine: IGameStateMachine * InitialGameData: InitialGameData
    | LoadingGameProgress of State: string
    | LoadedScenarioData of ScenarioData: ScenarioData
    | SelectScenario of Scenarios: string list

/// The interface to interact with a game being built.
type IGameServer =
    /// The current state of the builder
    abstract member CurrentState: GameServerState
    abstract member Request: GameServerRequest -> unit

Floor Generation

Not every piece of every logic has to be fully finished to ship a game. Rather, at least enough to fill some spaces should be present. Floor generation is a very clear example of where the correct place to hook into the logic and to expand floor generation is there for the future, but it’s very simple right now.

  1. Create a rectangular room and fill with impassable walls.
  2. For all interior spaces of the rectangular room, fill with an open basic floor tile.
  3. In each corner place an entrance and an exit.
let Create
    (parameters: FloorGenerationParameter)
    (tileFeatures: IReadonlyTable<TileFeature, TileFeatureID>)
    (rng: RNG.DefaultRNG)
    : (TileMap * Results) =
    let floorSize = Rectangle(Point(0, 0), RNG.RandomPoint rng parameters.SizeRange)

    let tileMap: TileMap = TileMap(floorSize, parameters.DefaultTile)

    let subFloorSize = floorSize.Expand(-1, -1)
    let mutable entraceCoordinate = Point.Zero

    match parameters.Strategy with
    | FloorGenerationStrategy.OpenFloor ->
        let openFloorTile =
            parameters.Tiles |> Seq.tryFind (fun t -> not (t.BlocksMovement))

        match openFloorTile with
        | Some(tile1: Tile) ->
            subFloorSize.Positions().ToEnumerable()
            |> Seq.iter (fun (vec2: Point) -> tileMap.Tile(vec2) <- tile1)

            entraceCoordinate <- subFloorSize.MinExtent
            let exitCoordinate = subFloorSize.MaxExtent

            let entrancePointFeature =
                tileFeatures
                |> TileFeatureTable.GetFeaturesForTile tile1.ID
                |> Seq.find (fun t -> t.EntryPoint)

            let exitPointFeature =
                tileFeatures
                |> TileFeatureTable.GetFeaturesForTile tile1.ID
                |> Seq.find (fun t -> t.ExitPoint)

            tileMap.TileFeature(entraceCoordinate) <- Some entrancePointFeature
            tileMap.TileFeature(exitCoordinate) <- Some exitPointFeature

        | None -> ()

    let results: Results =
        { EntranceCoordinate = entraceCoordinate
          Parameters = parameters }

    tileMap, results