added tetris game management

This commit is contained in:
Tzvetan Trave
2025-10-15 18:19:51 +02:00
parent 005e9ebbbb
commit e4e53e06f6
9 changed files with 130 additions and 54 deletions

View File

@ -87,13 +87,14 @@ Can return:
} }
``` ```
## POST `/users/:userId/matchHistory` ## POST `/users/:userId/matchHistory?game=<pong/tetris>`
Used to add a match result to an user Used to add a match result to an user to a specific game
Input needed : Input needed :
```json ```json
{ {
"game": "<pong/tetris>"
"opponent": "<the opponent's username>", "opponent": "<the opponent's username>",
"myScore": <my score>, "myScore": <my score>,
"opponentScore": <the opponent's score> "opponentScore": <the opponent's score>
@ -107,7 +108,7 @@ Can return:
"msg": "Match successfully saved to the blockchain" "msg": "Match successfully saved to the blockchain"
} }
``` ```
- 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative) - 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative, or the game specified is invalid)
```json ```json
{ {
"error": "<corresponding error>" "error": "<corresponding error>"
@ -139,8 +140,14 @@ Can return:
{ {
"username": "<the username>", "username": "<the username>",
"displayName": "<the display name>", "displayName": "<the display name>",
"wins": <the number of matches won>, "pong": {
"losses": <the number of matches lost> "wins": <the number of pong matches won>,
"losses": <the number of pong matches lost>
},
"tetris": {
"wins": <the number of tetris matches won>,
"losses": <the number of tetris matches lost>
}
}, },
... ...
] ]
@ -181,8 +188,14 @@ Can return:
{ {
"username": "<the username>", "username": "<the username>",
"displayName": "<the display name>", "displayName": "<the display name>",
"wins": <the number of matches won>, "pong": {
"losses": <the number of matches lost> "wins": <the number of pong matches won>,
"losses": <the number of pong matches lost>
},
"tetris": {
"wins": <the number of tetris matches won>,
"losses": <the number of tetris matches lost>
}
} }
``` ```
- 404 with response (if user does not exist) - 404 with response (if user does not exist)
@ -240,9 +253,9 @@ Can return:
} }
``` ```
## GET `/users/:userId/matchHistory?iStart=<starting index (included)>&iEnd=<ending index (excluded)>` ## GET `/users/:userId/matchHistory?game=<pong/tetris>&iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
Used to get the match history of an user Used to get the match history of an user for a specific game
Can return: Can return:
- 200 with response (list of matches results (between iStart and iEnd)) - 200 with response (list of matches results (between iStart and iEnd))
@ -264,7 +277,7 @@ Can return:
] ]
} }
``` ```
- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart) - 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid)
```json ```json
{ {
"error": "<corresponding error>" "error": "<corresponding error>"
@ -277,9 +290,9 @@ Can return:
} }
``` ```
## GET `/users/:userId/matchHistory/count` ## GET `/users/:userId/matchHistory/count?game=<pong/tetris>`
Used to get the number of matches an user played Used to get the number of matches an user played for a specific game
Can return: Can return:
- 200 with response - 200 with response
@ -288,6 +301,12 @@ Can return:
"n_<name of the counted objects>": <number of users> "n_<name of the counted objects>": <number of users>
} }
``` ```
- 400 with response (if game does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist) - 404 with response (if user does not exist)
```json ```json
{ {
@ -440,9 +459,9 @@ Can return:
} }
``` ```
## DELETE `/users/:userId/matchHistory` ## DELETE `/users/:userId/matchHistory?game=<pong/tetris>`
Used to delete the match history of an user Used to delete the match history of an user for a specific game
Can return: Can return:
- 200 with response - 200 with response
@ -451,7 +470,7 @@ Can return:
"msg": "Match history deleted successfully" "msg": "Match history deleted successfully"
} }
``` ```
- 400 with response (if user specified in header is neither admin nor user) - 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid)
```json ```json
{ {
"error": "<corresponding error>" "error": "<corresponding error>"

View File

@ -1,4 +1,4 @@
export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStats) { export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris) {
try { try {
if (!request.user) { if (!request.user) {
return reply.code(400).send({ error: "Please specify a user" }); return reply.code(400).send({ error: "Please specify a user" });
@ -10,8 +10,17 @@ export async function dMatchHistory(request, reply, fastify, getUserInfo, delete
if (request.user !== 'admin' && request.user !== userId) { if (request.user !== 'admin' && request.user !== userId) {
return reply.code(401).send({ error: "Unauthorized" }); return reply.code(401).send({ error: "Unauthorized" });
} }
deleteMatchHistory.run(userId); const { game } = request.query;
deleteStats.run(userId); if (game !== 'pong' && game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
deleteMatchHistory.run(game, userId);
if (game === 'pong') {
deleteStatsPong.run(userId);
}
else if (game === 'tetris') {
deleteStatsTetris.run(userId);
}
return reply.code(200).send({ msg: "Match history deleted successfully" }); return reply.code(200).send({ msg: "Match history deleted successfully" });
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);

View File

@ -3,7 +3,8 @@ export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHis
if (!getUserInfo.get(request.params.userId)) { if (!getUserInfo.get(request.params.userId)) {
return reply.code(404).send({ error: "User does not exist" }); return reply.code(404).send({ error: "User does not exist" });
} }
deleteMatchHistory.run(request.params.userId); deleteMatchHistory.run('pong', request.params.userId);
deleteMatchHistory.run('tetris', request.params.userId);
deleteFriends.run(request.params.userId); deleteFriends.run(request.params.userId);
deleteUser.run(request.params.userId); deleteUser.run(request.params.userId);
return reply.code(200).send({ msg: "User deleted successfully" }); return reply.code(200).send({ msg: "User deleted successfully" });

View File

@ -35,11 +35,15 @@ function prepareDB() {
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT, username TEXT,
displayName TEXT, displayName TEXT,
wins INTEGER, pongWins INTEGER,
losses INTEGER, pongLosses INTEGER,
tetrisWins INTEGER,
tetrisLosses INTEGER,
UNIQUE(username), UNIQUE(username),
CHECK(wins >= 0), CHECK(pongWins >= 0),
CHECK(losses >= 0) CHECK(pongLosses >= 0),
CHECK(tetrisWins >= 0),
CHECK(tetrisLosses >= 0)
) STRICT ) STRICT
`); `);
database.exec(` database.exec(`
@ -54,8 +58,12 @@ function prepareDB() {
database.exec(` database.exec(`
CREATE TABLE IF NOT EXISTS matchHistory ( CREATE TABLE IF NOT EXISTS matchHistory (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT, game TEXT,
matchId INTEGER player1 TEXT,
player2 TEXT,
matchId INTEGER,
CHECK(player1 != player2),
CHECK(game = 'pong' OR game = 'tetris')
) STRICT ) STRICT
`); `);
} }
@ -63,34 +71,40 @@ function prepareDB() {
prepareDB(); prepareDB();
// POST // POST
const createUser = database.prepare('INSERT INTO userData (username, displayName, wins, losses) VALUES (?, ?, 0, 0);'); const createUser = database.prepare('INSERT INTO userData (username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, 0, 0, 0, 0);');
const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);');
const addMatch = database.prepare('INSERT INTO matchHistory (username, matchId) VALUES (?, ?);'); const addMatch = database.prepare('INSERT INTO matchHistory (game, player1, player2, matchId) VALUES (?, ?, ?, ?);');
const incWins = database.prepare('UPDATE userData SET wins = wins + 1 WHERE username = ?;'); const incWinsPong = database.prepare('UPDATE userData SET pongWins = pongWins + 1 WHERE username = ?;');
const incLosses = database.prepare('UPDATE userData SET losses = losses + 1 WHERE username = ?'); const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLosses + 1 WHERE username = ?');
const incWinsTetris = database.prepare('UPDATE userData SET tetrisWins = tetrisWins + 1 WHERE username = ?;');
const incLossesTetris = database.prepare('UPDATE userData SET tetrisLosses = tetrisLosses + 1 WHERE username = ?');
// PATCH // PATCH
const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;');
// GET // GET
const getUserData = database.prepare('SELECT username, displayName, wins, losses FROM userData LIMIT ? OFFSET ?;'); const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;');
const getUserInfo = database.prepare('SELECT username, displayName, wins, losses FROM userData WHERE username = ?;'); const getUserInfo = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData WHERE username = ?;');
const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ? LIMIT ? OFFSET ?;'); const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ? LIMIT ? OFFSET ?;');
const getFriend = database.prepare('SELECT friendName FROM friends WHERE username = ? AND friendName = ?;'); const getFriend = database.prepare('SELECT friendName FROM friends WHERE username = ? AND friendName = ?;');
const getMatchHistory = database.prepare('SELECT matchId FROM matchHistory WHERE username = ? LIMIT ? OFFSET ?;'); const getMatchHistory = database.prepare('SELECT matchId FROM matchHistory WHERE game = ? AND ? IN (player1, player2) LIMIT ? OFFSET ?;');
const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_users FROM userData;'); const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_users FROM userData;');
const getNumberFriends = database.prepare('SELECT COUNT (DISTINCT friendName) AS n_friends FROM friends WHERE username = ?;'); const getNumberFriends = database.prepare('SELECT COUNT (DISTINCT friendName) AS n_friends FROM friends WHERE username = ?;');
const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE username = ?;') const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);')
// DELETE // DELETE
const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;');
const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;'); const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;');
const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;'); const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;');
const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE username = ?;'); const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE game = ? AND ? IN (player1, player2);');
const deleteStats = database.prepare('UPDATE userData SET wins = 0, losses = 0 WHERE username = ?;'); const deleteStatsPong = database.prepare('UPDATE userData SET pongWins = 0, pongLosses = 0 WHERE username = ?;');
const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, tetrisLosses = 0 WHERE username = ?;');
const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } }
const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } }
const querySchemaMatchHistory = { type: 'object', required: ['game', 'iStart', 'iEnd'], properties: { game: { type: 'string' }, iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } }
const bodySchemaMatchHistory = { type: 'object', required: ['game', 'opponent', 'myScore', 'opponentScore'], properties: { game: { type: 'string' }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } }
const querySchemaMatchHistoryGame = { type: 'object', required: ['game'], properties: { game: { type: 'string' } } }
export default async function(fastify, options) { export default async function(fastify, options) {
fastify.register(fastifyJWT, { fastify.register(fastifyJWT, {
@ -137,10 +151,10 @@ export default async function(fastify, options) {
fastify.get('/users/:userId/friends/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.get('/users/:userId/friends/count', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends); return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends);
}); });
fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => { fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchemaMatchHistory } }, async (request, reply) => {
return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory); return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory);
}); });
fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => {
return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches);
}); });
@ -151,8 +165,8 @@ export default async function(fastify, options) {
fastify.post('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.post('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend); return pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend);
}); });
fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchema } }, async (request, reply) => { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => {
return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses); return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris);
}); });
// PATCH // PATCH
@ -173,7 +187,7 @@ export default async function(fastify, options) {
fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend); return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend);
}); });
fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => {
return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStats); return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris);
}); });
} }

