Morgemil Part 14 – Minimalism

After a month’s sojourn I have returned to my long term project with more knowledge. I’m replacing my Object-Oriented F# that looks like

namespace Morgemil.Map
 
type TileType =
  | Void = 0
  | Land = 1
  | Water = 2
 
/// <summary>
/// This defines a tile. Each tile instance will hold a reference to one of these as a base attribute.
/// </summary>
type TileDefinition(id : int, name : string, description : string, blocksMovement : bool, blocksSight : bool, tileType : TileType) =
 
  /// <summary>
  /// Use this in file storage. When saving a chunk or map, use this ID.
  /// </summary>
  member this.ID = id
 
  /// <summary>
  /// A short name. eg: "Lush Grass"
  /// </summary>
  member this.Name = name
 
  /// <summary>
  /// A long description. eg: "Beware the burning sand. Scorpions and asps make their home here."
  /// </summary>
  member this.Description = description
 
  /// <summary>
  /// If true, this tile ALWAYS blocks ALL movement by ANYTHING.
  /// </summary>
  member this.BlocksMovement = blocksMovement
 
  /// <summary>
  /// If true, this tile ALWAYS blocks ALL Line-Of-Sight of ANYTHING.
  /// </summary>
  member this.BlocksSight = blocksSight
 
  /// <summary>
  /// The tile type determines some things like if they can breath or not.
  /// </summary>
  member this.Type = tileType
 
  /// <summary>
  /// A default, minimal definition. Could be used as the edge of the map blackness?
  /// </summary>
  static member Default = TileDefinition(0, "Empty", "", true, true, TileType.Void)
 
  static member IsDefault(tile : TileDefinition) = (tile.ID = TileDefinition.Default.ID)

with something that looks a bit more F#-ish.

namespace Morgemil.Map
 
type TileType = 
  | Void = 0
  | Land = 1
  | Water = 2
 
/// This defines a tile. Each tile instance will hold a reference to one of these as a base attribute.
type TileDefinition = 
  { ///Use this in file storage. When saving a chunk or map, use this ID.
    Id : int
    /// A short name. eg: "Lush Grass"
    Name : string
    ///A long description. eg: "Beware the burning sand. Scorpions and asps make their home here."
    Description : string
    ///If true, this tile ALWAYS blocks ALL movement by ANYTHING.
    BlocksMovement : bool
    ///If true, this tile ALWAYS blocks ALL Line-Of-Sight of ANYTHING.
    BlocksSight : bool
    ///The tile type determines some things like if they can breath or not.
    TileType : TileType }
 
  /// <summary>
  /// A default, minimal definition. Could be used as the edge of the map blackness?
  /// </summary>
  static member Default = 
    { Id = 0
      Name = "Empty"
      Description = ""
      BlocksMovement = true
      BlocksSight = true
      TileType = TileType.Void }
 
  static member IsDefault(tile : TileDefinition) = (tile.Id = TileDefinition.Default.Id)

I think this is a lot more concise and removes unnecessary encapsulation.

Here is another example of something I’ve learned. I have a test project that I script odd things in to test out concepts with and I’ve had this “Walkabout” class that I use to test movement from a console window window. It was defined as this

type Walkabout(dungeon : Morgemil.Map.Chunk, player : Morgemil.Game.Person) =
  member this.Dungeon = dungeon
  member this.Player = player
  member this.Act(act : Morgemil.Game.Actions) =
    let offset =
      match act with
      | Morgemil.Game.MoveEast -> Vector2i(1, 0)
      | Morgemil.Game.MoveWest -> Vector2i(-1, 0)
      | Morgemil.Game.MoveNorth -> Vector2i(0, 1)
      | Morgemil.Game.MoveSouth -> Vector2i(0, -1)
    Walkabout(dungeon,
              { Id = player.Id
                //Body = player.Body
                Position = player.Position + offset })

For now, whether it is good design or not, the position is included in a “Person” definition. So the only thing I wanted to change on that player was the Position, but I was having to pass everything which was a pain. Then I found the context of the “with” keyword with Record Types.

type Walkabout(dungeon : Morgemil.Map.Chunk, player : Morgemil.Game.Person) = 
  member this.Dungeon = dungeon
  member this.Player = player
  member this.Act(act : Morgemil.Game.Actions) = 
    let offset = 
      match act with
      | Morgemil.Game.MoveEast -> Vector2i(1, 0)
      | Morgemil.Game.MoveWest -> Vector2i(-1, 0)
      | Morgemil.Game.MoveNorth -> Vector2i(0, 1)
      | Morgemil.Game.MoveSouth -> Vector2i(0, -1)
    Walkabout(dungeon, { player with Position = player.Position + offset })

So the person can now be passed around effortlessly

///Wight, human, etc.
type Race = 
  { Id : int
    ///Proper noun
    Noun : string
    ///Proper adjective
    Adjective : string
    ///User-visible description
    Description : string }
 
///A body with base stats/characteristics.
///Any mutable data is in a higher level.
type Body =
  { Id : int
    Race : Race
    ///Normal "bodies" fit in one tile (1,1). Bosses and the largest entities can take up multiple tiles.
    Size : Morgemil.Math.Vector2i }
 
///This is a high level view of an entity. Typically holds any mutable data (can change each game step).
type Person = 
  { Id : int
    Body : Body
    ///This plus Body.Size constructs the person's hitbox
    Position : Morgemil.Math.Vector2i }

I’m testing this by console since my Monogame view is still very lacking.

let Instruct() = printfn "(E)ast (N)orth (W)est (S)outh (Q)uit"
 
let Prompt() = 
  let response = System.Console.ReadLine()
  match System.Char.ToLower(response.[0]) with
  | 'e' -> Some(Morgemil.Game.MoveEast)
  | 'n' -> Some(Morgemil.Game.MoveNorth)
  | 'w' -> Some(Morgemil.Game.MoveWest)
  | 's' -> Some(Morgemil.Game.MoveSouth)
  | _ -> None
 
let rec Continue (depth : int) (walkabout : Morgemil.Test.Walkabout) = 
  System.Console.WriteLine(walkabout.Player.Position)
  match Prompt() with
  | None -> ()
  | Some(act) -> 
    let filename = "map_test" + depth.ToString("0000") + ".bmp"
    let dungeonDraw = Morgemil.Test.DungeonVisualizer.Visualize [| walkabout.Dungeon |]
    Morgemil.Test.DungeonVisualizer.DrawPlayer walkabout.Player dungeonDraw
    dungeonDraw.Save(filename)
    Continue (depth + 1) (walkabout.Act act)
 
[<EntryPoint>]
let main argv = 
  let createdBspDungeon = Morgemil.Map.DungeonGeneration.Generate 656556
 
  let walkAbout = 
    Morgemil.Test.Walkabout(createdBspDungeon, 
                            { Id = 5
                              Body = 
                                { Id = 5
                                  Size = Morgemil.Math.Vector2i(1, 1)
                                  Race = 
                                    { Id = 5
                                      Noun = "Dwarf"
                                      Adjective = "Dwarven"
                                      Description = "I am a dwarf" } }
                              Position = Morgemil.Math.Vector2i(5, 5) })
  Instruct()
  Continue 0 walkAbout