refactors to nested array

This commit is contained in:
pb-coding 2023-10-02 14:48:01 +02:00
parent 96dfc35b1e
commit d0f6a3fdf2
5 changed files with 197 additions and 211 deletions

View file

@ -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 = export type ConcealableCardStack = {
| "darkblue" cards: ConcealableCard[];
| "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 class CardStack { export class CardStack {
@ -84,77 +31,77 @@ export class CardStack {
generateCards() { generateCards() {
for (let cardNumber = 1; cardNumber <= 150; cardNumber++) { for (let cardNumber = 1; cardNumber <= 150; cardNumber++) {
if (cardNumber <= 5) { if (cardNumber <= 5) {
this.cards.push(new Card(cardNumber, -2)); this.cards.push(-2);
continue; continue;
} }
if (cardNumber > 5 && cardNumber <= 15) { if (cardNumber > 5 && cardNumber <= 15) {
this.cards.push(new Card(cardNumber, -1)); this.cards.push(-1);
continue; continue;
} }
if (cardNumber > 15 && cardNumber <= 30) { if (cardNumber > 15 && cardNumber <= 30) {
this.cards.push(new Card(cardNumber, 0)); this.cards.push(0);
continue; continue;
} }
if (cardNumber > 30 && cardNumber <= 40) { if (cardNumber > 30 && cardNumber <= 40) {
this.cards.push(new Card(cardNumber, 1)); this.cards.push(1);
continue; continue;
} }
if (cardNumber > 40 && cardNumber <= 50) { if (cardNumber > 40 && cardNumber <= 50) {
this.cards.push(new Card(cardNumber, 2)); this.cards.push(2);
continue; continue;
} }
if (cardNumber > 50 && cardNumber <= 60) { if (cardNumber > 50 && cardNumber <= 60) {
this.cards.push(new Card(cardNumber, 3)); this.cards.push(3);
continue; continue;
} }
if (cardNumber > 60 && cardNumber <= 70) { if (cardNumber > 60 && cardNumber <= 70) {
this.cards.push(new Card(cardNumber, 4)); this.cards.push(4);
continue; continue;
} }
if (cardNumber > 70 && cardNumber <= 80) { if (cardNumber > 70 && cardNumber <= 80) {
this.cards.push(new Card(cardNumber, 5)); this.cards.push(5);
continue; continue;
} }
if (cardNumber > 80 && cardNumber <= 90) { if (cardNumber > 80 && cardNumber <= 90) {
this.cards.push(new Card(cardNumber, 6)); this.cards.push(6);
continue; continue;
} }
if (cardNumber > 90 && cardNumber <= 100) { if (cardNumber > 90 && cardNumber <= 100) {
this.cards.push(new Card(cardNumber, 7)); this.cards.push(7);
continue; continue;
} }
if (cardNumber > 100 && cardNumber <= 110) { if (cardNumber > 100 && cardNumber <= 110) {
this.cards.push(new Card(cardNumber, 8)); this.cards.push(8);
continue; continue;
} }
if (cardNumber > 110 && cardNumber <= 120) { if (cardNumber > 110 && cardNumber <= 120) {
this.cards.push(new Card(cardNumber, 9)); this.cards.push(9);
continue; continue;
} }
if (cardNumber > 120 && cardNumber <= 130) { if (cardNumber > 120 && cardNumber <= 130) {
this.cards.push(new Card(cardNumber, 10)); this.cards.push(10);
continue; continue;
} }
if (cardNumber > 130 && cardNumber <= 140) { if (cardNumber > 130 && cardNumber <= 140) {
this.cards.push(new Card(cardNumber, 11)); this.cards.push(11);
continue; continue;
} }
if (cardNumber > 140 && cardNumber <= 150) { if (cardNumber > 140 && cardNumber <= 150) {
this.cards.push(new Card(cardNumber, 12)); this.cards.push(12);
continue; continue;
} }
} }

View file

@ -4,8 +4,27 @@ import { Game } from "./game";
import { allGames } 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); socket.join(sessionId);
callback("success" satisfies SessionResponse);
console.log("User joined session:", sessionId); console.log("User joined session:", sessionId);
const numberOfClients = io.sockets.adapter.rooms.get(sessionId)?.size ?? 0; const numberOfClients = io.sockets.adapter.rooms.get(sessionId)?.size ?? 0;
@ -41,6 +60,7 @@ export const handleNewGame = (
if (players && players.size > 1) { if (players && players.size > 1) {
const game = new Game(socket, sessionId, players); const game = new Game(socket, sessionId, players);
removeOldGame(sessionId);
allGames.push(game); allGames.push(game);
console.log(`New Game created with ${game.playerCount} players!`); console.log(`New Game created with ${game.playerCount} players!`);
game.gameLoop(); game.gameLoop();
@ -62,3 +82,12 @@ export const handleDisconnect = (socket: Socket) => {
io.to(sessionName).emit("clients-in-session", socketIds.size); 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);
}
};

View file

@ -1,6 +1,6 @@
import { Player, ObfuscatedPlayer } from "./player"; import { Player, ObfuscatedPlayer, ConcealableColumn } from "./player";
import { CardStack } from "./card"; import { CardStack, ConcealableCard } from "./card";
import { Card, ObfuscatedCardStack } from "./card"; import { Card, ConcealableCardStack } from "./card";
import { Socket } from "socket.io"; import { Socket } from "socket.io";
import { io } from "../server"; import { io } from "../server";
@ -18,14 +18,14 @@ type PlayerAction<ActionDataType> = {
data: ActionDataType; data: ActionDataType;
}; };
type CardPosition = number; type CardPosition = [number, number];
// obfuscated types are used to send only necessary data to the client // obfuscated types are used to send only necessary data to the client
export type ObfuscatedGame = { export type ObfuscatedGame = {
sessionId: string; sessionId: string;
playerCount: number; playerCount: number;
players: ObfuscatedPlayer[]; players: ObfuscatedPlayer[];
cardStack: ObfuscatedCardStack; cardStack: ConcealableCardStack;
discardPile: Card[]; discardPile: Card[];
phase: string; phase: string;
round: number; round: number;
@ -78,13 +78,8 @@ export class Game {
let index = 0; let index = 0;
playerIds.forEach((socketId) => { playerIds.forEach((socketId) => {
index++; index++;
const playerCards = cardStack.cards.splice(0, 12);
const player = new Player( const player = new Player(index, socketId, `Player ${index}`, cardStack);
index,
socketId,
`Player ${index}`,
playerCards
);
players.push(player); players.push(player);
}); });
@ -98,8 +93,8 @@ export class Game {
this.cardStack = new CardStack(); this.cardStack = new CardStack();
this.cardStack.shuffleCards(); this.cardStack.shuffleCards();
this.players.forEach((player) => { this.players.forEach((player) => {
player.cards = this.cardStack.cards.splice(0, 12); player.deck = player.generateDeck(this.cardStack);
player.knownCardPositions = new Array(12).fill(false); player.knownCardPositions = player.createUnknownCardPositions();
player.playersTurn = true; player.playersTurn = true;
player.cardCache = null; player.cardCache = null;
player.tookDispiledCard = false; player.tookDispiledCard = false;
@ -117,6 +112,7 @@ export class Game {
this.sendObfuscatedGameUpdate(); this.sendObfuscatedGameUpdate();
while (this.phase !== gamePhase.gameEnded) { while (this.phase !== gamePhase.gameEnded) {
this.checkForFullRevealedCards(); this.checkForFullRevealedCards();
this.removeThreeOfAKinds();
switch (this.phase) { switch (this.phase) {
case gamePhase.revealTwoCards: case gamePhase.revealTwoCards:
console.log("\nGame phase: revealTwoCards"); console.log("\nGame phase: revealTwoCards");
@ -245,12 +241,15 @@ export class Game {
// Player Action Callbacks // Player Action Callbacks
revealCardAction(playerSocketId: string, cardPosition: number) { revealCardAction(playerSocketId: string, cardPosition: CardPosition) {
const player = this.getPlayerBySocketId(playerSocketId); const player = this.getPlayerBySocketId(playerSocketId);
const revealedCard = player.cards[cardPosition]; const [columnIndex, cardIndex] = cardPosition;
console.log(`Revealed card ${revealedCard} at position ${cardPosition}`); const revealedCard = player.deck[columnIndex][cardIndex];
console.log(
`Revealed card ${revealedCard} at column ${columnIndex} card ${cardIndex}`
);
const playerIndex = this.players.indexOf(player!); const playerIndex = this.players.indexOf(player!);
this.players[playerIndex].knownCardPositions[cardPosition] = true; this.players[playerIndex].knownCardPositions[columnIndex][cardIndex] = true;
this.sendObfuscatedGameUpdate(); this.sendObfuscatedGameUpdate();
} }
@ -281,15 +280,17 @@ export class Game {
this.sendObfuscatedGameUpdate(); this.sendObfuscatedGameUpdate();
} }
placeCardAction(playerSocketId: string, cardPosition: number) { placeCardAction(playerSocketId: string, cardPosition: CardPosition) {
const player = this.getPlayerBySocketId(playerSocketId); const player = this.getPlayerBySocketId(playerSocketId);
console.log(`Player ${player.name} placed a card.`); console.log(`Player ${player.name} placed a card.`);
const placedCard = player.cardCache!; const placedCard = player.cardCache!;
player.cardCache = null; player.cardCache = null;
const replacedCard = player.cards[cardPosition]; const [columnIndex, cardIndex] = cardPosition;
const replacedCard = player.deck[columnIndex][cardIndex];
this.discardPile.push(replacedCard); this.discardPile.push(replacedCard);
player.cards[cardPosition] = placedCard; player.deck[columnIndex][cardIndex] = placedCard;
player.knownCardPositions[cardPosition] = true; player.knownCardPositions[columnIndex][cardIndex] = true;
// TODO: check for three of a kind
this.nextPlayersTurn(); this.nextPlayersTurn();
this.phase = gamePhase.pickUpCard; this.phase = gamePhase.pickUpCard;
this.sendObfuscatedGameUpdate(); this.sendObfuscatedGameUpdate();
@ -379,32 +380,24 @@ export class Game {
phase: this.phase, phase: this.phase,
round: this.round, round: this.round,
discardPile: this.discardPile, discardPile: this.discardPile,
players: this.players.map(({ cards, ...player }) => { players: this.players.map(({ deck, ...player }) => {
return { return {
...player, ...player,
cards: cards.map((card: Card, index: number) => { deck: deck.map((column, columnIndex) => {
// unknown cards are obfuscated to X const concealableColumn = column.map((card, cardIndex) => {
return { // unknown cards are obfuscated to null
id: player.knownCardPositions[index] ? card.id : 0, return player.knownCardPositions[columnIndex][cardIndex]
value: player.knownCardPositions[index] ? card.value : "X", ? card
name: player.knownCardPositions[index] : (null as ConcealableCard);
? card.name });
: "Facedown Card", return concealableColumn as ConcealableColumn;
color: player.knownCardPositions[index] ? card.color : "black",
matchColorToCardValue: card.matchColorToCardValue,
};
}), }),
}; } satisfies ObfuscatedPlayer;
}), }),
cardStack: { cardStack: {
cards: this.cardStack.cards.map((card: Card) => { cards: this.cardStack.cards.map((card: Card) => {
// player may not see the value of the facedown cards in the cardStack // player may not see the value of the facedown cards in the cardStack
return { return null;
id: 0,
value: "X",
name: "Facedown Card",
color: "black",
};
}), }),
}, },
}; };
@ -419,13 +412,7 @@ export class Game {
updatePlayerRoundPoints() { updatePlayerRoundPoints() {
this.players.forEach((player) => { this.players.forEach((player) => {
const revealedCardValuesSum = player.getRevealedCardsValueSum(); const revealedCardValuesSum = player.getRevealedCardsValueSum();
const threeOfAKinds = player.getThreeOfAKinds(); player.roundPoints = revealedCardValuesSum;
const threeOfAKindPoints = threeOfAKinds.reduce(
(points, threeOfAKind) => points + threeOfAKind.value * 3,
0
);
player.roundPoints = revealedCardValuesSum - threeOfAKindPoints;
}); });
} }
@ -481,8 +468,8 @@ export class Game {
if (alreadyClosedPlayers.length > 0) return; // TODO: check if this is correct with more than 2 players if (alreadyClosedPlayers.length > 0) return; // TODO: check if this is correct with more than 2 players
const playerWithAllCardsRevealed = this.players.find((player) => const playerWithAllCardsRevealed = this.players.find((player) =>
player.knownCardPositions.every( player.knownCardPositions.every((knownCardsColumn) =>
(knownCardPosition) => knownCardPosition === true knownCardsColumn.every((knownCard) => knownCard === true)
) )
); );
if (playerWithAllCardsRevealed) { 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() { checkIfPointLimitReached() {
const highestPoints = Math.max( const highestPoints = Math.max(
...this.players.map((player) => player.totalPoints) ...this.players.map((player) => player.totalPoints)
@ -553,9 +556,11 @@ export class Game {
revealAllCards() { revealAllCards() {
this.players.forEach((player) => { this.players.forEach((player) => {
player.knownCardPositions = player.knownCardPositions.map( player.knownCardPositions.forEach((column, columnIndex) => {
(knownCardPosition) => true 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!`); 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) { sendMessageToAllPlayers(message: string) {
io.to(this.sessionId).emit("message", message); io.to(this.sessionId).emit("message", message);
console.log(`Sent Message (Session): ${message}`); console.log(`Sent Message (Session): ${message}`);

View file

@ -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 = { export type ObfuscatedPlayer = {
id: number; id: number;
socketId: string; socketId: string;
name: string; name: string;
cards: ObfuscatedCard[]; deck: ConcealableDeck;
knownCardPositions: boolean[]; knownCardPositions: KnownCardsColumn[];
playersTurn: boolean; playersTurn: boolean;
cardCache: Card | null; cardCache: Card | null;
tookDispiledCard: boolean; tookDispiledCard: boolean;
@ -14,26 +33,12 @@ export type ObfuscatedPlayer = {
closedRound: boolean; 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 { export class Player {
id: number; id: number;
socketId: string; socketId: string;
name: string; name: string;
cards: Card[]; deck: Deck;
knownCardPositions: boolean[]; knownCardPositions: KnownCardsColumn[];
playersTurn: boolean; playersTurn: boolean;
cardCache: Card | null; // this is where the card is temporarily stored when a player draws a card 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 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; totalPoints: number;
closedRound: boolean; closedRound: boolean;
place: number | null; // indicates the place the player got in the last round 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.id = id;
this.socketId = socketId; this.socketId = socketId;
this.name = name; this.name = name;
this.cards = cards; this.deck = this.generateDeck(cardStack);
this.knownCardPositions = new Array(12).fill(false); this.knownCardPositions = this.createUnknownCardPositions();
this.playersTurn = true; this.playersTurn = true;
this.cardCache = null; this.cardCache = null;
this.tookDispiledCard = false; this.tookDispiledCard = false;
@ -56,74 +66,79 @@ export class Player {
this.place = null; 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 { hasInitialCardsRevealed(): boolean {
const revealedCards = this.knownCardPositions.filter( const flattenedKnownCardPositions = this.knownCardPositions.flat();
(knownCard) => knownCard === true const revealedCards = flattenedKnownCardPositions.filter(
(position) => position
); );
if (revealedCards.length > 1) return true; if (revealedCards.length > 1) return true;
else return false; else return false;
} }
getRevealedCardCount(): number { getRevealedCardCount(): number {
return this.knownCardPositions.filter((position) => position).length; return this.knownCardPositions.flat().filter((position) => position == true)
.length;
} }
getThreeOfAKinds(): ThreeOfAKind[] { getThreeOfAKinds(): ThreeOfAKind[] {
const revealedCardPositions = this.getRevealedCardPositions();
const columns: ColumnPosition[] = COLUMN_POSITIONS;
const threeOfAKinds: ThreeOfAKind[] = []; const threeOfAKinds: ThreeOfAKind[] = [];
columns.forEach((column) => { this.deck.forEach((column, index) => {
const columnValues = column.map((position) => this.cards[position].value); const firstCard = column[0];
const columnHasSameValues = columnValues.every( const columnIndex = index;
(value) => value === columnValues[0]
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 (columnHasSameCards && columnIsRevealed) {
);
if (columnHasSameValues && columnIsRevealed) {
threeOfAKinds.push({ threeOfAKinds.push({
position: column, columnIndex: columnIndex as ColumnIndex,
value: columnValues[0], value: firstCard,
}); });
} }
}); });
return threeOfAKinds; return threeOfAKinds;
} }
getRevealedCardPositions(): number[] {
const revealedCardPositions: number[] = [];
this.knownCardPositions.forEach((position, index) => {
if (position) revealedCardPositions.push(index);
});
return revealedCardPositions;
}
getRevealedCards(): Card[] { getRevealedCards(): Card[] {
const revealedCards: Card[] = []; const revealedCards: Card[] = [];
this.knownCardPositions.forEach((position, index) => { this.knownCardPositions.forEach((column, columnIndex) => {
if (position) revealedCards.push(this.cards[index]); column.forEach((isCardRevealed, cardIndex) => {
if (isCardRevealed)
revealedCards.push(this.deck[columnIndex][cardIndex]);
});
}); });
return revealedCards; return revealedCards;
} }
getRevealedCardsValueSum(): number { getRevealedCardsValueSum(): number {
const revealedCards = this.getRevealedCards(); const revealedCards = this.getRevealedCards();
const revealedCardsValueSum = revealedCards.reduce( let revealedCardsValueSum = 0;
(sum, card) => sum + card.value, revealedCards.forEach((card) => (revealedCardsValueSum += card));
0
);
return revealedCardsValueSum; return revealedCardsValueSum;
} }
getHighestRevealedCardValue(): number { getHighestRevealedCardValue(): number {
const revealedCards = this.getRevealedCards(); const revealedCards = this.getRevealedCards();
const highestRevealedCardValue = revealedCards.reduce( const highestRevealedCardValue = Math.max(...revealedCards);
(highestValue, card) => {
if (card.value > highestValue) return card.value;
else return highestValue;
},
0
);
return highestRevealedCardValue; return highestRevealedCardValue;
} }
} }

View file

@ -34,8 +34,8 @@ export const io = new SocketIOServer(httpServer, {
io.on("connection", (socket: Socket) => { io.on("connection", (socket: Socket) => {
console.log("A user connected:", socket.id); console.log("A user connected:", socket.id);
socket.on("join-session", (sessionId: string) => socket.on("join-session", (sessionId: string, callback) =>
handleJoinSession(socket, sessionId) handleJoinSession(socket, sessionId, callback)
); );
socket.on("leave-session", (sessionId: string) => { socket.on("leave-session", (sessionId: string) => {