Files
NowChess-Frontend/src/app/pages/game/game.facade.ts
T
2026-04-22 08:35:58 +02:00

281 lines
8.2 KiB
TypeScript

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