From fa27f2c23bbbd025c40b99896b71d6f0ab971fa9 Mon Sep 17 00:00:00 2001 From: "Lala, Shahd" Date: Wed, 10 Jun 2026 09:31:27 +0000 Subject: [PATCH 1/3] feat: bots --- src/app/models/bot.models.ts | 10 ++++ src/app/models/tournament.models.ts | 66 ++++++++++++++++++++++++++ src/app/services/bot.service.ts | 22 +++++++++ src/app/services/game-api.service.ts | 2 +- src/app/services/tournament.service.ts | 50 +++++++++++++++++++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/app/models/bot.models.ts create mode 100644 src/app/models/tournament.models.ts create mode 100644 src/app/services/bot.service.ts create mode 100644 src/app/services/tournament.service.ts diff --git a/src/app/models/bot.models.ts b/src/app/models/bot.models.ts new file mode 100644 index 0000000..7385545 --- /dev/null +++ b/src/app/models/bot.models.ts @@ -0,0 +1,10 @@ +export interface Bot { + id: string; + name: string; + rating: number; + createdAt: string; +} + +export interface BotWithToken extends Bot { + token: string; +} diff --git a/src/app/models/tournament.models.ts b/src/app/models/tournament.models.ts new file mode 100644 index 0000000..b9ef3e1 --- /dev/null +++ b/src/app/models/tournament.models.ts @@ -0,0 +1,66 @@ +export interface BotRef { + id: string; + name: string; +} + +export interface Clock { + limit: number; + increment: number; +} + +export interface Variant { + key: string; + name: string; +} + +export interface ResultDto { + rank: number; + points: number; + tieBreak: number; + bot: BotRef; + nbGames: number; + wins: number; + draws: number; + losses: number; +} + +export interface Standing { + page: number; + players: ResultDto[]; +} + +export interface Tournament { + id: string; + fullName: string; + clock: Clock; + variant: Variant; + rated: boolean; + nbPlayers: number; + nbRounds: number; + createdBy: string; + startsAt: string | null; + status: string; + round: number; + standing: Standing; + winner: BotRef | null; +} + +export interface TournamentList { + created: Tournament[]; + started: Tournament[]; + finished: Tournament[]; +} + +export interface Pairing { + id: string; + round: number; + white: BotRef | null; + black: BotRef; + gameId: string | null; + winner: string | null; +} + +export interface RoundPairings { + round: number; + pairings: Pairing[]; +} diff --git a/src/app/services/bot.service.ts b/src/app/services/bot.service.ts new file mode 100644 index 0000000..0d7b558 --- /dev/null +++ b/src/app/services/bot.service.ts @@ -0,0 +1,22 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Bot, BotWithToken } from '../models/bot.models'; + +@Injectable({ providedIn: 'root' }) +export class BotService { + private readonly http = inject(HttpClient); + private readonly base = '/api/account/official-bots'; + + list(): Observable { + return this.http.get(this.base); + } + + create(name: string): Observable { + return this.http.post(this.base, { name }); + } + + delete(botId: string): Observable { + return this.http.delete(`${this.base}/${botId}`); + } +} diff --git a/src/app/services/game-api.service.ts b/src/app/services/game-api.service.ts index 833b747..9c242d2 100644 --- a/src/app/services/game-api.service.ts +++ b/src/app/services/game-api.service.ts @@ -40,7 +40,7 @@ export class GameApiService { ? { white: playerInfo, black: botInfo } : { white: botInfo, black: playerInfo }; - return this.http.post(`${this.apiBase}${this.apiPath}`, payload); + return this.http.post(`${this.apiBase}${this.apiPath}/vs-bot`, payload); } getGame(gameId: string): Observable { diff --git a/src/app/services/tournament.service.ts b/src/app/services/tournament.service.ts new file mode 100644 index 0000000..289a27d --- /dev/null +++ b/src/app/services/tournament.service.ts @@ -0,0 +1,50 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Tournament, TournamentList, RoundPairings } from '../models/tournament.models'; + +export interface CreateTournamentForm { + name: string; + nbRounds: number; + clockLimitMinutes: number; + clockIncrement: number; + rated: boolean; +} + +@Injectable({ providedIn: 'root' }) +export class TournamentService { + private readonly http = inject(HttpClient); + private readonly base = '/api/tournament'; + + list(): Observable { + return this.http.get(this.base); + } + + get(id: string): Observable { + return this.http.get(`${this.base}/${id}`); + } + + create(form: CreateTournamentForm): Observable { + const body = new URLSearchParams(); + body.set('name', form.name); + body.set('nbRounds', String(form.nbRounds)); + body.set('clockLimit', String(form.clockLimitMinutes * 60)); + body.set('clockIncrement', String(form.clockIncrement)); + body.set('rated', String(form.rated)); + return this.http.post(this.base, body.toString(), { + headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }) + }); + } + + start(id: string): Observable { + return this.http.post(`${this.base}/${id}/start`, null); + } + + join(id: string, botId: string, botName: string): Observable { + return this.http.post(`${this.base}/${id}/join`, { botId, botName }); + } + + roundPairings(id: string, round: number): Observable { + return this.http.get(`${this.base}/${id}/round/${round}`); + } +} -- 2.52.0 From 55b9bdf68c6200804df73fd3d38805b51a8cf2b6 Mon Sep 17 00:00:00 2001 From: "Lala, Shahd" Date: Wed, 10 Jun 2026 17:16:49 +0000 Subject: [PATCH 2/3] fix: build errors --- src/app/models/tournament.models.ts | 42 +++++++++++++------------- src/app/services/bot.service.ts | 9 ++++-- src/app/services/tournament.service.ts | 6 ++-- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/app/models/tournament.models.ts b/src/app/models/tournament.models.ts index b9ef3e1..7dc854c 100644 --- a/src/app/models/tournament.models.ts +++ b/src/app/models/tournament.models.ts @@ -1,48 +1,48 @@ -export interface BotRef { - id: string; - name: string; -} - -export interface Clock { +export interface TournamentClock { limit: number; increment: number; } -export interface Variant { +export interface TournamentVariant { key: string; name: string; } -export interface ResultDto { +export interface TournamentBotRef { + id: string; + name: string; +} + +export interface TournamentResult { rank: number; points: number; tieBreak: number; - bot: BotRef; + bot: TournamentBotRef; nbGames: number; wins: number; draws: number; losses: number; } -export interface Standing { +export interface TournamentStanding { page: number; - players: ResultDto[]; + players: TournamentResult[]; } export interface Tournament { id: string; fullName: string; - clock: Clock; - variant: Variant; + clock: TournamentClock; + variant: TournamentVariant; rated: boolean; nbPlayers: number; nbRounds: number; createdBy: string; startsAt: string | null; - status: string; + status: 'created' | 'started' | 'finished'; round: number; - standing: Standing; - winner: BotRef | null; + standing: TournamentStanding; + winner: TournamentBotRef | null; } export interface TournamentList { @@ -51,16 +51,16 @@ export interface TournamentList { finished: Tournament[]; } -export interface Pairing { +export interface TournamentPairing { id: string; round: number; - white: BotRef | null; - black: BotRef; + white: TournamentBotRef | null; + black: TournamentBotRef; gameId: string | null; - winner: string | null; + winner: 'white' | 'black' | 'draw' | null; } export interface RoundPairings { round: number; - pairings: Pairing[]; + pairings: TournamentPairing[]; } diff --git a/src/app/services/bot.service.ts b/src/app/services/bot.service.ts index 0d7b558..fa77c29 100644 --- a/src/app/services/bot.service.ts +++ b/src/app/services/bot.service.ts @@ -1,12 +1,12 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, map } from 'rxjs'; import { Bot, BotWithToken } from '../models/bot.models'; @Injectable({ providedIn: 'root' }) export class BotService { private readonly http = inject(HttpClient); - private readonly base = '/api/account/official-bots'; + private readonly base = '/api/account/bots'; list(): Observable { return this.http.get(this.base); @@ -16,6 +16,11 @@ export class BotService { return this.http.post(this.base, { name }); } + rotateToken(botId: string): Observable { + return this.http.post<{ token: string }>(`${this.base}/${botId}/rotate-token`, null) + .pipe(map(r => r.token)); + } + delete(botId: string): Observable { return this.http.delete(`${this.base}/${botId}`); } diff --git a/src/app/services/tournament.service.ts b/src/app/services/tournament.service.ts index 289a27d..493d400 100644 --- a/src/app/services/tournament.service.ts +++ b/src/app/services/tournament.service.ts @@ -40,8 +40,10 @@ export class TournamentService { return this.http.post(`${this.base}/${id}/start`, null); } - join(id: string, botId: string, botName: string): Observable { - return this.http.post(`${this.base}/${id}/join`, { botId, botName }); + joinWithBotToken(id: string, botToken: string): Observable { + return this.http.post(`${this.base}/${id}/join`, null, { + headers: new HttpHeaders({ Authorization: `Bearer ${botToken}` }) + }); } roundPairings(id: string, round: number): Observable { -- 2.52.0 From 31601e7971cb766195b059a16a0a5a67d945bad2 Mon Sep 17 00:00:00 2001 From: "Lala, Shahd" Date: Wed, 10 Jun 2026 18:38:10 +0000 Subject: [PATCH 3/3] fix: join tournaments with official bots via botId/botName The tournament join was sending a bot bearer token with an empty body, but the backend join endpoint expects a JSON body {botId, botName}, so it returned 400. Restore body-based join and source the join dialog from official (engine-backed) bots via /api/account/official-bots, since only official bots have a server-side engine that plays their moves; normal account bots have no engine and never move. Co-Authored-By: Claude Opus 4.8 --- .../tournaments/tournaments.component.html | 4 +- .../tournaments/tournaments.component.ts | 40 ++++++++----------- src/app/services/bot.service.ts | 5 +++ src/app/services/tournament.service.ts | 6 +-- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/app/pages/tournaments/tournaments.component.html b/src/app/pages/tournaments/tournaments.component.html index 6ce1fe6..b611ae9 100644 --- a/src/app/pages/tournaments/tournaments.component.html +++ b/src/app/pages/tournaments/tournaments.component.html @@ -192,12 +192,12 @@ Join with a bot -

Select one of your bots to enter this tournament. Its token will be rotated to authenticate the join.

+

Select an official (engine-backed) bot to enter this tournament. These are the bots that actually play their moves.

@if (botsLoading) {
Loading bots…
} @else if (userBots.length === 0) { -

You have no bots yet. Go to Bots in the nav to create one first.

+

No official bots are available. The official-bots engine service must be running to register them.

} @else {
@for (bot of userBots; track bot.id) { diff --git a/src/app/pages/tournaments/tournaments.component.ts b/src/app/pages/tournaments/tournaments.component.ts index adfbd83..cd60d7b 100644 --- a/src/app/pages/tournaments/tournaments.component.ts +++ b/src/app/pages/tournaments/tournaments.component.ts @@ -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.'; } }); } diff --git a/src/app/services/bot.service.ts b/src/app/services/bot.service.ts index fa77c29..47b04f8 100644 --- a/src/app/services/bot.service.ts +++ b/src/app/services/bot.service.ts @@ -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 { return this.http.get(this.base); } + listOfficial(): Observable { + return this.http.get(this.officialBase); + } + create(name: string): Observable { return this.http.post(this.base, { name }); } diff --git a/src/app/services/tournament.service.ts b/src/app/services/tournament.service.ts index 493d400..289a27d 100644 --- a/src/app/services/tournament.service.ts +++ b/src/app/services/tournament.service.ts @@ -40,10 +40,8 @@ export class TournamentService { return this.http.post(`${this.base}/${id}/start`, null); } - joinWithBotToken(id: string, botToken: string): Observable { - return this.http.post(`${this.base}/${id}/join`, null, { - headers: new HttpHeaders({ Authorization: `Bearer ${botToken}` }) - }); + join(id: string, botId: string, botName: string): Observable { + return this.http.post(`${this.base}/${id}/join`, { botId, botName }); } roundPairings(id: string, round: number): Observable { -- 2.52.0