From 199d7519f5ce37800ebbd33ce7ccccf45ba2d44d Mon Sep 17 00:00:00 2001 From: Kislay Date: Mon, 3 Jun 2024 13:17:30 +0530 Subject: [PATCH 01/15] events: Add mechanism to enqueue events. The implementation of the respective functions will be done through subsequent PRs. Also added the AppEvent type. --- backend/eventRecipients.ts | 37 +++++++++++++++++++++++++++++-------- backend/src/types.d.ts | 3 +++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/backend/eventRecipients.ts b/backend/eventRecipients.ts index 3861c85..9c97f9e 100644 --- a/backend/eventRecipients.ts +++ b/backend/eventRecipients.ts @@ -1,18 +1,39 @@ // this module is responsible for handling the clients currently connected to the server. -// It stores the clients in a Map object, where the key is the client's user_id and the value is the client's http response object. +// It stores the clients in a Map object, where the key is the client id and the value is the +// client's http response object. +// When sending an event, we first push the event to a queue. We need to use a queue because it +// is possible that the client hasn't sent the next polling request yet, i.e, we don't have a response +// object for that client yet. When the client sends the next polling request, we will send the events +// from the queue to the client. import { Response } from 'express'; -const clients = new Map(); +type ClientId = string; -export function addClient(userId: string, res: Response) { - clients.set(userId, res); +const clients = new Map(); +const eventQueue = new Map(); + +export function addClient(clientId: ClientId, res: Response) { + // todo: We can immediately send any events that are in the queue. + clients.set(clientId, res); + eventQueue.set(clientId, []); +} + +export function removeClient(clientId: ClientId) { + clients.delete(clientId); + eventQueue.delete(clientId); +} + +export function getClient(clientId: ClientId) { + return clients.get(clientId); } -export function removeClient(userId: string) { - clients.delete(userId); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function scheduleSend(clientId: ClientId, event: AppEvent) { + //todo: Enqueue the event for sending. } -export function getClient(userId: string) { - return clients.get(userId); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function doSendEvent(clientId: ClientId) { + //todo: Send all the events in the queue to the client, only if the response object is available. } diff --git a/backend/src/types.d.ts b/backend/src/types.d.ts index 4f21a45..cae437c 100644 --- a/backend/src/types.d.ts +++ b/backend/src/types.d.ts @@ -44,4 +44,7 @@ type GameEvent = card: UNOCard; }; }; + +// Represent all the events that can be sent to the client +type AppEvent = GameEvent; //todo: Add more events From 2e865b9e737afecc4953a97f44423d41174da7ce Mon Sep 17 00:00:00 2001 From: Divyansh Seth Date: Fri, 31 May 2024 23:47:48 +0530 Subject: [PATCH 02/15] deck: completed function to create and shuffle a UNO card deck. 1. Added black to CardColor in types.d.ts. 2. Split special and wild cards into specialValues and wildCardValues arrays. 3. For each color in the colors array: - Add two cards for each number (0-9), except one card for '0'. - Add two cards for each special value. - For 'black', add four cards for each wild card value. 4. Added test,in deck.test.ts, to check the correct functioning of getShuffledCardDeck() Fixes: #1 --- backend/src/types.d.ts | 8 ++--- backend/tests/deck.test.ts | 60 ++++++++++++++++++++++++++++++++- backend/uno-game-engine/deck.ts | 38 ++++++++++++++------- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/backend/src/types.d.ts b/backend/src/types.d.ts index cae437c..a75b4e8 100644 --- a/backend/src/types.d.ts +++ b/backend/src/types.d.ts @@ -3,12 +3,12 @@ type CardType = 'number' | 'special' | 'wild'; -type CardColor = 'red' | 'blue' | 'green' | 'yellow'; +type CardColor = 'red' | 'blue' | 'green' | 'yellow' | 'wild'; -type SpecialCardNames = 'skip' | 'reverse' | 'draw2' | 'draw4' | 'colchange'; -type CardNumbers = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; +type SpecialCardName = 'skip' | 'reverse' | 'draw2' | 'draw4' | 'colchange'; +type CardNumber = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; -type CardValue = SpecialCardNames | CardNumbers; +type CardValue = SpecialCardName | CardNumber; type UNOCard = { type: CardType; diff --git a/backend/tests/deck.test.ts b/backend/tests/deck.test.ts index d54e7b3..502a3f9 100644 --- a/backend/tests/deck.test.ts +++ b/backend/tests/deck.test.ts @@ -1,10 +1,68 @@ -import { shuffle, makeCard } from '../uno-game-engine/deck'; +import { + shuffle, + makeCard, + getShuffledCardDeck, +} from '../uno-game-engine/deck'; + +describe('getShuffledCardDeck', () => { + test('should return an array of 108 cards', () => { + const deck = getShuffledCardDeck(); + expect(deck.length).toBe(108); + { + // Check the number of cards of each type + const numberCards = deck.filter((card) => card.type === 'number'); + expect(numberCards.length).toBe(76); + + const specialCards = deck.filter((card) => card.type === 'special'); + expect(specialCards.length).toBe(24); + + const wildCards = deck.filter((card) => card.type === 'wild'); + expect(wildCards.length).toBe(8); + } + { + // Check the number of cards of red color + const redCards = deck.filter((card) => card.color === 'red'); + expect(redCards.length).toBe(25); // 25 * 4 = 100 color cards + } + { + // Check the number of cards of 0 and 1 values + const zeroValueCards = deck.filter((card) => card.value === '0'); + expect(zeroValueCards.length).toBe(4); + + const oneValueCards = deck.filter((card) => card.value === '1'); + expect(oneValueCards.length).toBe(8); + } + { + // Check the number of cards of special type + const skipCards = deck.filter((card) => card.value === 'skip'); + expect(skipCards.length).toBe(8); + + const draw2Cards = deck.filter((card) => card.value === 'draw2'); + expect(draw2Cards.length).toBe(8); + + const reverseCards = deck.filter( + (card) => card.value === 'reverse' + ); + expect(reverseCards.length).toBe(8); + } + { + // Check the number of cards of wild type + const wildCards = deck.filter((card) => card.value === 'colchange'); + expect(wildCards.length).toBe(4); + + const draw4Cards = deck.filter((card) => card.value === 'draw4'); + expect(draw4Cards.length).toBe(4); + } + }); +}); + describe('testing deck.ts', () => { test('makeCard', () => { const card = makeCard('number', 'blue', '3'); expect(card.color).toBe('blue'); }); }); + describe('shuffle function', () => { test('should change order of elements', () => { // Create a mock deck diff --git a/backend/uno-game-engine/deck.ts b/backend/uno-game-engine/deck.ts index 8a6c19e..5571f8a 100644 --- a/backend/uno-game-engine/deck.ts +++ b/backend/uno-game-engine/deck.ts @@ -1,5 +1,5 @@ -const colors: Array = ['red', 'yellow', 'green', 'blue']; -const values = [ +const colors: Array = ['red', 'yellow', 'green', 'blue', 'wild']; +const numValues: Array = [ '0', '1', '2', @@ -10,12 +10,9 @@ const values = [ '7', '8', '9', - 'skip', - 'reverse', - 'draw2', ]; -const specialCards = ['wild', 'draw4']; -const deck = []; +const specialValues: Array = ['skip', 'reverse', 'draw2']; +const wildCardValues: Array = ['colchange', 'draw4']; const sameCardCount = []; // to keep track of same cards in assigning unique id to each card /** @@ -42,11 +39,28 @@ const sameCardCount = []; // to keep track of same cards in assigning unique id @returns {Array} deck - An array of 108 UNO cards. */ export function getShuffledCardDeck(): Array { - const deck = []; - // todo: Implement the card generation logic - // dummy code: - // deck.push(makeCard('special', 'wild', 'wild')) - // deck.push(makeCard('number', 'red', '0')) + const deck: Array = []; + + colors.forEach((color) => { + if (color === 'wild') { + wildCardValues.forEach((value) => { + for (let i = 0; i < 4; i++) { + deck.push(makeCard('wild', color, value)); + } + }); + } else { + numValues.forEach((value) => { + deck.push(makeCard('number', color, value)); + if (value !== '0') { + deck.push(makeCard('number', color, value)); + } + }); + specialValues.forEach((value) => { + deck.push(makeCard('special', color, value)); + deck.push(makeCard('special', color, value)); + }); + } + }); shuffle(deck); return deck; From ad77ddb9ac3efb80607b0065ac50d7fbc5ddb061 Mon Sep 17 00:00:00 2001 From: Divyansh Seth Date: Sun, 2 Jun 2024 00:57:51 +0530 Subject: [PATCH 03/15] engine: improved card drawing logic. this commit handles the drawing logic from an empty cardDeck. - It reshuffles the thrown cards into the card deck when the card deck is empty and a player needs to draw a card. - It returns success message if the card is drawn successfully else returns message error. - Also added relevant unit test, in engine.test.ts, to verify this function (drawCardFromDeck) works correctly. --- backend/tests/engine.test.ts | 21 ++++++++++++++++++ backend/uno-game-engine/engine.ts | 36 +++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 backend/tests/engine.test.ts diff --git a/backend/tests/engine.test.ts b/backend/tests/engine.test.ts new file mode 100644 index 0000000..1fd7312 --- /dev/null +++ b/backend/tests/engine.test.ts @@ -0,0 +1,21 @@ +import { GameEngine } from '../uno-game-engine/engine'; + +describe('testing drawCardFromDeck()', () => { + test('draws a card when deck is empty but thrownCards is not', () => { + const game = new GameEngine(); + game.addPlayer({ id: '1', cards: [] }); + game.cardDeck = []; + game.thrownCards = [ + { + type: 'number', + color: 'yellow', + value: '1', + id: 'card-number-yellow-1-1', + }, + ]; + const player = game.players[0]; + const status: EventResult = game.drawCardFromDeck(player); + expect(player.cards.length).toBe(1); + expect(status.type).toBe('SUCCESS'); + }); +}); diff --git a/backend/uno-game-engine/engine.ts b/backend/uno-game-engine/engine.ts index 17bf2fe..0c97d04 100644 --- a/backend/uno-game-engine/engine.ts +++ b/backend/uno-game-engine/engine.ts @@ -1,4 +1,4 @@ -import { getShuffledCardDeck } from './deck'; +import { getShuffledCardDeck, shuffle } from './deck'; import { handleEvent } from './gameEvents'; const NUM_CARDS_PER_PLAYER = 7; @@ -47,11 +47,35 @@ export class GameEngine { this.currentPlayerIndex = (this.currentPlayerIndex + this.direction) % this.players.length; } - drawCardFromDeck(player: Player) { - //todo: Handle the case when the deck is empty and we have to move the thrown cards back to the deck - this.players - .find((p: Player) => p.id === player.id) - .cards.push(this.cardDeck.pop()); + drawCardFromDeck(player: Player): EventResult { + try { + if (this.cardDeck.length === 0) { + this.cardDeck = [...this.thrownCards]; + this.thrownCards = []; + shuffle(this.cardDeck); + } + const currentPlayer = this.players.find( + (p: Player) => p.id === player.id + ); + if (currentPlayer && this.cardDeck) { + const card = this.cardDeck.pop(); + if (card) { + currentPlayer.cards.push(card); + return { + type: 'SUCCESS', + message: 'Card drawn successfully', + }; + } else + return { type: 'ERROR', message: 'Unable to draw a card' }; + } else { + return { + type: 'ERROR', + message: 'Player not found or cardDeck is empty', + }; + } + } catch (error) { + return { type: 'ERROR', message: (error as Error).message }; + } } dispatchEvent(event: GameEvent) { // handle different types of events based on event.type From f4e8e18a3f9a6b7edc992761b7b3a3208301f625 Mon Sep 17 00:00:00 2001 From: Kislay Date: Tue, 4 Jun 2024 14:34:30 +0530 Subject: [PATCH 04/15] architecture: Add details about game event and API implementation. Each component can be worked on in a separate issue. --- ARCHITECTURE.md | 60 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d56e8ab..f96f292 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,3 +1,9 @@ +# Game rules +To be on the same page, we will be following [these rules](https://www.unorules.com/). + +--- + + # Communication between the client and the server Apart from the initial request-response exchange for initial setup, @@ -19,9 +25,8 @@ Simply put, long polling is a technique where the client sends a request to the # Protocol for transmitting game events Each action by the player, like drawing a card from deck, throwing a -card, announcing UNO, etc., is represented as a message. The client -sends these messages to the server via POST at the `/events` -endpoint, and the server sends these messages to all clients. The +card, announcing UNO, etc., is represented as an event. The client sends these event messages to the server via POST at the `/events` endpoint. +endpoint, and the server sends these messages to all clients through the long polling system. The format of the message is: ```json @@ -33,7 +38,54 @@ format of the message is: } } ``` -Other possible values for `type` are `THROW_CARD`, `ANNOUNCE_UNO`, etc. When such a request reaches the server, the server updates the game state and sends the message to all clients (through the polling mechanism). The clients update their game state accordingly, and make the necessary changes to their UI and game state. +We should design the code paths such that it would be easy to switch to WebSockets in the upcoming weeks. + +## Game Events +The following events will be handled by the game engine: +### `JOIN_GAME` +Sent by the client when a player joins the game. All the other clients should be notified about the new player, so that they can update their UI. +### `LEAVE_GAME` +Self explanatory. +### `START_GAME` +There needs to be some discussion on how we want to handle this. We can either start the game when all players have joined, or we can start the game when the host decides to start the game. We can also have a ready button for each player, and the game starts when all players are ready. +### `THROW_CARD` +Sent by the client when a player throws a card. The server will validate the move and update the game state accordingly. +### `DRAW_CARD` +Sent by the client when a player draws a card from the deck. + +### `ANNOUNCE_UNO` +Sent by the client when a player announces UNO. We expect a button to pop up when a player has only one card left. This should be handled by the client. +### `CHALLENGE_UNO` +Sent by the client when a player challenges another player's UNO. +### `DRAW_4_CHALLENGE` +Read about this in the game rules. This is related to `CHALLENGE_UNO` implementation wise. + +Note that the server will always notify all the clients about the game events. The clients will update their game state and UI accordingly. + +We will add more rules once these are implemented. +The data pertaining to these events will be decided while working on the events. +All the game event handling functions can reside in a directory inside the `uno-game-engine` directory. + +--- + +# API Structure +We will use express routers to group similar endpoints. The API structure will be as follows: + +``` +/api + /v1 + /auth + /login + /register + /game + /create + /join + /leave + /start + /end + /events +``` +The routers will be defined in the `routes` directory. The controllers will be defined in the `controllers` directory. The controllers will handle the business logic and call the game engine functions to handle the game events. All the controller functions should be wrapped in the error handling higher order function. \ No newline at end of file From 0623dca3aa93953ab1e1fbd3e5a1179a47dcd680 Mon Sep 17 00:00:00 2001 From: Kislay Date: Tue, 4 Jun 2024 14:37:24 +0530 Subject: [PATCH 05/15] backend: Change directory structure All the source code is moved to the `src` directory. --- backend/{ => src}/controllers/gameControllers.js | 0 backend/{ => src}/eventRecipients.ts | 0 backend/{ => src}/gameStore.ts | 0 backend/{ => src}/routes/gameRoutes.js | 0 backend/{ => src}/uno-game-engine/deck.ts | 0 backend/{ => src}/uno-game-engine/engine.ts | 0 backend/{ => src}/uno-game-engine/gameEvents.ts | 0 backend/tests/deck.test.ts | 2 +- backend/tests/engine.test.ts | 2 +- 9 files changed, 2 insertions(+), 2 deletions(-) rename backend/{ => src}/controllers/gameControllers.js (100%) rename backend/{ => src}/eventRecipients.ts (100%) rename backend/{ => src}/gameStore.ts (100%) rename backend/{ => src}/routes/gameRoutes.js (100%) rename backend/{ => src}/uno-game-engine/deck.ts (100%) rename backend/{ => src}/uno-game-engine/engine.ts (100%) rename backend/{ => src}/uno-game-engine/gameEvents.ts (100%) diff --git a/backend/controllers/gameControllers.js b/backend/src/controllers/gameControllers.js similarity index 100% rename from backend/controllers/gameControllers.js rename to backend/src/controllers/gameControllers.js diff --git a/backend/eventRecipients.ts b/backend/src/eventRecipients.ts similarity index 100% rename from backend/eventRecipients.ts rename to backend/src/eventRecipients.ts diff --git a/backend/gameStore.ts b/backend/src/gameStore.ts similarity index 100% rename from backend/gameStore.ts rename to backend/src/gameStore.ts diff --git a/backend/routes/gameRoutes.js b/backend/src/routes/gameRoutes.js similarity index 100% rename from backend/routes/gameRoutes.js rename to backend/src/routes/gameRoutes.js diff --git a/backend/uno-game-engine/deck.ts b/backend/src/uno-game-engine/deck.ts similarity index 100% rename from backend/uno-game-engine/deck.ts rename to backend/src/uno-game-engine/deck.ts diff --git a/backend/uno-game-engine/engine.ts b/backend/src/uno-game-engine/engine.ts similarity index 100% rename from backend/uno-game-engine/engine.ts rename to backend/src/uno-game-engine/engine.ts diff --git a/backend/uno-game-engine/gameEvents.ts b/backend/src/uno-game-engine/gameEvents.ts similarity index 100% rename from backend/uno-game-engine/gameEvents.ts rename to backend/src/uno-game-engine/gameEvents.ts diff --git a/backend/tests/deck.test.ts b/backend/tests/deck.test.ts index 502a3f9..686b7c4 100644 --- a/backend/tests/deck.test.ts +++ b/backend/tests/deck.test.ts @@ -2,7 +2,7 @@ import { shuffle, makeCard, getShuffledCardDeck, -} from '../uno-game-engine/deck'; +} from '../src/uno-game-engine/deck'; describe('getShuffledCardDeck', () => { test('should return an array of 108 cards', () => { diff --git a/backend/tests/engine.test.ts b/backend/tests/engine.test.ts index 1fd7312..2d6225f 100644 --- a/backend/tests/engine.test.ts +++ b/backend/tests/engine.test.ts @@ -1,4 +1,4 @@ -import { GameEngine } from '../uno-game-engine/engine'; +import { GameEngine } from '../src/uno-game-engine/engine'; describe('testing drawCardFromDeck()', () => { test('draws a card when deck is empty but thrownCards is not', () => { From 4c08d1886dabe82c935ab57ff6a867a083c0599e Mon Sep 17 00:00:00 2001 From: Kislay Date: Tue, 4 Jun 2024 14:43:29 +0530 Subject: [PATCH 06/15] utils: Create module. Added the catchError function to be used with controllers. --- backend/src/utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 backend/src/utils.ts diff --git a/backend/src/utils.ts b/backend/src/utils.ts new file mode 100644 index 0000000..316e774 --- /dev/null +++ b/backend/src/utils.ts @@ -0,0 +1,13 @@ +import { Request, Response } from 'express'; + +type ControllerFunction = (req: Request, res: Response) => Promise; + +export function catchError(fn: ControllerFunction): ControllerFunction { + return async function (req: Request, res: Response) { + try { + return await fn(req, res); + } catch (error) { + res.status(500).json({ error: error }); + } + }; +} From 652a0ec53d35da8817c8d464b384e6f6ab1ac11d Mon Sep 17 00:00:00 2001 From: Shivansh Bhatnagar Date: Tue, 4 Jun 2024 14:40:40 +0530 Subject: [PATCH 07/15] workflows: Add workflow for self-assigning an issue A contributor can self-assign an issue by posting `/assign` on the issue thread. credits @shivansh-bhatnagar18 --- .github/workflows/assign-on-comment.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/assign-on-comment.yml diff --git a/.github/workflows/assign-on-comment.yml b/.github/workflows/assign-on-comment.yml new file mode 100644 index 0000000..cc691c0 --- /dev/null +++ b/.github/workflows/assign-on-comment.yml @@ -0,0 +1,18 @@ +# .github/workflows/take.yml +name: Assign issue to contributor +on: + issue_comment: + +jobs: + assign: + name: Take an issue + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: take the issue + uses: bdougie/take-action@main + with: + message: Thanks for taking this issue! Let us know if you have any questions! + trigger: /assign + token: ${{ secrets.ISSUE_TOKEN }} \ No newline at end of file From bc535e764d27c96d3dcbca1e7ecdadc68882c034 Mon Sep 17 00:00:00 2001 From: Kislay Date: Tue, 4 Jun 2024 15:02:45 +0530 Subject: [PATCH 08/15] docs: Update info on maintainer reviews. --- CONTRIBUTING.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b37432..01c2b5a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ Given that you have already forked the repository and set it up locally: ```bash git fetch upstream - git checkout -b upstream/main + git checkout -b upstream/master ``` 6. **Implement Your Fix**: @@ -60,7 +60,7 @@ Given that you have already forked the repository and set it up locally: ``` 10. **Open a Pull Request**: - - Open a pull request (PR) against the `main` branch of the original repository. Provide a clear description of your changes and reference the issue number you are fixing. Fill the self review checklist. You should only solve one issue in one PR. + - Open a pull request (PR) against the `master` branch of the original repository. Provide a clear description of your changes and reference the issue number you are fixing. Fill the self review checklist. You should only solve one issue in one PR. 11. **Address Review Comments**: - If maintainers suggests changes to your code, make the necessary updates and push the changes to your branch. The fix/changes should not be in a separate commit - rather the original commit must be modified force-pushed to the branch. If merge conflicts arise, use `git rebase` to resolve them. See the section on [editing commit history](#editing-commit-history) for more details. @@ -70,9 +70,9 @@ Given that you have already forked the repository and set it up locally: ## Points to remember during contribution - Editing commit history and rebasing are very valuable tools for keeping the commit history clean and easy to understand. Please familiarize yourself with these concepts before contributing. In any case, the seniors will be there to help you out. -- Before starting work, run `git fetch upstream` and then `git rebase upstream/master`, to rebase your branch on top of the main branch. This will help you avoid merge conflicts, and sync your branch with the main branch. +- Before starting work, run `git fetch upstream` and then `git rebase upstream/master`, to rebase your branch on top of the master branch. This will help you avoid merge conflicts, and sync your branch with the master branch. - Addressing reviews on existing PRs is more important than creating new PRs. Please be responsive to the feedback and make the necessary updates. -- Create a new branch for each issue you are working on. This will help you keep your changes isolated and make it easier to manage multiple PRs. The branch should be created from upstream/master, and only after fetching the latest changes from the main branch from upstream first. +- Create a new branch for each issue you are working on. This will help you keep your changes isolated and make it easier to manage multiple PRs. The branch should be created from upstream/master, and only after fetching the latest changes from the master branch from upstream first. ## How to make a good Pull Request - Make sure your PR is solving only one issue. If you are solving multiple issues, create separate PRs for each issue. @@ -83,6 +83,7 @@ Given that you have already forked the repository and set it up locally: - ESLint checks (There should be no eslint errors at least in the files you have modified.) - Prettier checks - Unit tests + - The commit history should be clean, concise and descriptive. It must match the format specified in [CONVENTIONS.md](CONVENTIONS.md#commit-message-guidelines). ## Common Git Operations you may need to perform @@ -169,6 +170,20 @@ All contributions go through a code review process to ensure the quality and mai When you open a pull request, you can request a review from the maintainers. You can also request a review after making changes in response to feedback. The requested reviewer may then review the PR themselves or delegate it to another maintainer. When requesting a review, make sure that your PR doesn't have merge conflicts. If it does, resolve the conflicts before requesting a review. +After a round of code review, it is your duty to: +- Reply to all comments made by the reviewer. If you disagree with a comment, you can discuss it with the reviewer. +- Make the necessary changes to your code. +- Tag the reviewer in the PR thread if you want to discuss something with them. +- Request a review again after making the changes. (Using the `Request Review` button on the PR page.) + +Any change suggested by the reviewer should be made in the same commit, unless specified. The commit message should +be a summary of the changes you made. (Do not include things like `removed comment after review`). + + +# NOTE +- If you committed a bug, do not make another commit fixing it. Instead, amend the original commit using `git commit --amend`. This will add the changes to the original commit. +- Add tests for a function implemented, in the same commit as the implementation. + Your PR will be merged only after the maintainers approve it. Different areas of codebase are handled by different maintainers. ## Code of Conduct From 9360849315392482b00fc901917090f975c395c4 Mon Sep 17 00:00:00 2001 From: Divyansh Seth Date: Mon, 3 Jun 2024 03:00:14 +0530 Subject: [PATCH 09/15] errorPage: created 404 Page Not Found page This commit adds a UNO theme 404 page not found page. Ensured the page is fully responsive for various devices. Included Page not Found error message and navigation link back to the homepage. Fixes:#21 --- frontend/src/assets/tornCard.svg | 310 +++++++++++++++++++++++++++++++ frontend/src/pages/Error.tsx | 27 ++- 2 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 frontend/src/assets/tornCard.svg diff --git a/frontend/src/assets/tornCard.svg b/frontend/src/assets/tornCard.svg new file mode 100644 index 0000000..41c7436 --- /dev/null +++ b/frontend/src/assets/tornCard.svg @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/Error.tsx b/frontend/src/pages/Error.tsx index 7a93fd7..6f6b8dd 100644 --- a/frontend/src/pages/Error.tsx +++ b/frontend/src/pages/Error.tsx @@ -1,10 +1,31 @@ import { Link } from 'react-router-dom'; +import ErrorImage from '../assets/tornCard.svg'; function Error() { return ( -
- This page does not exist! - Go back to homepage +
+
+
+

