Skip to content

Commit

Permalink
GameEvent: created Announce UNO event
Browse files Browse the repository at this point in the history
Details:
- Created `announced_uno` event.
- Added `runningEvents` with `vulnerableToUNO` and `hasAnnouncedUNO` subfields in engine.ts.
- Created `getThrowableCards()` in eventHandlerUtils.
- Handled corner cases related to the `announced_uno` event.
- Created tests for the `announced_uno` event and its corner cases.

Fixes: #132
  • Loading branch information
sethdivyansh authored and kuv2707 committed Jun 22, 2024
1 parent 3a8332e commit fbb9905
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 23 deletions.
17 changes: 13 additions & 4 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type SpecialCardName =
| 'draw2'
| 'draw4'
| 'colchange';

export type CardNumber =
| '0'
| '1'
Expand All @@ -37,6 +38,11 @@ export type Player = {
cards: UNOCard[];
};

export type RunningEvents = {
vulnerableToUNO: Player | null;
hasAnnouncedUNO: Player | null;
};

export type EventResult = {
type: 'SUCCESS' | 'ERROR';
message: string;
Expand All @@ -47,16 +53,14 @@ export enum GameEventTypes {
THROW_CARD = 'THROW_CARD',
JOIN_GAME = 'JOIN_GAME',
LEAVE_GAME = 'LEAVE_GAME',
ANNOUNCE_UNO = 'ANNOUNCE_UNO',
STATE_SYNC = 'STATE_SYNC',
}

export type GameEvent =
| {
type: GameEventTypes.DRAW_CARD;
playerId: string;
data: {
cardId: string;
};
data?: null;
}
| {
type: GameEventTypes.THROW_CARD;
Expand All @@ -75,6 +79,11 @@ export type GameEvent =
playerId: string;
data?: null;
}
| {
type: GameEventTypes.ANNOUNCE_UNO;
playerId: string;
data?: null;
}
| {
type: GameEventTypes.STATE_SYNC;
data: {
Expand Down
17 changes: 15 additions & 2 deletions backend/src/uno-game-engine/engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { EventResult, GameEvent, Player, UNOCard } from '../types';
import type {
EventResult,
GameEvent,
Player,
RunningEvents,
UNOCard,
} from '../types';
import { getShuffledCardDeck, shuffle } from './deck';
import { handleEvent } from './gameEvents';

Expand All @@ -14,6 +20,7 @@ export class GameEngine {
currentColor: number;
direction: number;
status: 'READY' | 'STARTED';
runningEvents: RunningEvents;

constructor(id: string) {
this.id = id;
Expand All @@ -24,6 +31,10 @@ export class GameEngine {
this.lastThrownCard = null;
this.currentColor = 0;
this.direction = 1;
this.runningEvents = {
vulnerableToUNO: null,
hasAnnouncedUNO: null,
};

this.status = 'READY';
}
Expand Down Expand Up @@ -51,7 +62,6 @@ export class GameEngine {
(this.players.length + this.currentPlayerIndex + this.direction) %
this.players.length;
}

removePlayer(player: Player) {
this.cardDeck.push(...player.cards);
shuffle(this.cardDeck);
Expand All @@ -73,6 +83,9 @@ export class GameEngine {
const cards = this.cardDeck.splice(-numCards, numCards);
if (cards.length > 0) {
currentPlayer.cards.push(...cards);

// make the last player no longer vulnerable to UNO
this.runningEvents.vulnerableToUNO = null;
return {
type: 'SUCCESS',
message: `${numCards} card(s) drawn successfully`,
Expand Down
39 changes: 39 additions & 0 deletions backend/src/uno-game-engine/events/announceUno.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// The game continues until a player has one or two card(s) left.
// Once a player has two cards, they can announce "UNO!".
// The announcement of "UNO" needs to be repeated every time the player is left with one or two cards.
import assert from 'assert';
import { EventResult, GameEvent, GameEventTypes, Player } from '../../types';
import { GameEngine } from '../engine';
import { getPlayer, getThrowableCards } from './eventHandlerUtils';

export function canAnnounceUNO(game: GameEngine, player: Player): EventResult {
const throwableCards = getThrowableCards(game, player);
if (
player.cards.length > 2 ||
(player.cards.length === 2 && !throwableCards)
) {
return { type: 'ERROR', message: 'Cannot announce UNO' };
}

return { type: 'SUCCESS', message: 'Can announce UNO' };
}

export function announceUNO(game: GameEngine, event: GameEvent): EventResult {
assert(event.type === GameEventTypes.ANNOUNCE_UNO, 'Invalid event type');
const player = getPlayer(game, event.playerId);
if (!player) {
return { type: 'ERROR', message: 'Player not found' };
}

const canAnnounce = canAnnounceUNO(game, player);

if (canAnnounce.type === 'ERROR') {
return canAnnounce;
}

// if the player has announced UNO, they are no longer vulnerable to UNO
game.runningEvents.hasAnnouncedUNO = player;
game.runningEvents.vulnerableToUNO = null;

return { type: 'SUCCESS', message: 'UNO announced successfully' };
}
5 changes: 5 additions & 0 deletions backend/src/uno-game-engine/events/drawCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export function drawCard(game: GameEngine, event: GameEvent): EventResult {
return drawResult;
}

if (player.cards.length === 2) {
game.runningEvents.hasAnnouncedUNO = null;
game.runningEvents.vulnerableToUNO = null;
}

const drawnCard = player.cards[player.cards.length - 1]; // Last drawn card

// Check if the drawn card can be thrown
Expand Down
20 changes: 19 additions & 1 deletion backend/src/uno-game-engine/events/eventHandlerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ export function getPlayerCard(player: Player, cardId: string) {
return player.cards.find((c) => c.id === cardId);
}

export function getThrowableCards(game: GameEngine, player: Player) {
// get the cards that the player can throw
const { lastThrownCard } = game;
const throwableCards = player.cards.filter((card) => {
return (
!lastThrownCard ||
card.color === lastThrownCard.color ||
card.value === lastThrownCard.value ||
card.type === 'wild'
);
});

return throwableCards;
}

export function checkCurrentPlayer(
game: GameEngine,
player: Player
Expand All @@ -23,5 +38,8 @@ export function checkCurrentPlayer(
return { type: 'ERROR', message: 'It is not your turn' };
}

return { type: 'SUCCESS', message: 'Can draw/throw card' };
return {
type: 'SUCCESS',
message: 'Player is the current player',
};
}
5 changes: 4 additions & 1 deletion backend/src/uno-game-engine/events/joinGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { EventResult, GameEvent, Player } from '../../types';

export function joinGame(game: GameEngine, event: GameEvent): EventResult {
assert(event.type === 'JOIN_GAME', 'Invalid event type');
const player: Player = { id: event.playerId, cards: [] };
const player: Player = {
id: event.playerId,
cards: [],
};
game.addPlayer(player);
return { type: 'SUCCESS', message: 'player joined successfully' };
}
28 changes: 15 additions & 13 deletions backend/src/uno-game-engine/events/throwCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
checkCurrentPlayer,
getPlayer,
getPlayerCard,
getThrowableCards,
} from './eventHandlerUtils';

export function canThrowCard(
Expand All @@ -17,20 +18,9 @@ export function canThrowCard(
if (canThrow.type === 'ERROR') {
return canThrow;
}
// 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'
)
) {
const throwableCards = getThrowableCards(game, player);
if (!throwableCards.find((c) => c.id === card.id)) {
return { type: 'ERROR', message: 'Cannot throw this card' };
}

Expand All @@ -55,8 +45,20 @@ export function throwCard(game: GameEngine, event: GameEvent): EventResult {
return canThrow;
}

// make the last player no longer vulnerable to UNO
game.runningEvents.vulnerableToUNO = null;

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

// After throwing a card, check if the player has one card and forgot to announce UNO
if (
player.cards.length === 1 &&
game.runningEvents.hasAnnouncedUNO !== player
) {
game.runningEvents.vulnerableToUNO = player;
}

game.runningEvents.hasAnnouncedUNO = null;
game.thrownCards.push(card);
game.lastThrownCard = card;

Expand Down
2 changes: 2 additions & 0 deletions backend/src/uno-game-engine/gameEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { EventResult, GameEvent, GameEventTypes } from '../types';
import { type GameEngine } from './engine';
import { announceUNO } from './events/announceUno';
import { drawCard } from './events/drawCard';
import { joinGame } from './events/joinGame';
import { leaveGame } from './events/leaveGame';
Expand Down Expand Up @@ -30,3 +31,4 @@ registerEventHandler(GameEventTypes.JOIN_GAME, joinGame);
registerEventHandler(GameEventTypes.LEAVE_GAME, leaveGame);
registerEventHandler(GameEventTypes.DRAW_CARD, drawCard);
registerEventHandler(GameEventTypes.THROW_CARD, throwCard);
registerEventHandler(GameEventTypes.ANNOUNCE_UNO, announceUNO);
7 changes: 5 additions & 2 deletions backend/tests/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
NUM_CARDS_PER_PLAYER,
} from '../src/uno-game-engine/engine';

function generateMockPlayers(numPlayers: number) {
export function generateMockPlayers(numPlayers: number) {
const players: Player[] = [];
for (let i = 0; i < numPlayers; i++) {
players.push({ id: i.toString(), cards: [] });
Expand Down Expand Up @@ -86,7 +86,10 @@ describe('GameEngine', () => {
});

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

0 comments on commit fbb9905

Please sign in to comment.