inital commit

This commit is contained in:
pb-coding 2023-09-18 13:52:54 +02:00
commit 42517c0fb5
18 changed files with 3794 additions and 0 deletions

2
.env Normal file
View file

@ -0,0 +1,2 @@
PORT=3001
ENVIRONMENT=local

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
dist
src/logs/

2824
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

52
package.json Normal file
View file

@ -0,0 +1,52 @@
{
"name": "skyjo-be",
"version": "1.0.0",
"description": "Skyjo Backend",
"main": "server.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "ts-node src/server.ts",
"build": "prisma generate && tsc",
"start": "node dist/server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pb-coding/skyjo-be.git"
},
"author": "pb-coding",
"license": "ISC",
"bugs": {
"url": "https://github.com/pb-coding/skyjo-be/issues"
},
"homepage": "https://github.com/pb-coding/skyjo-be#readme",
"dependencies": {
"axios": "^1.4.0",
"bcrypt": "^5.1.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.1",
"node-cron": "^3.0.2",
"request": "^2.88.2",
"socket.io": "^4.7.2",
"ts-node": "^10.9.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.13",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.2",
"@types/node-cron": "^3.0.8",
"@types/request": "^2.48.8",
"@types/uuid": "^9.0.2",
"esbuild-register": "^3.4.2",
"typescript": "^5.1.6"
}
}

10
public/index.html Normal file
View file

@ -0,0 +1,10 @@
<html>
<head>
<title>SkyJo Server list</title>
</head>
<body>
<h1>SkyJo Server list</h1>
<button>Start new Game</button>
</body>
</html>

View file

@ -0,0 +1,3 @@
const allowedOrigins = ["http://localhost:3000", "https://localhost:3001"];
export default allowedOrigins;

17
src/config/corsOptions.ts Normal file
View file

@ -0,0 +1,17 @@
import allowedOrigins from "./allowedOrigins";
const corsOptions = {
origin: (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void
) => {
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
optionsSuccessStatus: 200,
};
export default corsOptions;

170
src/game/card.ts Normal file
View file

@ -0,0 +1,170 @@
type CardValue = -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type ObfuscatedCardValue = CardValue | "X";
type CardColor =
| "darkblue"
| "lightblue"
| "green"
| "yellow"
| "red"
| "black";
// TODO: use smarter types like Omit<>, Pick<>, etc.
export type ObfuscatedCard = {
id: number;
name: string;
value: ObfuscatedCardValue;
color: CardColor;
};
export class Card {
id: number;
name: string;
value: CardValue;
color: CardColor;
constructor(id: number, value: CardValue) {
this.id = id;
this.value = value;
this.name = `${value} Card`;
this.color = this.matchColorToCardValue(value);
}
matchColorToCardValue(value: CardValue | ObfuscatedCardValue): CardColor {
switch (value) {
case -2:
return "darkblue";
case -1:
return "darkblue";
case 0:
return "lightblue";
case 1:
return "green";
case 2:
return "green";
case 3:
return "green";
case 4:
return "green";
case 5:
return "yellow";
case 6:
return "yellow";
case 7:
return "yellow";
case 8:
return "yellow";
case 9:
return "red";
case 10:
return "red";
case 11:
return "red";
case 12:
return "red";
case "X":
return "black";
default:
return "red";
}
}
}
export type ObfuscatedCardStack = {
cards: ObfuscatedCard[];
};
export class CardStack {
cards: Card[];
constructor() {
this.cards = [];
this.generateCards();
}
generateCards() {
for (let cardNumber = 1; cardNumber <= 150; cardNumber++) {
if (cardNumber <= 5) {
this.cards.push(new Card(cardNumber, -2));
continue;
}
if (cardNumber > 5 && cardNumber <= 15) {
this.cards.push(new Card(cardNumber, -1));
continue;
}
if (cardNumber > 15 && cardNumber <= 30) {
this.cards.push(new Card(cardNumber, 0));
continue;
}
if (cardNumber > 30 && cardNumber <= 40) {
this.cards.push(new Card(cardNumber, 1));
continue;
}
if (cardNumber > 40 && cardNumber <= 50) {
this.cards.push(new Card(cardNumber, 2));
continue;
}
if (cardNumber > 50 && cardNumber <= 60) {
this.cards.push(new Card(cardNumber, 3));
continue;
}
if (cardNumber > 60 && cardNumber <= 70) {
this.cards.push(new Card(cardNumber, 4));
continue;
}
if (cardNumber > 70 && cardNumber <= 80) {
this.cards.push(new Card(cardNumber, 5));
continue;
}
if (cardNumber > 80 && cardNumber <= 90) {
this.cards.push(new Card(cardNumber, 6));
continue;
}
if (cardNumber > 90 && cardNumber <= 100) {
this.cards.push(new Card(cardNumber, 7));
continue;
}
if (cardNumber > 100 && cardNumber <= 110) {
this.cards.push(new Card(cardNumber, 8));
continue;
}
if (cardNumber > 110 && cardNumber <= 120) {
this.cards.push(new Card(cardNumber, 9));
continue;
}
if (cardNumber > 120 && cardNumber <= 130) {
this.cards.push(new Card(cardNumber, 10));
continue;
}
if (cardNumber > 130 && cardNumber <= 140) {
this.cards.push(new Card(cardNumber, 11));
continue;
}
if (cardNumber > 140 && cardNumber <= 150) {
this.cards.push(new Card(cardNumber, 12));
continue;
}
}
}
// Fisher-Yates shuffle algorithm
shuffleCards() {
for (let i = this.cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]];
}
}
}