404

+

+ Page Not Found +

+
+ 404 +
+

+ Sorry, the page you're looking for can't be found. + Please return to the Homepage! +

+ +
+
); } From 19abe9ba505738cba368183342c6c4c4ba5aed69 Mon Sep 17 00:00:00 2001 From: sksmagr23 Date: Tue, 4 Jun 2024 15:10:31 +0530 Subject: [PATCH 10/15] frontend: Added a Playoptions page This commit add a playoptions page that renders onn navigating to /play. It has been created using Heading and button components of library and custom styling is done using Tailwind css and also added a UNO game logo, the page is fully responsive with a uno theme design Fixes-#23 --- frontend/public/UNO_Logo.svg | 175 +++++++++++++++++++++++++++++ frontend/src/index.css | 4 + frontend/src/library/button.tsx | 9 +- frontend/src/library/heading.tsx | 13 +++ frontend/src/pages/PlayOptions.tsx | 65 ++++++++--- 5 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 frontend/public/UNO_Logo.svg diff --git a/frontend/public/UNO_Logo.svg b/frontend/public/UNO_Logo.svg new file mode 100644 index 0000000..278a3ed --- /dev/null +++ b/frontend/public/UNO_Logo.svg @@ -0,0 +1,175 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index b5c61c9..a168df0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; + +body::-webkit-scrollbar { + display: none; +} diff --git a/frontend/src/library/button.tsx b/frontend/src/library/button.tsx index d9f5396..319076e 100644 --- a/frontend/src/library/button.tsx +++ b/frontend/src/library/button.tsx @@ -11,7 +11,14 @@ type ButtonProps = { }; function Button({ onClick, children }: ButtonProps) { - return ; + return ( + + ); } export default Button; diff --git a/frontend/src/library/heading.tsx b/frontend/src/library/heading.tsx index e69de29..510cf54 100644 --- a/frontend/src/library/heading.tsx +++ b/frontend/src/library/heading.tsx @@ -0,0 +1,13 @@ +type InputProps = { + name?: string; +}; + +const heading = ({ name }: InputProps) => { + return ( +
+

