Technical Implementation - Strange4/RenaEngine GitHub Wiki

Requirements

  • Participants must be able to see and "debug" their local game being played.
  • Participants must be able test their latest uploaded code.
  • Admins must be able to see any game being played.
  • A subset of games must be able to be recorded and replayed.
  • Participants must be able to write their code in at least Typescript, C# and python.
  • Admins must be able to provide test code to play against any player who wants to test their code on their computer.
  • Admins must be able to create any kind of 2D grid based game of their liking without modifying the code that we (RenaEngine) ship.

System Architecture

architecture-beta
	group re[Game Server]
	group web[Web Server]
	group code[User Code]



	service renderer(internet)[Game Viewer] in web
	service admin(internet)[Admin Page] in web
	service userPage(internet)[User Page] in web

	service server(server)[Game Server] in re
	service generator(server)[SDK Generator Lib] in re

	service clientCode(disk)[Participant Agent Code] in code
	service testClient(disk)[Admin Agent Code] in code

	renderer:R <-- L:server
	userPage:L <--> R: clientCode
	admin:L <-- R:testClient
	admin:T --> B:renderer
	userPage:B --> T:renderer
	admin:R --> B:server{group}
	userPage:R --> T:server{group}
	server:R --> L:generator
Loading

Security

The main communication method between each component is HTTPS (via a self signed certificate but with verfication disabled) and what are called "user tokens". User tokens are a combination of ID+password to allow identifying and authorizing each player and admin. Each time a request is made to the web server, it must be sent with a password and be authorized for the request to go through.

Admin Token ADMIN-{randompassword}

Participant Token PARTICIPANT-{number}-{randompassword}

Game Viewer

A minimal React component that renders a series of server side events of game updates and uses PixiJs to render all of the assets in their positions.

Why SSE? Because its simpler and we don't need two sided communication. The web viewer is only to see whats going on.

The main part of the renderer is, given a series of SSEs, it renders every single renderable item given by the server event. The server shall only sends updated items (ie ones that have not changed in position, asset, or size). A renderable item looks like this:

{
"index": 0,
"changes": [{"id": "...", "position": [x,y], "assetPath": "/assets/ballon.svg", "size": [x,y], "type": "object", "layer": 2}, {..., "type": "object"}]
}

The game viewer is there for 2 reasons:

  1. Render the current game that is going on while the player is testing their code against the admin given opponent.
  2. Render previous games that are being played that were recorded as a series of these server side events to see what happened during the competition.

Web server

The web server is the one responsible for managing the server and allowing users to interact with the server is a user friendly way.

NextJS + Typescript and serves as a back-for-front (BFF) for the server. Is is also able to communicate securely to the server via HTTPS (self signed certificates) and a password (the admin token) shared via environment variables that both the server share.

The web server handles authorization and authentication for every player token and admin. It means that every request that each player or admin does must pass through the web server which gives it the authority to allow or deny certain requests (like denying to see the leader board towards the end of the event).

The only part of the game server that is not communicating to any user via the website are the SSE for individual games being played live for a specific player.

Participant Page

The purpose of the participant page is to help the participants manage their code and view their games.

Via this page the users should be able to upload a zip file of their code in their preferred language. They can also upload a recorded game file and see that game at the speed of their choice.

The user page is the one concerned with subscribing to SSE's and keeping them in touch. The SSE's will be sent with a number and if the client misses any of them, they can resubscribe to the event using the last index to see pickup where they left off.

Debugging, Testing, Playing

See Game Modes TLDR: Debugging is player code running on their computer, testing is player code running in a docker container on the server against admin agent, playing is against real opponents.

Game Server

The purpose of the Game Server is to run games. Everything that should be here should support this purpose.

Game Creation

