Compare commits

...

10 commits

12 changed files with 1982 additions and 447 deletions

View file

@ -1,2 +1,3 @@
node_modules
dist
coverage

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
node_modules
dist
coverage

View file

@ -4,10 +4,13 @@ EXPOSE 9800
WORKDIR /home/node/app
COPY . .
COPY ./package.json ./
RUN npm install -g pnpm
RUN pnpm install
COPY . .
RUN pnpm run build
CMD ["node", "dist/index.js"]

BIN
assets/extracted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -8,12 +8,16 @@
"start": "node dist/index.js",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
"buildDocker": "docker build -t nuark/huacu_server:latest .",
"pushDocker": "docker push nuark/huacu_server:latest"
"pushDocker": "docker push nuark/huacu_server:latest",
"test": "vitest",
"coverage": "vitest run --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@directus/sdk": "^10.3.1",
"@noble/hashes": "^1.2.0",
"@socket.io/admin-ui": "^0.5.1",
"dotenv": "^16.0.3",
"express": "^4.18.2",
@ -24,8 +28,10 @@
"@types/express": "^4.17.17",
"@types/node": "^18.14.4",
"@types/uuid": "^9.0.1",
"@vitest/coverage-c8": "^0.29.2",
"concurrently": "^7.6.0",
"nodemon": "^2.0.21",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"vitest": "^0.29.2"
}
}

1014
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

116
src/directus_db.ts Normal file
View file

@ -0,0 +1,116 @@
import { Directus } from "@directus/sdk";
import { blake3 } from "@noble/hashes/blake3";
export type HuacuUser = {
id: string;
login: string;
password: string;
};
export type HuacuStat = {
id: string;
dateCreated: string;
dateUpdated: string;
belongs_to: string;
games_won: number;
games_lost: number;
};
export type HuacuCollection = {
huacu_users: HuacuUser;
huacu_stats: HuacuStat;
};
function hexify(data: Uint8Array): string {
return Buffer.from(data).toString("hex");
}
export class DirectusDB {
private directus: Directus<HuacuCollection>;
constructor() {
const staticAccessToken = "dDAa9RSZKLuKESwMTfTWUQZcEQWqsKJn";
this.directus = new Directus<HuacuCollection>("https://directus.nuark.xyz", {
auth: {
staticToken: staticAccessToken,
},
});
}
async registerUser(login: string, password: string) {
const hash = hexify(blake3(password));
const user = {
login: login,
password: hash,
};
try {
const newUser = await this.directus.items("huacu_users").createOne(user);
// Now we need to create a stats entry for this user
const stats = {
belongs_to: newUser!.id,
};
await this.directus.items("huacu_stats").createOne(stats);
return newUser;
} catch (e) {
return null;
}
}
async authenticateUser(login: string, password: string): Promise<HuacuUser | null> {
const hash = hexify(blake3(password));
const response = await this.directus.items("huacu_users").readByQuery({
filter: {
login: {
_eq: login,
},
password: {
_eq: hash,
},
},
});
if (response.data?.length === 1) {
return response.data[0];
}
return null;
}
async removeUser(login: string, password: string) {
const user = await this.authenticateUser(login, password);
if (user === null) {
return false;
}
await this.directus.items("huacu_users").deleteOne(user.id);
return true;
}
async getStats(userId: string) {
try {
const response = await this.directus.items("huacu_stats").readByQuery({
filter: {
belongs_to: {
_eq: userId,
},
},
});
return response!.data![0];
} catch (e) {
// Actually, this should never happen in real life
// because we create a stats entry for every user
return null;
}
}
async updateStats(userId: string, incrementWin: boolean, incrementLost: boolean) {
try {
const stats = (await this.getStats(userId))!;
await this.directus.items("huacu_stats").updateOne(stats.id, {
games_won: stats.games_won + (incrementWin ? 1 : 0),
games_lost: stats.games_lost + (incrementLost ? 1 : 0),
});
return true;
} catch (e) {
return false;
}
}
}

View file