View File

@ -4,11 +4,14 @@ export async function gMatchHistory(request, reply, fastify, getUserInfo, getMat
if (!getUserInfo.get(userId)) { if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" }); return reply.code(404).send({ error: "User does not exist" });
} }
const { iStart, iEnd } = request.query; const { game, iStart, iEnd } = request.query;
if (game !== 'pong' && game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
if (Number(iEnd) < Number(iStart)) { if (Number(iEnd) < Number(iStart)) {
return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" });
} }
const matchHistoryId = getMatchHistory.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); const matchHistoryId = getMatchHistory.all(game, userId, Number(iEnd) - Number(iStart), Number(iStart));
if (!matchHistoryId.length) { if (!matchHistoryId.length) {
return reply.code(404).send({ error: "No matches exist in the selected range" }); return reply.code(404).send({ error: "No matches exist in the selected range" });
} }

View File

@ -4,7 +4,11 @@ export async function gNumberMatches(request, reply, fastify, getUserInfo, getNu
if (!getUserInfo.get(userId)) { if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" }); return reply.code(404).send({ error: "User does not exist" });
} }
const row = getNumberMatches.get(userId); const { game } = request.query;
if (game !== 'pong' && game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
const row = getNumberMatches.get(game, userId);
return reply.code(200).send({ n_matches: row.n_matches }); return reply.code(200).send({ n_matches: row.n_matches });
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);

