Morgemil Game Update #3

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 79 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

The player may move around the game level on screen with the arrow keys. This is a square level surrounded by walls, and in either corner are stairs up to the previous level and down to the next level. When the player reaches the stairs on the bottom right, he moves on to the next level and the stairs on the top right. (Currently the next level is a duplicate of the old level.)

Moving around


An interesting thing to note above is the events and updates that are happening on the background console screen.

I've refactored the game engine so that the visual on-screen game and the actual game logic are separate. The game logic is behind an interface, which forces the graphical engine to communicate by passing messages.

type Step< 'tRow when 'tRow :> IRow> =
    {   Event: ActionEvent
        Updates: 'tRow TableEvent list
    }

[<RequireQualifiedAccess>]
type GameState =
    | Processing
    | Results of Character Step list
    | WaitingForInput

[<RequireQualifiedAccess>]
type GameStateRequest =
    | Input of ActionRequest
    | QueryState of AsyncReplyChannel<GameState>
    | SetResults of Character Step list
    | Kill
    | Acknowledge

type IGameStateMachine =
    /// Stops the game engine
    abstract member Stop: unit -> unit
    /// Gets the current state of the game loop
    abstract member CurrentState: GameState with get
    /// Sends input
    abstract member Input: ActionRequest -> unit
    /// Acknowledge results
    abstract member Acknowledge: unit -> unit

Inside my graphical engine that is running at 60 fps, I can interact with the Game State above:

        match gameState.CurrentState with
        | GameState.WaitingForInput ->
            // event comes from keyboard input, if any
            if event.IsSome then
                gameState.Input event.Value
        | GameState.Processing ->
            // Nothing to do, just waiting on game logic to process
            printfn "processing"
        | GameState.Results results ->
            results
            |> List.iter (fun event ->
                printfn "%A" event
                match event.Event with
                | ActionEvent.MapChange mapChange ->
                    // Going to the next level gets special treatment as an event
                    // Overwrite the game map
                    viewOnlyTileMap <- createTileMapFromData mapChange.TileMapData
                    // Overwrite current characters (monsters, players, etc.) on screen
                    viewCharacterTable <- CharacterTable()
                    mapChange.Characters
                    |> Array.iter (Table.AddRow viewCharacterTable)
                | _ ->
                    // any other event besides a map change, just process character changes as normal
                    event.Updates
                    |> List.iter(fun tableEvent ->
                        match tableEvent with
                        | TableEvent.Added(row) -> Table.AddRow viewCharacterTable row
                        | TableEvent.Updated(_, row) -> Table.AddRow viewCharacterTable row
                        | TableEvent.Removed(row) -> Table.RemoveRow viewCharacterTable row
                    )
                )
            // acknowledge that the results have been handled
            gameState.Acknowledge()

By doing game logic in a background thread, and only communicating with the graphical engine by messages of what's changed, I can now take this further and create a game server. Anything that implements that interface, whether a Web API or such, can now serve up game requests. Even further, to record a game I merely need to record the player's inputs.

One difficulty has been in accurately recording changes to game characters. I can't miss any if I want to keep the front-end and the back-end in sync. I also want to display events on screen as they happen. Not every change should be a visual event on screen, and I also want events and other changes to be consistent with each other. When an event plays on-screen, only the changes so far should be apparent. And that's what led me to the below structure as a currently suitable representation of the timeline. Given an event, I also give the list of changes leading up to that event.

type Step< 'tRow when 'tRow :> IRow> =
    {   Event: ActionEvent
        Updates: 'tRow TableEvent list
    }

With the Step structure above, I've added a history to some of my table implementations.

type ITableEventHistory<'tRow when 'tRow :> IRow> =
    abstract member History: unit -> TableEvent<'tRow> list with get
    abstract member ClearHistory: unit -> unit

With the history capability implemented in a few tables, my game loop has become a little wordy, but I get a lot of recording for free.

type Loop(characters: CharacterTable, tileMap: TileMap) =

    member this.ProcessRequest(event: ActionRequest): Character Step list =
        Table.ClearHistory characters
        EventHistoryBuilder characters {
            match event with
            | ActionRequest.Move (characterID, direction) ->
                match characterID |> Table.TryGetRowByKey characters with
                | None -> ()
                | Some moveCharacter ->
                    let oldPosition = moveCharacter.Position 
                    let newPosition = oldPosition + direction
                    let blocksMovement = tileMap.Item(newPosition) |> TileMap.blocksMovement
                    if blocksMovement then
                        yield
                            {
                                CharacterID = moveCharacter.ID
                                OldPosition = oldPosition
                                RequestedPosition = newPosition
                            }
                            |> ActionEvent.RefusedMove
                    else 
                        Table.AddRow characters {
                            moveCharacter with
                                Position = newPosition
                        }
                        yield
                            {
                                CharacterID = moveCharacter.ID
                                OldPosition = oldPosition
                                NewPosition = newPosition
                            }
                            |> ActionEvent.AfterMove
            | ActionRequest.GoToNextLevel (characterID) ->
                match characterID |> Table.TryGetRowByKey characters with
                | None -> ()
                | Some moveCharacter ->
                    if tileMap.[moveCharacter.Position] |> TileMap.isExitPoint then
                        let items = characters
                                    |> Table.Items
                                    |> Seq.toArray
                        items
                        |> Seq.map(fun t -> {
                                t with
                                    Position = tileMap.EntryPoints |> Seq.head
                            })
                        |> Seq.iter (Table.AddRow characters)

                        yield
                            {
                                Characters =
                                    characters
                                    |> Table.Items
                                    |> Seq.map(fun t -> {
                                            t with
                                                Position = tileMap.EntryPoints |> Seq.head
                                        })
                                    |> Array.ofSeq
                                TileMapData = tileMap.TileMapData
                            }
                            |> ActionEvent.MapChange


            if Table.HasHistory characters then 
                yield ActionEvent.Empty
        }

Now if you notice, I'm doing a lot of "yield" inside a custom computation expression. Anytime I yield an ActionEvent, such as a character moved, I get a snapshot of the history on the table, and then clear the table history.

type EventHistoryBuilder<'T, 'U when 'T :> ITableEventHistory<'U> and 'U :> IRow>(table: 'T) =
    member this.Bind(m, f) =
        f m
    member this.Return(x: ActionEvent): 'U Step list =
        let step =
            {   Step.Event = x
                Updates = Table.History table
            }
        Table.ClearHistory table
        [ step ]
    member this.Return(x: 'U Step list ): 'U Step list =
        x
    member this.Zero(): 'U Step list = 
        []
    member this.Yield(x: ActionEvent): 'U Step list =
        let step =
            {   Step.Event = x
                Updates = Table.History table
            }
        Table.ClearHistory table
        [ step ]
    member this.Yield(x: 'U Step list): 'U Step list =
        x
    member this.Combine (a: 'U Step list, b: 'U Step list): 'U Step list =
        List.concat [ b; a ]
    member this.Delay(f) =
        f()

Given all the history snapshots, I display it to the debugging console as show in the gif above.

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.