52
src/game/events.ts Normal file
View file

@ -0,0 +1,52 @@
import { Socket } from "socket.io";
import { io } from "../server";
import { Game } from "./game";
import { Player, ObfuscatedPlayer } from "./player";
import { Card } from "./card";
export const handleJoinSession = (socket: Socket, sessionId: string) => {
socket.join(sessionId);
console.log("User joined session:", sessionId);
const numberOfClients = io.sockets.adapter.rooms.get(sessionId)?.size ?? 0;
console.log("Clients in room:", numberOfClients);
io.to(sessionId).emit("clients-in-session", numberOfClients);
};
export const handleNewGame = (
socket: Socket,
gameDetails: { sessionId: string }
) => {
console.log("Received game details:", gameDetails);
const { sessionId } = gameDetails;
const players = io.sockets.adapter.rooms.get(sessionId);
if (players && players.size > 1) {
const game = new Game(socket, sessionId, players);
game.sendObfuscatedGameUpdate();
} else {
// error handling
console.log("Not enough players to start a game");
}
};
const obfuscatePlayerCards = (player: Player): ObfuscatedPlayer => {
return {
id: player.id,
socketId: player.socketId,
name: player.name,
cards: player.cards.map((card: Card, index: number) => {
return {
id: card.id,
value: player.knownCardPositions[index] ? card.value : 0, // unknown cards are obfuscated to 0
name: card.name,
color: card.color,
matchColorToCardValue: card.matchColorToCardValue,
};
}),
knownCardPositions: player.knownCardPositions,
playersTurn: player.playersTurn,
cardCache: player.cardCache,
};
};

378
src/game/game.ts Normal file
View file

