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 =
| "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;
}
}

View file

@ -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);
}
};

View file

@ -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<ActionDataType> = {
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}`);

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 = {
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;
}
}

View file

@ -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) => {