diff --git a/src/app/pages/game/game.facade.ts b/src/app/pages/game/game.facade.ts index 03f9619..d545b33 100644 --- a/src/app/pages/game/game.facade.ts +++ b/src/app/pages/game/game.facade.ts @@ -1,7 +1,7 @@ import { DestroyRef, Injectable, OnDestroy, inject } from '@angular/core'; import { Router } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { interval, startWith, Subscription, switchMap } from 'rxjs'; +import { interval, startWith, Subscription, switchMap, delay } from 'rxjs'; import { getPieceAtSquare, isPieceColor } from '../../core/chess/fen.utils'; import { getErrorMessage } from '../../core/http/error-message.util'; import { GameFull, GameState, GameStreamEvent, LegalMove } from '../../models/game.models'; @@ -25,16 +25,37 @@ export class GameFacade implements OnDestroy { private readonly destroyRef = inject(DestroyRef); private streamSubscription: Subscription | null = null; private pollSubscription: Subscription | null = null; + private botMoveSubscription: Subscription | null = null; ngOnDestroy(): void { this.streamSubscription?.unsubscribe(); this.pollSubscription?.unsubscribe(); + this.botMoveSubscription?.unsubscribe(); } get state(): GameState | null { return this.game?.state ?? null; } + private isBotPlayer(playerId: string): boolean { + return playerId.startsWith('bot-'); + } + + private isPlayingAgainstBot(): boolean { + if (!this.game) { + return false; + } + return this.isBotPlayer(this.game.white.id) || this.isBotPlayer(this.game.black.id); + } + + private isCurrentPlayerBot(): boolean { + if (!this.game || !this.state) { + return false; + } + const currentPlayer = this.state.turn === 'white' ? this.game.white : this.game.black; + return this.isBotPlayer(currentPlayer.id); + } + setGameId(gameId: string): void { this.gameId = gameId; this.loadGame(); @@ -94,6 +115,7 @@ export class GameFacade implements OnDestroy { } this.moveInput = ''; this.clearSelection(); + this.tryMakeBotMove(); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Move rejected.'); @@ -153,6 +175,7 @@ export class GameFacade implements OnDestroy { this.clearSelection(); this.streamSubscription?.unsubscribe(); this.pollSubscription?.unsubscribe(); + this.botMoveSubscription?.unsubscribe(); this.pollSubscription = null; this.gameApi @@ -163,6 +186,7 @@ export class GameFacade implements OnDestroy { this.game = game; this.loading = false; this.startStream(); + this.tryMakeBotMove(); }, error: (error) => { this.errorMessage = getErrorMessage(error, `Could not load game ${this.gameId}.`); @@ -205,6 +229,7 @@ export class GameFacade implements OnDestroy { this.game = game; if (previousMoves !== game.state.moves.join(',')) { this.clearSelection(); + this.tryMakeBotMove(); } } }); @@ -214,6 +239,7 @@ export class GameFacade implements OnDestroy { if (event.type === 'gameFull') { this.game = event.game; this.clearSelection(); + this.tryMakeBotMove(); return; } @@ -222,6 +248,7 @@ export class GameFacade implements OnDestroy { this.game = { ...this.game, state: event.state }; if (event.state.moves.length !== moveCountBefore) { this.clearSelection(); + this.tryMakeBotMove(); } return; } @@ -231,6 +258,45 @@ export class GameFacade implements OnDestroy { } } + private tryMakeBotMove(): void { + if (!this.isPlayingAgainstBot() || !this.isCurrentPlayerBot() || !this.state) { + return; + } + + this.botMoveSubscription?.unsubscribe(); + this.botMoveSubscription = this.gameApi + .getLegalMoves(this.gameId) + .pipe( + delay(1000), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + next: (response) => { + if (response.moves.length === 0) { + return; + } + const botMove = response.moves[Math.floor(Math.random() * response.moves.length)]; + this.gameApi + .makeMove(this.gameId, botMove.uci) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (state) => { + if (this.game) { + this.game = { ...this.game, state }; + } + this.clearSelection(); + }, + error: (error) => { + this.errorMessage = getErrorMessage(error, 'Bot move failed.'); + } + }); + }, + error: () => { + this.errorMessage = 'Could not get legal moves for bot move.'; + } + }); + } + private clearSelection(): void { this.selectedSquare = null; this.selectedSquareMoves = []; diff --git a/src/app/pages/welcome/welcome.component.css b/src/app/pages/welcome/welcome.component.css index 167173f..5663f42 100644 --- a/src/app/pages/welcome/welcome.component.css +++ b/src/app/pages/welcome/welcome.component.css @@ -142,3 +142,69 @@ p { font-size: 0.95rem; } } + +.difficulty-selector { + grid-column: 1 / -1; + background: var(--color-bg-card); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-lg-padding); + margin: var(--size-md) 0; +} + +.difficulty-selector p { + margin: 0 0 var(--size-md); + font-weight: 600; +} + +.difficulty-buttons { + display: flex; + gap: var(--size-md-gap); + flex-wrap: wrap; +} + +.difficulty-btn { + flex: 1; + min-width: 80px; + padding: var(--size-md-padding); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + cursor: pointer; + font-weight: 600; + transition: all 0.3s ease; +} + +.difficulty-btn.easy { + background: var(--color-success-light, #d4edda); + color: var(--color-text-primary); +} + +.difficulty-btn.easy:hover:enabled { + background: var(--color-success, #28a745); + color: white; +} + +.difficulty-btn.medium { + background: var(--color-warning-light, #fff3cd); + color: var(--color-text-primary); +} + +.difficulty-btn.medium:hover:enabled { + background: var(--color-warning, #ffc107); + color: var(--color-text-primary); +} + +.difficulty-btn.hard { + background: var(--color-danger-light, #f8d7da); + color: var(--color-text-primary); +} + +.difficulty-btn.hard:hover:enabled { + background: var(--color-danger, #dc3545); + color: white; +} + +.difficulty-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/src/app/pages/welcome/welcome.component.html b/src/app/pages/welcome/welcome.component.html index 392bf2e..2da4e0d 100644 --- a/src/app/pages/welcome/welcome.component.html +++ b/src/app/pages/welcome/welcome.component.html @@ -4,11 +4,28 @@
Pick a mode to begin.