@ -0,0 +1,378 @@
import { Player, ObfuscatedPlayer } from "./player";
import { CardStack } from "./card";
import { Card, ObfuscatedCardStack } from "./card";
import { Socket } from "socket.io";
import { io } from "../server";
type PlayerSocketSet = Set<string>;
type PlayerActions = Array<
[string, (playerSocketId: string, data: any) => void]
>;
// obfuscated types are used to send only necessary data to the client
export type ObfuscatedGame = {
sessionId: string;
playerCount: number;
players: ObfuscatedPlayer[];
cardStack: ObfuscatedCardStack;
discardPile: Card[];
phase: string;
};
const gamePhase = {
revealTwoCards: "reveal two cards",
pickUpCard: "pick up card",
placeCard: "place card",
revealCard: "reveal card",
revealedLastCard: "revealed last card",
gameEnded: "game ended",
};
export class Game {
socket: Socket;
sessionId: string;
playerCount: number;
players: Player[];
cardStack: CardStack;
discardPile: Card[];
phase: string;
constructor(socket: Socket, sessionId: string, playerIds: PlayerSocketSet) {
this.socket = socket;
this.sessionId = sessionId;
this.cardStack = new CardStack();
this.cardStack.shuffleCards();
this.playerCount = playerIds.size;
this.players = this.initializePlayers(playerIds, this.cardStack);
// get the first card from the cardStack and put it in the discard pile
this.discardPile = [this.cardStack.cards.pop()!];
this.phase = gamePhase.revealTwoCards;
console.log(`New Game created with ${this.playerCount} players!`);
this.gameLoop();
}
initializePlayers(
playerIds: PlayerSocketSet,
cardStack: CardStack
): Player[] {
let players: Player[] = [];
let index = 0;
playerIds.forEach((socketId) => {
index++;
const playerCards = cardStack.cards.splice(0, 12);
const player = new Player(
index,
socketId,
`Player ${index}`,
playerCards
);
players.push(player);
});
this.cardStack = cardStack;
return players;
}
async gameLoop() {
console.log("Game started!");
while (this.phase !== gamePhase.gameEnded) {
switch (this.phase) {
case gamePhase.revealTwoCards:
console.log("Game phase: revealTwoCards");
this.listPlayerSocketListeners();
await this.revealInitialCards();
this.phase = gamePhase.pickUpCard;
break;
case gamePhase.pickUpCard:
console.log("Game phase: pickUpCard");
this.listPlayerSocketListeners();
await this.pickUpCard();
this.phase = gamePhase.placeCard;
break;
case gamePhase.placeCard:
console.log("Game phase: placeCard");
this.listPlayerSocketListeners();
await this.placeCard();
break;
case gamePhase.revealCard:
console.log("Game phase: revealCard");
this.listPlayerSocketListeners();
await this.revealCard();
this.phase = gamePhase.pickUpCard;
case gamePhase.revealedLastCard:
console.log("Game phase: revealedLastCard");
// add logic of last round
this.phase = gamePhase.gameEnded;
break;
default:
throw new Error(`Invalid game phase: ${this.phase}`);
}
}
}
getPlayersWithRevealedInitialCards(): Player[] {
return this.players.filter((player) => {
return player.hasInitialCardsRevealed();
});
}
allPlayersRevealedInitialCards() {
const playersWithRevealedInitialCards =
this.getPlayersWithRevealedInitialCards();
return playersWithRevealedInitialCards.length === this.playerCount;
}
setInitialPlayersTurn() {
const playersWithHighestRevealedCardsValueSum = this.players.reduce(
(playersWithHighestRevealedCardsValueSum, player) => {
if (
player.getRevealedCardsValueSum() ===
playersWithHighestRevealedCardsValueSum[0].getRevealedCardsValueSum()
) {
if (
player.getHighestRevealedCardValue() >
playersWithHighestRevealedCardsValueSum[0].getHighestRevealedCardValue()
) {
playersWithHighestRevealedCardsValueSum = [player];
} else if (
player.getHighestRevealedCardValue() ===
playersWithHighestRevealedCardsValueSum[0].getHighestRevealedCardValue()
) {
playersWithHighestRevealedCardsValueSum.push(player);
}
} else if (
player.getRevealedCardsValueSum() >
playersWithHighestRevealedCardsValueSum[0].getRevealedCardsValueSum()
) {
playersWithHighestRevealedCardsValueSum = [player];
}
return playersWithHighestRevealedCardsValueSum;
},
[this.players[0]]
);
const playerWithHighestRevealedCardsValueSum =
playersWithHighestRevealedCardsValueSum[
Math.floor(
Math.random() * playersWithHighestRevealedCardsValueSum.length
)
];
this.players.forEach((player) => {
if (player === playerWithHighestRevealedCardsValueSum) {
player.playersTurn = true;
} else {
player.playersTurn = false;
}
});
}
nextPlayersTurn() {
const playersTurn = this.players.find((player) => player.playersTurn);
const playersTurnIndex = this.players.indexOf(playersTurn!);
const nextPlayersTurnIndex = (playersTurnIndex + 1) % this.playerCount;
this.players[playersTurnIndex].playersTurn = false;
this.players[nextPlayersTurnIndex].playersTurn = true;
}
getPlayerBySocketId(playerSocketId: string): Player | undefined {
return this.players.find((player) => player.socketId === playerSocketId);
}
revealCardAction(playerSocketId: string, cardPosition: number) {
const player = this.getPlayerBySocketId(playerSocketId)!; // TODO: handle player not found
const revealedCard = player.cards[cardPosition];
console.log(`Revealed card ${revealedCard} at position ${cardPosition}`);
const playerIndex = this.players.indexOf(player!);
this.players[playerIndex].knownCardPositions[cardPosition] = true;
this.sendObfuscatedGameUpdate();
}
async revealInitialCards() {
while (!this.allPlayersRevealedInitialCards()) {
const playersWithRevealedInitialCards =
this.getPlayersWithRevealedInitialCards();
const playersWithUnrevealedInitialCards = this.players.filter(
(player) => !playersWithRevealedInitialCards.includes(player)
);
const playersSocketIds = playersWithUnrevealedInitialCards.map(
(player) => player.socketId
);
await this.waitForPlayerActions(
[["click-card", this.revealCardAction.bind(this)]],
playersSocketIds
);
}
this.setInitialPlayersTurn();
}
drawCardAction(playerSocketId: string, data: any) {
const player = this.getPlayerBySocketId(playerSocketId)!; // TODO: handle player not found
console.log(`Player ${player.name} drawed a card.`);
const drawnCard = this.cardStack.cards.pop()!;
player.cardCache = drawnCard;
this.sendObfuscatedGameUpdate();
}
takeDiscardPileAction(playerSocketId: string, data: any) {
const player = this.getPlayerBySocketId(playerSocketId)!; // TODO: handle player not found
console.log(`Player ${player.name} took the card from discard pile.`);
const discardPileCard = this.discardPile.pop()!;
player.cardCache = discardPileCard;
this.sendObfuscatedGameUpdate();
}
discardCardToPileAction(playerSocketId: string, data: any) {
const player = this.getPlayerBySocketId(playerSocketId)!; // TODO: handle player not found
console.log(`Player ${player.name} discarded a card to the pile.`);
const discardedCard = player.cardCache!;
this.discardPile.push(discardedCard);
player.cardCache = null;
this.sendObfuscatedGameUpdate();
this.phase = gamePhase.revealCard;
}
placeCardAction(playerSocketId: string, cardPosition: number) {
const player = this.getPlayerBySocketId(playerSocketId)!; // TODO: handle player not found
console.log(`Player ${player.name} placed a card.`);
const placedCard = player.cardCache!;
player.cardCache = null;
const replacedCard = player.cards[cardPosition];
this.discardPile.push(replacedCard);
player.cards[cardPosition] = placedCard;
player.knownCardPositions[cardPosition] = true;
this.nextPlayersTurn();
this.phase = gamePhase.pickUpCard;
this.sendObfuscatedGameUpdate();
}
async pickUpCard() {
const playersTurn = this.players.find((player) => player.playersTurn);
console.log(`Waiting for ${playersTurn?.name} to pick up card`);
await this.waitForPlayerActions(
[
["draw-from-card-stack", this.drawCardAction.bind(this)],
["click-discard-pile", this.takeDiscardPileAction.bind(this)],
],
[playersTurn!.socketId]
);
}
async placeCard(tookDiscardPileCard: boolean = false) {
console.log("Waiting for player to place card");
const playersTurn = this.players.find((player) => player.playersTurn);
const allowedActions: PlayerActions = [
["click-card", this.placeCardAction.bind(this)],
];
if (!tookDiscardPileCard) {
allowedActions.push([
"click-discard-pile",
this.discardCardToPileAction.bind(this),
]);
}
await this.waitForPlayerActions(allowedActions, [playersTurn!.socketId]);
}
async revealCard() {
console.log("Waiting for player to reveal a card");
const playersTurn = this.players.find((player) => player.playersTurn);
await this.waitForPlayerActions(
[["click-card", this.revealCardAction.bind(this)]],
[playersTurn!.socketId]
);
this.nextPlayersTurn();
}
waitForPlayerActions(
expectedActions: PlayerActions,
expectedFrom: Player["socketId"][]
): Promise<void> {
return new Promise<void>((resolve) => {
const eventListeners: Array<(playerSocketId: string, data: any) => void> =
[];
expectedFrom.forEach((playerSocketId) => {
const playerSocket = io.sockets.sockets.get(playerSocketId);
if (playerSocket) {
expectedActions.forEach((expectedAction) => {
const [actionName, processAction] = expectedAction;
const eventListener = (data: any) => {
console.log(`Received ${actionName} from ${playerSocketId}`);
processAction(playerSocketId, data);
// remove current and event listeners of alternative expected actions
eventListeners.forEach((eventListener) => {
playerSocket.off(actionName, eventListener);
});
resolve(data);
};
playerSocket.on(actionName, eventListener);
eventListeners.push(eventListener);
});
} else {
// TODO: handle error
}
});
});
}
sendObfuscatedGameUpdate() {
// console.trace("sendObfuscatedGameUpdate");
const obfuscatedGame: ObfuscatedGame = {
sessionId: this.sessionId,
playerCount: this.playerCount,
players: this.players.map((player: Player) => {
return {
id: player.id,
socketId: player.socketId,
name: player.name,
cards: player.cards.map((card: Card, index: number) => {
// unknown cards are obfuscated to 0
return {
id: player.knownCardPositions[index] ? card.id : 0,
value: player.knownCardPositions[index] ? card.value : "X",
name: player.knownCardPositions[index]
? card.name
: "Facedown Card",
color: player.knownCardPositions[index] ? card.color : "black",
matchColorToCardValue: card.matchColorToCardValue,
};
}),
knownCardPositions: player.knownCardPositions,
playersTurn: player.playersTurn,
cardCache: player.cardCache,
};
}),
cardStack: {
cards: this.cardStack.cards.map((card: Card) => {
// player may not see the value of the facedown cards in the cardStack
return {
id: 0,
value: "X",
name: "Facedown Card",
color: "black",
};
}),
},
discardPile: this.discardPile,
phase: this.phase,
};
console.log("Sending game update");
io.to(this.sessionId).emit("game-update", obfuscatedGame);
}
listPlayerSocketListeners() {
console.log("Player socket listeners:");
io.sockets.sockets.forEach((socket) => {
console.log(socket.id);
socket.eventNames().forEach((eventName) => {
console.log(`${eventName.toString()}`);
});
});
}
}