{name}

+
+ ); +}; + +export default heading; diff --git a/frontend/src/pages/PlayOptions.tsx b/frontend/src/pages/PlayOptions.tsx index 6600ba6..df10c29 100644 --- a/frontend/src/pages/PlayOptions.tsx +++ b/frontend/src/pages/PlayOptions.tsx @@ -1,26 +1,61 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import Input from '../library/input'; import Button from '../library/button'; +import Heading from '../library/heading'; +import '../index.css'; -function PlayOptions() { +const PlayOptions = () => { + const [gameCode, setGameCode] = useState(''); const navigate = useNavigate(); - function handleCreateGame() { - console.log('Creating a new game'); + const CreateGame = () => { + // Logic to create a game + console.log('Create Game'); navigate('/game'); - } + }; + + const JoinGame = () => { + // Logic to join a game + console.log('Join Game with code:', gameCode); + }; return ( - <> -
-

Create a new game

- -
-
-

Join a game

- +
+
+
+ UNO Logo +

+ Ready for Action? +

+
+
+
+
+ +
+ +
+
+
+ +
+
+ setGameCode(e.target.value)} + className="border-2 border-red-600 rounded-lg p-1 mb-4 text-md w-full bg-black text-white" + /> +
+ +
+
- +
); -} +}; export default PlayOptions; From 8dc54494fd76fee451499363023341257273cba1 Mon Sep 17 00:00:00 2001 From: ritwik-69 <72665321+ritwik-69@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:22:30 +0530 Subject: [PATCH 11/15] client:Design a Toast Component This commit creates the toast component with customizable position,duration and color. In main.tsx wrapped the app component in toastcontextprovider so that toast can be used everywhere in the app. fixes:#68 --- frontend/src/library/toast/Toast.tsx | 138 +++++++++++++++ frontend/src/library/toast/toast-context.ts | 20 +++ frontend/src/library/toast/toast.css | 181 ++++++++++++++++++++ frontend/src/main.tsx | 5 +- 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 frontend/src/library/toast/Toast.tsx create mode 100644 frontend/src/library/toast/toast-context.ts create mode 100644 frontend/src/library/toast/toast.css diff --git a/frontend/src/library/toast/Toast.tsx b/frontend/src/library/toast/Toast.tsx new file mode 100644 index 0000000..90e69cc --- /dev/null +++ b/frontend/src/library/toast/Toast.tsx @@ -0,0 +1,138 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ToastContext } from './toast-context'; +import './toast.css'; + +function useTimeout(callback: () => void, duration: number) { + const savedCallback = useRef(callback); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + const functionId = setTimeout(() => savedCallback.current(), duration); + + return () => { + clearTimeout(functionId); + }; + }, [duration]); +} +type toastProperties = { + message: string; + close: () => void; + duration: number; + position: string; + color: string; +}; + +export function Toast({ + message, + close, + duration, + position, + color, +}: toastProperties) { + useTimeout(() => { + close(); + }, duration); + return ( +
+

{message}

+ +
+ ); +} + +type ToastProviderProperties = { + children: React.ReactElement; +}; +type ToastType = { + message: string; + id: number; + duration: number; + position: string; + color: string; +}; + +export function ToastProvider({ children }: ToastProviderProperties) { + const [toasts, setToasts] = useState([]); + const [position, setPosition] = useState('top-left'); + type Options = { + message?: string; + duration?: number; + position?: string; + color?: 'info' | 'warning' | 'error' | 'success'; + }; + const openToast = useCallback( + ({ + message = '', + duration = 5000, + position = 'top-center', + color = 'info', + }: Options = {}) => { + const newToast = { + message: message, + id: Date.now(), + duration: duration, + position: position, + color: color, + }; + setToasts((prevToast) => [...prevToast, newToast]); + setPosition(position); + }, + [] + ); + + const closeToast = useCallback((id: number) => { + setTimeout(() => { + setToasts((prevToasts) => + prevToasts.filter((toast) => toast.id !== id) + ); + }, 300); + + setToasts((toasts) => { + return toasts.map((toast) => { + if (toast.id === id) { + if (toast.position == 'top-left') + toast.position = 'fade-out-left'; + else if (toast.position == 'top-right') + toast.position = 'fade-out-right'; + else if (toast.position == 'top-center') + toast.position = 'fade-out-center'; + } + return toast; + }); + }); + }, []); + const contextValue = useMemo( + () => ({ + open: openToast, + close: closeToast, + }), + [openToast, closeToast] + ); + return ( + + {children} +
+ {toasts && + toasts.map((toast) => { + return ( + { + closeToast(toast.id); + }} + duration={toast.duration} + position={toast.position} + color={toast.color} + /> + ); + })} +
+
+ ); +} diff --git a/frontend/src/library/toast/toast-context.ts b/frontend/src/library/toast/toast-context.ts new file mode 100644 index 0000000..625bc97 --- /dev/null +++ b/frontend/src/library/toast/toast-context.ts @@ -0,0 +1,20 @@ +import { createContext, useContext } from 'react'; + +type Options = { + message?: string; + duration?: number; + position?: string; + color?: 'info' | 'warning' | 'error' | 'success'; +}; + +type ToastContextValue = { + open: (options?: Options) => void; + close: (id: number) => void; +}; + +export const ToastContext = createContext({ + open: () => {}, + close: () => {}, +}); + +export const useToast = () => useContext(ToastContext); diff --git a/frontend/src/library/toast/toast.css b/frontend/src/library/toast/toast.css new file mode 100644 index 0000000..42cf727 --- /dev/null +++ b/frontend/src/library/toast/toast.css @@ -0,0 +1,181 @@ +.toasts { + display: flex; + flex-direction: column; + gap: 10px; +} + +.toast { + color: black; + border-radius: 5px; + padding: 10px 10px; + width: 300px; + position: relative; + display: flex; +} + +.top-right { + position: fixed; + top: 10px; + right: 10px; +} + +.top-left { + position: fixed; + top: 10px; + left: 10px; +} +.top-center { + position: fixed; + top: 10px; + left: 38%; +} + +.top-center-animation { + animation-name: slideinCenter; + animation-duration: 0.35s; +} + +.top-right-animation { + animation-name: slideinRight; + animation-duration: 0.35s; +} + +.top-left-animation { + animation-name: slideinLeft; + animation-duration: 0.35s; +} + +@keyframes slideinRight { + 0% { + transform: translateX(100%); + } + 60% { + transform: translateX(-15%); + } + 80% { + transform: translateX(5%); + } + 80% { + transform: translateX(0); + } +} +@keyframes slideinCenter { + 0% { + transform: translateY(-100%); + } + 60% { + transform: translateY(15%); + } + 80% { + transform: translateY(-5%); + } + 80% { + transform: translateY(0); + } +} + +@keyframes slideinLeft { + 0% { + transform: translateX(-100%); + } + 60% { + transform: translateX(15%); + } + 80% { + transform: translateX(-5%); + } + 80% { + transform: translateX(0); + } +} + +.fade-out-left-animation { + animation-name: fade-out-left; + animation-duration: 0.35s; +} + +.fade-out-right-animation { + animation-name: fade-out-right; + animation-duration: 0.35s; +} + +.fade-out-center-animation { + animation-name: fade-out-center; + animation-duration: 0.35s; +} + +@keyframes fade-out-left { + 0% { + transform: translateX(0%); + } + + 60% { + transform: translateX(-100%); + } + + 80% { + transform: translateX(-195%); + } + + 100% { + transform: translateX(-200%); + } +} +@keyframes fade-out-right { + 0% { + transform: translateX(0%); + } + + 60% { + transform: translateX(100%); + } + + 80% { + transform: translateX(195%); + } + + 100% { + transform: translateX(200%); + } +} + +@keyframes fade-out-center { + 0% { + transform: translateY(-100%); + } + 30% { + transform: translateY(-300%); + } + 80% { + transform: translateY(-700%); + } + 100% { + transform: translateY(-1000%); + } +} + +.info { + background-color: cyan; +} + +.success { + background-color: #5cb85c; +} + +.error { + background-color: #d9534f; +} +.warning { + background-color: #f0ad4e; +} + +.close-button { + position: absolute; + right: 0px; + top: 0px; + padding: 0px 5px; + background: none; + cursor: pointer; + border: transparent; + color: black; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 12afe2b..a3323d8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,9 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; +import { ToastProvider } from './library/toast/Toast.tsx'; ReactDOM.createRoot(document.getElementById('root')!).render( - + + + ); From 3c7dc8c7114a7513f62441f0e5a1997d119f2561 Mon Sep 17 00:00:00 2001 From: sksmagr23 Date: Fri, 7 Jun 2024 11:06:08 +0530 Subject: [PATCH 12/15] frontend: Added a customizable heading component I have created a highly customizable and reusable header component in src/library, and the styling is done using tailwind css and added various input fields as props. Fixes: #67 --- frontend/src/library/heading.tsx | 28 ++++++++++++++++++++++++---- frontend/src/pages/PlayOptions.tsx | 30 +++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/frontend/src/library/heading.tsx b/frontend/src/library/heading.tsx index 510cf54..7878f12 100644 --- a/frontend/src/library/heading.tsx +++ b/frontend/src/library/heading.tsx @@ -1,13 +1,33 @@ +import React from 'react'; + type InputProps = { - name?: string; + text: string; + fontSize?: string; + fontWeight?: string; + fontStyle?: string; + textColor?: string; + textAlign?: string; + className?: string; }; -const heading = ({ name }: InputProps) => { +const Heading: React.FC = ({ + text, + fontSize = 'text-sm', // default value + fontWeight = 'font-normal', // default value + fontStyle = 'font-sans', // default value + textColor = 'text-black', // default value + textAlign = 'text-center', // default value + className = '', // additional custom classes +}) => { return (
-

{name}

+

+ {text} +

); }; -export default heading; +export default Heading; diff --git a/frontend/src/pages/PlayOptions.tsx b/frontend/src/pages/PlayOptions.tsx index df10c29..ef531b1 100644 --- a/frontend/src/pages/PlayOptions.tsx +++ b/frontend/src/pages/PlayOptions.tsx @@ -27,20 +27,36 @@ const PlayOptions = () => { alt="UNO Logo" className="h-12 w-auto mr-2" /> -

- Ready for Action? -

+
-
+
- +
-
+
- +
Date: Fri, 7 Jun 2024 11:06:08 +0530 Subject: [PATCH 13/15] Server: Route Handler For /event The getAllClients function has been added in eventReceipts.ts to return an array of client IDs. The handleEvent function has been implemented in gameControllers.js to handle event propagation. This function retrieves all client IDs and schedules the event sending to all clients. The routing in gameRoutes.js has been set up to handle the /events endpoint, including both GET and POST handlers. Fixes #45 --- backend/src/controllers/gameControllers.js | 22 +++++++++++++++++++++- backend/src/eventRecipients.ts | 4 ++++ backend/src/routes/gameRoutes.js | 7 +++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/gameControllers.js b/backend/src/controllers/gameControllers.js index a4d73c4..14cdcde 100644 --- a/backend/src/controllers/gameControllers.js +++ b/backend/src/controllers/gameControllers.js @@ -1 +1,21 @@ -// Implement the handler for `/events` endpoint. Refer to ARCHITECTURE.md for implementation details. +import { getAllClients, scheduleSend } from '../eventRecipients'; + +async function handleEvent(req, res) { + const event = req.body; + + const clientIds = getAllClients(); + + const eligibleClientIds = filterEligibleClients(clientIds); + + eligibleClientIds.forEach((clientId) => { + scheduleSend(clientId, event); + }); + + res.status(200).send({ message: 'Event propagated to clients.' }); +} + +function filterEligibleClients(clientIds) { + return clientIds; +} + +export default handleEvent; diff --git a/backend/src/eventRecipients.ts b/backend/src/eventRecipients.ts index 9c97f9e..64db5c4 100644 --- a/backend/src/eventRecipients.ts +++ b/backend/src/eventRecipients.ts @@ -28,6 +28,10 @@ export function getClient(clientId: ClientId) { return clients.get(clientId); } +export function getAllClients(): ClientId[] { + return Array.from(clients.keys()); +} + // eslint-disable-next-line @typescript-eslint/no-unused-vars export function scheduleSend(clientId: ClientId, event: AppEvent) { //todo: Enqueue the event for sending. diff --git a/backend/src/routes/gameRoutes.js b/backend/src/routes/gameRoutes.js index fa43827..7864512 100644 --- a/backend/src/routes/gameRoutes.js +++ b/backend/src/routes/gameRoutes.js @@ -1,10 +1,13 @@ import express from 'express'; import { addClient } from '../eventRecipients'; +import handleEvent from '../controllers/gameControllers'; + const router = express.Router(); router.get('/events', (req, res) => { addClient('user_id', res); }); -// the post handler should retrieve the game the user is currently in, and update the game state. -// The request body contains the event data, as described in ARCHITECTURE.md +router.post('/events', handleEvent); + +export default router; From 4acd8fe9ce68bd28f198798e4c88ed444e28cd29 Mon Sep 17 00:00:00 2001 From: sksmagr23 Date: Fri, 7 Jun 2024 11:06:08 +0530 Subject: [PATCH 14/15] Server: Implement doSendEvents Function The doSendEvents function has been added to eventRecipients.ts. This function sends the events array as the response body and clears the event queue after sending. Additionally, the client is removed from the clients map after events are sent. Fixes: #61 --- backend/src/eventRecipients.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/src/eventRecipients.ts b/backend/src/eventRecipients.ts index 64db5c4..c96fefd 100644 --- a/backend/src/eventRecipients.ts +++ b/backend/src/eventRecipients.ts @@ -37,7 +37,13 @@ export function scheduleSend(clientId: ClientId, event: AppEvent) { //todo: Enqueue the event for sending. } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function doSendEvent(clientId: ClientId) { - //todo: Send all the events in the queue to the client, only if the response object is available. +export function doSendEvents(clientId: ClientId) { + const res = clients.get(clientId); + const events = eventQueue.get(clientId) || []; + + if (res && events.length > 0) { + res.json({ events }); + eventQueue.set(clientId, []); + clients.delete(clientId); + } } From b2f5d81432bbb48f7645f411411974142299fd3e Mon Sep 17 00:00:00 2001 From: Kislay Date: Fri, 7 Jun 2024 23:31:01 +0530 Subject: [PATCH 15/15] gameRoutes: Convert to TypeScript. --- backend/src/routes/{gameRoutes.js => gameRoutes.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/routes/{gameRoutes.js => gameRoutes.ts} (100%) diff --git a/backend/src/routes/gameRoutes.js b/backend/src/routes/gameRoutes.ts similarity index 100% rename from backend/src/routes/gameRoutes.js rename to backend/src/routes/gameRoutes.ts