Skip to content

MrPio/Scheda-DnD-5e-Backend

Repository files navigation

Scheda DnD 5e Backend

Postgres NPM NodeJS Express.js JWT TypeScript Sequelize Docker Postman Redis RxJS Axios

📘 Table of Contents

🎯 Project Goal

Complete Management of DnD Combat Sessions

The backend is designed to offer complete management of combat sessions in "Dungeons & Dragons 5e", integrating directly with character created by players and npc sheets created by the master. These are created through the dedicated Flutter application, "SchedaDnD5e", which serves as the frontend for interaction with the system.

Main Backend Features

The requirements for this project are detailed in the following document, unfortunately in 🇮🇹 language.

🐋 Docker

The project is containerized using Docker and Docker Compose. In particular, the docker.api file contains the instructions for containerizing the API server, while the docker.websocket file outlines the process of containerizing the WebSocket server. Additionally, the docker-compose.yml file contains also instructions for creating containers dedicated to the Postgres and Redis databases. These containers are based on public images sourced from Docker Hub.

The following commands are used to start the four containers:

  • Build API container with image tag api_img: docker build -t api_img -f .\Dockerfile.api .
  • Build Websocket container with image tag websocket_img: docker build -t websocket_img -f .\Dockerfile.websocket .
  • Start all four containers: docker-compose up -d

In compose up -d option, or detached mode, enables the creation and initiation of containers that run in the background, freeing up the terminal for other tasks.

How to refresh containers with latest changes:

These commands, executed sequentially, copy the contents of the src directory to the /usr/src/app/ directory of both the API and Websocket containers, and then restart the two containers to apply any changes to the copied files.

  • docker cp .\src\ api:/usr/src/app/;
  • docker cp .\src\ websocket:/usr/src/app/;
  • docker-compose restart api;
  • docker-compose restart websocket

✅ How to test with Postman

Before testing, make sure you extract the following archive to the root directory. It contains unversioned files such as .env, .dev.env, websocket private key and firebase secrets.

The password to decrypt the archive can be requested to the authors.

The postman collection created for testing the routes is:

When started, the API server automatically seeds the PostgreSQL database with a test session. In this session there is a character controlled by a player and a monster controlled by the master.

To test the /attack, /savingThrow and /reaction, which require the players to be connected, it is also needed to create two websockets in Postman, one for the master and one for the player.

Before creating the websocket, ensure that .env file contains the row NODE_ENV= dev. This, in conjunction with USE_JWT= false in the .dev.env file, allows token validation to be bypassed and allows the same player to handle multiple websocket connections.

The player websocket should be created as follows:

  • The connection URL is wss://localhost:8080/sessions/1.

The master websocket should be created as follows:

  • The connection URL is wss://localhost:8080/sessions/1.
  • In Headers section, add a token entry with value HbE4YvSXSx6tbdBxB9Sn, which is the identifier of the master player. This only works in development mode.

📄 Use case diagram

Actors

The player roles can be mapped as follows. Note that although the client must be authenticated via JWT to participate in the combat session, there is still a route that does not require authentication, namely the diceRoll/ route.

Session management

Turn management

Attack management

Entity management

History management

🚩 App Routes

The API server endpoints are listed in the following table. Blank lines separate the routes following the semantic division of the previous use cases.

Session Routes

Type Route Parameters Description
GET /sessions - Provides the index of all sessions in which the authenticated user has the role of player or master.
POST /sessions characters, npc, monsters, mapSize Creates a new session. Returns the new session.
GET /sessions/{sessionId} - Returns all information from sessionId.
DELETE /sessions/{sessionId} - Deletes sessionId.
PATCH /sessions/{sessionId}/start - Starts sessionId. Its current status must be created.
PATCH /sessions/{sessionId}/pause - Pauses sessionId. Its current status must be ongoing.
PATCH /sessions/{sessionId}/continue - Resumes sessionId. Its current status must be paused.
PATCH /sessions/{sessionId}/stop - Ends sessionId. Its current status must be ongoing or paused.

Turn Routes

Type Route Parameters Description
GET /sessions/{sessionId}/turn - Provides the current turn of sessionId.
PATCH /sessions/{sessionId}/turn/postpone entityId, predecessorEntityId Postpones the turn of the entityId after the turn of the predecessorEntityId.
PATCH /sessions/{sessionId}/turn/end entityId Ends the turn of the entityId. Notifies the next playing entity.

Attack Routes

Type Route Parameters Description
GET /diceRoll diceList, modifier? Rolls the dice in the diceList and adds up any modifier. The diceList must be non empty.
PATCH /sessions/{sessionId}/attack entityId, attackInfo, attackType Causes attackerId to attack an entity. The attackType must contain the type of attack being made, which can be melee or enchantment. The attackInfo must contain the attempt dice roll. If this is greater than the target's AC, the attacker is asked to roll the damage dice.
GET /sessions/{sessionId}/savingThrow entitiesId, difficultyClass, skill Requests all the entitiesId to make a save roll on skill. The result is positive if greater than difficultyClass.
PATCH /sessions/{sessionId}/effect entityId, effect Assigns the effect to the entityId. If effect is null, the effects of the entities are deleted.
PATCH /sessions/{sessionId}/reaction entityId Enables the reaction for the entityId. Notifies it.