The core of the game lives here (C# + Asp Net) via a public ClientState DoAction(string playerId, Action action) loop. Every time that the player client code sends a GRPC request to do an action, the server executes that action for a player and returns the new state that the player should see.

// All games must implement this interface
public interface IGame<S, A>
where S : class
where A : class
{
    Dictionary<string, S> DoAction(Dictionary<string, A> playerToAction);
    Collection<IRenderable> GetRenderables() { return []; }
}

// Walking around game
public Dictionary<string, PlayerState> DoAction(Dictionary<string, PlayerAction> playerToAction)
{
	foreach ((string id, PlayerAction action) in playerToAction)
	{
		Vector2 delta = new();
		switch (action.Direction)
		{
			case MovementDirection.Up:
				delta = new Vector2(0, 1); break;
			case MovementDirection.Down:
				delta = new Vector2(0, -1); break;
			case MovementDirection.Left:
				delta = new Vector2(-1, 0); break;
			case MovementDirection.Right:
				delta = new Vector2(1, 0); break;
		}
		this.players[id].Position += delta;
	}
	return this.players.ToDictionary(kvp => kvp.Key, kvp => new PlayerState { Position = kvp.Value.Position });
}

Code Generation

The code generation will be done as a separate tested library that will generate all of the SDK's that will be dynamically called via the server.

Since the Server is done in C# and Protobuf, we use protobuf-net to dynamically create GRPC type descriptors and server descriptors to receive and send data. Something that looks like:

[DataContract]
public class PlayerState
{
    [DataMember(Order = 1)]
    public Vector2 Position { get; set; }

    private string myThing {get; set;}

    [DataMember(Order = 2)]
    public string hello;
}

[DataContract]
public class PlayerAction
{
    [DataMember(Order = 1)]
    public MovementDirection Direction { get; set; }
}

[DataContract]
public enum MovementDirection
{
    Up,
    Down,
    Left,
    Right
}

The server will use these dynamically created GRPC types to create .proto files that are used by client generators to create the client code for every language that we support. These will be generated via HTTP GET /clients/[language] which lazily generates the code, saves it to a folder in the server, creates a ZIP file and returns said zip file. Later requests for the same language will reuse the same zip file.

function playChest(state1: State) -> Action{
	time.sleep(0.5);
	
	return {queen: e4};
}


while (true) {
  // Put your action here!
  const action: Action = {};
  const state = await client.doAction(action);
  console.log(state);
}

Recordings

Each round of plays between players, the server will choose a (configurable) number of random plays to record. Every game event that would be sent as an SSE to the client as well as the final game statistics (score and number of steps, player) will be recorded.

These recordings will be stored via files and make them accessible via static HTTPS requests done through the web server HTTP GET /games/[recordingNumber] using their user token so users can only see their own games.

Leader board

The leader board is the main way that users can see the score of each player (counted by the number of wins) and their games. The leader board can be hidden at any time. The leader board only shows the ID of the users.

Participant Code

The participant code will be written on their own code editor and import the generated code from the library that we provide. Participants shall only upload the non generated code via the Game Viewer website using one file named main.lang that will be used as the entrypoint to their program.

The code that is uploaded to the game server will be assigned in a folder for that playerId, used to programatically build a docker image for that specific player that will be started and killed to play games.

Example participant code

server_name = env["SERVER_NAME"]

while True:
	server.doAction({...})
	time.sleep(1)
SERVER_NAME = "http://localhost/" python3 main.py
SERVER_NAME = "http://server.name" python3 main.py

Admin Page

The admin page is the visual overview of what is going on in the game server. It is responsible for making the server accept new connections. See all of the playerIds that are allocated and allowed. as well as allocate new ones. See each individual game being played. See who has uploaded uploaded their code and when. It is also the only page that is able to upload the admin test opponent for players to test against.

Communication Diagrams

Uploading code

sequenceDiagram

Player->>+Server: POST /code (using user token to upload latest code)

Server-->>-Player: OK

Loading

Game Loop

sequenceDiagram

PlayerCode->>+Server: GRPC /DoAction
Server-->>+Viewer: SSE {"updated": [objectId: position]}
Server->>-PlayerCode: GRPC {"updatedState": {...}}
Loading

Duel Strategy and Scoring

To allow each player to play against other, the game server will keep a list of all the players that are allowed. A game is comprised of an arbitrary number of players that will be assigned to a "room".

Each game tick, the players will have a limited amount of time to send a request to the server to do an action. When all actions are gathered, they are given to the game code to decide what is the new state given to all players. When the game is over, the score is kept by the server and each player who has won gets an additional win count. The player leader board is also kept ranking each player with the amount of wins that they have.

The player combination for rotation is not yet decided but here are the options.

  1. Make every player play against every other player. total = $n(n-1)/2$. Exponentially grows the number of games
  2. Bracket style * number: If you win, you play more. If you loose you stop playing. Biases strong players to be way higher on the leader board.
  3. Elo style: make players play against other players who have similar elo's. Elo goes up and down depending if the player wins. Leaderboard is based on elo. Kinda boring
⚠️ **GitHub.com Fallback** ⚠️