shoehive
Version:
WebSocket-based multiplayer game framework for real-time, event-driven gameplay
514 lines (513 loc) • 19.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Table = exports.TableState = void 0;
const EventTypes_1 = require("../events/EventTypes");
const index_1 = require("./card/index");
const Seat_1 = require("./Seat");
const crypto_1 = __importDefault(require("crypto"));
const index_2 = require("./commands/index");
var TableState;
(function (TableState) {
TableState["WAITING"] = "waiting";
TableState["ACTIVE"] = "active";
TableState["ENDED"] = "ended";
})(TableState || (exports.TableState = TableState = {}));
/**
* Represents a game table with players, seats, and game state.
*
* ✅ Attribute Support
*
* The Table class manages a group of players and seats for a specific game.
* It emits various events to notify other components about changes in the table state.
*
* This class is responsible for:
* - Managing player connections and disconnections
* - Handling player messages and commands
* - Distributing game events to all players
* - Maintaining the game state and rules
*/
class Table {
constructor(eventBus, totalSeats, maxSeatsPerPlayer, id) {
this.players = new Map();
this.state = TableState.WAITING;
this.attributes = new Map();
this.deck = null;
this.id = id || crypto_1.default.randomUUID();
this.eventBus = eventBus;
this.totalSeats = totalSeats;
this.maxSeatsPerPlayer = maxSeatsPerPlayer;
// Initialize seats with Seat objects
this.seats = new Array(totalSeats).fill(null).map(() => new Seat_1.Seat());
}
/*
* Card and deck related methods
*/
/**
* Creates a new deck for the table. Emits TABLE_EVENTS.DECK_CREATED when the deck is created.
* @param numberOfDecks - The number of decks to create.
*/
createDeck(numberOfDecks = 1) {
this.deck = new index_1.Deck(numberOfDecks);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.DECK_CREATED, this, numberOfDecks);
}
/**
* Gets the current deck.
* @returns The deck object or null if no deck has been created.
*/
getDeck() {
return this.deck;
}
/**
* Shuffles the current deck. Emits TABLE_EVENTS.DECK_SHUFFLED when the deck is shuffled.
* @returns True if the deck was shuffled, false if no deck exists.
*/
shuffleDeck() {
if (!this.deck)
return false;
this.deck.shuffle();
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.DECK_SHUFFLED, this);
return true;
}
/**
* Draws a card from the deck. Emits TABLE_EVENTS.DECK_CARD_DRAWN when a card is drawn.
* @param isVisible - Whether the card should be visible to the player.
* @returns The drawn card or null if no deck exists.
*/
drawCard(isVisible = true) {
if (!this.deck)
return null;
const card = this.deck.drawCard(isVisible);
if (card) {
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.DECK_CARD_DRAWN, this, card);
}
return card;
}
/**
* Deals a card to a seat. Emits TABLE_EVENTS.CARD_DEALT when a card is dealt.
* @param seatIndex - The index of the seat to deal the card to.
* @param isVisible - Whether the card should be visible to the player.
* @param handId - The ID of the hand to deal the card to.
* @returns True if the card was dealt, false if no deck exists.
*/
dealCardToSeat(seatIndex, isVisible = true, handId = "main") {
if (!this.deck)
return false;
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return false;
}
const card = this.deck.drawCard(isVisible);
if (!card)
return false;
const seat = this.seats[seatIndex];
// Create the hand if it doesn't exist
if (!seat.getHand(handId)) {
seat.addHand(handId);
}
const hand = seat.getHand(handId);
if (!hand)
return false;
hand.addCard(card);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.CARD_DEALT, this, seatIndex, card, handId);
return true;
}
/**
* Gets a hand at a specific seat.
* @param seatIndex - The index of the seat to get the hand from.
* @param handId - The ID of the hand to get.
* @returns The hand object or null if no hand exists.
*/
getHandAtSeat(seatIndex, handId = "main") {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return null;
}
return this.seats[seatIndex].getHand(handId);
}
/**
* Deals a card to a hand. Emits TABLE_EVENTS.CARD_DEALT when a card is dealt.
* @param hand <Hand> - The hand to deal the card to.
* @returns True if the card was dealt, false if no seat or hand exists.
*/
dealCardToHand(hand) {
if (!this.deck)
return false;
const card = this.deck.drawCard(true);
if (!card)
return false;
hand.addCard(card);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.CARD_DEALT, this, card, hand.getId());
return true;
}
/**
* Gets all hands at a specific seat.
* @param seatIndex - The index of the seat to get the hands from.
* @returns A map of hand IDs to hand objects or null if no seat exists.
*/
getAllHandsAtSeat(seatIndex) {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return null;
}
return this.seats[seatIndex].getAllHands();
}
/**
* Clears a hand at a specific seat. Emits TABLE_EVENTS.SEAT_HAND_CLEARED when a hand is cleared.
* @param seatIndex - The index of the seat to clear the hand from.
* @param handId - The ID of the hand to clear.
* @returns True if the hand was cleared, false if no seat exists.
*/
clearHandAtSeat(seatIndex, handId = "main") {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return false;
}
const result = this.seats[seatIndex].clearHand(handId);
if (result) {
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.SEAT_HAND_CLEARED, this, seatIndex, handId);
}
return result;
}
/**
* Clears all hands at the table. Emits TABLE_EVENTS.SEATS_HANDS_CLEARED when all hands are cleared.
*/
clearAllHands() {
for (let i = 0; i < this.totalSeats; i++) {
this.seats[i].clearAllHands();
}
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.SEATS_HANDS_CLEARED, this);
}
/**
* Adds a hand to a seat. Emits TABLE_EVENTS.SEAT_HAND_ADDED when a hand is added to a seat.
* @param seatIndex - The index of the seat to add the hand to.
* @param handId - The ID of the hand to add.
* @returns True if the hand was added, false if no seat exists.
*/
addHandToSeat(seatIndex, handId) {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return false;
}
const result = this.seats[seatIndex].addHand(handId);
if (result) {
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.SEAT_HAND_ADDED, this, seatIndex, handId);
}
return result;
}
/**
* Removes a hand from a seat. Emits TABLE_EVENTS.SEAT_HAND_REMOVED when a hand is removed from a seat.
* @param seatIndex - The index of the seat to remove the hand from.
* @param handId - The ID of the hand to remove.
* @returns True if the hand was removed, false if no seat exists.
*/
removeHandFromSeat(seatIndex, handId) {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return false;
}
const result = this.seats[seatIndex].removeHand(handId);
if (result) {
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.SEAT_HAND_REMOVED, this, seatIndex, handId);
}
return result;
}
/**
* Adds a player to the table. Emits TABLE_EVENTS.PLAYER_JOINED when a player joins the table.
* @param player - The player to add.
* @returns True if the player was added, false if the player is already at a table.
*/
addPlayer(player) {
// Check if player is already at a table
if (player.getTable()) {
return false;
}
this.players.set(player.id, player);
player.setTable(this);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.PLAYER_JOINED, player, this);
return true;
}
/**
* Removes a player from the table. Emits TABLE_EVENTS.PLAYER_LEFT when a player leaves the table.
* @param playerId - The ID of the player to remove.
* @returns True if the player was removed, false if the player is not at the table.
*/
removePlayer(playerId) {
const player = this.players.get(playerId);
if (!player)
return false;
// Remove player from all seats
for (let i = 0; i < this.seats.length; i++) {
if (this.seats[i].getPlayer()?.id === playerId) {
this.seats[i].setPlayer(null);
}
}
this.players.delete(playerId);
player.setTable(null);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.PLAYER_LEFT, player, this);
// Check if table is empty
if (this.players.size === 0) {
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.EMPTY, this);
}
return true;
}
/**
* Sits a player at a specific seat. Emits TABLE_EVENTS.PLAYER_SAT when a player sits at a seat.
* @param playerId - The ID of the player to sit.
* @param seatIndex - The index of the seat to sit the player at.
* @returns True if the player was seated, false if the seat is invalid or already taken.
*/
sitPlayerAtSeat(playerId, seatIndex) {
// Check if seat index is valid
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return false;
}
// Check if seat is already taken
if (this.seats[seatIndex].getPlayer() !== null) {
return false;
}
const player = this.players.get(playerId);
if (!player)
return false;
// Check if player is already seated at too many seats
const playerSeats = this.getPlayerSeatCount(playerId);
if (playerSeats >= this.maxSeatsPerPlayer) {
return false;
}
this.seats[seatIndex].setPlayer(player);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.PLAYER_SAT, player, this, seatIndex);
return true;
}
/**
* Removes a player from a seat. Emits TABLE_EVENTS.PLAYER_STOOD when a player stands up from a seat.
* @param seatIndex - The index of the seat to remove the player from.
* @returns True if the player was removed from the seat, false if the seat is invalid or no player is seated.
*/
removePlayerFromSeat(seatIndex) {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return false;
}
const player = this.seats[seatIndex].getPlayer();
if (!player)
return false;
this.seats[seatIndex].setPlayer(null);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.PLAYER_STOOD, player, this, seatIndex);
return true;
}
/**
* Gets the number of seats a player is seated at.
* @param playerId - The ID of the player to get the seat count for.
* @returns The number of seats the player is seated at.
*/
getPlayerSeatCount(playerId) {
let count = 0;
for (const seat of this.seats) {
if (seat.getPlayer()?.id === playerId) {
count++;
}
}
return count;
}
/**
* Gets the current state of the table.
* @returns The current state of the table.
*/
getState() {
return this.state;
}
/**
* Sets the state of the table. Emits TABLE_EVENTS.STATE_UPDATED when the state is updated.
* @param state - The new state of the table.
*/
setState(state) {
this.state = state;
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.STATE_UPDATED, this, state);
// Also broadcast the full table state to all players when state enum changes
this.broadcastTableState();
}
/**
* Gets the number of players at the table.
* @returns The number of players at the table.
*/
getPlayerCount() {
return this.players.size;
}
/**
* Gets all players at the table.
* @returns An array of all players at the table.
*/
getPlayers() {
return Array.from(this.players.values());
}
/**
* Gets a seat at a specific index.
* @param seatIndex - The index of the seat to get.
* @returns The seat object or null if the index is invalid.
*/
getSeat(seatIndex) {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return null;
}
return this.seats[seatIndex];
}
/**
* Gets all seats at the table.
* @returns An array of all seats at the table.
*/
getSeats() {
return [...this.seats];
}
/**
* Gets the player at a specific seat.
* @param seatIndex - The index of the seat to get the player from.
* @returns The player object or null if the seat is invalid.
*/
getPlayerAtSeat(seatIndex) {
if (seatIndex < 0 || seatIndex >= this.totalSeats) {
return null;
}
return this.seats[seatIndex].getPlayer();
}
/**
* Broadcasts a message to all players at the table.
* @param message - The message to broadcast.
*/
broadcastMessage(message) {
const players = Array.from(this.players.values());
for (const player of players) {
player.sendMessage(message);
}
}
/**
* Broadcasts the current table state to all players at the table.
* This includes all game-specific state and is only meant for players at this table.
* Broadcasts a TABLE_EVENTS.STATE_UPDATED event that can be used by other components.
*/
broadcastTableState() {
const tableState = this.getTableState();
this.broadcastMessage({
type: index_2.CLIENT_MESSAGE_TYPES.TABLE.STATE,
data: tableState
});
// Also emit an event that can be used by other components
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.STATE_UPDATED, this, tableState);
}
/**
* Gets the complete table state including all attributes and game state.
* This is used for players who are at the table and need full information.
* Emits a TABLE_EVENTS.STATE_UPDATED event that can be used by other components.
*
* @returns The complete table state.
*/
getTableState() {
return {
id: this.id,
state: this.state,
seats: this.seats.map(seat => ({
player: seat.getPlayer() ? {
id: seat.getPlayer().id,
attributes: seat.getPlayer().getAttributes()
} : null,
hands: Array.from(seat.getAllHands() || new Map()).reduce((obj, [key, hand]) => {
obj[key] = hand.getVisibleState();
return obj;
}, {})
})),
attributes: Object.fromEntries(this.attributes.entries()),
playerCount: this.players.size
};
}
/**
* Gets the table metadata for lobby display.
* This includes only the essential information needed to display in the lobby.
*
* @returns The table metadata.
*/
getTableMetadata() {
return {
id: this.id,
state: this.state,
seats: this.seats.map(seat => seat.getPlayer()?.id || null),
playerCount: this.players.size,
gameId: this.getAttribute("gameId"),
gameName: this.getAttribute("gameName"),
options: this.getAttribute("options")
};
}
/**
* Sets an attribute on the table. Emits TABLE_EVENTS.ATTRIBUTE_CHANGED when the attribute is updated.
* @param key - The key of the attribute to set.
* @param value - The value of the attribute to set.
*/
setAttribute(key, value) {
this.attributes.set(key, value);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.ATTRIBUTE_CHANGED, this, key, value);
}
/**
* Gets an attribute from the table.
* @param key - The key of the attribute to get.
* @returns The value of the attribute or null if the attribute does not exist.
*/
getAttribute(key) {
return this.attributes.get(key);
}
/**
* Checks if the table has an attribute.
* @param key - The key of the attribute to check.
* @returns True if the attribute exists, false otherwise.
*/
hasAttribute(key) {
return this.attributes.has(key);
}
/**
* Gets all attributes from the table.
* @returns An object containing all attributes.
*/
getAttributes() {
return Object.fromEntries(this.attributes.entries());
}
/**
* Sets multiple attributes on the table. Emits TABLE_EVENTS.ATTRIBUTES_CHANGED when any attributes are updated.
* @param attributes - An object containing key-value pairs of attributes to set.
* @param broadcast - Whether to broadcast the table state after updating the attributes.
* @returns True if any attributes were changed, false otherwise.
*/
setAttributes(attributes, broadcast = false) {
let shouldUpdateLobby = false;
const metadataAttributes = ["gameId", "gameName", "options"];
const changedKeys = [];
// Set all attributes
for (const [key, value] of Object.entries(attributes)) {
this.attributes.set(key, value);
changedKeys.push(key);
// Check if any metadata attributes were changed
if (metadataAttributes.includes(key)) {
shouldUpdateLobby = true;
}
// Emit individual events for backwards compatibility
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.ATTRIBUTE_CHANGED, this, key, value);
}
// Emit a bulk event if any attributes were changed
if (changedKeys.length > 0) {
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.ATTRIBUTES_CHANGED, this, changedKeys, attributes);
}
// Broadcast the table state if requested
if (broadcast && changedKeys.length > 0) {
this.broadcastTableState();
}
return shouldUpdateLobby;
}
/**
* Updates the attributes of the table. Emits TABLE_EVENTS.ATTRIBUTES_CHANGED when any attributes are updated.
* @param attributes - An object containing key-value pairs of attributes to set.
* @returns True if any attributes were changed, false otherwise.
*/
updateAttributes(attributes) {
return this.setAttributes(attributes, true);
}
/**
* Removes an attribute from the table. Emits TABLE_EVENTS.ATTRIBUTE_CHANGED when the attribute is removed.
* @param key - The key of the attribute to remove.
*/
removeAttribute(key) {
this.attributes.delete(key);
this.eventBus.emit(EventTypes_1.TABLE_EVENTS.ATTRIBUTE_CHANGED, this, key, undefined);
}
}
exports.Table = Table;