Morgemil Part 7 – A whole new world

My first order of business is to create a world. So before I go off typing up a code-storm, I want to give the one defining rule:

Interesting results is more important to Morgemil than consistent or realistic results. I will leave making an amazingly complex believable world up to the genius making Ultima Ratio Regum.

Now that the rule is out of the way, I’m going to start: The portion of the world I’m building today is a simple dungeon. The generation of this dungeon will be absurdly simple. The bigger concern is storage of this dungeon.

The most basic unit of a tile-based game is the tile, obviously, and so I made a simple definition that encompasses everything I’ve thought of so far:

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)

This is an individual tile definition that could be stored in a text file or read from a database. Now how do I store a lot of these to make a map?

I’m going to go with a chunk-based storage system. My reasons are that I want dungeons that do not fit in the traditional rectangular area efficiently and also that I want to be able to re-use this code in the “over-world” which will be prohibitively large to store without chunks. So here is my chunk definition:

namespace Morgemil.Map
 
/// <summary>
/// A 2d Chunk of tiles. Used in the overworld and in dungeons.
/// </summary>
/// <param name="area">edge-inclusive area of tiles</param>
/// <param name="tiles">2d array [row,column]</param>
type Chunk(area : Morgemil.Math.Rectangle, tiles : TileDefinition array) =
  member this.Area = area
  member this.Tiles = tiles
 
  /// <summary>
  /// Local coordinates. Zero-based indices: [y * area.Width + x]
  /// No check for valid coordinates
  /// </summary>
  member this.TileLocal(vec2 : Morgemil.Math.Vector2i) = tiles.[vec2.Y * area.Width + vec2.X]
 
  /// <summary>
  /// Global coordinates. Zero-based indices relative to this.Area.Position
  ///</summary>
  member this.Tile(vec2 : Morgemil.Math.Vector2i) = (vec2 - area.Position) |> this.TileLocal

I’m not sure what else I need yet, so this will suffice until I get a clearer picture.

Now I’m going to make the simplest dungeon I can think of. The model for my first dungeon will be a series of rooms in a straight line. They will not even be connected.

namespace Morgemil.Test
 
module SimpleDungeon =
  //This is essentially a constant since it takes no arguments
  let DungeonGenerator =
    let dungeonLength = 3 //Lets make 3 rooms.
    let dungeonFloor =
      Morgemil.Map.TileDefinition
        (1, "Floor", "Dungeon floors are often trapped", false, true, Morgemil.Map.TileType.Land)
    let chunkSize = 10 //A chunk will be 10x10 square
 
    ///(0,0) (10,0) (20,0)...
    let chunksToCreatePositions =
      { 0..(dungeonLength - 1) } |> Seq.map (fun x -> new Morgemil.Math.Vector2i(chunkSize * x, 0))
 
    //Given the corner of the chunk to create
    let CreateRoomChunk(chunkPosition : Morgemil.Math.Vector2i) =
      let roomArea =
        Morgemil.Math.Rectangle(chunkPosition, Morgemil.Math.Vector2i(chunkSize, chunkSize))
 
      //border is empty tiles and the contained area is dungeon floor
      let ChooseTile(tilePosition : Morgemil.Math.Vector2i) =
        match tilePosition with
        | _ when tilePosition.X = roomArea.Left || tilePosition.X = roomArea.Right
                 || tilePosition.Y = roomArea.Top || tilePosition.Y = roomArea.Bottom ->
          Morgemil.Map.TileDefinition.Default
        | _ -> dungeonFloor
 
      //Maps each coordinate in the room into a tile
      let tileArray =
        roomArea.Coordinates
        |> Seq.map (ChooseTile)
        |> Seq.toArray
 
      Morgemil.Map.Chunk(roomArea, tileArray)
 
    chunksToCreatePositions |> Seq.map (CreateRoomChunk)

I wrote this out but I didn’t know if it was correct, so I made a quick and dirty print to file that will only work for this specific linear dungeon.

[<EntryPoint>]
let main argv =
  let createdSampleDungeon = Morgemil.Test.SimpleDungeon.DungeonGenerator
 
  let ChunkRowToString y (chunk : Morgemil.Map.Chunk) =
    { chunk.Area.Left..chunk.Area.Right }
    |> Seq.map (fun x -> Morgemil.Math.Vector2i(x, y))
    |> Seq.map (chunk.Tile)
    |> Seq.map (fun tile -> tile.ID.ToString())
    |> Seq.reduce (+)
 
  let ChunkToString(chunk : Morgemil.Map.Chunk) =
    { chunk.Area.Top..chunk.Area.Bottom } |> Seq.map (fun y -> ChunkRowToString y chunk)
 
  let CombineChunks =
    createdSampleDungeon
    |> Seq.map (ChunkToString)
    |> Seq.concat
 
  let filename = "map_test.txt"
  System.IO.File.WriteAllLines(filename, CombineChunks)
  System.Console.ReadKey() |> ignore
  0 // return an integer exit code

The results is a line of three rooms that are not connected. (1) being the dungeon floor and (0) being empty space

0000000000
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0000000000
0000000000
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0000000000
0000000000
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0111111110
0000000000

This is not interesting results but I am a step closer to that.