Morgemil Part 19 – Throw me for a loop

Games require human input. Games that do not require human input are simulations.

At the heart of every game is a way of acquiring the player’s input. So most games have the concept of a game loop. A game loop keeps the game running by continuously checking for user input, drawing to the screen, and updating state internally.

I have a basic game loop with some mocked up data

open Morgemil.Core
open Morgemil.Logic
 
[<EntryPoint>]
let main argv = 
  let player = 
    { Entity.Id = EntityId 3
      Type = EntityType.Person }
 
  let position = 
    { PositionComponent.EntityId = player.Id
      Position = Vector2i(5, 5)
      Mobile = true }
 
  let controller = 
    { PlayerComponent.EntityId = player.Id
      IsHumanControlled = true }
 
  let resource = 
    { ResourceComponent.EntityId = player.Id
      ResourceAmount = 50.0 }
 
  let level = 
    Morgemil.Core.DungeonGeneration.Generate({ DungeonParameter.Depth = 1
                                               RngSeed = 5
                                               Type = DungeonGenerationType.Square })
 
  let game = Morgemil.Logic.Game(level, [| player |], [| position |], [| controller |], [| resource |])
 
  let rec _continue() = 
    game.Update()
    let key = System.Console.ReadKey()
 
    let direction = 
      match key.Key with
      | System.ConsoleKey.W -> Vector2i(-1, 0)
      | System.ConsoleKey.E -> Vector2i(1, 0)
      | System.ConsoleKey.N -> Vector2i(0, -1)
      | System.ConsoleKey.S -> Vector2i(0, 1)
      | _ -> Vector2i()
    game.HumanRequest({ RequestedMovement.EntityId = player.Id
                        Direction = direction })
    |> ignore
    _continue()
  _continue()
  0

Running this from console gives this output
19_1
Those messages shown is the results of each turn as generated by this engine

namespace Morgemil.Logic
 
open Morgemil.Core
 
type Game(level : Level, entities : seq<Entity>, positions : seq<PositionComponent>, players : seq<PlayerComponent>, resources : seq<ResourceComponent>) = 
  let _world = World(level, Set.ofSeq (positions), Set.ofSeq (resources), Set.ofSeq (players))
  //TODO: fix
  let mutable _globalTurnQueue = (entities |> Seq.toList).Head
 
  let _handleRequest request = 
    TurnBuilder () { 
      match request with
      | EventResult.EntityMovementRequested(req) -> 
        let old_position = _world.Spatial.[req.EntityId]
        //TODO: Check that this move is actually valid
        let new_position = 
          _world.Spatial.Replace(old_position, { old_position with Position = old_position.Position + req.Direction })
        yield Message.PositionChange(old_position, new_position)
        //Movement takes one resource. More for testing purposes. 
        let old_resource = _world.Resources.[req.EntityId]
        let new_resource = 
          _world.Resources.Replace
            (old_resource, { old_resource with ResourceAmount = old_resource.ResourceAmount - 1.0 })
        yield Message.ResourceChange(old_resource, new_resource)
      | _ -> ()
    }
 
  member this.Update() = 
    let nextEntity = _globalTurnQueue //TODO: Actually have more than one entity (the player)
    let player = _world.Players.[nextEntity.Id]
    match player.IsHumanControlled with
    | true -> ()
    | false -> () //TODO: process AI turn
 
  ///Humans can only move units right now
  member this.HumanRequest(request : RequestedMovement) = 
    let results = TurnQueue.HandleMessages _handleRequest (EventResult.EntityMovementRequested request)
    //TODO: Display results through gui
    printfn ""
    results |> Seq.iter (fun res -> printfn "%A" res)
    true

Notice something interesting about those messages’ names? They are verbs in the past tense. The messages are saying that something happened. It’s then up to everything else to react.

The ability for the human to interact with the system is this method

  ///Humans can only move units right now
  member this.HumanRequest(request : RequestedMovement) = 
    let results = TurnQueue.HandleMessages _handleRequest (EventResult.EntityMovementRequested request)
    //TODO: Display results through gui
    printfn ""
    results |> Seq.iter (fun res -> printfn "%A" res)
    true

