diff --git a/src/game/card.ts b/src/game/card.ts index 0a52410..26df685 100644 --- a/src/game/card.ts +++ b/src/game/card.ts @@ -1,77 +1,24 @@ -type CardValue = -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; +export type Card = + | -2 + | -1 + | 0 + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12; -export type ObfuscatedCardValue = CardValue | "X"; +export type ConcealableCard = Card | null; -type CardColor = - | "darkblue" - | "lightblue" - | "green" - | "yellow" - | "red" - | "black"; - -// TODO: use smarter types like Omit<>, Pick<>, etc. -export type ObfuscatedCard = { - id: number; - name: string; - value: ObfuscatedCardValue; - color: CardColor; -}; - -export class Card { - id: number; - name: string; - value: CardValue; - color: CardColor; - constructor(id: number, value: CardValue) { - this.id = id; - this.value = value; - this.name = `${value} Card`; - this.color = this.matchColorToCardValue(value); - } - - matchColorToCardValue(value: CardValue | ObfuscatedCardValue): CardColor { - switch (value) { - case -2: - return "darkblue"; - case -1: - return "darkblue"; - case 0: - return "lightblue"; - case 1: - return "green"; - case 2: - return "green"; - case 3: - return "green"; - case 4: - return "green"; - case 5: - return "yellow"; - case 6: - return "yellow"; - case 7: - return "yellow"; - case 8: - return "yellow"; - case 9: - return "red"; - case 10: - return "red"; - case 11: - return "red"; - case 12: - return "red"; - case "X": - return "black"; - default: - return "red"; - } - } -} - -export type ObfuscatedCardStack = { - cards: ObfuscatedCard[]; +export type ConcealableCardStack = { + cards: ConcealableCard[]; }; export class CardStack { @@ -84,77 +31,77 @@ export class CardStack { generateCards() { for (let cardNumber = 1; cardNumber <= 150; cardNumber++) { if (cardNumber <= 5) { - this.cards.push(new Card(cardNumber, -2)); + this.cards.push(-2); continue; } if (cardNumber > 5 && cardNumber <= 15) { - this.cards.push(new Card(cardNumber, -1)); + this.cards.push(-1); continue; } if (cardNumber > 15 && cardNumber <= 30) { - this.cards.push(new Card(cardNumber, 0)); + this.cards.push(0); continue; } if (cardNumber > 30 && cardNumber <= 40) { - this.cards.push(new Card(cardNumber, 1)); + this.cards.push(1); continue; } if (cardNumber > 40 && cardNumber <= 50) { - this.cards.push(new Card(cardNumber, 2)); + this.cards.push(2); continue; } if (cardNumber > 50 && cardNumber <= 60) { - this.cards.push(new Card(cardNumber, 3)); + this.cards.push(3); continue; } if (cardNumber > 60 && cardNumber <= 70) { - this.cards.push(new Card(cardNumber, 4)); + this.cards.push(4); continue; } if (cardNumber > 70 && cardNumber <= 80) { - this.cards.push(new Card(cardNumber, 5)); + this.cards.push(5); continue; } if (cardNumber > 80 && cardNumber <= 90) { - this.cards.push(new Card(cardNumber, 6)); + this.cards.push(6); continue; } if (cardNumber > 90 && cardNumber <= 100) { - this.cards.push(new Card(cardNumber, 7)); + this.cards.push(7); continue; } if (cardNumber > 100 && cardNumber <= 110) { - this.cards.push(new Card(cardNumber, 8)); + this.cards.push(8); continue; } if (cardNumber > 110 && cardNumber <= 120) { - this.cards.push(new Card(cardNumber, 9)); + this.cards.push(9); continue; } if (cardNumber > 120 && cardNumber <= 130) { - this.cards.push(new Card(cardNumber, 10)); + this.cards.push(10); continue; } if (cardNumber > 130 && cardNumber <= 140) { - this.cards.push(new Card(cardNumber, 11)); + this.cards.push(11); continue; } if (cardNumber > 140 && cardNumber <= 150) { - this.cards.push(new Card(cardNumber, 12)); + this.cards.push(12); continue; } } diff --git a/src/game/events.ts b/src/game/events.ts index 3c300f3..daf07a8 100644 --- a/src/game/events.ts +++ b/src/game/events.ts @@ -4,8 +4,27 @@ import { Game } from "./game"; import { allGames } from "./game"; -export const handleJoinSession = (socket: Socket, sessionId: string) => { +type SessionResponse = "success" | "error:full" | "error:running"; + +export const handleJoinSession = ( + socket: Socket, + sessionId: string, + callback: Function +) => { + const isSessionRunning = allGames.some( + (game) => game.sessionId === sessionId + ); + if (isSessionRunning) { + callback("error:running" satisfies SessionResponse); + socket.emit( + "message", + "A Game is already running in this session. Please join another session." + ); + return; + } + socket.join(sessionId); + callback("success" satisfies SessionResponse); console.log("User joined session:", sessionId); const numberOfClients = io.sockets.adapter.rooms.get(sessionId)?.size ?? 0; @@ -41,6 +60,7 @@ export const handleNewGame = ( if (players && players.size > 1) { const game = new Game(socket, sessionId, players); + removeOldGame(sessionId); allGames.push(game); console.log(`New Game created with ${game.playerCount} players!`); game.gameLoop(); @@ -62,3 +82,12 @@ export const handleDisconnect = (socket: Socket) => { io.to(sessionName).emit("clients-in-session", socketIds.size); }); }; + +const removeOldGame = (sessionId: string) => { + const oldGameIndex = allGames.findIndex( + (game) => game.sessionId === sessionId + ); + if (oldGameIndex !== -1) { + allGames.splice(oldGameIndex, 1); + } +}; diff --git a/src/game/game.ts b/src/game/game.ts index 5b24c09..e01b94d 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -1,6 +1,6 @@ -import { Player, ObfuscatedPlayer } from "./player"; -import { CardStack } from "./card"; -import { Card, ObfuscatedCardStack } from "./card"; +import { Player, ObfuscatedPlayer, ConcealableColumn } from "./player"; +import { CardStack, ConcealableCard } from "./card"; +import { Card, ConcealableCardStack } from "./card"; import { Socket } from "socket.io"; import { io } from "../server"; @@ -18,14 +18,14 @@ type PlayerAction = { data: ActionDataType; }; -type CardPosition = number; +type CardPosition = [number, number]; // obfuscated types are used to send only necessary data to the client export type ObfuscatedGame = { sessionId: string; playerCount: number; players: ObfuscatedPlayer[]; - cardStack: ObfuscatedCardStack; + cardStack: ConcealableCardStack; discardPile: Card[]; phase: string; round: number; @@ -78,13 +78,8 @@ export class Game { let index = 0; playerIds.forEach((socketId) => { index++; - const playerCards = cardStack.cards.splice(0, 12); - const player = new Player( - index, - socketId, - `Player ${index}`, - playerCards - ); + + const player = new Player(index, socketId, `Player ${index}`, cardStack); players.push(player); }); @@ -98,8 +93,8 @@ export class Game { this.cardStack = new CardStack(); this.cardStack.shuffleCards(); this.players.forEach((player) => { - player.cards = this.cardStack.cards.splice(0, 12); - player.knownCardPositions = new Array(12).fill(false); + player.deck = player.generateDeck(this.cardStack); + player.knownCardPositions = player.createUnknownCardPositions(); player.playersTurn = true; player.cardCache = null; player.tookDispiledCard = false; @@ -117,6 +112,7 @@ export class Game { this.sendObfuscatedGameUpdate(); while (this.phase !== gamePhase.gameEnded) { this.checkForFullRevealedCards(); + this.removeThreeOfAKinds(); switch (this.phase) { case gamePhase.revealTwoCards: console.log("\nGame phase: revealTwoCards"); @@ -245,12 +241,15 @@ export class Game { // Player Action Callbacks - revealCardAction(playerSocketId: string, cardPosition: number) { + revealCardAction(playerSocketId: string, cardPosition: CardPosition) { const player = this.getPlayerBySocketId(playerSocketId); - const revealedCard = player.cards[cardPosition]; - console.log(`Revealed card ${revealedCard} at position ${cardPosition}`); + const [columnIndex, cardIndex] = cardPosition; + const revealedCard = player.deck[columnIndex][cardIndex]; + console.log( + `Revealed card ${revealedCard} at column ${columnIndex} card ${cardIndex}` + ); const playerIndex = this.players.indexOf(player!); - this.players[playerIndex].knownCardPositions[cardPosition] = true; + this.players[playerIndex].knownCardPositions[columnIndex][cardIndex] = true; this.sendObfuscatedGameUpdate(); } @@ -281,15 +280,17 @@ export class Game { this.sendObfuscatedGameUpdate(); } - placeCardAction(playerSocketId: string, cardPosition: number) { + placeCardAction(playerSocketId: string, cardPosition: CardPosition) { const player = this.getPlayerBySocketId(playerSocketId); console.log(`Player ${player.name} placed a card.`); const placedCard = player.cardCache!; player.cardCache = null; - const replacedCard = player.cards[cardPosition]; + const [columnIndex, cardIndex] = cardPosition; + const replacedCard = player.deck[columnIndex][cardIndex]; this.discardPile.push(replacedCard); - player.cards[cardPosition] = placedCard; - player.knownCardPositions[cardPosition] = true; + player.deck[columnIndex][cardIndex] = placedCard; + player.knownCardPositions[columnIndex][cardIndex] = true; + // TODO: check for three of a kind this.nextPlayersTurn(); this.phase = gamePhase.pickUpCard; this.sendObfuscatedGameUpdate(); @@ -379,32 +380,24 @@ export class Game { phase: this.phase, round: this.round, discardPile: this.discardPile, - players: this.players.map(({ cards, ...player }) => { + players: this.players.map(({ deck, ...player }) => { return { ...player, - cards: cards.map((card: Card, index: number) => { - // unknown cards are obfuscated to X - return { - id: player.knownCardPositions[index] ? card.id : 0, - value: player.knownCardPositions[index] ? card.value : "X", - name: player.knownCardPositions[index] - ? card.name - : "Facedown Card", - color: player.knownCardPositions[index] ? card.color : "black", - matchColorToCardValue: card.matchColorToCardValue, - }; + deck: deck.map((column, columnIndex) => { + const concealableColumn = column.map((card, cardIndex) => { + // unknown cards are obfuscated to null + return player.knownCardPositions[columnIndex][cardIndex] + ? card + : (null as ConcealableCard); + }); + return concealableColumn as ConcealableColumn; }), - }; + } satisfies ObfuscatedPlayer; }), cardStack: { cards: this.cardStack.cards.map((card: Card) => { // player may not see the value of the facedown cards in the cardStack - return { - id: 0, - value: "X", - name: "Facedown Card", - color: "black", - }; + return null; }), }, }; @@ -419,13 +412,7 @@ export class Game { updatePlayerRoundPoints() { this.players.forEach((player) => { const revealedCardValuesSum = player.getRevealedCardsValueSum(); - const threeOfAKinds = player.getThreeOfAKinds(); - const threeOfAKindPoints = threeOfAKinds.reduce( - (points, threeOfAKind) => points + threeOfAKind.value * 3, - 0 - ); - - player.roundPoints = revealedCardValuesSum - threeOfAKindPoints; + player.roundPoints = revealedCardValuesSum; }); } @@ -481,8 +468,8 @@ export class Game { if (alreadyClosedPlayers.length > 0) return; // TODO: check if this is correct with more than 2 players const playerWithAllCardsRevealed = this.players.find((player) => - player.knownCardPositions.every( - (knownCardPosition) => knownCardPosition === true + player.knownCardPositions.every((knownCardsColumn) => + knownCardsColumn.every((knownCard) => knownCard === true) ) ); if (playerWithAllCardsRevealed) { @@ -490,6 +477,22 @@ export class Game { } } + removeThreeOfAKinds() { + this.players.forEach((player) => { + const threeOfAKinds = player.getThreeOfAKinds(); + if (threeOfAKinds.length == 0) return; + threeOfAKinds.forEach((threeOfAKind) => { + const { columnIndex, value } = threeOfAKind; + this.discardPile.push(value as Card); + this.discardPile.push(value as Card); + this.discardPile.push(value as Card); + player.deck.splice(columnIndex, 1); + player.knownCardPositions.splice(columnIndex, 1); + }); + this.sendObfuscatedGameUpdate(); + }); + } + checkIfPointLimitReached() { const highestPoints = Math.max( ...this.players.map((player) => player.totalPoints) @@ -553,9 +556,11 @@ export class Game { revealAllCards() { this.players.forEach((player) => { - player.knownCardPositions = player.knownCardPositions.map( - (knownCardPosition) => true - ); + player.knownCardPositions.forEach((column, columnIndex) => { + column.forEach((card, cardIndex) => { + player.knownCardPositions[columnIndex][cardIndex] = true; + }); + }); }); } @@ -650,16 +655,6 @@ export class Game { else throw new Error(`No player with socketId ${playerSocketId} found!`); } - listPlayerSocketListeners() { - console.log("Player socket listeners:"); - io.sockets.sockets.forEach((socket) => { - console.log(socket.id); - socket.eventNames().forEach((eventName) => { - console.log(`${eventName.toString()}`); - }); - }); - } - sendMessageToAllPlayers(message: string) { io.to(this.sessionId).emit("message", message); console.log(`Sent Message (Session): ${message}`); diff --git a/src/game/player.ts b/src/game/player.ts index a88fd1c..b6cb92a 100644 --- a/src/game/player.ts +++ b/src/game/player.ts @@ -1,11 +1,30 @@ -import { Card, ObfuscatedCard } from "./card"; +import { Card, CardStack, ConcealableCard } from "./card"; + +type Column = [Card, Card, Card]; +type Deck = Column[]; + +export type ConcealableColumn = [ + ConcealableCard, + ConcealableCard, + ConcealableCard +]; +type ConcealableDeck = ConcealableColumn[]; + +type ColumnIndex = number; + +type ThreeOfAKind = { + columnIndex: ColumnIndex; + value: number; +}; + +type KnownCardsColumn = [boolean, boolean, boolean]; export type ObfuscatedPlayer = { id: number; socketId: string; name: string; - cards: ObfuscatedCard[]; - knownCardPositions: boolean[]; + deck: ConcealableDeck; + knownCardPositions: KnownCardsColumn[]; playersTurn: boolean; cardCache: Card | null; tookDispiledCard: boolean; @@ -14,26 +33,12 @@ export type ObfuscatedPlayer = { closedRound: boolean; }; -type ColumnPosition = [number, number, number]; - -type ThreeOfAKind = { - position: ColumnPosition; - value: number; -}; - -const COLUMN_POSITIONS: ColumnPosition[] = [ - [0, 4, 8], - [1, 5, 9], - [2, 6, 10], - [3, 7, 11], -]; - export class Player { id: number; socketId: string; name: string; - cards: Card[]; - knownCardPositions: boolean[]; + deck: Deck; + knownCardPositions: KnownCardsColumn[]; playersTurn: boolean; cardCache: Card | null; // this is where the card is temporarily stored when a player draws a card tookDispiledCard: boolean; // this is used to check if a player took a dispiled card in the current turn @@ -41,12 +46,17 @@ export class Player { totalPoints: number; closedRound: boolean; place: number | null; // indicates the place the player got in the last round - constructor(id: number, socketId: string, name: string, cards: Card[]) { + constructor( + id: number, + socketId: string, + name: string, + cardStack: CardStack + ) { this.id = id; this.socketId = socketId; this.name = name; - this.cards = cards; - this.knownCardPositions = new Array(12).fill(false); + this.deck = this.generateDeck(cardStack); + this.knownCardPositions = this.createUnknownCardPositions(); this.playersTurn = true; this.cardCache = null; this.tookDispiledCard = false; @@ -56,74 +66,79 @@ export class Player { this.place = null; } + generateDeck(cardStack: CardStack): Deck { + const deck: Deck = []; + for (let i = 0; i < 4; i++) { + deck.push(cardStack.cards.splice(0, 3) as Column); + } + return deck; + } + + createUnknownCardPositions(): KnownCardsColumn[] { + const knownCardPositions: KnownCardsColumn[] = []; + for (let i = 0; i < 4; i++) { + knownCardPositions.push([false, false, false]); + } + return knownCardPositions; + } + hasInitialCardsRevealed(): boolean { - const revealedCards = this.knownCardPositions.filter( - (knownCard) => knownCard === true + const flattenedKnownCardPositions = this.knownCardPositions.flat(); + const revealedCards = flattenedKnownCardPositions.filter( + (position) => position ); if (revealedCards.length > 1) return true; else return false; } getRevealedCardCount(): number { - return this.knownCardPositions.filter((position) => position).length; + return this.knownCardPositions.flat().filter((position) => position == true) + .length; } getThreeOfAKinds(): ThreeOfAKind[] { - const revealedCardPositions = this.getRevealedCardPositions(); - const columns: ColumnPosition[] = COLUMN_POSITIONS; const threeOfAKinds: ThreeOfAKind[] = []; - columns.forEach((column) => { - const columnValues = column.map((position) => this.cards[position].value); - const columnHasSameValues = columnValues.every( - (value) => value === columnValues[0] + this.deck.forEach((column, index) => { + const firstCard = column[0]; + const columnIndex = index; + + const columnHasSameCards = column.every((card) => card === firstCard); + const columnIsRevealed = this.knownCardPositions[columnIndex].every( + (isCardRevealed) => + isCardRevealed === this.knownCardPositions[columnIndex][0] ); - const columnIsRevealed = column.every((position) => - revealedCardPositions.includes(position) - ); - if (columnHasSameValues && columnIsRevealed) { + + if (columnHasSameCards && columnIsRevealed) { threeOfAKinds.push({ - position: column, - value: columnValues[0], + columnIndex: columnIndex as ColumnIndex, + value: firstCard, }); } }); return threeOfAKinds; } - getRevealedCardPositions(): number[] { - const revealedCardPositions: number[] = []; - this.knownCardPositions.forEach((position, index) => { - if (position) revealedCardPositions.push(index); - }); - return revealedCardPositions; - } - getRevealedCards(): Card[] { const revealedCards: Card[] = []; - this.knownCardPositions.forEach((position, index) => { - if (position) revealedCards.push(this.cards[index]); + this.knownCardPositions.forEach((column, columnIndex) => { + column.forEach((isCardRevealed, cardIndex) => { + if (isCardRevealed) + revealedCards.push(this.deck[columnIndex][cardIndex]); + }); }); return revealedCards; } getRevealedCardsValueSum(): number { const revealedCards = this.getRevealedCards(); - const revealedCardsValueSum = revealedCards.reduce( - (sum, card) => sum + card.value, - 0 - ); + let revealedCardsValueSum = 0; + revealedCards.forEach((card) => (revealedCardsValueSum += card)); return revealedCardsValueSum; } getHighestRevealedCardValue(): number { const revealedCards = this.getRevealedCards(); - const highestRevealedCardValue = revealedCards.reduce( - (highestValue, card) => { - if (card.value > highestValue) return card.value; - else return highestValue; - }, - 0 - ); + const highestRevealedCardValue = Math.max(...revealedCards); return highestRevealedCardValue; } } diff --git a/src/server.ts b/src/server.ts index cbe65e2..1f1bcc4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -34,8 +34,8 @@ export const io = new SocketIOServer(httpServer, { io.on("connection", (socket: Socket) => { console.log("A user connected:", socket.id); - socket.on("join-session", (sessionId: string) => - handleJoinSession(socket, sessionId) + socket.on("join-session", (sessionId: string, callback) => + handleJoinSession(socket, sessionId, callback) ); socket.on("leave-session", (sessionId: string) => {