UNPKG

the-game-domain

Version:

Complete domain layer for 'The Game' card game with rich business entities, automatic persistence, action tracking, and advanced game mechanics. Features Game, Player, Card, Deck, Pile entities with repository pattern, turn management, victory/defeat dete

560 lines 22.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Game = void 0; const Card_1 = require("./Card"); const Pile_1 = require("./Pile"); const Deck_1 = require("./Deck"); const Player_1 = require("./Player"); const GameAction_1 = require("./GameAction"); const uuid_1 = require("../utils/uuid"); const GameRepositoryManager_1 = require("../repositories/GameRepositoryManager"); class Game { constructor(numberOfPlayers, repository) { if (numberOfPlayers < 1 || numberOfPlayers > 5) { throw new Error('Number of players must be between 1 and 5'); } Player_1.Player.resetCounter(); this._id = (0, uuid_1.generateUUID)(); this._createdAt = new Date(); this._repository = repository; this._actions = []; this._initializeGame(numberOfPlayers); this._recordAction(GameAction_1.GameAction.gameCreated(this._id, numberOfPlayers)); this._persist().catch(error => { console.error('Error persisting newly created game:', error); }); } _initializeGame(numberOfPlayers) { this._piles = Pile_1.Pile.createInitialPiles(); this._deck = Deck_1.Deck.createInitialDeck(); this._isFinished = false; delete this._currentPlayerId; this._players = []; for (let playerIndex = 1; playerIndex <= numberOfPlayers; playerIndex++) { this._addPlayer(6); } this._validateGame(); } get id() { return this._id; } get piles() { return [...this._piles]; } get players() { return [...this._players]; } get currentPlayerId() { return this._currentPlayerId; } get isFinished() { return this._isFinished; } get gameResult() { if (!this._isFinished) { return 'ongoing'; } const lastAction = this._actions .filter(action => action.type === 'GAME_WON' || action.type === 'GAME_LOST') .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())[0]; if (lastAction?.type === 'GAME_WON') { return 'won'; } if (lastAction?.type === 'GAME_LOST') { return 'lost'; } return 'ongoing'; } get winnerMessage() { if (this.gameResult === 'won') { return 'VITÓRIA! Todos os jogadores descartaram suas cartas e o deck está vazio!'; } if (this.gameResult === 'lost') { const lastAction = this._actions .filter(action => action.type === 'GAME_LOST') .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())[0]; if (lastAction?.playerName) { return `DERROTA! ${lastAction.playerName} não conseguiu jogar suas cartas obrigatórias`; } return 'DERROTA! Nenhum jogador conseguiu completar suas jogadas obrigatórias'; } return null; } get deck() { return this._deck; } get createdAt() { return new Date(this._createdAt); } _validateGame() { if (this._piles.length !== 4) { throw new Error('Game must have exactly 4 piles'); } if (this._players.length === 0) { throw new Error('Game must have at least one player'); } } async _persist() { const repository = this._repository || GameRepositoryManager_1.GameRepositoryManager.getInstance().getDefaultRepository(); try { console.log(`🔄 Game._persist: Starting persistence for game ${this._id}`); await repository.save(this); console.log(`✅ Game ${this._id} persisted successfully`); } catch (error) { console.error(`❌ Error persisting game ${this._id}:`, error); console.error(`❌ Error details:`, error); } } _recordAction(action) { console.log(`Recording action: ${action.type} - ${action.description}`); this._actions.push(action); console.log(`Game action ${action.id} recorded successfully`); } recordAction(action) { console.log(`🔍 Game.recordAction: Recording action ${action.type} - ${action.description}`); this._recordAction(action); console.log(`🔍 Game.recordAction: Action recorded successfully`); } setRepository(repository) { this._repository = repository; } removeRepository() { this._repository = undefined; } hasRepository() { return this._repository !== undefined; } hasExternalRepository() { return this._repository !== undefined; } async persist() { await this._persist(); } get actions() { return [...this._actions]; } getActionsSortedByTimestamp() { return [...this._actions].sort((a, b) => { const timeDiff = b.timestamp.getTime() - a.timestamp.getTime(); if (timeDiff !== 0) { return timeDiff; } return b.id.localeCompare(a.id); }); } _addPlayer(cardCount = 6) { if (cardCount <= 0) { throw new Error('Card count must be greater than 0'); } if (this._deck.size < cardCount) { throw new Error(`Not enough cards in deck. Available: ${this._deck.size}, requested: ${cardCount}`); } const player = new Player_1.Player(); const cards = this._deck.drawCards(cardCount); player.addCards(cards); this._players.push(player); if (!this._currentPlayerId) { this._currentPlayerId = player.id; player.setCurrentTurn(true); } } getPlayerCards(playerId) { const player = this._players.find(p => p.id === playerId); if (!player) { throw new Error(`Player ${playerId} not found in the game`); } return player.cards; } getPlayer(playerId) { return this._players.find(p => p.id === playerId); } canPlaceCardOnPile(cardValue, pileNumber) { const pile = this._piles.find(p => p.number === pileNumber); if (!pile) { console.log(`❌ Pile ${pileNumber} not found`); return false; } const tempCard = new Card_1.Card(cardValue); const result = pile.canAcceptCard(tempCard); console.log(`🔍 Game.canPlaceCardOnPile: card ${cardValue} on pile ${pileNumber} (${pile.direction}, lastCard: ${pile.lastCard.value}) = ${result}`); return result; } async moveCardToPile(playerId, cardValue, pileNumber) { console.log(`🔍 moveCardToPile: playerId=${playerId}, currentPlayerId=${this._currentPlayerId}, isEqual=${this._currentPlayerId === playerId}`); const player = this._players.find(p => p.id === playerId); if (!player) { throw new Error(`Player ${playerId} not found in the game`); } if (this._currentPlayerId !== playerId) { throw new Error(`Player ${playerId} cannot move cards - it's not their turn`); } const pile = this._piles.find(p => p.number === pileNumber); if (!pile) { throw new Error(`Pile ${pileNumber} not found`); } const card = player.removeCard(cardValue); if (!card) { throw new Error(`Card with value ${cardValue} not found in player ${playerId}'s hand`); } if (!pile.canAcceptCard(card)) { player.addCard(card); throw new Error(`Card ${cardValue} cannot be added to pile ${pileNumber}`); } pile.addCard(card); this._recordAction(GameAction_1.GameAction.cardPlayed(this._id, playerId, player.name, cardValue, pileNumber)); await this._checkGameEnd(); await this._persist(); } async _checkGameEnd() { if (this._checkVictory()) { this._isFinished = true; this._recordAction(GameAction_1.GameAction.gameWon(this._id)); await this._persist(); console.log('[DEBUG][_checkGameEnd] Vitória detectada'); return; } if (!this._currentPlayerId) { console.log('[DEBUG][_checkGameEnd] currentPlayerId indefinido'); return; } const currentPlayer = this._players.find(p => p.id === this._currentPlayerId); if (!currentPlayer) { console.log('[DEBUG][_checkGameEnd] currentPlayer não encontrado'); return; } if (currentPlayer.cardCount === 0) { console.log('[DEBUG][_checkGameEnd] currentPlayer sem cartas'); return; } const playableCards = this._getPlayableCards(currentPlayer); if (playableCards.length === 0) { this._isFinished = true; this._recordAction(GameAction_1.GameAction.gameLost(this._id, currentPlayer.name)); await this._persist(); console.log('[DEBUG][_checkGameEnd] DERROTA detectada por softlock (nenhuma carta jogável)'); return; } const requiredCards = this.getRequiredCardsToPlay(currentPlayer.id); const cardsPlayedThisTurn = this._getCardsPlayedThisTurn(currentPlayer.id); const remainingRequiredCards = requiredCards - cardsPlayedThisTurn; console.log(`[DEBUG][_checkGameEnd] Player: ${currentPlayer.name}, Required: ${requiredCards}, Played: ${cardsPlayedThisTurn}, Remaining: ${remainingRequiredCards}`); if (remainingRequiredCards <= 0) { console.log('[DEBUG][_checkGameEnd] Não precisa jogar mais cartas, não é derrota'); return; } const canPlay = this._canPlayerPlayRequiredCards(currentPlayer, remainingRequiredCards); console.log(`[DEBUG][_checkGameEnd] _canPlayerPlayRequiredCards: ${canPlay}`); if (!canPlay) { this._isFinished = true; this._recordAction(GameAction_1.GameAction.gameLost(this._id, currentPlayer.name)); await this._persist(); console.log('[DEBUG][_checkGameEnd] DERROTA detectada!'); return; } console.log('[DEBUG][_checkGameEnd] Ainda pode jogar, não é derrota'); } _checkVictory() { const allPlayersHaveNoCards = this._players.every(player => player.cardCount === 0); const deckIsEmpty = this._deck.isEmpty; return allPlayersHaveNoCards && deckIsEmpty; } _checkDefeat() { if (!this._currentPlayerId) { return false; } const currentPlayer = this._players.find(p => p.id === this._currentPlayerId); if (!currentPlayer) { return false; } if (currentPlayer.cardCount === 0) { return false; } const requiredCards = this.getRequiredCardsToPlay(currentPlayer.id); const cardsPlayedThisTurn = this._getCardsPlayedThisTurn(currentPlayer.id); const remainingRequiredCards = requiredCards - cardsPlayedThisTurn; if (remainingRequiredCards <= 0) { return false; } return !this._canPlayerPlayRequiredCards(currentPlayer, remainingRequiredCards); } checkForImmediateDefeat() { return this._checkDefeat(); } async forceDefeatCheck() { if (this._checkDefeat()) { this._isFinished = true; const currentPlayer = this._players.find(p => p.id === this._currentPlayerId); this._recordAction(GameAction_1.GameAction.gameLost(this._id, currentPlayer?.name || 'Unknown Player')); await this._persist(); } } async restart() { console.log(`🔄 Game: Restarting game ${this._id}`); const existingPlayers = this._players.map(player => ({ id: player.id, name: player.name })); const allCards = []; for (const player of this._players) { allCards.push(...player.cards); player.clearCards(); } this._piles = Pile_1.Pile.createInitialPiles(); this._deck.addCards(allCards); this._deck.shuffle(); for (const playerData of existingPlayers) { const player = this._players.find(p => p.id === playerData.id); if (player) { const cards = this._deck.drawCards(6); player.addCards(cards); } } this._isFinished = false; delete this._currentPlayerId; if (this._players.length > 0) { const firstPlayer = this._players[0]; if (firstPlayer) { firstPlayer.setCurrentTurn(true); this._currentPlayerId = firstPlayer.id; } } this._createdAt = new Date(); const originalActions = this._actions.filter(action => action.type === 'GAME_CREATED'); this._actions = [...originalActions]; this._recordAction(GameAction_1.GameAction.gameRestarted(this._id, existingPlayers.length)); await this._persist(); console.log(`✅ Game: Game ${this._id} restarted successfully`); } _canPlayerPlayAnyCard(player) { for (const card of player.cards) { for (const pile of this._piles) { if (pile.canAcceptCard(card)) { return true; } } } return false; } _canPlayerPlayRequiredCards(player, requiredCards) { if (requiredCards <= 0) { return true; } if (player.cardCount < requiredCards) { console.log(`[DEBUG][_canPlayerPlayRequiredCards] player.cardCount < requiredCards (${player.cardCount} < ${requiredCards})`); return false; } const playableCards = this._getPlayableCards(player); console.log(`[DEBUG][_canPlayerPlayRequiredCards] playableCards.length: ${playableCards.length}, requiredCards: ${requiredCards}`); if (playableCards.length > 0) { console.log('[DEBUG][_canPlayerPlayRequiredCards] playableCards:', playableCards.map(pc => ({ value: pc.card.value, pile: pc.pileNumber }))); } return playableCards.length >= requiredCards; } _getPlayableCards(player) { const playableCards = []; for (const card of player.cards) { for (const pile of this._piles) { if (pile.canAcceptCard(card)) { playableCards.push({ card, pileNumber: pile.number }); break; } } } console.log(`[DEBUG][_getPlayableCards] Player: ${player.name}, Cartas jogáveis:`, playableCards.map(pc => ({ value: pc.card.value, pile: pc.pileNumber }))); return playableCards; } _findNextPlayablePlayer(startPlayerId) { const startIndex = this._players.findIndex(p => p.id === startPlayerId); if (startIndex === -1) { return null; } for (let i = 1; i <= this._players.length; i++) { const playerIndex = (startIndex + i) % this._players.length; const player = this._players[playerIndex]; if (!player) { continue; } if (player.cardCount === 0) { continue; } if (this._canPlayerPlayAnyCard(player)) { return player.id; } } return null; } async _autoDrawCardsForPlayer(player) { const maxCardsPerPlayer = 6; const currentCardCount = player.cardCount; const cardsToDraw = Math.min(maxCardsPerPlayer - currentCardCount, this._deck.size); if (cardsToDraw <= 0) { return; } const drawnCards = []; for (let i = 0; i < cardsToDraw; i++) { const card = this._deck.drawCard(); if (card) { player.addCard(card); drawnCards.push(card); } } if (drawnCards.length > 0) { this._recordAction(GameAction_1.GameAction.cardsDrawn(this._id, player.id, player.name, drawnCards.length)); } } canPlayerEndTurn(playerId) { const player = this._players.find(p => p.id === playerId); if (!player) { return false; } const cardsPlayedThisTurn = this._getCardsPlayedThisTurn(playerId); if (this._deck.size > 0) { return cardsPlayedThisTurn >= 2; } return cardsPlayedThisTurn >= 1; } getRequiredCardsToPlay(playerId) { const player = this._players.find(p => p.id === playerId); if (!player) { return 0; } const cardsPlayedThisTurn = this._getCardsPlayedThisTurn(playerId); if (this._deck.size > 0) { return Math.max(0, 2 - cardsPlayedThisTurn); } return Math.max(0, 1 - cardsPlayedThisTurn); } _getCardsPlayedThisTurn(playerId) { const turnChangeAction = this._actions .filter(action => action.type === 'TURN_CHANGED' && action.playerId === playerId) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())[0]; const turnStartTime = turnChangeAction?.timestamp || this._createdAt; return this._actions.filter(action => action.type === 'CARD_PLAYED' && action.playerId === playerId && action.timestamp.getTime() >= turnStartTime.getTime()).length; } async nextTurn() { if (this._isFinished) { throw new Error('Cannot change turn in a finished game'); } if (this._players.length === 0) { throw new Error('No players in the game'); } const currentPlayer = this._players.find(p => p.id === this._currentPlayerId); if (!currentPlayer) { throw new Error('Current player not found'); } if (!this.canPlayerEndTurn(currentPlayer.id)) { const requiredCards = this.getRequiredCardsToPlay(currentPlayer.id); const deckStatus = this._deck.size > 0 ? 'com cartas no deck' : 'vazio'; throw new Error(`Jogador deve jogar pelo menos ${requiredCards} carta(s) antes de terminar sua jogada (deck ${deckStatus})`); } await this._autoDrawCardsForPlayer(currentPlayer); const nextPlayerId = currentPlayer.endTurn(this._players); this._currentPlayerId = nextPlayerId; const nextPlayer = this._players.find(p => p.id === nextPlayerId); if (nextPlayer) { this._recordAction(GameAction_1.GameAction.turnChanged(this._id, nextPlayerId, nextPlayer.name)); } await this._handleNextPlayerValidation(); await this._persist(); } async _handleNextPlayerValidation() { if (!this._currentPlayerId) { return; } let currentPlayerId = this._currentPlayerId; let attempts = 0; const maxAttempts = this._players.length; while (attempts < maxAttempts) { const currentPlayer = this._players.find(p => p.id === currentPlayerId); if (!currentPlayer) { break; } if (currentPlayer.cardCount === 0) { const nextPlayerId = this._findNextPlayablePlayer(currentPlayerId); if (nextPlayerId && nextPlayerId !== currentPlayerId) { currentPlayerId = nextPlayerId; this._currentPlayerId = nextPlayerId; const nextPlayer = this._players.find(p => p.id === nextPlayerId); if (nextPlayer) { this._recordAction(GameAction_1.GameAction.turnChanged(this._id, nextPlayerId, nextPlayer.name)); } attempts++; continue; } else { break; } } if (!this._canPlayerPlayAnyCard(currentPlayer)) { this._isFinished = true; this._recordAction(GameAction_1.GameAction.gameLost(this._id, currentPlayer.name)); return; } break; } } equals(other) { return this._id === other._id; } toString() { return `Game(${this._id}, ${this._piles.length} piles, ${this._players.length} players, ${this._deck.size} cards in deck)`; } toJSON() { return { id: this._id, createdAt: this._createdAt.toISOString(), piles: this._piles.map(pile => pile.toJSON()), players: this._players.map(player => player.toJSON()), deck: this._deck.toJSON(), ...(this._currentPlayerId && { currentPlayerId: this._currentPlayerId }), isFinished: this._isFinished, gameResult: this.gameResult, ...(this.winnerMessage && { winnerMessage: this.winnerMessage }), actions: this._actions.map(action => ({ ...action.toJSON(), timestamp: action.timestamp.toISOString() })) }; } static fromJSON(data, repository) { const piles = data.piles.map(pileData => { const pileDataTyped = { number: pileData.number, direction: pileData.direction, lastCard: pileData.lastCard }; if (pileData.id) { return Pile_1.Pile.fromJSON({ ...pileDataTyped, id: pileData.id }); } return Pile_1.Pile.fromJSON(pileDataTyped); }); const players = data.players.map(playerData => Player_1.Player.fromJSON(playerData)); const deck = Deck_1.Deck.fromJSON(data.deck); const game = Object.create(Game.prototype); game._id = data.id; game._createdAt = data.createdAt ? new Date(data.createdAt) : new Date(); game._piles = piles; game._players = players; game._deck = deck; game._currentPlayerId = data.currentPlayerId; game._isFinished = data.isFinished; game._repository = repository; game._actions = data.actions ? data.actions.map(actionData => GameAction_1.GameAction.fromJSON({ ...actionData, type: actionData.type, timestamp: new Date(actionData.timestamp) })).sort((a, b) => { const timeDiff = b.timestamp.getTime() - a.timestamp.getTime(); if (timeDiff !== 0) { return timeDiff; } return b.id.localeCompare(a.id); }) : []; return game; } } exports.Game = Game; //# sourceMappingURL=Game.js.map