Entity Routes

Type Route Parameters Description
PATCH /sessions/{sessionId}/entities entityType, entityInfo Adds an entity to the sessionId. If the entityType is monster, entityInfo must contain all of its information. Otherwise it must only contain the uid.
DELETE /sessions/{sessionId}/entities/{entityId} - Removes entityId from sessionId. Fails if not found.
GET /sessions/{sessionId}/entities/{entityId} - Returns all the info from entityId. Fails if not found in sessionId.
PATCH /sessions/{sessionId}/entities/{entityId} entityInfo Updates the info of entityId. Fails if not found in sessionId.

History Routes

Type Route Parameters Description
GET /sessions/{sessionId}/history actionType? Returns the whole sessionId history. Filter it by actionType if provided.
POST /sessions/{sessionId}/history message Adds a message to the sessionId history. Notifies all players except the one who posted the message.

⏱ Sequence diagram

Create Session

Start Session

Attack

Enable Reaction

Connect To Session

📐 Class diagram

🪄 Patterns Used

The following patterns have been used in the development of solutions for the most critical aspects of the project.


Chain of Responsability

The adoption of the Model-View-Controller architectural pattern makes it possible to decouple the management of the business logic, which is handled by the model, from the routing, which is handled by the controller.

However, the introduction of middleware in conjunction with the Chain of Responsability pattern allows the request validation phase to be decoupled from the response generation phase. In essence, the controller is only reached if all the middleware succeeds.

Middleware is natively supported by Express, and each can be thought of as a validator for a single aspect of the client request, independent of all others. This allows them to be reused across multiple routes in a very elegant way.


Higher Order Functions

To generalize the middleware, higher order functions were used.

For example, body parameter type validation has been separated from the per-route middleware and delegated to the standalone checkMandadoryParams middleware.

app.get('/diceRoll',
  checkMandadoryParams(['diceList']),
  checkParamsType({ diceList: ARRAY(ENUM(Dice)), modifier: INTEGER }),
  ...

checkParamsType is a higher order function that takes as input an object where the keys are the body parameters and the value is the type checker function, and returns the middleware for that particular type check validation.

The type checker functions may be themselves higher order functions. For example, ARRAY takes a type checker function as input and returns a type checker function that checks that the object provided is an array and then applies the latter to all the elements of the array.

export const ARRAY =
  (next: (arg0: object) => boolean) =>
    (obj: object) =>
      Array.isArray(obj) && obj.every(it => next(it));

Factory Method

The factory method pattern has been used to centralize both client-side and server-side exceptions that may arise when handling the client request. It provides a lever of abstraction that helps the client code to ignore the actual error response generation, instead only requiring it to specify which ErrorProduct to use.

This is implemented in the /src/error directory.


Repository

In this project, three different data sources are used:

  • Firebase Firestore: Stores the mobile app objects, such as players, characters, enchantments and npc.
  • PostgreSQL: Stores the combat session related information, such as sessions and monsters.
  • Redis: Implements an abstraction layer on top of the other two. Also caches the Firebase JWT with a short TTL to help reduce validation calls to the Firebase API (necessary because Firebase Auth uses a rolling public key, which is needed to validate the JWT signature).

As different objects are stored in different places, the repository pattern comes in very handy for decoupling from the location of the data.

Thanks to the repository pattern, client code can ignore not only which database the object is stored in, but also the caching policy, which is handled entirely by the repository itself.

This is implemented in the /src/repository directory.


Observer

The websocket server makes extensive use of the observer design pattern using the RxJS library.

Callback functions are subscribed to Subject objects, which are called repeatedly by the open, message and close websocket events.

The timer observable is used to prevent starvation when waiting for a player response through websocket. If the player answers or disconnects instead, the timer is interrupted using the takeUntil function, which interrupts the timer emission before it reaches the abort Subject.

This is implemented in the /src/websocket/websocket.ts directory.

⚙️ Technologies used

  • Database: Sequelize with support for PostgreSQL for entity and session management.
  • Data Modeling: Sequelize ORM for defining models and managing relationships between entities.
  • Authentication: JWT to ensure secure and authorized access.
  • Caching: Redis for cache management and performance improvement.
  • API: RESTful API for communication between the frontend and backend, managed with Express.
  • WebSocket: For real-time communication during combat sessions.
  • Package Management: NPM for package and dependency management.
  • Containerization: Docker for creating and managing isolated environments.
  • Reactive Programming: RxJS for handling data streams and asynchronous events.
  • HTTP requests: Axios for handling HTTP requests and interaction between the two API servers.
  • Testing: Postman for creating API requests used for testing.

👨🏻‍💻 Authors

Name Email GitHub
Valerio Morelli [email protected] MrPio
Enrico Maria Sardellini [email protected] Ems01
Federico Staffolani [email protected] fedeStaffo

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •