Techorama talk: the making of (part 2)

Techorama talk: the making of (part 2)

A simple HTTP API and Scripting Middleware

by Johnny Hooyberghs

CSharpWars is not a hardcore game that needs real-time server/client communication, so I opted for a very simple approach of using HTTP APIs for communication between the game front-end and the back-end. The state of the game world will be stored inside a relational database, with entities like Player and Bot, and will only be updated by the processing middleware once every two seconds.  The processing middleware takes the scripts from the database, compiles, initializes and later runs them for all robots in parallel. If the game front-end polls the game state once every two seconds, animating the assets between their previous and current state should be sufficient.

ENTITIES

The relational database will contain a list of robots, storing their state, and a list of players, grouping their deployed robots. When deploying a robot, a player needs to provide a C# script to define the behavior of the robot. This C# script is only needed once by the processing middleware and is therefore accessed using a separate entity. It is however stored in the same table as the robot state itself.

HTTP API

Because I want to use this project to play around with .NET Core 3, ASP.NET Core WebAPI is my choice as the technology for the HTTP APIs. The only important component that will use the HTTP API for now, is the game front-end. Because of this, only an endpoint on the robot entity is required. This endpoint will return all active robots, which are robots that have not died, or that have died within the last 10 seconds. The game front-end needs these 10 seconds to animate the death of the robot because face it: we like to see virtual robots die! In the future, multiple arenas will be supported. Right now, the arena endpoint will always return a single arena instance with a predefined width and height.

The ASP.NET Core 3 application is dockerized and runs on a Linux based Synology NAS for demo purposes.

Fetching and storing data in the relational database is performed by Entity Framework Core 3. A table for players and a table for robots are mapped to Player, Bot and BotScript entities. Nothing fancy there.

Just for the fun of it, Microsoft SQL Server from a Docker Container also runs on the same Synology NAS.

SCRIPTING MIDDLEWARE

The scripting middleware is a .NET Core 3 Console application using The Microsoft Compiler Platform, also known as Roslyn, to compile and run robot scripts. If the Console application is running, it will trigger a processor once every two seconds to run all active robot scripts in parallel. Running a robot script will take place in three stages:

    1. Preprocessing:
      This stage will prepare an object model containing all active robots, their current stats, memory and their awareness of other robots. Their current stats are a representation of their current state. Their memory is managed by the script that is able to use a number of functions to persist data between game turns. Awareness of other robots is determined based on the orientation of the current robot and the location of the other robots.
    2. Processing:
      This stage will compile, initialize and run the actual bot scripts for all active bots in parallel. Thanks to the Microsoft Compiler Platform, a simple String object, containing C# code can be compiled and run from memory using the C# Scripting API. From these scripts, the processing stage will distill a list of moves that need to be performed by all robots.
    3. Postprocessing:
      This stage will perform the listed moves one by one if possible and update the object model to reflect the new game state.

    The core idea is that every robot script can result in an actual move that needs to be performed by that robot. If a script tries to perform multiple moves within the same turn, only the first move is registered and any additional moves are ignored. The moves are then categorized and prioritized in order to create a trustworthy result when all robots are performing their moves simultaneously. For example, attacks are executed before moves in order to make sure that a robot cannot escape an attack within the same turn.

    Thanks to the Microsoft Compiler Platform Scripting API, a C# script without a class or method context, can be written and run from memory. As a help, I can make the script run inside the context of a class that is part of my Scripting Middleware and thus add some context to that script. In the Scripting Middleware, this context is called ScriptGlobals.

    public async Task Process(BotDto bot, ProcessingContext context)
    {
        var botProperties = context.GetBotProperties(bot.Id);
        try
        {
            var botScript = await GetCompiledBotScript(bot);
            var scriptGlobals = ScriptGlobals.Build(botProperties);
            await botScript.RunAsync(scriptGlobals);
        }
        catch
        {
            botProperties.CurrentMove = PossibleMoves.ScriptError;
        }
    }

    The ScriptGlobals class contains a number of Properties and Methods that are available to the player to call from within their script. Using this context, a player can write an intelligent script that makes a decision based on these Properties and calls a Method to perform a move.

    public Int32 Width { get; }
    public Int32 Height { get; }
    public Int32 X { get; }
    public Int32 Y { get; }
    public PossibleOrientations Orientation { get; }
    public PossibleMoves LastMove { get; }
    public Int32 MaximumHealth { get; }
    public Int32 CurrentHealth { get; }
    public Int32 MaximumStamina { get; }
    public Int32 CurrentStamina { get; }
    public Vision Vision { get; }
    
    public void WalkForward();
    public void TurnLeft();
    public void TurnRight();
    public void TurnAround();
    public void SelfDestruct();
    public void MeleeAttack();
    public void RangedAttack(Int32 x, Int32 y);
    public void Teleport(Int32 x, Int32 y);
    
    public void StoreInMemory<T>(String key, T value);
    public T LoadFromMemory<T>(String key);
    public void RemoveFromMemory(String key);
    public void Talk(String message);

     

     

  1. Source code: https://github.com/Djohnnie/CSharpWars