79
src/game/player.ts Normal file
View file

@ -0,0 +1,79 @@
import { Card, ObfuscatedCard } from "./card";
export type ObfuscatedPlayer = {
id: number;
socketId: string;
name: string;
cards: ObfuscatedCard[];
knownCardPositions: boolean[];
playersTurn: boolean;
cardCache: Card | null;
};
export class Player {
id: number;
socketId: string;
name: string;
cards: Card[];
knownCardPositions: boolean[];
playersTurn: boolean;
cardCache: Card | null; // this is where the card is temporarily stored when a player draws a card
constructor(id: number, socketId: string, name: string, cards: Card[]) {
this.id = id;
this.socketId = socketId;
this.name = name;
this.cards = cards;
this.knownCardPositions = new Array(12).fill(false);
this.playersTurn = true;
this.cardCache = null;
}
hasInitialCardsRevealed(): boolean {
const revealedCards = this.knownCardPositions.filter(
(knownCard) => knownCard === true
);
if (revealedCards.length > 1) return true;
else return false;
}
getRevealedCardCount(): number {
return this.knownCardPositions.filter((position) => position).length;
}
getRevealedCardPositions(): number[] {
const revealedCardPositions: number[] = [];
this.knownCardPositions.forEach((position, index) => {
if (position) revealedCardPositions.push(index);
});
return revealedCardPositions;
}
getRevealedCards(): Card[] {
const revealedCards: Card[] = [];
this.knownCardPositions.forEach((position, index) => {
if (position) revealedCards.push(this.cards[index]);
});
return revealedCards;
}
getRevealedCardsValueSum(): number {
const revealedCards = this.getRevealedCards();
const revealedCardsValueSum = revealedCards.reduce(
(sum, card) => sum + card.value,
0
);
return revealedCardsValueSum;
}
getHighestRevealedCardValue(): number {
const revealedCards = this.getRevealedCards();
const highestRevealedCardValue = revealedCards.reduce(
(highestValue, card) => {
if (card.value > highestValue) return card.value;
else return highestValue;
},
0
);
return highestRevealedCardValue;
}
}

