Skip to content

Commit

Permalink
gameEvent: Implement Throw Card Event
Browse files Browse the repository at this point in the history
Adds the throw card event handler.

Fixes #44

Signed-off-by: Sagnik Mandal <[email protected]>
  • Loading branch information
criticic committed Jun 12, 2024
1 parent d0f7164 commit 435d475
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 16 deletions.
9 changes: 4 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ format of the message is:

```json
{
"type": "DRAW_CARD",
"player": "player1",
"data": {
"cardID": "red-5"
}
"type": "THROW_CARD",
"playerId": "1",
"cardId": "card-number-red-5",
}
```
Other possible values for `type` are `DRAW_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.

Expand Down
19 changes: 11 additions & 8 deletions backend/src/uno-game-engine/engine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getShuffledCardDeck, shuffle } from './deck';
import { handleEvent } from './gameEvents';

const NUM_CARDS_PER_PLAYER = 7;
export const NUM_CARDS_PER_PLAYER = 7;

export class GameEngine {
cardDeck: UNOCard[];
Expand Down Expand Up @@ -47,9 +47,9 @@ export class GameEngine {
this.currentPlayerIndex =
(this.currentPlayerIndex + this.direction) % this.players.length;
}
drawCardFromDeck(player: Player): EventResult {
drawCardFromDeck(player: Player, numCards = 1): EventResult {
try {
if (this.cardDeck.length === 0) {
if (this.cardDeck.length < numCards) {
this.cardDeck = [...this.thrownCards];
this.thrownCards = [];
shuffle(this.cardDeck);
Expand All @@ -58,15 +58,18 @@ export class GameEngine {
(p: Player) => p.id === player.id
);
if (currentPlayer && this.cardDeck) {
const card = this.cardDeck.pop();
if (card) {
currentPlayer.cards.push(card);
const cards = this.cardDeck.splice(-numCards, numCards);
if (cards.length > 0) {
currentPlayer.cards.push(...cards);
return {
type: 'SUCCESS',
message: 'Card drawn successfully',
message: `${numCards} card(s) drawn successfully`,
};
} else
return { type: 'ERROR', message: 'Unable to draw a card' };
return {
type: 'ERROR',
message: `Unable to draw ${numCards} card(s)`,
};
} else {
return {
type: 'ERROR',
Expand Down
97 changes: 97 additions & 0 deletions backend/src/uno-game-engine/events/throwCard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { registerEventHandler } from '../gameEvents';
import { GameEngine } from '../engine';

function findPlayer(game: GameEngine, playerId: string) {
return game.players.find((p) => p.id === playerId);
}

function findCard(player, cardId: string) {
return player.cards.find((c) => c.id === cardId);
}

function canThrowCard(game: GameEngine, player, card): EventResult {
const { currentPlayerIndex, players } = game;
const currentPlayer = players[currentPlayerIndex];

// check if the player is the current player
if (currentPlayer.id !== player.id) {
return { type: 'ERROR', message: 'It is not your turn' };
}

// check if the player actually possesses the card
if (!player.cards.some((c) => c.id === card.id)) {
return { type: 'ERROR', message: 'Player does not possess the card' };
}

// check if the card can be thrown
if (
game.lastThrownCard &&
!(
game.lastThrownCard.color === card.color ||
game.lastThrownCard.value === card.value ||
card.color === 'wild'
)
) {
return { type: 'ERROR', message: 'Cannot throw this card' };
}

return { type: 'SUCCESS', message: 'Can throw card' };
}

export function throwCard(game: GameEngine, event: GameEvent): EventResult {
const player = findPlayer(game, event.playerId);
if (!player) {
return { type: 'ERROR', message: 'Player not found' };
}

const card = findCard(player, event.data.card.id);
if (!card) {
return { type: 'ERROR', message: 'Card not found' };
}

const canThrow = canThrowCard(game, player, card);
if (canThrow.type === 'ERROR') {
return canThrow;
}

player.cards = player.cards.filter((c) => c.id !== card.id);

game.thrownCards.push(card);
game.lastThrownCard = card;

game.nextPlayer();

// handle special cards
if (card.type === 'special') {
handleSpecialCard(game, card);
}

return { type: 'SUCCESS', message: 'Card thrown successfully' };
}

function handleSpecialCard(game: GameEngine, card: UNOCard) {
switch (card.value) {
case 'skip':
game.nextPlayer();
break;
case 'reverse':
game.direction *= -1;
break;
case 'draw2':
{
const nextPlayer = game.players[game.currentPlayerIndex];
game.drawCardFromDeck(nextPlayer, 2);
}
break;
case 'draw4':
{
const currentPlayer = game.players[game.currentPlayerIndex];
game.drawCardFromDeck(currentPlayer, 4);
}
break;
default:
break;
}
}

registerEventHandler('THROW_CARD', throwCard);
77 changes: 74 additions & 3 deletions backend/tests/engine.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { GameEngine } from '../src/uno-game-engine/engine';
import {
GameEngine,
NUM_CARDS_PER_PLAYER,
} from '../src/uno-game-engine/engine';

function generateMockPlayers(numPlayers: number) {
const players: Player[] = [];
for (let i = 0; i < numPlayers; i++) {
players.push({ id: i.toString(), cards: [] });
}
return players;
}

describe('GameEngine', () => {
let game: GameEngine;

beforeEach(() => {
game = new GameEngine();
});

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 = [
Expand All @@ -18,4 +34,59 @@ describe('testing drawCardFromDeck()', () => {
expect(player.cards.length).toBe(1);
expect(status.type).toBe('SUCCESS');
});

test('allotCards() throws an error when there are not enough cards to distribute', () => {
const MAX_CARDS = 108;
const MAXIMUM_PLAYERS = Math.ceil(MAX_CARDS / NUM_CARDS_PER_PLAYER) + 1;
game.players = generateMockPlayers(MAXIMUM_PLAYERS);
expect(() => game.allotCards()).toThrow(
'Not enough cards to distribute'
);
});

test('addPlayer() adds a player to the game', () => {
game.addPlayer({ id: '1', cards: [] });
expect(game.players.length).toBe(1);
});

test('startGame() throws an error when there are not enough players', () => {
expect(() => game.startGame()).toThrow(
'Not enough players to start the game'
);
});

test('startGame() changes the status to STARTED when there are enough players', () => {
game.players = generateMockPlayers(2);
game.startGame();
expect(game.status).toBe('STARTED');
});

test('nextPlayer() changes the current player index', () => {
game.players = generateMockPlayers(2);

// Initial player index is 0, so next player should be 1
game.nextPlayer();
expect(game.currentPlayerIndex).toBe(1);

// As only 2 players are there, next player should loop back to 0
game.nextPlayer();
expect(game.currentPlayerIndex).toBe(0);
});

test('drawCardFromDeck() draws a card from the deck', () => {
game.players = generateMockPlayers(2);
const player = game.players[0];
const status = game.drawCardFromDeck(player);
expect(game.cardDeck.length).toBe(107);
expect(player.cards.length).toBe(1);
expect(status.type).toBe('SUCCESS');

expect(game.cardDeck).not.toContain(player.cards[0]);
});

test('drawCardFromDeck() returns an error when the player is not found', () => {
const status = game.drawCardFromDeck({ id: '1', cards: [] });
expect(status.type).toBe('ERROR');
expect(status.message).toBe('Player not found or cardDeck is empty');
});
});

0 comments on commit 435d475

Please sign in to comment.