From 8afc38d05f8764cbdff7887c2ce4ca28cabf92a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pir=C3=B3g?= <69601940+aetn23@users.noreply.github.com> Date: Sun, 21 May 2023 23:56:04 +0200 Subject: [PATCH] @aetn23/call (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sormys Co-authored-by: Karol WÄ…sowski --- src/app.ts | 3 ++ src/routes/gameplay/call.ts | 89 +++++++++++++++++++++++++++++++++ src/routes/gameplay/fold.ts | 13 ++++- src/tests/call.test.ts | 98 +++++++++++++++++++++++++++++++++++++ src/utils/commonRequest.ts | 34 +++++++++++-- 5 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 src/routes/gameplay/call.ts create mode 100644 src/tests/call.test.ts diff --git a/src/app.ts b/src/app.ts index 2d66214..2837bff 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,7 @@ import startGame from './routes/startGame' import actionFold from './routes/gameplay/fold' import actionRaise from './routes/gameplay/raise' import actionCheck from './routes/gameplay/check' +import actionCall from './routes/gameplay/call' import { rateLimiter } from './utils/rateLimiter' export const app = express() @@ -48,4 +49,6 @@ app.use(actionRaise) app.use(actionCheck) +app.use(actionCall) + app.use(errorHandling) diff --git a/src/routes/gameplay/call.ts b/src/routes/gameplay/call.ts new file mode 100644 index 0000000..ec72381 --- /dev/null +++ b/src/routes/gameplay/call.ts @@ -0,0 +1,89 @@ +import { getClient } from '../../utils/databaseConnection' +import { celebrate, Joi, Segments } from 'celebrate' +import { + sendFirebaseMessageToEveryone, + verifyFCMToken, +} from '../../utils/firebase' +import express, { type Router } from 'express' +import { rateLimiter } from '../../utils/rateLimiter' +import { + isPlayerInGame, + isPlayersTurn, + setPlayerState, + setNewCurrentPlayer, + changeGameRoundIfNeeded, + playerHasEnoughMoney, + playerRaised, + getMaxBet, +} from '../../utils/commonRequest' +import sha256 from 'crypto-js/sha256' +import { PlayerState } from '../../utils/types' + +const router: Router = express.Router() + +router.get( + '/actionCall', + rateLimiter, + celebrate({ + [Segments.QUERY]: Joi.object().keys({ + playerToken: Joi.string().required().min(1).max(250).label('playerToken'), + gameId: Joi.number().required().min(0).max(999999).label('gameId'), + }), + }), + async (req, res) => { + const playerToken = req.query.playerToken as string + const gameId = req.query.gameId as string + if (!(await verifyFCMToken(playerToken))) { + return res.sendStatus(401) + } + + const client = getClient() + + client + .connect() + .then(async () => { + if (!(await isPlayerInGame(playerToken, gameId, client))) { + return res.sendStatus(400) + } + + if (!(await isPlayersTurn(playerToken, gameId, client))) { + return res.sendStatus(402) + } + + const maxBet = await getMaxBet(gameId, client) + + if ( + !(await playerHasEnoughMoney(gameId, playerToken, maxBet, client)) + ) { + return res.sendStatus(403) + } + + const newPlayer = await setNewCurrentPlayer(playerToken, gameId, client) + + await setPlayerState(playerToken, client, PlayerState.Called) + await playerRaised(gameId, playerToken, maxBet, client) + await changeGameRoundIfNeeded(gameId, newPlayer, client) + + const message = { + data: { + player: sha256(playerToken).toString(), + type: PlayerState.Called, + actionPayload: maxBet.toString(), + }, + token: '', + } + + await sendFirebaseMessageToEveryone(message, gameId, client) + res.sendStatus(200) + }) + .catch((err) => { + console.log(err.stack) + return res.sendStatus(500) + }) + .finally(async () => { + await client.end() + }) + } +) + +export default router diff --git a/src/routes/gameplay/fold.ts b/src/routes/gameplay/fold.ts index 9be604e..fd9a8d5 100644 --- a/src/routes/gameplay/fold.ts +++ b/src/routes/gameplay/fold.ts @@ -50,9 +50,10 @@ router.get( await setPlayerState(playerToken, client, PlayerState.Folded) const newPlayer = await setNewCurrentPlayer(playerToken, gameId, client) if (newPlayer === '') { + const winner = (await playersStillInGame(gameId, client))[0] const message = { data: { - player: sha256(newPlayer).toString(), + player: sha256(winner).toString(), type: PlayerState.Won, actionPayload: '', }, @@ -88,3 +89,13 @@ router.get( ) export default router + +export async function playersStillInGame(gameId: string, client) { + const query = `SELECT token + FROM players + WHERE game_id = $1 AND last_action <> $2 AND last_action <> $3 + AND last_action IS NOT NULL + ` + const values = [gameId, PlayerState.Folded, PlayerState.NoAction] + return (await client.query(query, values)).rows[0] +} diff --git a/src/tests/call.test.ts b/src/tests/call.test.ts new file mode 100644 index 0000000..cc64b07 --- /dev/null +++ b/src/tests/call.test.ts @@ -0,0 +1,98 @@ +import { app } from '../app' +import request from 'supertest' +import { getClient } from '../utils/databaseConnection' +import type { NewGameInfo } from '../utils/types' +import { getGameIdAndStatus, getPlayersInGame } from '../utils/commonRequest' + +test('Call, correct arguments 1', async () => { + const gameMasterToken = 'CALLTEST' + const gameMasterNick = 'CALLNICK' + const playerToken = 'CALLTEST2' + const playerNick = 'CALLNICK2' + const player2Token = 'CALLTEST3' + const player2Nick = 'CALLNICK3' + + const client = getClient() + await client.connect() + const res = await request(app) + .get( + `/createGame?creatorToken=${gameMasterToken}&nickname=${gameMasterNick}` + ) + .expect(200) + + const key = (res.body as NewGameInfo).gameId + await request(app) + .get( + `/joinGame?playerToken=${playerToken}&nickname=${playerNick}&gameId=${key}` + ) + .expect(200) + + await request(app) + .get( + `/joinGame?playerToken=${player2Token}&nickname=${player2Nick}&gameId=${key}` + ) + .expect(200) + + await request(app) + .get(`/startGame?creatorToken=${gameMasterToken}`) + .expect(200) + + const gameId = + (await getGameIdAndStatus(gameMasterToken, client)).gameId ?? '' + const players = await getPlayersInGame(gameId, client) + console.log(players) + + await request(app) + .get( + `/actionRaise?playerToken=${players[1].token}&gameId=${gameId}&amount=5` + ) + .expect(402) + + await request(app) + .get( + `/actionRaise?playerToken=${players[0].token}&gameId=${gameId}&amount=300` + ) + .expect(200) + await request(app) + .get(`/actionCall?playerToken=${players[1].token}&gameId=${gameId}`) + .expect(200) + const getRound = 'SELECT game_round FROM Games WHERE game_id=$1' + + expect( + await ( + await client.query(getRound, [gameId]) + ).rows[0].game_round + ).toEqual('1') // Player 2 can still act + + await request(app) + .get( + `/actionRaise?playerToken=${players[2].token}&gameId=${gameId}&amount=2` + ) + .expect(404) + + await request(app) + .get( + `/actionRaise?playerToken=${players[2].token}&gameId=${gameId}&amount=2000000000` + ) + .expect(403) + + await request(app) + .get( + `/actionRaise?playerToken=${players[2].token}&gameId=${gameId}&amount=400` + ) + .expect(200) + + await request(app) + .get(`/actionCall?playerToken=${players[0].token}&gameId=${gameId}`) + .expect(200) + + await request(app) + .get(`/actionFold?playerToken=${players[1].token}&gameId=${gameId}`) + .expect(200) + + await request(app) + .get(`/actionFold?playerToken=${players[2].token}&gameId=${gameId}`) + .expect(402) // round ended + + await client.end() +}, 20000) diff --git a/src/utils/commonRequest.ts b/src/utils/commonRequest.ts index e0ef622..457ebce 100644 --- a/src/utils/commonRequest.ts +++ b/src/utils/commonRequest.ts @@ -59,9 +59,12 @@ export async function setPlayerState( await client.query(query, [state, playerToken]) } -export async function getPlayerState(playerToken: string, client: Client) { - const query = 'SELECT last_action FROM Players WHERE player_token=$1' - await client.query(query, [playerToken]) +export async function getPlayerState( + playerToken: string, + client: Client +): Promise { + const query = 'SELECT last_action FROM Players WHERE token=$1' + return (await client.query(query, [playerToken])).rows[0].last_action } export async function setNewCurrentPlayer( @@ -92,6 +95,7 @@ export async function setNewCurrentPlayer( playersTurns.rows[i].token, gameId, ]) + console.log(playersTurns.rows[i].token) return playersTurns.rows[i].token } } @@ -100,6 +104,7 @@ export async function setNewCurrentPlayer( playersTurns.rows[0].token, gameId, ]) + console.log(playersTurns.rows[0].token) return playersTurns.rows[0].token } } @@ -197,7 +202,20 @@ export async function playerHasEnoughMoney( amount: string, client: Client ): Promise { - const query = 'SELECT 1 FROM Players WHERE token=$1 AND funds+bet>=$2' + const smallBlindValue = await getSmallBlindValue(gameId, client) + const playerSize = (await getPlayersInGame(gameId, client)).length + const smallBlind = await getSmallBlind(gameId, playerSize, client) + const smallBlindState = await getPlayerState(smallBlind, client) + const bigBlind = await getBigBlind(gameId, playerSize, client) + const bigBlindState = await getPlayerState(bigBlind, client) + + if (playerToken === smallBlind && smallBlindState == null) { + amount = (+amount - +smallBlindValue).toString() + } else if (playerToken === bigBlind && bigBlindState == null) { + amount = (+amount - +smallBlindValue * 2).toString() + } + + const query = 'SELECT 1 FROM Players WHERE token=$1 AND funds>=$2' return (await client.query(query, [playerToken, amount])).rowCount !== 0 } @@ -227,3 +245,11 @@ export async function playerRaised( await client.query(setNewBet, [amount, playerToken]) await client.query(putMoneyToTable, [parseInt(amount) - oldBet, gameId]) } + +export async function getMaxBet( + gameId: string, + client: Client +): Promise { + const query = 'SELECT MAX(bet) as max FROM Players WHERE game_id=$1' + return (await client.query(query, [gameId])).rows[0].max +}