diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java index ba5e7d1..cca78e4 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java @@ -23,6 +23,7 @@ public class BlackJackService { BlackJackGameEntity game = new BlackJackGameEntity(); game.setUser(user); game.setBet(betAmount); + game.setState("IN_PROGRESS"); for (int i = 0; i < 2; i++) { CardEntity playerCard = createRandomCard(game); diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.html b/frontend/src/app/feature/game/blackjack/blackjack.component.html index db4d698..ae130d4 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.html +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.html @@ -3,20 +3,23 @@ <div class="container mx-auto px-4 py-6 space-y-8"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div class="lg:col-span-3 space-y-6 flex flex-col gap-4"> - <app-dealer-hand [cards]="dealerCards"></app-dealer-hand> - <app-player-hand [cards]="playerCards"></app-player-hand> - <app-game-controls - (hit)="onHit()" - (stand)="onStand()" - (leave)="leaveGame()" - ></app-game-controls> + <app-dealer-hand [cards]="dealerCards()"></app-dealer-hand> + <app-player-hand [cards]="playerCards()"></app-player-hand> + @if (gameInProgress()) { + <app-game-controls + (hit)="onHit()" + (stand)="onStand()" + (leave)="leaveGame()" + ></app-game-controls> + } </div> <div class="lg:col-span-1 space-y-6"> <app-game-info [balance]="balance()" - [currentBet]="currentBet" - (newGame)="onNewGame()" + [currentBet]="currentBet()" + [gameInProgress]="gameInProgress()" + (newGame)="onNewGame($event)" ></app-game-info> </div> </div> diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 4e66587..174c358 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -8,7 +8,8 @@ import { DealerHandComponent } from './components/dealer-hand/dealer-hand.compon import { PlayerHandComponent } from './components/player-hand/player-hand.component'; import { GameControlsComponent } from './components/game-controls/game-controls.component'; import { GameInfoComponent } from './components/game-info/game-info.component'; -import { Card } from './models/card.model'; +import { Card, BlackjackGame } from './models/blackjack.model'; +import { BlackjackService } from './services/blackjack.service'; @Component({ selector: 'app-blackjack', @@ -28,16 +29,14 @@ import { Card } from './models/card.model'; export default class BlackjackComponent { private router = inject(Router); private userService = inject(UserService); + private blackjackService = inject(BlackjackService); - dealerCards: Card[] = [ - { value: '2', suit: '♥', hidden: false }, - { value: '3', suit: '♦', hidden: false }, - { value: 'B', suit: '□', hidden: true }, - ]; - - playerCards: Card[] = []; - currentBet = 0; + dealerCards = signal<Card[]>([]); + playerCards = signal<Card[]>([]); + currentBet = signal(0); balance = signal(0); + currentGameId = signal<number | undefined>(undefined); + gameInProgress = signal(false); constructor() { this.userService.getCurrentUser().subscribe((user) => { @@ -45,19 +44,65 @@ export default class BlackjackComponent { }); } + private updateGameState(game: BlackjackGame) { + console.log('Game state update:', game); + this.currentGameId.set(game.id); + this.currentBet.set(game.bet); + this.gameInProgress.set(game.state === 'IN_PROGRESS'); + + this.dealerCards.set( + game.dealerCards.map((card, index) => ({ + ...card, + hidden: index === 1 && game.state === 'IN_PROGRESS', + })) + ); + + this.playerCards.set( + game.playerCards.map((card) => ({ + ...card, + hidden: false, + })) + ); + } + + onNewGame(bet: number): void { + this.blackjackService.startGame(bet).subscribe({ + next: (game) => { + this.updateGameState(game); + }, + error: (error) => { + console.error('Failed to start game:', error); + }, + }); + } + onHit(): void { - // Implementation for hit action + if (!this.currentGameId()) return; + + this.blackjackService.hit(this.currentGameId()!).subscribe({ + next: (game) => { + this.updateGameState(game); + }, + error: (error) => { + console.error('Failed to hit:', error); + }, + }); } onStand(): void { - // Implementation for stand action + if (!this.currentGameId()) return; + + this.blackjackService.stand(this.currentGameId()!).subscribe({ + next: (game) => { + this.updateGameState(game); + }, + error: (error) => { + console.error('Failed to stand:', error); + }, + }); } leaveGame(): void { this.router.navigate(['/home']); } - - onNewGame(): void { - // Implementation for new game - } } diff --git a/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts b/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts index 6b8cdce..89517a1 100644 --- a/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { PlayingCardComponent } from '../playing-card/playing-card.component'; -import { Card } from '../../models/card.model'; +import { Card } from '../../models/blackjack.model'; @Component({ selector: 'app-dealer-hand', @@ -12,12 +12,19 @@ import { Card } from '../../models/card.model'; <h3 class="section-heading text-2xl mb-4">Croupier's Karten</h3> <div class="card p-6 !bg-accent-red"> <div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-red-400 rounded-lg"> - <app-playing-card - *ngFor="let card of cards" - [value]="card.value" - [suit]="card.suit" - [hidden]="card.hidden" - ></app-playing-card> + @if (cards.length > 0) { + @for (card of cards; track card) { + <app-playing-card + [rank]="card.rank" + [suit]="card.suit" + [hidden]="card.hidden" + ></app-playing-card> + } + } @else { + <div class="flex items-center justify-center text-white/70 text-lg font-medium"> + Warte auf Spielstart... + </div> + } </div> </div> </div> diff --git a/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts b/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts index 8f9d532..e35bf73 100644 --- a/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts @@ -1,10 +1,19 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, +} from '@angular/core'; import { CommonModule, CurrencyPipe } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; @Component({ selector: 'app-game-info', standalone: true, - imports: [CommonModule, CurrencyPipe], + imports: [CommonModule, CurrencyPipe, ReactiveFormsModule], template: ` <div class="card p-4"> <h3 class="section-heading text-xl mb-4">Spiel Informationen</h3> @@ -19,14 +28,112 @@ import { CommonModule, CurrencyPipe } from '@angular/common'; {{ currentBet | currency: 'EUR' }} </span> </div> - <button class="button-primary w-full py-2" (click)="newGame.emit()">Neues Spiel</button> + + @if (!gameInProgress) { + <div class="grid grid-cols-2 gap-2 mb-4"> + <button + (click)="setBetAmount(0.1)" + class="button-primary py-2 text-sm" + [disabled]="gameInProgress" + > + 10% + </button> + <button + (click)="setBetAmount(0.25)" + class="button-primary py-2 text-sm" + [disabled]="gameInProgress" + > + 25% + </button> + <button + (click)="setBetAmount(0.5)" + class="button-primary py-2 text-sm" + [disabled]="gameInProgress" + > + 50% + </button> + <button + (click)="setBetAmount(1)" + class="button-primary py-2 text-sm" + [disabled]="gameInProgress" + > + 100% + </button> + </div> + } + + <form [formGroup]="betForm" (ngSubmit)="onSubmit()" class="space-y-2"> + <div class="space-y-1"> + <label for="bet" class="text-sm text-text-secondary">Einsatz</label> + <input + type="number" + id="bet" + formControlName="bet" + class="w-full px-3 py-2 bg-deep-blue-light text-white rounded focus:outline-none focus:ring-2 focus:ring-emerald" + [min]="1" + [max]="balance" + step="0.01" + /> + @if (betForm.get('bet')?.errors?.['required'] && betForm.get('bet')?.touched) { + <span class="text-xs text-accent-red">Bitte geben Sie einen Einsatz ein</span> + } + @if (betForm.get('bet')?.errors?.['min'] && betForm.get('bet')?.touched) { + <span class="text-xs text-accent-red">Mindestens 1€ setzen</span> + } + @if (betForm.get('bet')?.errors?.['max'] && betForm.get('bet')?.touched) { + <span class="text-xs text-accent-red">Nicht genügend Guthaben</span> + } + </div> + <button + type="submit" + class="button-primary w-full py-2" + [disabled]="!betForm.valid || gameInProgress" + > + Neues Spiel + </button> + </form> </div> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class GameInfoComponent { +export class GameInfoComponent implements OnChanges { @Input() balance = 0; @Input() currentBet = 0; - @Output() newGame = new EventEmitter<void>(); + @Input() gameInProgress = false; + @Output() newGame = new EventEmitter<number>(); + + betForm: FormGroup; + + constructor(private fb: FormBuilder) { + this.betForm = this.fb.group({ + bet: ['', [Validators.required, Validators.min(1)]], + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['balance']) { + this.betForm + .get('bet') + ?.setValidators([Validators.required, Validators.min(1), Validators.max(this.balance)]); + this.betForm.get('bet')?.updateValueAndValidity(); + } + } + + setBetAmount(percentage: number) { + const betAmount = Math.floor(this.balance * percentage * 100) / 100; + if (betAmount >= 1) { + this.betForm.patchValue({ bet: betAmount }); + } + } + + onSubmit() { + if (this.betForm.valid) { + const betAmount = parseFloat(this.betForm.value.bet); + if (betAmount <= this.balance) { + this.newGame.emit(betAmount); + this.betForm.reset(); + } + } + } } diff --git a/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts b/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts index 2dc50c0..e158f67 100644 --- a/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { PlayingCardComponent } from '../playing-card/playing-card.component'; -import { Card } from '../../models/card.model'; +import { Card } from '../../models/blackjack.model'; @Component({ selector: 'app-player-hand', @@ -14,12 +14,19 @@ import { Card } from '../../models/card.model'; <div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg" > - <app-playing-card - *ngFor="let card of cards" - [value]="card.value" - [suit]="card.suit" - [hidden]="card.hidden" - ></app-playing-card> + @if (cards.length > 0) { + @for (card of cards; track card) { + <app-playing-card + [rank]="card.rank" + [suit]="card.suit" + [hidden]="card.hidden" + ></app-playing-card> + } + } @else { + <div class="flex items-center justify-center text-white/70 text-lg font-medium"> + Platziere eine Wette um zu spielen... + </div> + } </div> </div> </div> diff --git a/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts b/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts index dc93df8..186ac9b 100644 --- a/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { suitSymbols, Suit } from '../../models/blackjack.model'; @Component({ selector: 'app-playing-card', @@ -10,21 +11,49 @@ import { CommonModule } from '@angular/common'; class="w-24 h-36 rounded-lg p-2 relative flex flex-col justify-between shadow-lg" [class]="hidden ? 'bg-red-800' : 'bg-white'" > - <span *ngIf="!hidden" class="text-xl font-bold text-accent-red">{{ value }}</span> - <span - *ngIf="!hidden" - class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl text-accent-red" - >{{ suit }}</span - > - <span *ngIf="!hidden" class="text-xl font-bold text-accent-red self-end rotate-180">{{ - value - }}</span> + @if (!hidden) { + <span class="text-xl font-bold text-accent-red">{{ getDisplayRank(rank) }}</span> + } + @if (!hidden) { + <span + class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl text-accent-red" + >{{ getSuitSymbol(suit) }}</span + > + } + @if (!hidden) { + <span class="text-xl font-bold text-accent-red self-end rotate-180">{{ + getDisplayRank(rank) + }}</span> + } </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PlayingCardComponent { - @Input({ required: true }) value!: string; - @Input({ required: true }) suit!: string; + @Input({ required: true }) rank!: string; + @Input({ required: true }) suit!: Suit; @Input({ required: true }) hidden!: boolean; + + protected getSuitSymbol(suit: Suit): string { + return suitSymbols[suit]; + } + + protected getDisplayRank(rank: string): string { + const rankMap: Record<string, string> = { + TWO: '2', + THREE: '3', + FOUR: '4', + FIVE: '5', + SIX: '6', + SEVEN: '7', + EIGHT: '8', + NINE: '9', + TEN: '10', + JACK: 'J', + QUEEN: 'Q', + KING: 'K', + ACE: 'A', + }; + return rankMap[rank] || rank; + } } diff --git a/frontend/src/app/feature/game/blackjack/models/blackjack.model.ts b/frontend/src/app/feature/game/blackjack/models/blackjack.model.ts new file mode 100644 index 0000000..55f2ee1 --- /dev/null +++ b/frontend/src/app/feature/game/blackjack/models/blackjack.model.ts @@ -0,0 +1,23 @@ +export type Suit = 'HEARTS' | 'DIAMONDS' | 'CLUBS' | 'SPADES'; + +export interface Card { + suit: Suit; + rank: string; + hidden: boolean; +} + +export interface BlackjackGame { + id: number; + state: string; + bet: number; + playerCards: Card[]; + dealerCards: Card[]; + userId: number; +} + +export const suitSymbols: Record<Suit, string> = { + HEARTS: '♥', + DIAMONDS: '♦', + CLUBS: '♣', + SPADES: '♠', +}; diff --git a/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts b/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts new file mode 100644 index 0000000..86ab7cf --- /dev/null +++ b/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts @@ -0,0 +1,44 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, catchError } from 'rxjs'; +import { BlackjackGame } from '../models/blackjack.model'; + +@Injectable({ + providedIn: 'root', +}) +export class BlackjackService { + private http = inject(HttpClient); + + startGame(bet: number): Observable<BlackjackGame> { + return this.http + .post<BlackjackGame>('/backend/blackjack/start', { betAmount: bet }, { responseType: 'json' }) + .pipe( + catchError((error) => { + console.error('Start game error:', error); + throw error; + }) + ); + } + + hit(gameId: number): Observable<BlackjackGame> { + return this.http + .post<BlackjackGame>(`/backend/blackjack/${gameId}/hit`, {}, { responseType: 'json' }) + .pipe( + catchError((error) => { + console.error('Hit error:', error); + throw error; + }) + ); + } + + stand(gameId: number): Observable<BlackjackGame> { + return this.http + .post<BlackjackGame>(`/backend/blackjack/${gameId}/stand`, {}, { responseType: 'json' }) + .pipe( + catchError((error) => { + console.error('Stand error:', error); + throw error; + }) + ); + } +}