4
src/lib/app.ts Normal file
View file

@ -0,0 +1,4 @@
import express from "express";
const app = express();
export default app;

View file

@ -0,0 +1,12 @@
import { Request, Response, NextFunction } from "express";
import allowedOrigins from "../config/allowedOrigins";
const credentials = (req: Request, res: Response, next: NextFunction) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin as string)) {
res.setHeader("Access-Control-Allow-Credentials", "true");
}
next();
};
export default credentials;

View file

@ -0,0 +1,35 @@
import { Request, Response, NextFunction } from "express";
import { logEvents } from "./logEvents";
import { log } from "./logEvents";
const errorHandler = (
error: unknown,
req: Request,
res: Response,
next: NextFunction
) => {
// TODO: Handle loads of different errors
console.error(error);
let errorMessage = "An unknown error occurred!";
let statusCode = 500;
if (error instanceof Error) {
logEvents(`${error.name}: ${error.message}`, "errorLog.txt");
errorMessage = error.message;
}
log("Error Handler", errorMessage);
res.status(statusCode).json({
error: errorMessage,
});
};
export const asyncHandler = (fn: any) => {
return (req: Request, res: Response, next: NextFunction) => {
return Promise.resolve(fn(req, res, next)).catch(next);
};
};
export default errorHandler;

