Compare commits
5 Commits
0.2.7
..
9dc0b4a7df
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dc0b4a7df | |||
| a3e51bade5 | |||
| 48959daae3 | |||
| ae952d70b0 | |||
| a62073511f |
@@ -51,3 +51,13 @@
|
||||
|
||||
* NCWF-4 Token Issues ([#8](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/8)) ([95eff42](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/95eff42dfe6d9c23ede08c7297614369a1b00d9f))
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.6...0.0.0) (2026-06-06)
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.7...0.0.0) (2026-06-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* route play-vs-bot to /vs-bot endpoint ([a620735](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/a62073511f2ac912ceb0f6b4730bef37545dd8ea))
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.8...0.0.0) (2026-06-10)
|
||||
|
||||
### Features
|
||||
|
||||
* bots ([#9](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/9)) ([48959da](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/48959daae36e709ea7782ca04fdde699854f8e66))
|
||||
|
||||
@@ -192,12 +192,12 @@
|
||||
<span class="dialog-brand">Join with a bot</span>
|
||||
<button type="button" class="dialog-close" (click)="closeJoinDialog()">×</button>
|
||||
</div>
|
||||
<p class="join-hint">Select one of your bots to enter this tournament. Its token will be rotated to authenticate the join.</p>
|
||||
<p class="join-hint">Select an official (engine-backed) bot to enter this tournament. These are the bots that actually play their moves.</p>
|
||||
|
||||
@if (botsLoading) {
|
||||
<div class="dialog-loading"><span class="pulse"></span>Loading bots…</div>
|
||||
} @else if (userBots.length === 0) {
|
||||
<p class="join-empty">You have no bots yet. Go to <strong>Bots</strong> in the nav to create one first.</p>
|
||||
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
|
||||
} @else {
|
||||
<div class="bot-pick-list">
|
||||
@for (bot of userBots; track bot.id) {
|
||||
|
||||
@@ -158,7 +158,7 @@ export class TournamentsComponent implements OnInit {
|
||||
this.joinDialogTournamentId = tournamentId;
|
||||
this.joinError = null;
|
||||
this.botsLoading = true;
|
||||
this.botService.list()
|
||||
this.botService.listOfficial()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: bots => { this.userBots = bots; this.botsLoading = false; },
|
||||
@@ -176,30 +176,22 @@ export class TournamentsComponent implements OnInit {
|
||||
if (!this.joinDialogTournamentId || this.joiningBotId) return;
|
||||
this.joiningBotId = bot.id;
|
||||
this.joinError = null;
|
||||
this.botService.rotateToken(bot.id).subscribe({
|
||||
next: token => {
|
||||
this.tournamentService.joinWithBotToken(this.joinDialogTournamentId!, token).subscribe({
|
||||
next: () => {
|
||||
this.joiningBotId = null;
|
||||
const tid = this.joinDialogTournamentId!;
|
||||
this.closeJoinDialog();
|
||||
this.tournamentService.get(tid)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(updated => {
|
||||
this.created = this.created.map(x => x.id === tid ? updated : x);
|
||||
this.started = this.started.map(x => x.id === tid ? updated : x);
|
||||
if (this.selectedTournament?.id === tid) this.selectedTournament = updated;
|
||||
});
|
||||
},
|
||||
error: err => {
|
||||
this.joiningBotId = null;
|
||||
this.joinError = err.error?.message ?? err.error?.error ?? 'Failed to join tournament.';
|
||||
}
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.tournamentService.join(this.joinDialogTournamentId, bot.id, bot.name).subscribe({
|
||||
next: () => {
|
||||
this.joiningBotId = null;
|
||||
this.joinError = 'Failed to get bot token.';
|
||||
const tid = this.joinDialogTournamentId!;
|
||||
this.closeJoinDialog();
|
||||
this.tournamentService.get(tid)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(updated => {
|
||||
this.created = this.created.map(x => x.id === tid ? updated : x);
|
||||
this.started = this.started.map(x => x.id === tid ? updated : x);
|
||||
if (this.selectedTournament?.id === tid) this.selectedTournament = updated;
|
||||
});
|
||||
},
|
||||
error: err => {
|
||||
this.joiningBotId = null;
|
||||
this.joinError = err.error?.message ?? err.error?.error ?? 'Failed to join tournament.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@ import { Bot, BotWithToken } from '../models/bot.models';
|
||||
export class BotService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = '/api/account/bots';
|
||||
private readonly officialBase = '/api/account/official-bots';
|
||||
|
||||
list(): Observable<Bot[]> {
|
||||
return this.http.get<Bot[]>(this.base);
|
||||
}
|
||||
|
||||
listOfficial(): Observable<Bot[]> {
|
||||
return this.http.get<Bot[]>(this.officialBase);
|
||||
}
|
||||
|
||||
create(name: string): Observable<BotWithToken> {
|
||||
return this.http.post<BotWithToken>(this.base, { name });
|
||||
}
|
||||
|
||||
@@ -6,110 +6,115 @@ import { ChallengeService } from './challenge.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ChallengeWebSocketService {
|
||||
private readonly challengeEventService = inject(ChallengeEventService);
|
||||
private readonly challengeService = inject(ChallengeService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly challengeEventService = inject(ChallengeEventService);
|
||||
private readonly challengeService = inject(ChallengeService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private readonly maxReconnectAttempts = 5;
|
||||
private readonly reconnectDelay = 3000;
|
||||
private intentionalClose = false;
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private readonly maxReconnectAttempts = 5;
|
||||
private readonly reconnectDelay = 3000;
|
||||
private intentionalClose = false;
|
||||
|
||||
connect(): void {
|
||||
if (this.ws) return;
|
||||
connect(): void {
|
||||
if (this.ws) return;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const url = `${environment.userWsBaseUrl}/api/user/ws?token=${encodeURIComponent(token)}`;
|
||||
const url = `${environment.userWsBaseUrl}/api/user/ws`;
|
||||
|
||||
try {
|
||||
this.intentionalClose = false;
|
||||
this.ws = new WebSocket(url);
|
||||
try {
|
||||
this.intentionalClose = false;
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data as string);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
// onclose fires right after, handles reconnect
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null;
|
||||
if (!this.intentionalClose) {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.intentionalClose = true;
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectAttempts = 0;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
this.ws?.send(JSON.stringify({ type: 'auth', token }));
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data as string);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
// onclose fires right after, handles reconnect
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null;
|
||||
if (!this.intentionalClose) {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.intentionalClose = true;
|
||||
this.reconnectAttempts = 0;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
let message: Record<string, unknown>;
|
||||
try {
|
||||
message = JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
let message: Record<string, unknown>;
|
||||
try {
|
||||
message = JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
switch (message['type']) {
|
||||
case 'CONNECTED':
|
||||
break;
|
||||
|
||||
case 'challengeCreated': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeService.getChallenge(challengeId).subscribe({
|
||||
next: (challenge) => this.challengeEventService.onChallengeReceived(challenge),
|
||||
error: () => {
|
||||
/* challenge may have already expired */
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (message['type']) {
|
||||
case 'CONNECTED':
|
||||
break;
|
||||
|
||||
case 'challengeCreated': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeService.getChallenge(challengeId).subscribe({
|
||||
next: challenge => this.challengeEventService.onChallengeReceived(challenge),
|
||||
error: () => { /* challenge may have already expired */ }
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'challengeAccepted': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
const gameId = message['gameId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
if (gameId) {
|
||||
void this.router.navigate(['/game', gameId]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'challengeDeclined':
|
||||
case 'challengeExpired':
|
||||
case 'challengeCancelled': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'challengeAccepted': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
const gameId = message['gameId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
}
|
||||
if (gameId) {
|
||||
void this.router.navigate(['/game', gameId]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
private attemptReconnect(): void {
|
||||
if (this.intentionalClose || this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => { this.connect(); }, this.reconnectDelay);
|
||||
case 'challengeDeclined':
|
||||
case 'challengeExpired':
|
||||
case 'challengeCancelled': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private attemptReconnect(): void {
|
||||
if (this.intentionalClose || this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GameState,
|
||||
GameStreamEvent,
|
||||
LegalMovesResponse,
|
||||
PlayerInfo
|
||||
PlayerInfo,
|
||||
} from '../models/game.models';
|
||||
import { StreamHandlerService } from './stream-handler.service';
|
||||
|
||||
@@ -28,11 +28,11 @@ export class GameApiService {
|
||||
const playerColor = Math.random() > 0.5 ? 'white' : 'black';
|
||||
const playerInfo: PlayerInfo = {
|
||||
id: `player-${Date.now()}`,
|
||||
displayName: 'You'
|
||||
displayName: 'You',
|
||||
};
|
||||
const botInfo: PlayerInfo = {
|
||||
id: `bot-${difficulty}`,
|
||||
displayName: `Bot (${difficulty})`
|
||||
displayName: `Bot (${difficulty})`,
|
||||
};
|
||||
|
||||
const payload =
|
||||
@@ -40,7 +40,7 @@ export class GameApiService {
|
||||
? { white: playerInfo, black: botInfo }
|
||||
: { white: botInfo, black: playerInfo };
|
||||
|
||||
return this.http.post<GameFull>(`${this.apiBase}${this.apiPath}`, payload);
|
||||
return this.http.post<GameFull>(`${this.apiBase}${this.apiPath}/vs-bot`, payload);
|
||||
}
|
||||
|
||||
getGame(gameId: string): Observable<GameFull> {
|
||||
@@ -56,7 +56,9 @@ export class GameApiService {
|
||||
if (square) {
|
||||
params = params.set('square', square);
|
||||
}
|
||||
return this.http.get<LegalMovesResponse>(`${this.apiBase}${this.apiPath}/${gameId}/moves`, { params });
|
||||
return this.http.get<LegalMovesResponse>(`${this.apiBase}${this.apiPath}/${gameId}/moves`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
importFen(fen: string): Observable<GameFull> {
|
||||
@@ -85,11 +87,8 @@ export class GameApiService {
|
||||
}
|
||||
|
||||
streamGame(gameId: string): Observable<GameStreamEvent> {
|
||||
const token = localStorage.getItem('token');
|
||||
let wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`;
|
||||
if (token) {
|
||||
wsUrl += `?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
return this.streamHandler.createGameStream(wsUrl, gameId);
|
||||
const wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`;
|
||||
const token = localStorage.getItem('token') ?? '';
|
||||
return this.streamHandler.createGameStream(wsUrl, gameId, token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const WS_CONNECT_TIMEOUT_MS = 3000;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StreamHandlerService {
|
||||
createGameStream(wsUrl: string, gameId: string): Observable<GameStreamEvent> {
|
||||
createGameStream(wsUrl: string, gameId: string, token: string): Observable<GameStreamEvent> {
|
||||
return new Observable<GameStreamEvent>((observer) => {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let connected = false;
|
||||
@@ -14,7 +14,7 @@ export class StreamHandlerService {
|
||||
const emitErrorEvent = (message: string): void => {
|
||||
const errorEvent: ErrorEvent = {
|
||||
type: 'error',
|
||||
error: { code: 'STREAM_ERROR', message }
|
||||
error: { code: 'STREAM_ERROR', message },
|
||||
};
|
||||
observer.next(errorEvent);
|
||||
};
|
||||
@@ -36,6 +36,7 @@ export class StreamHandlerService {
|
||||
connected = true;
|
||||
clearTimeout(connectionTimeoutId);
|
||||
console.log(`[StreamHandler] WebSocket connected for ${gameId}`);
|
||||
ws.send(JSON.stringify({ type: 'auth', token }));
|
||||
};
|
||||
|
||||
ws.onmessage = (message) => {
|
||||
|
||||
@@ -40,10 +40,8 @@ export class TournamentService {
|
||||
return this.http.post<Tournament>(`${this.base}/${id}/start`, null);
|
||||
}
|
||||
|
||||
joinWithBotToken(id: string, botToken: string): Observable<void> {
|
||||
return this.http.post<void>(`${this.base}/${id}/join`, null, {
|
||||
headers: new HttpHeaders({ Authorization: `Bearer ${botToken}` })
|
||||
});
|
||||
join(id: string, botId: string, botName: string): Observable<void> {
|
||||
return this.http.post<void>(`${this.base}/${id}/join`, { botId, botName });
|
||||
}
|
||||
|
||||
roundPairings(id: string, round: number): Observable<RoundPairings> {
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=2
|
||||
PATCH=7
|
||||
MINOR=3
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user