@ -1,11 +1,16 @@
import express, { Express, Request, Response } from "express";
import express from "express";
import http from "http";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { Server, Socket } from "socket.io";
import { instrument } from "@socket.io/admin-ui";
import { Client, AvailableGame, Game } from "./models";
import { pallette, availableColors } from "./pallette";
import { DirectusDB } from "./directus_db";
const db = new DirectusDB();
const app = express();
const server = http.createServer(app);
@ -17,166 +22,7 @@ const io = new Server(server, {
},
});
class Client {
private _login: string;
private _password: string;
private _inGame: Boolean;
public get login() {
return this._login;
}
public get password() {
return this._password;
}
public get inGame() {
return this._inGame;
}
constructor(login: string, password: string) {
this._login = login;
this._password = password;
this._inGame = false;
}
public setInGame(inGame: Boolean) {
this._inGame = inGame;
}
public simplify() {
return {
login: this.login,
inGame: this.inGame,
};
}
}
class Game {
private id: string;
private guesser: Client;
private suggester: Client;
private tries: number;
private guesses: Set<string>;
private colors: Set<string>;
public get Id() {
return this.id;
}
public get Guesser() {
return this.guesser;
}
public get Suggester() {
return this.suggester;
}
public get Tries() {
return this.tries;
}
public get Guesses() {
return this.guesses;
}
public get Colors() {
return this.colors;
}
constructor(id: string, guesser: Client, suggester: Client, tries: number) {
this.id = id;
this.guesser = guesser;
this.suggester = suggester;
this.tries = tries;
this.guesses = new Set<string>();
this.colors = new Set<string>();
while (this.colors.size < 6) {
const key = availableColors[Math.floor(Math.random() * availableColors.length)];
this.colors.add(key);
}
}
public Opponent(player: Client): Client {
if (this.guesser === player) {
return this.suggester;
}
return this.guesser;
}
public Guess(player: Client, guess: string) {
if (this.guesses.has(guess)) {
return true;
}
this.guesses.add(guess);
const opponent = this.Opponent(player);
const opponentSocket = Array.from(onlineClients.keys()).find((socket) => onlineClients.get(socket) === opponent);
if (opponentSocket) {
opponentSocket.emit("guess", guess);
} else {
console.log("opponent socket not found");
return false;
}
return true;
}
public GameWon() {
for (const color of this.colors) {
if (!this.guesses.has(color)) {
return false;
}
}
return true;
}
public GameLost() {
const countOfGuesses = this.guesses.size;
const countOfCorrectGuesses = Array.from(this.guesses).filter((guess) => this.colors.has(guess)).length;
return countOfGuesses - countOfCorrectGuesses >= this.tries;
}
public simplify() {
return {
id: this.id,
guesser: this.guesser.login,
suggester: this.suggester.login,
tries: this.tries,
};
}
}
class AvailableGame {
private _id: string;
private _player: Client;
private _tries: number;
private _neededRole: string;
public get id(): string {
return this._id;
}
public get player(): Client {
return this._player;
}
public get tries(): number {
return this._tries;
}
public get neededRole(): string {
return this._neededRole;
}
constructor(id: string, player: Client, tries: number, neededRole: string) {
this._id = id;
this._player = player;
this._tries = tries;
this._neededRole = neededRole;
}
simplify() {
return {
id: this.id,
player: this.player.login,
tries: this.tries,
neededRole: this.neededRole,
};
}
}
let registeredClients: Client[] = [];
let cachedClients: Client[] = [];
let onlineClients: Map<Socket, Client> = new Map<Socket, Client>();
let availableGames: Map<string, AvailableGame> = new Map<string, AvailableGame>();
let runningGames: Map<string, Game> = new Map<string, Game>();
@ -185,52 +31,56 @@ app.get("/", (req, res) => {
res.send("<h1>Hello world</h1>");
});
app.get("/asyncapi", (req, res) => {
res.sendFile(path.resolve(__dirname + "/../assets/asyncapi.html"));
});
app.get("/extracted", (req, res) => {
res.sendFile(path.resolve(__dirname + "/../assets/extracted.png"));
});
app.get("/pallette", (req, res) => {
res.json(pallette);
res.json({
availableColors,
pallette,
});
});
io.on("connection", (socket) => {
console.log("someone connected");
socket.emit("hello", "I don't know you");
socket.on("register", (login, password) => {
console.log("user send register with login: " + login + " and password: " + password);
socket.on("register", async (login, password) => {
if (login.length < 3 || login.length > 20 || password.length < 3 || password.length > 20) {
socket.emit("register", false, "Login or password is too short or too long");
console.log("user tried to register with too short or too long login or password");
return;
}
if (registeredClients.find((client) => client.login === login)) {
const newUser = await db.registerUser(login, password);
if (newUser === null) {
socket.emit("register", false, "User with this login already exists");
console.log("user tried to register with existing login");
return;
}
registeredClients.push(new Client(login, password));
onlineClients.set(socket, new Client(login, password));
const client = new Client(newUser!.id, login, socket);
cachedClients.push(client);
onlineClients.set(socket, client);
socket.emit("register", true, "User registered successfully");
console.log("user registered successfully");
});
socket.on("login", (login, password) => {
console.log("user send login with login: " + login + " and password: " + password);
socket.on("login", async (login, password) => {
if (login.length < 3 || login.length > 20 || password.length < 3 || password.length > 20) {
socket.emit("login", false, "Login or password is too short or too long");
console.log("user tried to login with too short or too long login or password");
return;
}
if (!registeredClients.find((client) => client.login === login)) {
socket.emit("login", false, "User with this login does not exist");
console.log("user tried to login with non existing login");
const user = await db.authenticateUser(login, password);
if (user === null) {
socket.emit("login", false, "Wrong login or password");
return;
}
if (registeredClients.find((client) => client.login === login && client.password !== password)) {
socket.emit("login", false, "Wrong password");
console.log("user tried to login with wrong password");
return;
}
onlineClients.set(socket, new Client(login, password));
socket.emit("login", true, "User logged in successfully");
onlineClients.set(socket, new Client(user!.id, login, socket));
socket.emit("login", true, "User logged in successfully", user!.id);
registerSocketLoggedInFunctions(socket);
@ -240,9 +90,8 @@ io.on("connection", (socket) => {
socket.on("disconnect", () => {
const client = onlineClients.get(socket);
if (client !== undefined) {
console.log("user " + client.login + " disconnected");
for (const game of Array.from(availableGames.values())) {
if (game.player === client) {
if (game.player.id === client.id) {
availableGames.delete(game.id);
return;
}
@ -251,10 +100,8 @@ io.on("connection", (socket) => {
const game = Array.from(runningGames.values()).find((game) => game.Guesser === client || game.Suggester === client);
if (game !== undefined) {
const opponentPlayer = game.Opponent(client);
const otherClientSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === opponentPlayer.login);
if (otherClientSocket !== undefined) {
otherClientSocket.emit("leaveGameResponse", true, 410);
}
opponentPlayer.socket.emit("leaveGameResponse", true, 410);
client.setInGame(false);
opponentPlayer.setInGame(false);
@ -262,8 +109,9 @@ io.on("connection", (socket) => {
}
}
onlineClients.delete(socket);
} else {
console.log("anonymous disconnected");
// now need to remove client from cachedClients
cachedClients = cachedClients.filter((e) => e.id !== client.id);
}
io.emit("updateNeeded");
@ -279,11 +127,8 @@ instrument(io, {
},
});
server.listen(9800, () => {
console.log("⚡️ listening on *:9800");
});
function registerSocketLoggedInFunctions(socket: Socket) {
socket.on("logout", logoutEventHandler);
socket.on("getUpdate", getUpdateEventHandler);
socket.on("createGame", createGameEventHandler);
socket.on("joinGame", joinGameEventHandler);
@ -291,9 +136,16 @@ function registerSocketLoggedInFunctions(socket: Socket) {
socket.on("chat", chatEventHandler);
socket.on("guess", guessEventHandler);
socket.on("leaveGame", leaveGameEventHandler);
socket.on("deleteAccount", deleteAccountEventHandler);
socket.on("getUserData", getUserDataEventHandler);
const client = onlineClients.get(socket)!;
function logoutEventHandler(): void {
socket.emit("hello");
socket.disconnect(true);
}
function getUpdateEventHandler(): void {
socket.emit("update", true, {
availableGames: Array.from(availableGames.values()).map((e) => e.simplify()),
@ -345,12 +197,6 @@ function registerSocketLoggedInFunctions(socket: Socket) {
return;
}
const gameSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === game.player.login);
if (gameSocket === undefined) {
socket.emit("joinGameResponse", false, "Player is not online");
return;
}
availableGames.delete(gameId);
io.emit("updateNeeded");
@ -366,10 +212,8 @@ function registerSocketLoggedInFunctions(socket: Socket) {
suggester.setInGame(true);
guesser.setInGame(true);
const gameInfo = new Game(uuidv4(), guesser, suggester, game.tries);
const suggesterSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === suggester.login)!;
const guesserSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === guesser.login)!;
suggesterSocket.emit("joinGameResponse", true, gameInfo.simplify(), Array.from(gameInfo.Colors));
guesserSocket.emit("joinGameResponse", true, gameInfo.simplify(), []);
suggester.socket.emit("joinGameResponse", true, gameInfo.simplify(), Array.from(gameInfo.Colors));
guesser.socket.emit("joinGameResponse", true, gameInfo.simplify(), []);
runningGames.set(gameInfo.Id, gameInfo);
}
@ -393,25 +237,15 @@ function registerSocketLoggedInFunctions(socket: Socket) {
const game = runningGames.get(gameId);
if (game === undefined || !client.inGame) {
socket.emit("chatResponse", false, 400);
console.log("user " + client.login + " tried to chat in non existing game");
return;
}
const opponentPlayer = game.Opponent(client);
const otherClientSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === opponentPlayer.login);
if (otherClientSocket === undefined) {
socket.emit("chatResponse", false, 404);
console.log("user " + client.login + " tried to chat with non existing user");
return;
}
socket.emit("chatResponse", true, {
from: client.login,
message,
});
otherClientSocket.emit("chatResponse", true, {
opponentPlayer.socket.emit("chatResponse", true, {
from: client.login,
message,
});
@ -421,47 +255,46 @@ function registerSocketLoggedInFunctions(socket: Socket) {
const game = runningGames.get(gameId);
if (game === undefined || !client.inGame) {
socket.emit("guessResponse", false, 400);
console.log("user " + client.login + " tried to guess in non existing game");
return;
}
if (game.Guesser.login !== client.login) {
socket.emit("guessResponse", false, 400);
console.log("user " + client.login + " tried to guess in game he is not guessing in");
return;
}
if (!game.Guess(client, guess)) {
socket.emit("guessResponse", false, 400);
console.log("user " + client.login + " tried to guess but something gone terribly wrong");
return;
}
const opponentPlayer = game.Opponent(client);
const otherClientSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === opponentPlayer.login)!;
if (game.GameWon()) {
console.log(`game ${gameId} won by ${client.login} and ${opponentPlayer.login}`);
socket.emit("gameStatus", [true]);
otherClientSocket.emit("gameStatus", [true]);
opponentPlayer.socket.emit("gameStatus", [true]);
client.setInGame(false);
opponentPlayer.setInGame(false);
runningGames.delete(gameId);
db.updateStats(client.id, true, false);
db.updateStats(opponentPlayer.id, true, false);
return;
}
if (game.GameLost()) {
console.log(`game ${gameId} lost by ${client.login} and ${opponentPlayer.login}`);
socket.emit("gameStatus", [false]);
otherClientSocket.emit("gameStatus", [false]);
opponentPlayer.socket.emit("gameStatus", [false]);
client.setInGame(false);
opponentPlayer.setInGame(false);
runningGames.delete(gameId);
db.updateStats(client.id, false, true);
db.updateStats(opponentPlayer.id, false, true);
}
}
@ -469,20 +302,59 @@ function registerSocketLoggedInFunctions(socket: Socket) {
const game = runningGames.get(gameId);
if (game === undefined || !client.inGame) {
socket.emit("guessResponse", false, 400);
console.log("user " + client.login + " tried to guess in non existing game");
return;
}
socket.emit("leaveGameResponse", true, 200);
const opponentPlayer = game.Opponent(client);
const otherClientSocket = Array.from(onlineClients.keys()).find((key) => onlineClients.get(key)?.login === opponentPlayer.login);
if (otherClientSocket !== undefined) {
otherClientSocket.emit("leaveGameResponse", true, 410);
}
opponentPlayer.socket.emit("leaveGameResponse", true, 410);
socket.emit("leaveGameResponse", true, 200);
client.setInGame(false);
opponentPlayer.setInGame(false);
runningGames.delete(game.Id);
}
async function deleteAccountEventHandler(password: string): Promise<void> {
try {
const deletionResult = await db.removeUser(client.login, password);
if (deletionResult) {
socket.emit("deleteAccountResponse", [true]);
socket.emit("hello");
socket.disconnect(true);
} else {
socket.emit("deleteAccountResponse", [false, "Wrong password or something went wrong"]);
}
} catch (e) {
if (e instanceof Error) {
socket.emit("deleteAccountResponse", false, e.message);
} else {
socket.emit("deleteAccountResponse", false, "Unknown error");
// TODO: log error
}
}
}
async function getUserDataEventHandler(userId: string | undefined): Promise<void> {
try {
const stats = await db.getStats(userId ?? client.id);
if (stats === undefined) {
socket.emit("getUserDataResponse", false, "User does not exist");
return;
}
socket.emit("getUserDataResponse", true, {
[userId ?? "client"]: { wins: stats!.games_won, losses: stats!.games_lost },
});
} catch (e) {
if (e instanceof Error) {
socket.emit("getUserDataResponse", false, e.message);
} else {
socket.emit("getUserDataResponse", false, "Unknown error");
}
}
}
}
server.listen(9800, () => {
console.log("⚡ Listening on *:9800");
});

160
src/models.ts Normal file
View file

@ -0,0 +1,160 @@
import { Socket } from "socket.io";
import { availableColors } from "./pallette";
export class Client {
private _id: string;
private _login: string;
private _inGame: Boolean;
private _socket: Socket;
public get id() {
return this._id;
}
public get login() {
return this._login;
}
public get inGame() {
return this._inGame;
}
public get socket() {
return this._socket;
}
constructor(id: string, login: string, socket: Socket) {
this._id = id;
this._login = login;
this._inGame = false;
this._socket = socket;
}
public setInGame(inGame: Boolean) {
this._inGame = inGame;
}
public simplify() {
return {
id: this.id,
login: this.login,
inGame: this.inGame,
};
}
}
export class Game {
private id: string;
private guesser: Client;
private suggester: Client;
private tries: number;
private guesses: Set<string>;
private colors: Set<string>;
public get Id() {
return this.id;
}
public get Guesser() {
return this.guesser;
}
public get Suggester() {
return this.suggester;
}
public get Tries() {
return this.tries;
}
public get Guesses() {
return this.guesses;
}
public get Colors() {
return this.colors;
}
constructor(id: string, guesser: Client, suggester: Client, tries: number) {
this.id = id;
this.guesser = guesser;
this.suggester = suggester;
this.tries = tries;
this.guesses = new Set<string>();
this.colors = new Set<string>();
while (this.colors.size < 6) {
const key = availableColors[Math.floor(Math.random() * availableColors.length)];
this.colors.add(key);
}
}
public Opponent(player: Client): Client {
if (this.guesser === player) {
return this.suggester;
}
return this.guesser;
}
public Guess(player: Client, guess: string) {
if (this.guesses.has(guess)) {
return true;
}
this.guesses.add(guess);
this.Opponent(player).socket.emit("guess", guess);
return true;
}
public GameWon() {
for (const color of this.colors) {
if (!this.guesses.has(color)) {
return false;
}
}
return true;
}
public GameLost() {
const countOfGuesses = this.guesses.size;
const countOfCorrectGuesses = Array.from(this.guesses).filter((guess) => this.colors.has(guess)).length;
return countOfGuesses - countOfCorrectGuesses >= this.tries;
}
public simplify() {
return {
id: this.id,
guesser: this.guesser.login,
suggester: this.suggester.login,
tries: this.tries,
};
}
}
export class AvailableGame {
private _id: string;
private _player: Client;
private _tries: number;
private _neededRole: string;
public get id(): string {
return this._id;
}
public get player(): Client {
return this._player;
}
public get tries(): number {
return this._tries;
}
public get neededRole(): string {
return this._neededRole;
}
constructor(id: string, player: Client, tries: number, neededRole: string) {
this._id = id;
this._player = player;
this._tries = tries;
this._neededRole = neededRole;
}
simplify() {
return {
id: this.id,
player: this.player.login,
tries: this.tries,
neededRole: this.neededRole,
};
}
}

View file

@ -1,204 +1,484 @@
export const pallette = {
A0: [0, 0, 186],
A1: [0, 26, 186],
A2: [0, 51, 186],
A3: [0, 77, 186],
A4: [0, 102, 186],
A5: [0, 128, 186],
A6: [0, 153, 186],
A7: [0, 179, 186],
A8: [0, 204, 186],
A9: [0, 230, 186],
B0: [13, 0, 186],
B1: [13, 26, 186],
B2: [13, 51, 186],
B3: [13, 77, 186],
B4: [13, 102, 186],
B5: [13, 128, 186],
B6: [13, 153, 186],
B7: [13, 179, 186],
B8: [13, 204, 186],
B9: [13, 230, 186],
C0: [26, 0, 186],
C1: [26, 26, 186],
C2: [26, 51, 186],
C3: [26, 77, 186],
C4: [26, 102, 186],
C5: [26, 128, 186],
C6: [26, 153, 186],
C7: [26, 179, 186],
C8: [26, 204, 186],
C9: [26, 230, 186],
D0: [38, 0, 186],
D1: [38, 26, 186],
D2: [38, 51, 186],
D3: [38, 77, 186],
D4: [38, 102, 186],
D5: [38, 128, 186],
D6: [38, 153, 186],
D7: [38, 179, 186],
D8: [38, 204, 186],
D9: [38, 230, 186],
E0: [51, 0, 186],
E1: [51, 26, 186],
E2: [51, 51, 186],
E3: [51, 77, 186],
E4: [51, 102, 186],
E5: [51, 128, 186],
E6: [51, 153, 186],
E7: [51, 179, 186],
E8: [51, 204, 186],
E9: [51, 230, 186],
F0: [64, 0, 186],
F1: [64, 26, 186],
F2: [64, 51, 186],
F3: [64, 77, 186],
F4: [64, 102, 186],
F5: [64, 128, 186],
F6: [64, 153, 186],
F7: [64, 179, 186],
F8: [64, 204, 186],
F9: [64, 230, 186],
G0: [77, 0, 186],
G1: [77, 26, 186],
G2: [77, 51, 186],
G3: [77, 77, 186],
G4: [77, 102, 186],
G5: [77, 128, 186],
G6: [77, 153, 186],
G7: [77, 179, 186],
G8: [77, 204, 186],
G9: [77, 230, 186],
H0: [89, 0, 186],
H1: [89, 26, 186],
H2: [89, 51, 186],
H3: [89, 77, 186],
H4: [89, 102, 186],
H5: [89, 128, 186],
H6: [89, 153, 186],
H7: [89, 179, 186],
H8: [89, 204, 186],
H9: [89, 230, 186],
I0: [102, 0, 186],
I1: [102, 26, 186],
I2: [102, 51, 186],
I3: [102, 77, 186],
I4: [102, 102, 186],
I5: [102, 128, 186],
I6: [102, 153, 186],
I7: [102, 179, 186],
I8: [102, 204, 186],
I9: [102, 230, 186],
J0: [115, 0, 186],
J1: [115, 26, 186],
J2: [115, 51, 186],
J3: [115, 77, 186],
J4: [115, 102, 186],
J5: [115, 128, 186],
J6: [115, 153, 186],
J7: [115, 179, 186],
J8: [115, 204, 186],
J9: [115, 230, 186],
K0: [128, 0, 186],
K1: [128, 26, 186],
K2: [128, 51, 186],
K3: [128, 77, 186],
K4: [128, 102, 186],
K5: [128, 128, 186],
K6: [128, 153, 186],
K7: [128, 179, 186],
K8: [128, 204, 186],
K9: [128, 230, 186],
L0: [140, 0, 186],
L1: [140, 26, 186],
L2: [140, 51, 186],
L3: [140, 77, 186],
L4: [140, 102, 186],
L5: [140, 128, 186],
L6: [140, 153, 186],
L7: [140, 179, 186],
L8: [140, 204, 186],
L9: [140, 230, 186],
M0: [153, 0, 186],
M1: [153, 26, 186],
M2: [153, 51, 186],
M3: [153, 77, 186],
M4: [153, 102, 186],
M5: [153, 128, 186],
M6: [153, 153, 186],
M7: [153, 179, 186],
M8: [153, 204, 186],
M9: [153, 230, 186],
N0: [166, 0, 186],
N1: [166, 26, 186],
N2: [166, 51, 186],
N3: [166, 77, 186],
N4: [166, 102, 186],
N5: [166, 128, 186],
N6: [166, 153, 186],
N7: [166, 179, 186],
N8: [166, 204, 186],
N9: [166, 230, 186],
O0: [179, 0, 186],
O1: [179, 26, 186],
O2: [179, 51, 186],
O3: [179, 77, 186],
O4: [179, 102, 186],
O5: [179, 128, 186],
O6: [179, 153, 186],
O7: [179, 179, 186],
O8: [179, 204, 186],
O9: [179, 230, 186],
P0: [191, 0, 186],
P1: [191, 26, 186],
P2: [191, 51, 186],
P3: [191, 77, 186],
P4: [191, 102, 186],
P5: [191, 128, 186],
P6: [191, 153, 186],
P7: [191, 179, 186],
P8: [191, 204, 186],
P9: [191, 230, 186],
Q0: [204, 0, 186],
Q1: [204, 26, 186],
Q2: [204, 51, 186],
Q3: [204, 77, 186],
Q4: [204, 102, 186],
Q5: [204, 128, 186],
Q6: [204, 153, 186],
Q7: [204, 179, 186],
Q8: [204, 204, 186],
Q9: [204, 230, 186],
R0: [217, 0, 186],
R1: [217, 26, 186],
R2: [217, 51, 186],
R3: [217, 77, 186],
R4: [217, 102, 186],
R5: [217, 128, 186],
R6: [217, 153, 186],
R7: [217, 179, 186],
R8: [217, 204, 186],
R9: [217, 230, 186],
S0: [230, 0, 186],
S1: [230, 26, 186],
S2: [230, 51, 186],
S3: [230, 77, 186],
S4: [230, 102, 186],
S5: [230, 128, 186],
S6: [230, 153, 186],
S7: [230, 179, 186],
S8: [230, 204, 186],
S9: [230, 230, 186],
T0: [242, 0, 186],
T1: [242, 26, 186],
T2: [242, 51, 186],
T3: [242, 77, 186],
T4: [242, 102, 186],
T5: [242, 128, 186],
T6: [242, 153, 186],
T7: [242, 179, 186],
T8: [242, 204, 186],
T9: [242, 230, 186],
A1: [107, 38, 5],
A2: [117, 33, 5],
A3: [128, 26, 12],
A4: [148, 13, 17],
A5: [162, 3, 21],
A6: [174, 0, 25],
A7: [190, 0, 26],
A8: [217, 1, 25],
A9: [246, 1, 18],
A10: [255, 0, 13],
A11: [255, 0, 6],
A12: [255, 0, 8],
A13: [255, 0, 30],
A14: [255, 0, 48],
A15: [255, 1, 65],
A16: [255, 0, 80],
A17: [250, 0, 98],
A18: [247, 0, 117],
A19: [236, 0, 136],
A20: [230, 1, 146],
A21: [218, 0, 146],
A22: [203, 19, 149],
A23: [198, 35, 152],
A24: [183, 39, 153],
A25: [173, 46, 153],
A26: [158, 49, 154],
A27: [151, 56, 156],
A28: [142, 57, 158],
A29: [135, 59, 159],
A30: [124, 62, 161],
B1: [146, 71, 14],
B2: [161, 63, 16],
B3: [171, 55, 16],
B4: [185, 52, 21],
B5: [199, 43, 21],
B6: [216, 12, 24],
B7: [237, 2, 19],
B8: [250, 0, 14],
B9: [255, 0, 8],
B10: [255, 0, 24],
B11: [255, 0, 34],
B12: [254, 0, 52],
B13: [255, 1, 55],
B14: [255, 0, 76],
B15: [255, 0, 94],
B16: [255, 0, 110],
B17: [255, 1, 126],
B18: [255, 0, 144],
B19: [240, 1, 147],
B20: [226, 35, 151],
B21: [207, 49, 159],
B22: [198, 48, 158],
B23: [180, 48, 157],
B24: [170, 54, 155],
B25: [165, 54, 156],
B26: [152, 55, 152],
B27: [140, 58, 158],
B28: [137, 59, 160],
B29: [120, 58, 157],
B30: [113, 57, 158],
C1: [176, 94, 21],
C2: [184, 85, 18],
C3: [200, 84, 11],
C4: [208, 69, 14],
C5: [223, 57, 17],
C6: [235, 45, 19],
C7: [249, 25, 13],
C8: [254, 0, 3],
C9: [255, 7, 23],
C10: [255, 26, 55],
C11: [255, 35, 70],
C12: [255, 25, 74],
C13: [255, 21, 90],
C14: [255, 0, 104],
C15: [255, 0, 120],
C16: [255, 1, 136],
C17: [255, 0, 154],
C18: [239, 47, 156],
C19: [220, 60, 158],
C20: [200, 66, 161],
C21: [195, 64, 160],
C22: [179, 64, 157],
C23: [172, 62, 157],
C24: [161, 60, 156],
C25: [152, 62, 158],
C26: [142, 57, 160],
C27: [132, 57, 158],
C28: [124, 56, 157],
C29: [110, 55, 156],
C30: [97, 46, 149],
D1: [213, 128, 1],
D2: [230, 125, 0],
D3: [238, 112, 1],
D4: [248, 114, 3],
D5: [249, 94, 1],
D6: [253, 81, 0],
D7: [255, 83, 0],
D8: [255, 70, 29],
D9: [254, 61, 44],
D10: [255, 71, 73],
D11: [255, 79, 95],
D12: [255, 81, 104],
D13: [255, 73, 111],
D14: [255, 66, 124],
D15: [255, 60, 142],
D16: [255, 59, 157],
D17: [241, 73, 161],
D18: [223, 76, 165],
D19: [205, 88, 169],
D20: [195, 81, 168],
D21: [177, 79, 166],
D22: [167, 72, 162],
D23: [160, 71, 165],
D24: [153, 69, 163],
D25: [140, 65, 160],
D26: [132, 62, 160],
D27: [119, 59, 159],
D28: [110, 54, 155],
D29: [95, 50, 153],
D30: [73, 46, 143],
E1: [247, 152, 0],
E2: [254, 148, 0],
E3: [255, 139, 0],
E4: [255, 133, 3],
E5: [255, 125, 1],
E6: [255, 110, 1],
E7: [254, 110, 39],
E8: [255, 100, 52],
E9: [255, 100, 69],
E10: [254, 111, 97],
E11: [255, 109, 112],
E12: [255, 110, 125],
E13: [254, 109, 138],
E14: [254, 102, 153],
E15: [255, 93, 168],
E16: [249, 103, 176],
E17: [226, 107, 175],
E18: [211, 108, 177],
E19: [200, 109, 178],
E20: [183, 99, 174],
E21: [168, 91, 171],
E22: [157, 85, 171],
E23: [148, 82, 169],
E24: [141, 79, 165],
E25: [130, 69, 162],
E26: [118, 68, 163],
E27: [105, 61, 158],
E28: [95, 50, 151],
E29: [79, 42, 156],
E30: [54, 43, 135],
F1: [254, 177, 1],
F2: [255, 173, 1],
F3: [255, 166, 0],
F4: [251, 171, 0],
F5: [254, 160, 24],
F6: [255, 159, 49],
F7: [254, 146, 71],
F8: [255, 136, 80],
F9: [255, 130, 90],
F10: [255, 129, 110],
F11: [254, 136, 122],
F12: [254, 138, 139],
F13: [254, 135, 154],
F14: [255, 134, 165],
F15: [255, 129, 182],
F16: [239, 136, 189],
F17: [221, 136, 193],
F18: [208, 137, 193],
F19: [196, 128, 189],
F20: [180, 119, 186],
F21: [165, 115, 186],
F22: [152, 101, 178],
F23: [138, 93, 174],
F24: [130, 88, 172],
F25: [119, 79, 168],
F26: [105, 75, 165],
F27: [95, 66, 158],
F28: [79, 51, 154],
F29: [61, 46, 149],
F30: [46, 41, 125],
G1: [254, 189, 0],
G2: [254, 189, 1],
G3: [255, 180, 0],
G4: [250, 180, 0],
G5: [255, 183, 75],
G6: [255, 168, 63],
G7: [254, 167, 87],
G8: [254, 158, 98],
G9: [255, 157, 110],
G10: [255, 156, 127],
G11: [255, 154, 146],
G12: [255, 153, 151],
G13: [255, 153, 166],
G14: [255, 150, 170],
G15: [255, 152, 199],
G16: [237, 162, 203],
G17: [218, 165, 207],
G18: [211, 172, 216],
G19: [190, 149, 205],
G20: [174, 138, 199],
G21: [159, 130, 194],
G22: [141, 118, 186],
G23: [129, 109, 180],
G24: [118, 101, 179],
G25: [105, 94, 173],
G26: [89, 85, 170],
G27: [81, 75, 163],
G28: [70, 66, 163],
G29: [50, 55, 155],
G30: [42, 47, 139],
H1: [255, 205, 2],
H2: [255, 204, 0],
H3: [255, 197, 1],
H4: [255, 196, 4],
H5: [255, 196, 32],
H6: [255, 188, 63],
H7: [253, 186, 73],
H8: [255, 182, 93],
H9: [255, 175, 112],
H10: [255, 171, 122],
H11: [255, 171, 137],
H12: [255, 176, 157],
H13: [254, 180, 171],
H14: [252, 181, 179],
H15: [247, 179, 200],
H16: [223, 181, 217],
H17: [210, 183, 218],
H18: [206, 189, 221],
H19: [183, 170, 214],
H20: [165, 162, 207],
H21: [154, 149, 205],
H22: [131, 137, 199],
H23: [116, 126, 195],
H24: [104, 118, 191],
H25: [90, 108, 180],
H26: [77, 100, 180],
H27: [68, 90, 175],
H28: [59, 80, 169],
H29: [40, 70, 160],
H30: [30, 60, 158],
I1: [254, 225, 1],
I2: [254, 220, 0],
I3: [255, 216, 1],
I4: [252, 216, 0],
I5: [255, 222, 5],
I6: [254, 214, 54],
I7: [254, 216, 69],
I8: [255, 212, 82],
I9: [251, 219, 108],
I10: [241, 226, 133],
I11: [239, 221, 149],
I12: [233, 226, 174],
I13: [227, 224, 193],
I14: [220, 225, 202],
I15: [208, 226, 210],
I16: [192, 223, 228],
I17: [188, 218, 244],
I18: [187, 217, 243],
I19: [158, 203, 236],
I20: [144, 187, 229],
I21: [130, 172, 222],
I22: [112, 162, 221],
I23: [105, 146, 210],
I24: [94, 135, 201],
I25: [76, 123, 191],
I26: [68, 110, 186],
I27: [57, 99, 181],
I28: [48, 93, 178],
I29: [43, 80, 169],
I30: [31, 72, 164],
J1: [252, 236, 1],
J2: [253, 235, 1],
J3: [254, 234, 0],
J4: [255, 236, 0],
J5: [252, 239, 0],
J6: [251, 243, 48],
J7: [250, 240, 80],
J8: [240, 239, 112],
J9: [230, 236, 126],
J10: [221, 233, 147],
J11: [213, 232, 166],
J12: [207, 233, 186],
J13: [206, 233, 202],
J14: [199, 235, 209],
J15: [193, 232, 214],
J16: [176, 232, 231],
J17: [164, 230, 244],
J18: [164, 229, 249],
J19: [139, 223, 249],
J20: [107, 216, 255],
J21: [98, 201, 242],
J22: [89, 183, 234],
J23: [76, 172, 222],
J24: [72, 154, 214],
J25: [58, 141, 207],
J26: [59, 126, 197],
J27: [44, 113, 191],
J28: [35, 104, 182],
J29: [42, 92, 177],
J30: [36, 82, 168],
K1: [249, 240, 0],
K2: [249, 240, 1],
K3: [248, 239, 0],
K4: [249, 237, 3],
K5: [244, 236, 47],
K6: [236, 237, 74],
K7: [231, 234, 105],
K8: [222, 230, 118],
K9: [212, 229, 117],
K10: [198, 223, 132],
K11: [182, 223, 144],
K12: [171, 219, 157],
K13: [170, 220, 169],
K14: [167, 220, 178],
K15: [162, 221, 191],
K16: [165, 222, 215],
K17: [158, 222, 222],
K18: [153, 224, 228],
K19: [131, 219, 233],
K20: [109, 217, 243],
K21: [71, 213, 249],
K22: [40, 205, 250],
K23: [45, 192, 236],
K24: [52, 173, 226],
K25: [47, 160, 220],
K26: [45, 140, 208],
K27: [33, 127, 199],
K28: [36, 116, 187],
K29: [34, 103, 181],
K30: [34, 95, 178],
L1: [244, 237, 1],
L2: [242, 235, 0],
L3: [235, 233, 0],
L4: [234, 233, 1],
L5: [229, 231, 0],
L6: [219, 229, 36],
L7: [209, 224, 71],
L8: [186, 219, 86],
L9: [168, 217, 89],
L10: [157, 214, 107],
L11: [146, 212, 115],
L12: [136, 211, 129],
L13: [126, 210, 148],
L14: [122, 207, 152],
L15: [121, 210, 166],
L16: [120, 209, 177],
L17: [122, 211, 191],
L18: [116, 212, 200],
L19: [101, 212, 205],
L20: [91, 209, 213],
L21: [58, 206, 220],
L22: [0, 205, 226],
L23: [3, 201, 246],
L24: [0, 190, 242],
L25: [0, 172, 232],
L26: [2, 156, 218],
L27: [25, 138, 208],
L28: [21, 121, 196],
L29: [37, 115, 190],
L30: [30, 102, 184],
M1: [226, 227, 1],
M2: [218, 226, 1],
M3: [211, 226, 1],
M4: [203, 223, 1],
M5: [198, 219, 0],
M6: [180, 215, 11],
M7: [168, 212, 41],
M8: [148, 208, 52],
M9: [126, 202, 67],
M10: [112, 200, 78],
M11: [96, 199, 92],
M12: [81, 199, 103],
M13: [78, 197, 117],
M14: [77, 197, 126],
M15: [76, 199, 136],
M16: [78, 200, 143],
M17: [78, 199, 156],
M18: [81, 203, 164],
M19: [85, 203, 169],
M20: [63, 202, 181],
M21: [35, 200, 186],
M22: [0, 198, 197],
M23: [3, 196, 211],
M24: [0, 197, 224],
M25: [0, 190, 234],
M26: [0, 174, 234],
M27: [0, 160, 222],
M28: [0, 142, 210],
M29: [1, 129, 200],
M30: [0, 116, 189],
N1: [191, 218, 1],
N2: [182, 215, 0],
N3: [172, 212, 1],
N4: [159, 207, 1],
N5: [147, 206, 0],
N6: [133, 205, 1],
N7: [109, 197, 27],
N8: [93, 196, 29],
N9: [77, 191, 41],
N10: [47, 185, 46],
N11: [23, 184, 55],
N12: [0, 181, 71],
N13: [0, 186, 75],
N14: [0, 187, 98],
N15: [0, 191, 108],
N16: [12, 193, 112],
N17: [27, 191, 120],
N18: [29, 195, 133],
N19: [40, 196, 135],
N20: [47, 194, 140],
N21: [25, 195, 156],
N22: [0, 198, 171],
N23: [3, 196, 177],
N24: [0, 195, 197],
N25: [0, 195, 209],
N26: [0, 189, 221],
N27: [0, 177, 227],
N28: [1, 170, 227],
N29: [0, 154, 216],
N30: [1, 135, 206],
O1: [149, 197, 1],
O2: [142, 198, 1],
O3: [134, 197, 4],
O4: [121, 194, 29],
O5: [107, 191, 33],
O6: [81, 182, 42],
O7: [69, 179, 44],
O8: [40, 172, 53],
O9: [28, 170, 52],
O10: [3, 160, 53],
O11: [0, 161, 58],
O12: [3, 167, 55],
O13: [1, 172, 58],
O14: [2, 180, 56],
O15: [2, 183, 68],
O16: [3, 186, 82],
O17: [2, 188, 89],
O18: [0, 190, 104],
O19: [2, 189, 110],
O20: [2, 190, 113],
O21: [0, 193, 128],
O22: [3, 191, 141],
O23: [3, 193, 157],
O24: [3, 191, 167],
O25: [1, 192, 185],
O26: [3, 195, 200],
O27: [1, 193, 214],
O28: [3, 182, 215],
O29: [1, 173, 221],
O30: [4, 162, 227],
P1: [112, 167, 40],
P2: [100, 172, 36],
P3: [92, 170, 50],
P4: [80, 165, 46],
P5: [57, 167, 52],
P6: [29, 162, 55],
P7: [0, 154, 56],
P8: [0, 153, 57],
P9: [0, 149, 59],
P10: [1, 139, 61],
P11: [0, 144, 59],
P12: [0, 148, 56],
P13: [0, 158, 58],
P14: [0, 165, 61],
P15: [0, 174, 61],
P16: [0, 181, 60],
P17: [0, 183, 59],
P18: [0, 184, 72],
P19: [0, 185, 84],
P20: [3, 186, 93],
P21: [0, 188, 104],
P22: [0, 190, 120],
P23: [2, 189, 136],
P24: [0, 187, 152],
P25: [0, 191, 160],
P26: [0, 191, 175],
P27: [0, 191, 192],
P28: [1, 192, 202],
P29: [0, 187, 218],
P30: [0, 182, 232],
};
export const availableColors = Object.keys(pallette);

88
test/directus_db.test.ts Normal file
View file

@ -0,0 +1,88 @@
import { assert, describe, expect, it } from "vitest";
import { DirectusDB } from "../src/directus_db";
describe(
"Direcuts DB wrapper tests",
() => {
const db = new DirectusDB();
it("Test user registration, query and deletion", async () => {
const login = "__________test";
const password = "__________test";
// To clean if previous times test failed
await db.removeUser(login, password);
// Creating user
const resp1 = await db.registerUser(login, password);
expect(resp1).not.toBe(null);
expect(resp1!.login).toBe(login);
// Checking if user exists
const resp2 = await db.authenticateUser(login, password);
expect(resp2).not.toBe(null);
expect(resp2!.login).toBe(login);
// Creating user again, which is not allowed
const resp3 = await db.registerUser(login, password);
expect(resp3).toBe(null);
// Using double password to produce erroring response
const resp4 = await db.authenticateUser(login, password + password);
expect(resp4).toBe(null);
// Deleting user with wrong password
const result1 = await db.removeUser(login, password + password);
expect(result1).toBe(false);
// Deleting user
const result2 = await db.removeUser(login, password);
expect(result2).toBe(true);
// Checking if user exists
const resp5 = await db.authenticateUser(login, password);
expect(resp5).toBe(null);
});
it("Test user stats", async () => {
const login = "__________test";
const password = "__________test";
await db.removeUser(login, password);
const testUser = await db.registerUser(login, password);
// Checking if stats exist
let stats = await db.getStats(testUser!.id);
expect(stats).not.toBe(null);
expect(stats!.belongs_to).toBe(testUser!.id);
expect(stats!.games_won).toBe(0);
expect(stats!.games_lost).toBe(0);
// Updating stats (win times) and checking if they were updated
const result1 = await db.updateStats(testUser!.id, true, false);
expect(result1).toBe(true);
stats = await db.getStats(testUser!.id);
expect(stats).not.toBe(null);
expect(stats!.belongs_to).toBe(testUser!.id);
expect(stats!.games_won).toBe(1);
expect(stats!.games_lost).toBe(0);
// Updating stats (lost times) and checking if they were updated
const result2 = await db.updateStats(testUser!.id, false, true);
expect(result2).toBe(true);
stats = await db.getStats(testUser!.id);
expect(stats).not.toBe(null);
expect(stats!.belongs_to).toBe(testUser!.id);
expect(stats!.games_won).toBe(1);
expect(stats!.games_lost).toBe(1);
// Garbage user id should return false
const result3 = await db.updateStats("asdasdasd", false, true);
expect(result3).toBe(false);
// Deleting user
await db.removeUser(login, password);
});
},
{ timeout: 30000 }
);

View file

@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
@ -25,7 +25,7 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
"module": "NodeNext" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
@ -99,5 +99,7 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
},
"exclude": ["test"]
}