Given a human input, the game loop will process while messages are still being generated. That happens thanks to this method

    let results = TurnQueue.HandleMessages _handleRequest (EventResult.EntityMovementRequested request)

The logic in that is a continuous loop while messages are available to process

namespace Morgemil.Logic
 
///The callback to handle a request and results.
type EventMessageHandler = EventResult -> TurnStep
 
///Receives and processes events that make up a turn.
///Example: The action of moving onto a tile with a trap causes a new message chain starting with the trap's activation.
module TurnQueue = 
  let HandleMessages (handler : EventMessageHandler) (initialRequest : EventResult) : TurnStep = 
    let _processedEvents = new System.Collections.Generic.List<EventResult>()
    let _eventQueue = new System.Collections.Generic.Queue<EventResult>()
 
    let rec _handle() = 
      match _eventQueue.Count with
      | 0 -> ()
      | _ -> 
        let request = _eventQueue.Dequeue()
        request |> _processedEvents.Add
        request
        |> handler
        |> Seq.iter _eventQueue.Enqueue
        _handle()
    _eventQueue.Enqueue(initialRequest)
    _handle()
    (List.ofSeq _processedEvents)

See that fancy type definition of a callback at the top?

///The callback to handle a request and results.
type EventMessageHandler = EventResult -> TurnStep

Perhaps the definition of an EventResult will start to clear things up

namespace Morgemil.Logic
 
open Morgemil.Core
 
type RequestedMovement = 
  { EntityId : EntityId
    Direction : Vector2i }
 
type ResultMoved = 
  { EntityId : EntityId
    MovedFrom : Vector2i
    MovedTo : Vector2i }
 
type ResultResourceChanged = 
  { EntityId : EntityId
    OldValue : double
    NewValue : double }
  member this.ResourceChanged = this.OldValue - this.NewValue
 
///This represents the results of an action
type EventResult = 
  | EntityMoved of ResultMoved
  | EntityMovementRequested of RequestedMovement
  | EntityResourceChanged of ResultResourceChanged

But that doesn’t say what a TurnStep is

type TurnStep = List<EventResult>

So an EventMessageHandler is a type definition for a function that given an EventResult, returns a list of EventResult.

///The callback to handle a request and results.
type EventMessageHandler = EventResult -> TurnStep

The game engine has a method which is exactly that:

  let _handleRequest request = 
    TurnBuilder () { 
      match request with
      | EventResult.EntityMovementRequested(req) -> 
        let old_position = _world.Spatial.[req.EntityId]
        //TODO: Check that this move is actually valid
        let new_position = 
          _world.Spatial.Replace(old_position, { old_position with Position = old_position.Position + req.Direction })
        yield Message.PositionChange(old_position, new_position)
        //Movement takes one resource. More for testing purposes. 
        let old_resource = _world.Resources.[req.EntityId]
        let new_resource = 
          _world.Resources.Replace
            (old_resource, { old_resource with ResourceAmount = old_resource.ResourceAmount - 1.0 })
        yield Message.ResourceChange(old_resource, new_resource)
      | _ -> ()
    }

Now you might notice that the function in no way returns a list of event results. But… it does yield EventResult within the context of a TurnBuilder.

  let _handleRequest request = 
    TurnBuilder () { 
      ...
    }

A TurnBuilder is a computation expression which takes away the hassle of having to keep track of a return list as it does it for me.

type TurnBuilder() = 
  member this.Bind(x, f) = f x
  member this.Zero() = TurnStep.Empty
  member this.Yield(expr : EventResult) : TurnStep = [ expr ]
  member this.Return(expr) = TurnStep.Empty
  member this.Yield(expr) = TurnStep.Empty
  member this.Combine(a : TurnStep, b : TurnStep) = List.concat [ a; b ]
  member this.Delay(f) = f()

This post ended up being me throwing a lot of code out there with minimal explanation. That’s probably a sign for me to nail down some of this logic.