View file

@ -0,0 +1,59 @@
import { Request, Response, NextFunction } from "express";
import { format } from "date-fns";
import { v4 as uuid } from "uuid";
import * as fs from "fs";
import { promises as fsPromises } from "fs";
import path from "path";
export const logEvents = async (message: string, logName: string) => {
const dateTime = format(new Date(), "yyyy-MM-dd HH:mm:ss");
const logItem = `${dateTime}\t${uuid()}\t${message}\n`;
try {
if (!fs.existsSync(path.join(__dirname, "..", "logs"))) {
await fsPromises.mkdir(path.join(__dirname, "..", "logs"));
}
await fsPromises.appendFile(
path.join(__dirname, "..", "logs", logName),
logItem
);
} catch (error) {
console.error(error);
}
};
export const log = async (
topic: string,
message: string,
userId?: number,
logName: string = "logs.txt"
) => {
const dateTime = format(new Date(), "yyyy-MM-dd HH:mm:ss");
let logItem = `[${dateTime}]: `;
logItem += `${topic}: ${message} `;
logItem += userId ? `[User: ${userId}] ` : "";
console.log(logItem);
logItem += "\n";
try {
if (!fs.existsSync(path.join(__dirname, "..", "logs"))) {
await fsPromises.mkdir(path.join(__dirname, "..", "logs"));
}
await fsPromises.appendFile(
path.join(__dirname, "..", "logs", logName),
logItem
);
} catch (error) {
console.error(error);
}
};
export const logger = (req: Request, res: Response, next: NextFunction) => {
logEvents(`${req.method}\t${req.headers.origin}\t${req.url}`, "reqLog.txt");
console.log(`${req.method} ${req.path}`);
next();
};
export default logger;

9
src/routes/root.ts Normal file
View file

@ -0,0 +1,9 @@
import { Router, Request, Response } from "express";
const rootRouter = Router();
rootRouter.get("/", (req: Request, res: Response) => {
res.send("Skyjo API");
});
export default rootRouter;

69
src/server.ts Normal file
View file

@ -0,0 +1,69 @@
// thanks to https://github.com/Apollon77/meross-cloud for the Meross Cloud API
import express, { Request, Response } from "express";
import { Server } from "http";
import { Server as SocketIOServer, Socket } from "socket.io";
import app from "./lib/app";
import * as path from "path";
import logger, { log } from "./middleware/logEvents";
import errorHandler from "./middleware/errorHandler";
import credentials from "./middleware/credentials";
import cors from "cors";
import corsOptions from "./config/corsOptions";
import cookieParser from "cookie-parser";
import rootRouter from "./routes/root";
import { handleJoinSession, handleNewGame } from "./game/events";
const httpServer = new Server(app);
export const io = new SocketIOServer(httpServer, {
cors: {
origin: "http://localhost:5173",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket: Socket) => {
console.log("A user connected:", socket.id);
socket.on("join-session", (sessionId: string) =>
handleJoinSession(socket, sessionId)
);
socket.on("new-game", (gameDetails: { sessionId: string }) =>
handleNewGame(socket, gameDetails)
);
socket.on("disconnect", () => {
console.log("A user disconnected:", socket.id);
});
});
// helps to debug reading envs
const environment = process.env.ENVIRONMENT ?? "can not read envs";
const PORT = process.env.PORT || 3001;
app.use(logger);
app.use(credentials);
app.use(cors(corsOptions));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "..", "public")));
app.use(cookieParser());
app.use("/", rootRouter);
app.all("*", (req: Request, res: Response) => {
res.status(404).send("Not Found");
});
app.use(errorHandler);
httpServer.listen(PORT, () => {
log("ExpressJS", `Server listening on ${PORT} - Environment: ${environment}`);
});

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}