View File

@ -5,7 +5,7 @@ export async function gUser(request, reply, fastify, getUserInfo) {
if (!userInfo) { if (!userInfo) {
return reply.code(404).send({ error: "User does not exist" }); return reply.code(404).send({ error: "User does not exist" });
} }
return reply.code(200).send({ username: userInfo.username, displayName: userInfo.displayName, wins: userInfo.wins, losses: userInfo.losses }); return reply.code(200).send({ username: userInfo.username, displayName: userInfo.displayName, pong: { wins: userInfo.pongWins, losses: userInfo.pongLosses }, tetris: { wins: userInfo.tetrisWins, losses: userInfo.tetrisLosses } });
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" }); return reply.code(500).send({ error: "Internal server error" });

View File

@ -8,7 +8,19 @@ export async function gUsers(request, reply, fastify, getUserData) {
if (!users.length) { if (!users.length) {
return reply.code(404).send({ error: "No users exist in the selected range" }); return reply.code(404).send({ error: "No users exist in the selected range" });
} }
return reply.code(200).send({ users }); const usersFormat = users.map(obj => ({
username: obj.username,
displayName: obj.displayName,
pong: {
wins: obj.pongWins,
losses: obj.pongLosses
},
tetris: {
wins: obj.tetrisWins,
losses: obj.tetrisLosses
}
}));
return reply.code(200).send({ usersFormat });
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" }); return reply.code(500).send({ error: "Internal server error" });

View File

@ -4,10 +4,10 @@ async function fetchSave(request, reply, userId, addMatch) {
throw new Error('Internal server error'); throw new Error('Internal server error');
} }
const data = await res.json(); const data = await res.json();
addMatch.run(userId, data.id); addMatch.run(request.body.game, userId, request.body.opponent, data.id);
} }
export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses) { export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris) {
try { try {
const userId = request.params.userId; const userId = request.params.userId;
if (!request.user) { if (!request.user) {
@ -16,6 +16,9 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat
if (request.user !== 'admin' && request.user !== userId) { if (request.user !== 'admin' && request.user !== userId) {
return reply.code(401).send({ error: "Unauthorized" }); return reply.code(401).send({ error: "Unauthorized" });
} }
if (request.body.game !== 'pong' && request.body.game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
if (!getUserInfo.get(userId)) { if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" }); return reply.code(404).send({ error: "User does not exist" });
} }
@ -26,12 +29,23 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat
return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" }); return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" });
} }
await fetchSave(request, reply, userId, addMatch); await fetchSave(request, reply, userId, addMatch);
if (request.body.myScore > request.body.opponentScore) { if (request.body.game === 'pong') {
incWins.run(userId); if (request.body.myScore > request.body.opponentScore) {
incLosses.run(request.body.opponent); incWinsPong.run(userId);
} else if (request.body.myScore < request.body.opponentScore) { incLossesPong.run(request.body.opponent);
incWins.run(request.body.opponent); } else if (request.body.myScore < request.body.opponentScore) {
incLosses.run(userId); incWinsPong.run(request.body.opponent);
incLossesPong.run(userId);
}
}
else if (request.body.game === 'tetris') {
if (request.body.myScore > request.body.opponentScore) {
incWinsTetris.run(userId);
incLossesTetris.run(request.body.opponent);
} else if (request.body.myScore < request.body.opponentScore) {
incWinsTetris.run(request.body.opponent);
incLossesTetris.run(userId);
}
} }
return reply.code(200).send({ msg: "Match successfully saved to the blockchain" }); return reply.code(200).send({ msg: "Match successfully saved to the blockchain" });
} catch (err) { } catch (err) {