Building a Player Service from Scratch

This page describes how you can build a player service from scratch, without using one of the predefined skeletons. This is only recommended if you have a good understanding of the game mechanics and architecture. In addition, you need to be quite familiar with the programming language and framework you want to use. If you are, go ahead :-). This is your decision.

Player Basics

You should have a basic understanding of the game mechanics and architecture before start reading this guide as it assumes this knowledge.

You can start building your own player by using one of the predefined skeletons or start from scratch. That’s up to you. Using one of the skeletons consider the readme document of those for further details.

When starting from scratch, you are required to fullfill the following requirements by yourself:

  • Project setup (Choosing Programming Language, Framework, creating project, repository, etc.)
  • Container image creation (You should be able to create a docker image)
  • CI-Pipeline (You should write your own Pipeline definition in which you build the latest container image and push it into a registry)
  • Helm chart (You need to write your own Helm chart so the player is deployable on the kubernetes cluster)

Local Development environment

In my experience the most successful and motivational way of building a player it is crucial you can test your player with all services locally. To do so, we provide a development enviornment which is documented here. Of course Unit- and integration tests are more advantageous but for quick prototyping and validating the logic against the real service APIs it is nearly irreplaceable.

A Minimal Player

This paragraph will guide you a little bit through the process of creating a player. We will also show some code-examples in a pseudo-code like TypeScript way to further support the documentation with practices. In order to develop a player, you must be able to send HTTP-Requests, consume events over AMQP 0-9-1.

Registering your player

The first step you should do is registering your player at the game service so you get your personal identifier. To do so, you need to choose a name and email adress and send a simple POST request to /players.

export async function registerPlayer(
  name: string,
  email: string
) {
  const body = {
    name: name
    email: email
  };

  await fetch("http://game-service/players", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body),
  });
}

If the registration will fail, you might already have registered your player. In this case you can obtain your personal identifier using a GET request to /players:

export async function getPlayer(
  name: string,
  email: string
) {
  const response = await fetch(`http://game-service/players?name=${name}&email=${name}`, {
    method: "GET",
  });
  const responseBody = await response.json();
  console.log(`User is ${responseBody.name}`);
}

Once your player is registered, you are ready to join the next available game. There are two possibilities to do this:

  1. The Game Service will create a player-owned exchange and queue once you have registered your player. It is possible to listen to game creation events on this queue and once one is available join the game.
  2. You can poll the Game Service HTTP API in intervals.

for the sake of simplicity we will stick to the second method here.

export async function getAvailableGame(): Promise<string | undefined> {
  const response = await fetch(`http://game-service/games`, {
    method: "GET",
  });
  const games = await response.json();

  if(games.length === 0 || !games.some(g => g.gameStatus === "created")) {
    return undefined;
  }

  return games[0].gameId;
}

export async function joinGame(gameId: string) {
  const playerId = getPlayerId();
  await fetch(`http://game-service/games/${gameId}/players/${playerId}`, {
    method: "PUT",
  });
}

export async function pollForNextAvailableGame() {
  const pollInIntervals = setInterval(() => {
    const availableGame = getAvailableGame();
    if(availableGame === undefined) return;

    joinGame(availableGame);
    clearInterval(pollInIntervals);
  }, 5000);
}

Hooray! Now you’re participating in the game and you can focus on event handling now! We haven’t covered that yet as it is a little bit more complicated.

For the player communication we use RabbitMQ which itself implements the AMQP 0-9-1 protocol we make use of. It is really powerful, dynamic and advantagous. Please make sure you understand the basics of the AMQP protocol especially about Exchanges, Queues and Routing Keys.

Once you create a player (technically also joining a game does the same) the game service will create an exchange that is (conceptually!) mutually exclusive for this player and forwards all events of interest for this player to this particular exchange. It will also create a queue that is (conceptually!) mutually exclusive for this player and will be filled with all messages from the mentioned exchange. Their corresponding names will be returned to you right after you join the game. However, they are also statically known, as the pattern for those is player-${playerName}. So when your player name is alice both the queue and exchange will be named player-alice and you can assume that in your implementation. However, you should not declare those on your own.

You can use this simple queue for everything you need to. However, you are free to rely on a more sophisticated implementation with specialized queues. For example you could create a queue that just handles Trading events. Just note that queue names are always unique and you don’t have a collission with other players.

The following illustatration shows how this design works.

Messaging Scheme

For sake of simplicity we introduce an imaginary API that serves as an example.

  • connect() allows us to connect to the message Broker
  • declare(queue) declares a queue at the message broker
  • bind(queue, exchange, bindingKey) binds a queue to an exchange with a defined binding key
  • listen(queue, func) listen to a specific queue
connect();
listen("player-alice", (msg) => {
  console.log(`Horray I've got a message: ${msg}`);
});

const tradingQueue = declare("player-alice-trading-queue");
bind(tradingQueue.name, "player-alice", "event.TradablePrices");
bind(tradingQueue.name, "player-alice", "event.TradableBought");
bind(tradingQueue.name, "player-alice", "event.TradableSold");

listen(tradingQueue.name, (msg) => {
  console.log(`Received a new trading event: ${msg}`);
});

Skeleton Players as a Reference

Remember that you can always checkout the predefined skeletons as a reference if you require more insights and implementation hints.


What’s next?

Deep Dive Commands.

How does the world/map what else do i need to now to play?

What can I do with my robot?

What is the economy and what items can i buy?

Last modified February 4, 2025: fix go & npm dependencies (8ff1fa0)