Files
NowChess-Frontend/src/app/pages/game/game.facade.ts
T
shosho996 95eff42dfe fix: NCWF-4 Token Issues (#8)
Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de>
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #8
2026-06-02 21:55:55 +02:00

369 lines
11 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';
import { GameHistoryService } from '../../services/game-history.service';
@Injectable()
export class GameFacade implements OnDestroy {
gameId = '';
game: GameFull | null = null;
clockSyncedAt = 0;
errorMessage = '';
moveInput = '';
fenInput = '';
pgnInput = '';
loading = true;
gameCompletionMessage = '';
isGameFinished = false;
isPromotionDialogOpen = false;
resignConfirmPending = false;
private fenHistory: string[] = [];
private sessionStartPly = 0;
viewingPly: number | null = null;
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);
private readonly gameHistory = inject(GameHistoryService);
get state(): GameState | null {
return this.game?.state ?? null;
}
get selectedSquare(): string | null {
return this.boardSelection.selectedSquare;
}
get highlightedSquares(): string[] {
return this.boardSelection.highlightedSquares;
}
get displayFen(): string {
if (this.viewingPly !== null) {
const historyIndex = this.viewingPly - this.sessionStartPly;
return this.fenHistory[historyIndex] ?? this.game?.state.fen ?? '';
}
return this.game?.state.fen ?? '';
}
get isReviewing(): boolean {
return this.viewingPly !== null;
}
navigateToPly(ply: number): void {
const historyIndex = ply - this.sessionStartPly;
if (historyIndex < 0 || historyIndex >= this.fenHistory.length) return;
this.viewingPly = ply;
this.boardSelection = this.boardSelectionService.clearSelection();
}
navigateHistory(direction: 'first' | 'prev' | 'next' | 'last'): void {
const totalPly = this.sessionStartPly + this.fenHistory.length - 1;
const current = this.viewingPly ?? totalPly;
let next: number;
switch (direction) {
case 'first': next = this.sessionStartPly; break;
case 'prev': next = Math.max(this.sessionStartPly, current - 1); break;
case 'next': next = Math.min(totalPly, current + 1); break;
case 'last':
default: next = totalPly; break;
}
if (next === totalPly) {
this.viewingPly = null;
} else {
this.viewingPly = next;
}
this.boardSelection = this.boardSelectionService.clearSelection();
}
ngOnDestroy(): void {
this.streamService.cleanup();
this.botMoveService.cleanup();
}
setGameId(gameId: string): void {
this.gameId = gameId;
this.loadGame();
}
onBoardSquareSelected(square: string): void {
if (!this.state || this.viewingPly !== null) {
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.clockSyncedAt = Date.now();
this.pushFen(state.fen);
this.viewingPly = null;
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 = [];
}
requestResign(): void {
this.resignConfirmPending = true;
}
cancelResign(): void {
this.resignConfirmPending = false;
}
confirmResign(): void {
this.resignConfirmPending = false;
this.gameApi
.resignGame(this.gameId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
error: (error) => {
this.errorMessage = getErrorMessage(error, 'Could not resign.');
}
});
}
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.fenHistory = [];
this.viewingPly = null;
this.gameApi
.getGame(this.gameId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (game) => {
this.game = game;
this.clockSyncedAt = Date.now();
this.loading = false;
this.sessionStartPly = game.state.moves.length;
this.fenHistory = [game.state.fen];
this.updateGameCompletion();
this.gameHistory.recordGame(this.gameId);
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),
() => { /* polling fallback — not an error */ }
);
}
private applyStreamEvent(event: GameStreamEvent): void {
if (event.type === 'gameFull') {
this.game = event.game;
this.clockSyncedAt = Date.now();
this.pushFen(event.game.state.fen);
if (this.viewingPly === null) {
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.clockSyncedAt = Date.now();
this.pushFen(event.state.fen);
this.updateGameCompletion();
if (event.state.moves.length !== moveCountBefore && this.viewingPly === null) {
this.boardSelection = this.boardSelectionService.clearSelection();
this.tryMakeBotMove();
}
return;
}
if (event.type === 'error') {
this.errorMessage = event.error.message;
}
}
private pushFen(fen: string): void {
const last = this.fenHistory[this.fenHistory.length - 1];
if (last !== fen) {
this.fenHistory.push(fen);
}
}
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;
}
}