Skip to content
Snippets Groups Projects
Commit 0e9f2d8d authored by Fredrik Fonn Hansen's avatar Fredrik Fonn Hansen :8ball:
Browse files

Resolve "Create logic for lobbies in backend"

parent 0b5e99d3
No related branches found
No related tags found
1 merge request!12Resolve "Create logic for lobbies in backend"
# API Documentation
This is an API documentation for an Express server that handles users, lobbies, and game state. The server uses Firebase Firestore for database storage.
### Base URL
> http://localhost:3000
### GET /user/
Description: Returns a list of all user IDs
Response:
Status: 200 OK
Body: Array of user IDs
> - 204 No users found (DB is empty)
> - 200 List of user-id's
......@@ -10,16 +21,163 @@
> - 404 User not found
> - 200 User
### GET /user/{username: string}
### GET /user/{idOrUsername: string}
Returns data for the user with the specified ID or username. If the parameter provided matches an ID, the response will contain data for that user. If the parameter matches a username, the response will contain data for the first user with that username found in the database.
```json
Status: 200 OK
{
"username": "user1",
"highscore": 50,
"games": 10,
"wins": 5,
"losses": 5
}
```
> - 404 User not found
> - 200 User
### POST /user/create/{id}
### POST /user/create/{username: string}
Creates a new user with the specified username. The username must be unique. If the username is already taken, the response will contain an error message.
> - 409 User already exists
> - 201 User created
### GET /highscores
> - 204 No highscores found
Returns an array of the top 10 users with the highest scores, sorted in descending order.
> - 204 No highscores found
> - 200 List of users with highest score
```json
Status: 200 OK
{
[
{
"username": "user1",
"highscore": 50,
"games": 10,
"wins": 5,
"losses": 5
},
{
"username": "user2",
"highscore": 40,
"games": 8,
"wins": 4,
"losses": 4
},
...
]
}
```
### POST /lobby/:id
Joins a lobby with the specified ID. If the lobby does not exist, a new lobby is created.
```json
POST /lobby/lobby1
Body: { "userId": "user1" }
```
> - 201 Created
### POST /lobby/:id/leave
Leaves a lobby with the specified ID.
Example request:
```json
POST /lobby/lobby1/leave
Body: { "userId": "user1" }
```
### GET /game/:gameid/currentTurn
Checks if it is the specified user's turn in the specified game. Returns the current game state if it is the user's turn.
Example request:
```json
GET /game/game1/currentTurn
Body: { "userName": "user1" }
```
Example response:
```json
Status: 200 OK
{
"gameId": "12345678",
"gameStatus": false,
"currentTurn": 0,
"users": {
[
"User": {
"id": string;
"username": string;
"wins": number;
"losses": number;
"highscore": number;
},
"IStat": {
"position": [0,0];
"turretAngle": 0.5;
"isMirrored": true;
"health": 100;
"ammunition": 100;
"tankDirection": "left";
"tankType": "M107";
"score": 100;
},
...
]
},
}
```
### POST /game/:gameid/move
Updates the game state for the specified game with the new game state provided in the request body.
Example request:
```json
POST /game/123456/move
Body:
{ "newGameState":
{
"gameId": "12345678",
"gameStatus": false,
"currentTurn": 0,
"users": {
[
"User": {
"id": string;
"username": string;
"wins": number;
"losses": number;
"highscore": number;
},
"IStat": {
"position": [0,0];
"turretAngle": 0.5;
"isMirrored": true;
"health": 100;
"ammunition": 100;
"tankDirection": "left";
"tankType": "M107";
"score": 100;
},
...
]
},
}
}
```
// class for handling game logic
import { User } from '../../types/User';
import { IGame } from '../interfaces/IGame';
import { ILobby } from '../interfaces/ILobby';
import { IStats } from '../interfaces/IStats';
import { Stats } from './Stats';
export class Game implements IGame {
currentTurn: number;
lobby: ILobby;
gameStatus: boolean;
gameId: string;
users: [User, IStats][]; // left [0] and right [1] user
constructor(lobby: ILobby) {
this.lobby = lobby;
this.gameStatus = false; // game not finished
this.gameId = Math.random().toString(36);
// insert the lobby users into the game and create a new stats object for each user
this.users = lobby.getUsers().map((user) => [user, new Stats()]);
// set the stats for the left and right user
// todo add random tank type
this.users[0][1].setTankType('M107');
this.users[0][1].setTankDirection('left');
this.users[0][1].setIsMirrored(true); // this mirroring can also be done locally.
this.users[1][1].setTankType('M1A2');
this.users[1][1].setTankDirection('right');
this.users[1][1].setIsMirrored(false);
// make random number 0 or 1 to determine who starts
this.currentTurn = Math.round(Math.random());
this.notifyUsers(); // TODO: implement this method
}
setIsFinished(status: boolean): void {
this.gameStatus = status;
}
notifyUsers(): void {
throw new Error('Method not implemented.');
}
setGameStatus(status: boolean): void {
this.gameStatus = status;
}
getGameStatus(): boolean {
return this.gameStatus;
}
setScore(user: number, score: number): void {
if (!this.isValidUserNumber(user)) {
throw new Error('Unable to update score. Invalid userID.');
}
this.users[user][1].setScore(score);
}
getScore(user: number): number {
if (!this.isValidUserNumber(user)) {
throw new Error('Unable to get score. Invalid userID.');
}
return this.users[user][1].getScore();
}
getWinner(): User {
// return the the user with the highest score in stats
if (this.getGameStatus() == false) {
throw new Error('Game is not finished');
}
if (this.getScore(0) > this.getScore(1)) {
return this.users[0][0];
} else {
return this.users[1][0];
}
}
getLoser(): User {
// return the the user with the highest score in stats (duplicate code from getWinner)
if (this.getGameStatus() == false) {
throw new Error('Game is not finished');
}
if (this.getScore(0) < this.getScore(1)) {
return this.users[0][0];
} else {
return this.users[1][0];
}
}
calculateNextGameState(newGameStateJSON: string): void {
throw new Error('Method not implemented.');
}
getGameState(): IGame {
throw new Error('Method not implemented.');
}
isValidUserNumber(userNumber: number): boolean {
return userNumber === 0 || userNumber === 1;
}
getCurrentTurn(): number {
return this.currentTurn;
}
getCurrentTurnUser(): User {
return this.users[this.currentTurn][0];
}
// make json object of the game state (to send to clients)
getGameStateJSON(): string {
return JSON.stringify({
gameId: this.gameId,
gameStatus: this.gameStatus,
currentTurn: this.currentTurn,
users: this.users,
});
}
}
import { IStats } from '../interfaces/IStats';
export class Stats implements IStats {
position: number[][];
turretAngle: number;
isMirrored: boolean;
health: number;
ammunition: number;
tankDirection: string;
tankType: string;
score: number;
constructor() {
this.position = [
[0, 0],
[0, 0],
];
this.turretAngle = 0;
this.health = 100;
this.ammunition = 100;
this.score = 0;
this.isMirrored = false;
this.tankDirection = 'left';
this.tankType = 'M107';
}
// create getters and setters
getPosition(): number[][] {
return this.position;
}
setPosition(position: number[][]): void {
this.position = position;
}
getTurretAngle(): number {
return this.turretAngle;
}
setTurretAngle(turretAngle: number): void {
this.turretAngle = turretAngle;
}
getIsMirrored(): boolean {
return this.isMirrored;
}
setIsMirrored(isMirrored: boolean): void {
this.isMirrored = isMirrored;
}
getHealth(): number {
return this.health;
}
setHealth(health: number): void {
this.health = health;
}
getAmmunition(): number {
return this.ammunition;
}
setAmmunition(ammunition: number): void {
this.ammunition = ammunition;
}
getTankDirection(): string {
return this.tankDirection;
}
setTankDirection(tankDirection: string): void {
this.tankDirection = tankDirection;
}
getTankType(): string {
return this.tankType;
}
setTankType(tankType: string): void {
this.tankType = tankType;
}
getScore(): number {
return this.score;
}
setScore(score: number): void {
this.score = score;
}
}
// this module is responsible for handling the ongoing games.
import { Game } from './game/Game';
import { IGame } from './interfaces/IGame';
import { ILobby } from './interfaces/ILobby';
import { Lobby } from './lobby/Lobby';
// this class is responsible for handling the ongoing games. and the lobbies.
export class GameHandler {
private static instance: GameHandler;
static getInstance(): GameHandler {
if (!GameHandler.instance) {
GameHandler.instance = new GameHandler();
}
return GameHandler.instance;
}
private lobbies: ILobby[] = [];
private games: IGame[] = [];
constructor() {
this.lobbies = [];
this.games = [];
}
addLobby(lobby: ILobby) {
this.lobbies.push(lobby);
}
createGame(lobby: ILobby) {
const game = new Game(lobby);
this.addGame(game);
return game;
}
removeLobby(lobby: ILobby) {
this.lobbies = this.lobbies.filter((l) => l.id !== lobby.id);
}
addGame(game: IGame) {
this.games.push(game);
}
removeGame(game: IGame) {
this.games = this.games.filter((g) => g.gameId !== game.gameId);
}
getLobbies() {
return this.lobbies;
}
getGames() {
return this.games;
}
getGameById(gameId: string) {
return this.games.find((g) => g.gameId === gameId);
}
getLobbyById(id: string) {
return this.lobbies.find((l) => l.id === id);
}
createLobby() {
const lobby = new Lobby();
this.addLobby(lobby);
return lobby;
}
}
import express from 'express';
import path from 'path';
import { User } from '../types/User';
import { GameHandler } from './gameHandler';
const app = express();
const port = 3000;
const gameHandler = new GameHandler(); // singleton ;)
// const cors = require('cors');
// app.use(cors());
......@@ -87,9 +90,7 @@ app.post('/user/create/:username', async (req, res) => {
}
});
// returns users with top 10 highscore
app.get('/highscores', async (req, res) => {
const usersRef = admin.firestore().collection('users');
const querySnapshot = await usersRef.orderBy('highscore', 'desc').limit(10).get();
......@@ -100,4 +101,63 @@ app.get('/highscores', async (req, res) => {
const users = querySnapshot.docs.map((doc: any) => doc.data() as User);
res.status(200).send(users);
}
})
});
// join a lobby with specific id
app.post('/lobby/:id', async (req, res) => {
if (gameHandler.getLobbyById(req.params.id)) {
const lobby = gameHandler.getLobbyById(req.params.id);
if (!lobby) {
res.status(404).send('Lobby not found');
return;
}
lobby.addUser(req.body.userId);
gameHandler.createGame(lobby); // start game since 2 players are in lobby
res.status(200).send(lobby.getId());
} else {
// if lobby doesn't exist, create a new lobby
const lobby = gameHandler.createLobby();
lobby.addUser(req.body.userId);
res.status(201).send(lobby.getId());
}
});
// leave a lobby with specific id
app.post('/lobby/:id/leave', async (req, res) => {
const lobby = gameHandler.getLobbyById(req.params.id);
lobby?.removeUser(req.body.userId);
// if lobby is empty, dispose
if (lobby?.getUsers().length === 0) {
gameHandler.removeLobby(lobby);
}
res.status(200).send(lobby?.getId());
});
// Polling endpoint to check if its your turn (sends username)
app.get('/game/:gameid/currentTurn', async (req, res) => {
const game = gameHandler.getGameById(req.params.gameid);
const user = req.body.userName;
if (game) {
if (user === game.getCurrentTurnUser().username) {
// send the gamestate to the client
res.status(200).send(game.getGameStateJSON());
} else {
res.status(200).send('Not your turn');
}
} else {
res.status(404).send('Game not found (or not created yet because lack of opponent)');
}
});
// endpoint to send a move / gamestate
app.post('/game/:gameid/move', async (req, res) => {
const game = gameHandler.getGameById(req.params.gameid);
if (game) {
game.calculateNextGameState(req.body.newGameState);
res.status(200).send('Move made');
} else {
res.status(404).send('Game not found');
}
});
import { User } from '../../types/User';
import { ILobby } from './ILobby';
import { IStats } from './IStats';
// interface for a game instance
export interface IGame {
notifyUsers(): void;
setIsFinished(status: boolean): void;
getWinner(): User;
getLoser(): User;
setScore(user: number, score: number): void;
getScore(user: number): number;
calculateNextGameState(newGameStateJSON: string): void;
getGameState(): IGame;
getCurrentTurnUser(): User;
getGameStateJSON(): string;
users: [User, IStats][];
currentTurn: number;
lobby: ILobby;
gameStatus: boolean;
gameId: string;
}
import { User } from '../../types/User';
export interface ILobby {
addUser(user: User): void;
removeUser(user: User): void;
getUsers(): User[];
id: string;
getId(): string;
// maybe add a relation to a game?
}
import { User } from '../../types/User';
export interface IStats {
position: number[][];
turretAngle: number;
isMirrored: boolean;
health: number;
ammunition: number;
tankDirection: string; // "left" or "right"
tankType: string;
score: number;
getPosition(): number[][];
setPosition(position: number[][]): void;
getTurretAngle(): number;
setTurretAngle(turretAngle: number): void;
getIsMirrored(): boolean;
setIsMirrored(isMirrored: boolean): void;
getHealth(): number;
setHealth(health: number): void;
getAmmunition(): number;
setAmmunition(ammunition: number): void;
getTankDirection(): string;
setTankDirection(tankDirection: string): void;
getTankType(): string;
setTankType(tankType: string): void;
getScore(): number;
setScore(score: number): void;
}
import { User } from '../../types/User';
import { IGame } from '../interfaces/IGame';
import { ILobby } from '../interfaces/ILobby';
/**
* A simple class for defining a lobby with a set of users.
* Used to create a game.
*
* @class Lobby
* @property {User[]} users - The users in the lobby
* @property {string} id - The id of the lobby
* @method addUser - Adds a user to the lobby
* @method removeUser - Removes a user from the lobby
* @method getUsers - Returns all users from the lobby
*/
export class Lobby implements ILobby {
constructor() {
this.users = [];
this.id =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
getId(): string {
return this.id;
}
private users: User[];
private game?: IGame;
id: string;
addUser(user: User) {
this.users.push(user);
}
removeUser(user: User) {
this.users = this.users.filter((u) => u.id !== user.id);
}
addGame(game: IGame) {
this.game = game;
}
getUsers() {
return this.users;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment