import { DestroyRef, Injectable, OnDestroy, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getErrorMessage } from '../../core/http/error-message.util'; import { GameFull, GameState, GameStreamEvent, LegalMove } from '../../models/game.models'; import { GameApiService } from '../../services/game-api.service'; import { BotMoveService } from '../../services/bot-move.service'; import { GameCompletionService } from '../../services/game-completion.service'; import { GameImportService } from '../../services/game-import.service'; import { BoardSelectionService, BoardSelection } from '../../services/board-selection.service'; import { GameStreamService } from '../../services/game-stream.service'; @Injectable() export class GameFacade implements OnDestroy { gameId = ''; game: GameFull | null = null; errorMessage = ''; moveInput = ''; fenInput = ''; pgnInput = ''; loading = true; gameCompletionMessage = ''; isGameFinished = false; isPromotionDialogOpen = false; private boardSelection: BoardSelection = { selectedSquare: null, highlightedSquares: [], selectedSquareMoves: [] }; private pendingPromotionMoves: LegalMove[] = []; private readonly gameApi = inject(GameApiService); private readonly destroyRef = inject(DestroyRef); private readonly botMoveService = inject(BotMoveService); private readonly completionService = inject(GameCompletionService); private readonly importService = inject(GameImportService); private readonly boardSelectionService = inject(BoardSelectionService); private readonly streamService = inject(GameStreamService); get state(): GameState | null { return this.game?.state ?? null; } get selectedSquare(): string | null { return this.boardSelection.selectedSquare; } get highlightedSquares(): string[] { return this.boardSelection.highlightedSquares; } ngOnDestroy(): void { this.streamService.cleanup(); this.botMoveService.cleanup(); } setGameId(gameId: string): void { this.gameId = gameId; this.loadGame(); } onBoardSquareSelected(square: string): void { if (!this.state) { return; } // Handle move selection if (this.boardSelection.selectedSquare && this.boardSelection.highlightedSquares.includes(square)) { const selectedMove = this.boardSelection.selectedSquareMoves.find((move) => move.to === square); if (selectedMove) { // If multiple promotion outcomes exist for the target, ask player to choose one. const promotionMoves = this.boardSelection.selectedSquareMoves.filter( (move) => move.to === square && !!move.promotion ); if (promotionMoves.length > 0) { this.pendingPromotionMoves = promotionMoves; this.isPromotionDialogOpen = true; return; } this.moveInput = selectedMove.uci; this.submitMove(); } return; } // Load moves for selected square this.errorMessage = ''; const newSelection = this.boardSelectionService.handleSquareSelection( square, this.gameId, this.state, this.boardSelection, (moves) => { this.boardSelection = { selectedSquare: square, highlightedSquares: moves.map((move) => move.to), selectedSquareMoves: moves }; }, (error) => { this.errorMessage = error; this.boardSelection = this.boardSelectionService.clearSelection(); } ); this.boardSelection = newSelection; } submitMove(): void { const uci = this.moveInput.trim(); if (!uci) { return; } this.errorMessage = ''; this.gameApi .makeMove(this.gameId, uci) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (state) => { if (this.game) { this.game = { ...this.game, state }; this.updateGameCompletion(); } this.moveInput = ''; this.boardSelection = this.boardSelectionService.clearSelection(); this.botMoveService.tryMakeBotMove( this.gameId, this.game, this.state, (updatedState) => { if (this.game) { this.game = { ...this.game, state: updatedState }; this.updateGameCompletion(); } this.boardSelection = this.boardSelectionService.clearSelection(); }, (error) => { this.errorMessage = error; } ); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Move rejected.'); } }); } onPromotionSelected(promotionPiece: 'queen' | 'rook' | 'bishop' | 'knight'): void { const selectedPromotionMove = this.pendingPromotionMoves.find((move) => move.promotion === promotionPiece); if (!selectedPromotionMove) { this.errorMessage = 'Selected promotion move is unavailable.'; this.isPromotionDialogOpen = false; this.pendingPromotionMoves = []; return; } this.moveInput = selectedPromotionMove.uci; this.isPromotionDialogOpen = false; this.boardSelection = this.boardSelectionService.clearSelection(); this.pendingPromotionMoves = []; this.submitMove(); } onPromotionClosed(): void { this.isPromotionDialogOpen = false; this.boardSelection = this.boardSelectionService.clearSelection(); this.pendingPromotionMoves = []; } importFen(): void { this.errorMessage = ''; this.importService.importFen( this.fenInput, () => { this.fenInput = ''; this.pgnInput = ''; }, (error) => { this.errorMessage = error; } ); } importPgn(): void { this.errorMessage = ''; this.importService.importPgn( this.pgnInput, () => { this.pgnInput = ''; this.fenInput = ''; }, (error) => { this.errorMessage = error; } ); } private loadGame(): void { this.loading = true; this.errorMessage = ''; this.boardSelection = this.boardSelectionService.clearSelection(); this.streamService.cleanup(); this.gameApi .getGame(this.gameId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (game) => { this.game = game; this.loading = false; this.updateGameCompletion(); this.startStreaming(); this.tryMakeBotMove(); }, error: (error) => { this.errorMessage = getErrorMessage(error, `Could not load game ${this.gameId}.`); this.loading = false; } }); } private startStreaming(): void { this.streamService.startStreaming( this.gameId, (event) => this.applyStreamEvent(event), () => { this.errorMessage = 'Live stream disconnected. Falling back to polling.'; } ); } private applyStreamEvent(event: GameStreamEvent): void { if (event.type === 'gameFull') { this.game = event.game; this.boardSelection = this.boardSelectionService.clearSelection(); this.updateGameCompletion(); this.tryMakeBotMove(); return; } if (event.type === 'gameState' && this.game) { const moveCountBefore = this.game.state.moves.length; this.game = { ...this.game, state: event.state }; this.updateGameCompletion(); if (event.state.moves.length !== moveCountBefore) { this.boardSelection = this.boardSelectionService.clearSelection(); this.tryMakeBotMove(); } return; } if (event.type === 'error') { this.errorMessage = event.error.message; } } private tryMakeBotMove(): void { this.botMoveService.tryMakeBotMove( this.gameId, this.game, this.state, (updatedState) => { if (this.game) { this.game = { ...this.game, state: updatedState }; this.updateGameCompletion(); } this.boardSelection = this.boardSelectionService.clearSelection(); }, (error) => { this.errorMessage = error; } ); } private updateGameCompletion(): void { const completion = this.completionService.getGameCompletion(this.game, this.state); this.gameCompletionMessage = completion.message; this.isGameFinished = completion.isFinished; } }