How I created the online game “backgammon” (part four). Server

Hi all!

In the previous article I described the “authorization” module that I use in my project and told how user authentication and authorization occurs, what libraries and methods are used in the system, and today, according to the survey conducted in the second part, I will tell you about the “game” module. It is in it that all the “magic” of the game occurs.

The module itself is no different from the others. Architecturally, it is a set of methods that allow you to implement the mechanics of the game, and it is not so important which one, I tried to make it universal, so that later it would be easier to add other games.

Let me remind you that after user authentication, all communication between the client and the server occurs using WebSocket transport, and all requests are sent to the server as RPC packets, which are processed in the message broker. I described its operation in the second part. All WebSocket connections are authorized during the connection process and contain the necessary information about the user who initiated it. By the way, last week I rewrote part of the server for use as a code execution environment and package manager – BUN. This decision was made, among other things, because it uses the library for implementing HTTP and WebSocket transport. uWebSockets.js. A description of all the advantages of switching to BUN would require a separate article, so for now I can recommend that anyone interested read about this wonderful product on its official website.

At the start of the game, you need to create the game itself, for this there is a special method game.create, which takes as parameters the name and game mode, at the moment there is one game, but there are already several game modes – “game with a friend”, “game with AI” and “game with a random opponent” – PvP. When creating a game, I generate a special key, which is used later to connect to the game and transfer all other data to the game. I use nanoid with its own set of symbols to eliminate minus signs, underscores, and lowercase letters, and to eliminate confusion with zero and the letter “O”. I have been using this library for quite some time, it is fast, reliable, and has collision calculatorwhich allows you to select the appropriate length of the generated key, taking into account the generation frequency per unit of time or the required number of generations before the first collision occurs.

I chose 8 characters, but I think it could be reduced to 6 or 5, since the key is only used during the connection process and during the game itself. This way, you can safely clear keys from finished or cancelled games and not increase their number to the point of collisions.

During the game creation process, a random set of dice rolls (156 pairs) is also pre-generated. This is how the fairness mechanism of the game is implemented. It is impossible to change the set of rolls after generation, and before the end of the game, you can download an archive with rolls, which is locked with a password. You can find out the password only after the end of the game. This functionality has not yet been implemented in the client interface, but the methods on the server side have already been implemented and tested. In addition, a lifespan is set for the game, after which the game is considered expired and it will no longer be possible to finish it.

Each game has a status – “waiting”, “game”, “finished”, “interrupted” and “expired”. When creating a new game, depending on the mode passed in the parameters, I set the status “waiting” – for the modes “game with a friend” and “game with a random opponent”, or the status “game” if the mode “game with AI” is selected. After that, all information is saved, and a response containing all the data necessary to start the game is sent to the client.

Below is a description of this package (BackgammonData).

type BackgammonData = {
  token: string;
  expired: number;
  players: TGamePlayer[];
  status: string;
  data: BackgammonGameState;
}

type BackgammonGameState = {
  firstDice: number[];
  steps: BackgammonStep[];
  board: BackgammonBoard;
}

type BackgammonStep = {
  subSteps: BackgammonSubStep[];
  player: number;
  dice: number[];
  done: boolean;
}

type BackgammonSubStep = {
  from: number;
  to: number;
}

type BackgammonBoard = {
  white: number[][];
  black: number[][];
}

After receiving this packet on the client side, the application sends a request to connect to the game and goes to the “game” screen. In the case of the “play with a friend” mode, the application displays the game key that must be given to the partner and, as in the “play with a random opponent” mode, goes into the state of waiting for the opponent. In the “play with AI” mode, the opponent is always connected, so in this case the game begins with a roll of the dice to display the first move. At this point, I finish describing the process of creating a game and move on.

After creating a game, as I wrote above, a player is connected to the game. For this purpose, the “game” module has a game.join method that takes a key (token) as the only parameter. The method searches for a game with the passed key in the database and, if found, checks its status, it must match the status “waiting” or “game”. If the status is “waiting”, then the ID of the user who sent this request is added to the players field, and the game is given the status “game”, if the status value matches “game”, then the presence of the user ID in the players field is checked. If these checks are passed, the method returns an updated game description packet and signs the sender's WebSocket connection to the game channel, and then sends a message to the game channel that a new player has joined the game.

After the client application receives a message that the players are ready to play, the first dice roll is displayed, which is transmitted in the step (BackgammonStep) and the application enables the interface for interacting with the chips on the board. After moving a chip, the client application transmits information about this to the server. Since there can be several moves in a single step in the game, they are all stored in the BackgammonSubStep array.[]the elements of which contain information about where the chips were moved from and where they were moved to.

To process information about movements, the game.move method is used, which takes the BackgammonSubStep array as a parameter.[]this is done in order to implement the transmission of several moves at once, when the same chip is moved sequentially. After checking the presence of the user ID in the list of players and the game status, the current state of the game is restored in a separate class containing the game logic. This class is universal and is used both on the client and on the server to check possible moves according to the rules of the game. After restoring the state, the conformity of the move is checked, whether the player should move now and the possibility of moving the chip to the specified fields sent by the user. If all checks are passed, the data on the moves made are saved, and a new state of the game is transmitted to the game channel.

Before sending, the fact of the end of the move is checked and if the move is finished, then a new element BackgammonStep is added to the array of moves steps, which specifies the identifier of the player who is making this move, and also takes the next value of the dice roll from the list of rolls that was generated at the time of creating the game. It is important to note here that the list of rolls is never transmitted to the client side and it is impossible for the player to know which dice will fall in the next move. In addition, the fact of the end of the game is also checked and if the game is finished, then the winner is awarded a certain number of points and information about this is transmitted to the game channel, and then the players unsubscribe from this channel.

If the game is played in the “AI game” mode, then after the user's turn is over, when a new turn is added to the game state, information about the game is transmitted to the AI, which analyzes the board state and selects the best move from all available ones, taking into account the dice that have fallen, after which the information about the move is transmitted from the AI ​​back to the game.move method and everything happens according to the scenario described above. The AI ​​in the system is an equal user, with the only difference being that it has the ability to play an unlimited number of games simultaneously, while the others can only have one active game.

If for some reason the user decides to interrupt the game, the application uses the game.abort method, where the game key is passed. This method sets the status “interrupted” and also saves information about which player interrupted the game. Of course, the method checks that the user who sent the request is on the list of players and the game is in the status “waiting” or “playing”. After publishing information in the game channel that it was interrupted, the server unsubscribes players from this channel.

In addition to the methods described above, the “game” module contains several other methods available to clients:

  • game.getActive – to obtain information about the presence of an active game, it is necessary to restore the game session in case of loss of connection or forced disconnection from the game, and it is also used to control the ability to play only one game at a time

  • game.getFile – to load an archive with a list of dice rolls

  • game.getLeaderboard – to get lists of the best players, for all time, for the current day and for the current week

  • game.getCallCredentials – to get the access key to organize a call to the opponent during the game

  • game.getPublicGames – to get a list of current public games (mode “game with a random opponent”)

  • game.getLogic – to load the game logic on the client side (the same general class for checking possible moves and more)

  • game.view – to connect to the game in observer mode

These methods do not affect the game process itself but add the necessary functionality. If you are interested in learning more about them, write in the comments.

This is where I will end today and I propose, as is traditional, to hold a vote on what to talk about next time.

You can watch and play my backgammon live at website or in telegram.

Have a nice day everyone!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *