Skip to content

Commit

Permalink
frontend: added error page
Browse files Browse the repository at this point in the history
added an error page which is rendered when an unknown route is accessed

Fixes: shivansh-bhatnagar18#21
  • Loading branch information
PrathamX595 committed Jun 7, 2024
2 parents 41d906e + b2f5d81 commit 5115c26
Show file tree
Hide file tree
Showing 27 changed files with 1,286 additions and 81 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/assign-on-comment.yml
Original file line number Diff line number Diff line change
@@ -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 }}
60 changes: 56 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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.
23 changes: 19 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Given that you have already forked the repository and set it up locally:

```bash
git fetch upstream
git checkout -b <your branch name> upstream/main
git checkout -b <your branch name> upstream/master
```

6. **Implement Your Fix**:
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion backend/controllers/gameControllers.js

This file was deleted.

18 changes: 0 additions & 18 deletions backend/eventRecipients.ts

This file was deleted.

21 changes: 21 additions & 0 deletions backend/src/controllers/gameControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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;
49 changes: 49 additions & 0 deletions backend/src/eventRecipients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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 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';

type ClientId = string;

const clients = new Map<ClientId, Response>();
const eventQueue = new Map<ClientId, AppEvent[]>();

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 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.
}

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);
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 7 additions & 4 deletions backend/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const colors: Array<CardColor> = ['red', 'yellow', 'green', 'blue'];
const values = [
const colors: Array<CardColor> = ['red', 'yellow', 'green', 'blue', 'wild'];
const numValues: Array<CardNumber> = [
'0',
'1',
'2',
Expand All @@ -10,12 +10,9 @@ const values = [
'7',
'8',
'9',
'skip',
'reverse',
'draw2',
];
const specialCards = ['wild', 'draw4'];
const deck = [];
const specialValues: Array<SpecialCardName> = ['skip', 'reverse', 'draw2'];
const wildCardValues: Array<SpecialCardName> = ['colchange', 'draw4'];
const sameCardCount = []; // to keep track of same cards in assigning unique id to each card

/**
Expand All @@ -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<UNOCard> {
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<UNOCard> = [];

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getShuffledCardDeck } from './deck';
import { getShuffledCardDeck, shuffle } from './deck';
import { handleEvent } from './gameEvents';

const NUM_CARDS_PER_PLAYER = 7;
Expand Down Expand Up @@ -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
Expand Down
File renamed without changes.
Loading

0 comments on commit 5115c26

Please sign in to comment.