281 lines
8.2 KiB
TypeScript
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;
|
|
}
|
|
}
|