Merge pull request 'feat: add stand and get game features to blackjack game with animations' (!105) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 1m1s
All checks were successful
Release / Release (push) Successful in 1m1s
Reviewed-on: #105 Reviewed-by: Jan K9f <jan@kjan.email>
This commit is contained in:
commit
626e28ab65
25 changed files with 884 additions and 111 deletions
|
@ -10,3 +10,11 @@ Content-Type: application/json
|
|||
POST http://localhost:8080/blackjack/54/hit
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
###
|
||||
POST http://localhost:8080/blackjack/202/stand
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
###
|
||||
GET http://localhost:8080/blackjack/202
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
|
|
@ -26,6 +26,23 @@ public class BlackJackGameController {
|
|||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/blackjack/{id}")
|
||||
public ResponseEntity<Object> getGame(@PathVariable Long id, @RequestHeader("Authorization") String token) {
|
||||
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||
|
||||
if (optionalUser.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
UserEntity user = optionalUser.get();
|
||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(game);
|
||||
}
|
||||
|
||||
@PostMapping("/blackjack/{id}/hit")
|
||||
public ResponseEntity<Object> hit(@PathVariable Long id, @RequestHeader("Authorization") String token) {
|
||||
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||
|
@ -40,13 +57,41 @@ public class BlackJackGameController {
|
|||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
||||
Map<String, String> errorResponse = new HashMap<>();
|
||||
errorResponse.put("error", "Invalid state");
|
||||
return ResponseEntity.badRequest().body(errorResponse);
|
||||
return ResponseEntity.ok(blackJackService.hit(game));
|
||||
}
|
||||
|
||||
@PostMapping("/blackjack/{id}/stand")
|
||||
public ResponseEntity<Object> stand(@PathVariable Long id, @RequestHeader("Authorization") String token) {
|
||||
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||
|
||||
if (optionalUser.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(blackJackService.hit(game, user));
|
||||
UserEntity user = optionalUser.get();
|
||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(blackJackService.stand(game));
|
||||
}
|
||||
|
||||
@PostMapping("/blackjack/{id}/doubleDown")
|
||||
public ResponseEntity<Object> doubleDown(@PathVariable Long id, @RequestHeader("Authorization") String token) {
|
||||
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||
|
||||
if (optionalUser.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
UserEntity user = optionalUser.get();
|
||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(blackJackService.doubleDown(game));
|
||||
}
|
||||
|
||||
@PostMapping("/blackjack/start")
|
||||
|
|
|
@ -29,7 +29,6 @@ public class BlackJackGameEntity {
|
|||
@JsonIgnore
|
||||
private UserEntity user;
|
||||
|
||||
// Expose UserID to JSON output
|
||||
public Long getUserId() {
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package de.szut.casino.blackjack;
|
|||
import de.szut.casino.user.UserEntity;
|
||||
import de.szut.casino.user.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
@ -13,52 +14,167 @@ import java.util.Random;
|
|||
public class BlackJackService {
|
||||
private final BlackJackGameRepository blackJackGameRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final Random random = new Random();
|
||||
|
||||
public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) {
|
||||
this.blackJackGameRepository = blackJackGameRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
public BlackJackGameEntity getBlackJackGame(Long id) {
|
||||
Optional<BlackJackGameEntity> optionalBlackJackGame = blackJackGameRepository.findById(id);
|
||||
return optionalBlackJackGame.orElse(null);
|
||||
return blackJackGameRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BlackJackGameEntity createBlackJackGame(UserEntity user, BigDecimal betAmount) {
|
||||
BlackJackGameEntity game = new BlackJackGameEntity();
|
||||
game.setUser(user);
|
||||
game.setBet(betAmount);
|
||||
|
||||
initializeDeck(game);
|
||||
dealInitialCards(game);
|
||||
|
||||
game.setState(getState(game));
|
||||
deductBetFromBalance(user, betAmount);
|
||||
|
||||
return blackJackGameRepository.save(game);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
CardEntity playerCard = drawCardFromDeck(game);
|
||||
playerCard.setCardType(CardType.PLAYER);
|
||||
game.getPlayerCards().add(playerCard);
|
||||
@Transactional
|
||||
public BlackJackGameEntity hit(BlackJackGameEntity game) {
|
||||
game = refreshGameState(game);
|
||||
|
||||
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
||||
return game;
|
||||
}
|
||||
|
||||
dealCardToPlayer(game);
|
||||
updateGameStateAndBalance(game);
|
||||
|
||||
return blackJackGameRepository.save(game);
|
||||
}
|
||||
|
||||
CardEntity dealerCard = drawCardFromDeck(game);
|
||||
dealerCard.setCardType(CardType.DEALER);
|
||||
game.getDealerCards().add(dealerCard);
|
||||
|
||||
BlackJackState state = handleState(game, user);
|
||||
game.setState(state);
|
||||
|
||||
userRepository.save(user);
|
||||
blackJackGameRepository.save(game);
|
||||
@Transactional
|
||||
public BlackJackGameEntity stand(BlackJackGameEntity game) {
|
||||
game = refreshGameState(game);
|
||||
|
||||
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
||||
return game;
|
||||
}
|
||||
|
||||
dealCardsToDealerUntilMinimumScore(game);
|
||||
determineWinnerAndUpdateBalance(game);
|
||||
|
||||
return blackJackGameRepository.save(game);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BlackJackGameEntity doubleDown(BlackJackGameEntity game) {
|
||||
game = refreshGameState(game);
|
||||
|
||||
if (game.getState() != BlackJackState.IN_PROGRESS || game.getPlayerCards().size() != 2) {
|
||||
return game;
|
||||
}
|
||||
|
||||
UserEntity user = getUserWithFreshData(game.getUser());
|
||||
BigDecimal additionalBet = game.getBet();
|
||||
|
||||
if (user.getBalance().compareTo(additionalBet) < 0) {
|
||||
return game;
|
||||
}
|
||||
|
||||
deductBetFromBalance(user, additionalBet);
|
||||
game.setBet(game.getBet().add(additionalBet));
|
||||
|
||||
dealCardToPlayer(game);
|
||||
updateGameStateAndBalance(game);
|
||||
|
||||
if (game.getState() == BlackJackState.IN_PROGRESS) {
|
||||
return stand(game);
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
public BlackJackGameEntity hit(BlackJackGameEntity game, UserEntity user) {
|
||||
CardEntity drawnCard = drawCardFromDeck(game);
|
||||
drawnCard.setCardType(CardType.PLAYER);
|
||||
game.getPlayerCards().add(drawnCard);
|
||||
private BlackJackGameEntity refreshGameState(BlackJackGameEntity game) {
|
||||
return blackJackGameRepository.findById(game.getId()).orElse(game);
|
||||
}
|
||||
|
||||
private UserEntity getUserWithFreshData(UserEntity user) {
|
||||
return userRepository.findById(user.getId()).orElse(user);
|
||||
}
|
||||
|
||||
private void dealInitialCards(BlackJackGameEntity game) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
dealCardToPlayer(game);
|
||||
}
|
||||
|
||||
dealCardToDealer(game);
|
||||
}
|
||||
|
||||
private void dealCardToPlayer(BlackJackGameEntity game) {
|
||||
CardEntity card = drawCardFromDeck(game);
|
||||
card.setCardType(CardType.PLAYER);
|
||||
game.getPlayerCards().add(card);
|
||||
}
|
||||
|
||||
private void dealCardToDealer(BlackJackGameEntity game) {
|
||||
CardEntity card = drawCardFromDeck(game);
|
||||
card.setCardType(CardType.DEALER);
|
||||
game.getDealerCards().add(card);
|
||||
}
|
||||
|
||||
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
|
||||
while (calculateHandValue(game.getDealerCards()) < 17) {
|
||||
dealCardToDealer(game);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGameStateAndBalance(BlackJackGameEntity game) {
|
||||
game.setState(getState(game));
|
||||
|
||||
if (game.getState() == BlackJackState.PLAYER_WON) {
|
||||
updateUserBalance(game, true);
|
||||
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
|
||||
updateUserBalance(game, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void determineWinnerAndUpdateBalance(BlackJackGameEntity game) {
|
||||
int playerValue = calculateHandValue(game.getPlayerCards());
|
||||
int dealerValue = calculateHandValue(game.getDealerCards());
|
||||
|
||||
game.setState(handleState(game, user));
|
||||
if (dealerValue > 21 || playerValue > dealerValue) {
|
||||
game.setState(BlackJackState.PLAYER_WON);
|
||||
updateUserBalance(game, true);
|
||||
} else if (playerValue < dealerValue) {
|
||||
game.setState(BlackJackState.PLAYER_LOST);
|
||||
updateUserBalance(game, false);
|
||||
} else {
|
||||
game.setState(BlackJackState.DRAW);
|
||||
updateUserBalance(game, false); // For draw, player gets their bet back
|
||||
}
|
||||
}
|
||||
|
||||
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
|
||||
user.setBalance(user.getBalance().subtract(betAmount));
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
return blackJackGameRepository.save(game);
|
||||
@Transactional
|
||||
private void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
|
||||
UserEntity user = getUserWithFreshData(game.getUser());
|
||||
BigDecimal betAmount = game.getBet();
|
||||
BigDecimal balance = user.getBalance();
|
||||
|
||||
if (isWin) {
|
||||
balance = balance.add(betAmount.multiply(BigDecimal.valueOf(2)));
|
||||
} else if (game.getState() == BlackJackState.DRAW) {
|
||||
balance = balance.add(betAmount);
|
||||
}
|
||||
|
||||
user.setBalance(balance);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
private void initializeDeck(BlackJackGameEntity game) {
|
||||
|
@ -84,7 +200,7 @@ public class BlackJackService {
|
|||
return game.getDeck().removeFirst();
|
||||
}
|
||||
|
||||
private BlackJackState handleState(BlackJackGameEntity game, UserEntity user) {
|
||||
private BlackJackState getState(BlackJackGameEntity game) {
|
||||
int playerHandValue = calculateHandValue(game.getPlayerCards());
|
||||
|
||||
if (playerHandValue == 21) {
|
||||
|
@ -95,14 +211,14 @@ public class BlackJackService {
|
|||
int dealerHandValue = calculateHandValue(game.getDealerCards());
|
||||
|
||||
if (dealerHandValue == 21) {
|
||||
return BlackJackState.STANDOFF;
|
||||
return BlackJackState.DRAW;
|
||||
} else {
|
||||
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
|
||||
UserEntity user = getUserWithFreshData(game.getUser());
|
||||
user.setBalance(user.getBalance().add(blackjackWinnings));
|
||||
return BlackJackState.PLAYER_BLACKJACK;
|
||||
}
|
||||
} else if (playerHandValue > 21) {
|
||||
user.setBalance(user.getBalance().subtract(game.getBet()));
|
||||
return BlackJackState.PLAYER_LOST;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,5 +4,6 @@ public enum BlackJackState {
|
|||
IN_PROGRESS,
|
||||
PLAYER_BLACKJACK,
|
||||
PLAYER_LOST,
|
||||
STANDOFF,
|
||||
PLAYER_WON,
|
||||
DRAW,
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@ import {
|
|||
} from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
import { DepositService } from '../../service/deposit.service';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { ModalAnimationService } from '../../shared/services/modal-animation.service';
|
||||
import gsap from 'gsap';
|
||||
import { DepositService } from '@service/deposit.service';
|
||||
import { environment } from '@environments/environment';
|
||||
import { ModalAnimationService } from '@shared/services/modal-animation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-deposit',
|
||||
|
|
|
@ -5,10 +5,28 @@
|
|||
<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>
|
||||
|
||||
@if (isActionInProgress()) {
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="card p-4 flex items-center gap-3 animate-pulse bg-deep-blue-light border border-deep-blue-light/50"
|
||||
>
|
||||
<div
|
||||
class="w-5 h-5 rounded-full border-2 border-white border-t-transparent animate-spin"
|
||||
></div>
|
||||
<span>{{ currentAction() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (gameInProgress()) {
|
||||
<app-game-controls
|
||||
[playerCards]="playerCards()"
|
||||
[gameState]="gameState()"
|
||||
[isActionInProgress]="isActionInProgress()"
|
||||
(hit)="onHit()"
|
||||
(stand)="onStand()"
|
||||
(doubleDown)="onDoubleDown()"
|
||||
(leave)="leaveGame()"
|
||||
></app-game-controls>
|
||||
}
|
||||
|
@ -19,8 +37,17 @@
|
|||
[balance]="balance()"
|
||||
[currentBet]="currentBet()"
|
||||
[gameInProgress]="gameInProgress()"
|
||||
[isActionInProgress]="isActionInProgress()"
|
||||
(newGame)="onNewGame($event)"
|
||||
></app-game-info>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Result Modal -->
|
||||
<app-game-result
|
||||
[gameState]="gameState()"
|
||||
[amount]="currentBet()"
|
||||
[show]="showGameResult()"
|
||||
(gameResultClosed)="onCloseGameResult()"
|
||||
></app-game-result>
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NavbarComponent } from '../../../shared/components/navbar/navbar.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserService } from '../../../service/user.service';
|
||||
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
|
||||
import { DealerHandComponent } from './components/dealer-hand/dealer-hand.component';
|
||||
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, BlackjackGame } from './models/blackjack.model';
|
||||
import { BlackjackService } from './services/blackjack.service';
|
||||
import { Card, BlackjackGame } from '@blackjack/models/blackjack.model';
|
||||
import { BlackjackService } from '@blackjack/services/blackjack.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { GameResultComponent } from '@blackjack/components/game-result/game-result.component';
|
||||
import { GameState } from '@blackjack/enum/gameState';
|
||||
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||
import { UserService } from '@service/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blackjack',
|
||||
|
@ -22,6 +25,7 @@ import { BlackjackService } from './services/blackjack.service';
|
|||
PlayerHandComponent,
|
||||
GameControlsComponent,
|
||||
GameInfoComponent,
|
||||
GameResultComponent,
|
||||
],
|
||||
templateUrl: './blackjack.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
@ -37,8 +41,17 @@ export default class BlackjackComponent {
|
|||
balance = signal(0);
|
||||
currentGameId = signal<number | undefined>(undefined);
|
||||
gameInProgress = signal(false);
|
||||
gameState = signal<GameState>(GameState.IN_PROGRESS);
|
||||
showGameResult = signal(false);
|
||||
|
||||
isActionInProgress = signal(false);
|
||||
currentAction = signal<string>('');
|
||||
|
||||
constructor() {
|
||||
this.refreshUserBalance();
|
||||
}
|
||||
|
||||
private refreshUserBalance(): void {
|
||||
this.userService.getCurrentUser().subscribe((user) => {
|
||||
this.balance.set(user?.balance ?? 0);
|
||||
});
|
||||
|
@ -48,12 +61,15 @@ export default class BlackjackComponent {
|
|||
console.log('Game state update:', game);
|
||||
this.currentGameId.set(game.id);
|
||||
this.currentBet.set(game.bet);
|
||||
this.gameInProgress.set(game.state === 'IN_PROGRESS');
|
||||
this.gameInProgress.set(game.state === GameState.IN_PROGRESS);
|
||||
this.gameState.set(game.state as GameState);
|
||||
|
||||
const isGameOver = game.state !== GameState.IN_PROGRESS;
|
||||
|
||||
this.dealerCards.set(
|
||||
game.dealerCards.map((card, index) => ({
|
||||
...card,
|
||||
hidden: index === 1 && game.state === 'IN_PROGRESS',
|
||||
hidden: !isGameOver && index === 1 && game.state === GameState.IN_PROGRESS,
|
||||
}))
|
||||
);
|
||||
|
||||
|
@ -63,41 +79,136 @@ export default class BlackjackComponent {
|
|||
hidden: false,
|
||||
}))
|
||||
);
|
||||
|
||||
if (isGameOver) {
|
||||
console.log('Game is over, state:', game.state);
|
||||
this.userService.refreshCurrentUser();
|
||||
this.showGameResult.set(true);
|
||||
console.log('Game result dialog should be shown now');
|
||||
}
|
||||
}
|
||||
|
||||
onNewGame(bet: number): void {
|
||||
this.isActionInProgress.set(true);
|
||||
this.currentAction.set('Spiel wird gestartet...');
|
||||
|
||||
this.blackjackService.startGame(bet).subscribe({
|
||||
next: (game) => {
|
||||
this.updateGameState(game);
|
||||
this.userService.refreshCurrentUser();
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to start game:', error);
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onHit(): void {
|
||||
if (!this.currentGameId()) return;
|
||||
if (!this.currentGameId() || this.isActionInProgress()) return;
|
||||
|
||||
this.isActionInProgress.set(true);
|
||||
this.currentAction.set('Karte wird gezogen...');
|
||||
|
||||
this.blackjackService.hit(this.currentGameId()!).subscribe({
|
||||
next: (game) => {
|
||||
this.updateGameState(game);
|
||||
if (game.state !== 'IN_PROGRESS') {
|
||||
this.userService.refreshCurrentUser();
|
||||
}
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to hit:', error);
|
||||
this.handleGameError(error);
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStand(): void {
|
||||
if (!this.currentGameId()) return;
|
||||
if (!this.currentGameId() || this.isActionInProgress()) return;
|
||||
|
||||
if (this.gameState() !== GameState.IN_PROGRESS) {
|
||||
console.log('Cannot stand: game is not in progress');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActionInProgress.set(true);
|
||||
this.currentAction.set('Dealer zieht Karten...');
|
||||
|
||||
this.blackjackService.stand(this.currentGameId()!).subscribe({
|
||||
next: (game) => {
|
||||
this.updateGameState(game);
|
||||
this.userService.refreshCurrentUser();
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to stand:', error);
|
||||
this.handleGameError(error);
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onDoubleDown(): void {
|
||||
if (!this.currentGameId() || this.isActionInProgress()) return;
|
||||
|
||||
if (this.gameState() !== GameState.IN_PROGRESS || this.playerCards().length !== 2) {
|
||||
console.log('Cannot double down: game is not in progress or more than 2 cards');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActionInProgress.set(true);
|
||||
this.currentAction.set('Einsatz wird verdoppelt...');
|
||||
|
||||
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
|
||||
next: (game) => {
|
||||
this.updateGameState(game);
|
||||
this.userService.refreshCurrentUser();
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to double down:', error);
|
||||
this.handleGameError(error);
|
||||
this.isActionInProgress.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onCloseGameResult(): void {
|
||||
console.log('Closing game result dialog');
|
||||
this.showGameResult.set(false);
|
||||
}
|
||||
|
||||
private handleGameError(error: HttpErrorResponse): void {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
if (error.status === 400 && error.error?.error === 'Invalid state') {
|
||||
this.gameInProgress.set(false);
|
||||
|
||||
this.refreshUserBalance();
|
||||
} else if (error.status === 500) {
|
||||
console.log('Server error occurred. The game may have been updated in another session.');
|
||||
|
||||
this.gameInProgress.set(false);
|
||||
|
||||
this.refreshUserBalance();
|
||||
|
||||
if (this.currentGameId()) {
|
||||
this.refreshGameState(this.currentGameId()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private refreshGameState(gameId: number): void {
|
||||
this.blackjackService.getGame(gameId).subscribe({
|
||||
next: (game) => {
|
||||
this.updateGameState(game);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to refresh game state:', err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Card } from '@blackjack/models/blackjack.model';
|
||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||
import { Card } from '../../models/blackjack.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dealer-hand',
|
||||
|
@ -13,11 +13,12 @@ import { Card } from '../../models/blackjack.model';
|
|||
<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">
|
||||
@if (cards.length > 0) {
|
||||
@for (card of cards; track card) {
|
||||
@for (card of cardsWithState; track card.id) {
|
||||
<app-playing-card
|
||||
[rank]="card.rank"
|
||||
[suit]="card.suit"
|
||||
[hidden]="card.hidden"
|
||||
[isNew]="card.isNew"
|
||||
></app-playing-card>
|
||||
}
|
||||
} @else {
|
||||
|
@ -31,6 +32,31 @@ import { Card } from '../../models/blackjack.model';
|
|||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DealerHandComponent {
|
||||
export class DealerHandComponent implements OnChanges {
|
||||
@Input() cards: Card[] = [];
|
||||
cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
|
||||
|
||||
private lastCardCount = 0;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['cards']) {
|
||||
this.updateCardsWithState();
|
||||
}
|
||||
}
|
||||
|
||||
private updateCardsWithState(): void {
|
||||
const newCards = this.cards.length > this.lastCardCount;
|
||||
|
||||
this.cardsWithState = this.cards.map((card, index) => {
|
||||
const isNew = newCards && index >= this.lastCardCount;
|
||||
|
||||
return {
|
||||
...card,
|
||||
isNew,
|
||||
id: `${card.suit}-${card.rank}-${index}`,
|
||||
};
|
||||
});
|
||||
|
||||
this.lastCardCount = this.cards.length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,100 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { GameState } from '@blackjack/enum/gameState';
|
||||
import { Card } from '@blackjack/models/blackjack.model';
|
||||
import { GameControlsService } from '@blackjack/services/game-controls.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-game-controls',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<div class="flex justify-center gap-4">
|
||||
<button
|
||||
(click)="hit.emit()"
|
||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px]"
|
||||
>
|
||||
Ziehen
|
||||
</button>
|
||||
<button
|
||||
(click)="stand.emit()"
|
||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px]"
|
||||
>
|
||||
Halten
|
||||
</button>
|
||||
<button
|
||||
(click)="leave.emit()"
|
||||
class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-center text-lg mb-5">
|
||||
<div class="card p-4">
|
||||
<div class="text-emerald font-bold mb-1">
|
||||
Deine Punkte: {{ gameControlsService.calculateHandValue(playerCards) }}
|
||||
</div>
|
||||
<div class="text-text-secondary">
|
||||
Status:
|
||||
<span [class]="gameControlsService.getStatusClass(gameState)">{{
|
||||
gameControlsService.getStatusText(gameState)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center gap-4">
|
||||
<button
|
||||
(click)="hit.emit()"
|
||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
||||
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
|
||||
[class.opacity-50]="isActionInProgress"
|
||||
>
|
||||
<span [class.invisible]="isActionInProgress">Ziehen</span>
|
||||
@if (isActionInProgress) {
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
(click)="stand.emit()"
|
||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
||||
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
|
||||
[class.opacity-50]="isActionInProgress"
|
||||
>
|
||||
<span [class.invisible]="isActionInProgress">Halten</span>
|
||||
@if (isActionInProgress) {
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
(click)="doubleDown.emit()"
|
||||
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
|
||||
[disabled]="
|
||||
gameState !== GameState.IN_PROGRESS || playerCards.length !== 2 || isActionInProgress
|
||||
"
|
||||
[class.opacity-50]="isActionInProgress"
|
||||
>
|
||||
<span [class.invisible]="isActionInProgress">Verdoppeln</span>
|
||||
@if (isActionInProgress) {
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
(click)="leave.emit()"
|
||||
class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
|
||||
[disabled]="isActionInProgress"
|
||||
[class.opacity-50]="isActionInProgress"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class GameControlsComponent {
|
||||
@Input() playerCards: Card[] = [];
|
||||
@Input() gameState: GameState = GameState.IN_PROGRESS;
|
||||
@Input() isActionInProgress = false;
|
||||
|
||||
@Output() hit = new EventEmitter<void>();
|
||||
@Output() stand = new EventEmitter<void>();
|
||||
@Output() doubleDown = new EventEmitter<void>();
|
||||
@Output() leave = new EventEmitter<void>();
|
||||
|
||||
protected readonly GameState = GameState;
|
||||
|
||||
constructor(protected gameControlsService: GameControlsService) {}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,6 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angula
|
|||
<div class="card p-4">
|
||||
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-secondary">Guthaben:</span>
|
||||
<span class="text-emerald">{{ balance | currency: 'EUR' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-secondary">Aktuelle Wette:</span>
|
||||
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'">
|
||||
|
@ -86,10 +82,17 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angula
|
|||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="button-primary w-full py-2"
|
||||
[disabled]="!betForm.valid || gameInProgress"
|
||||
class="button-primary w-full py-2 relative"
|
||||
[disabled]="!betForm.valid || gameInProgress || isActionInProgress"
|
||||
>
|
||||
Neues Spiel
|
||||
<span [class.invisible]="isActionInProgress">Neues Spiel</span>
|
||||
@if (isActionInProgress) {
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -101,6 +104,7 @@ export class GameInfoComponent implements OnChanges {
|
|||
@Input() balance = 0;
|
||||
@Input() currentBet = 0;
|
||||
@Input() gameInProgress = false;
|
||||
@Input() isActionInProgress = false;
|
||||
@Output() newGame = new EventEmitter<number>();
|
||||
|
||||
betForm: FormGroup;
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { GameState } from '../../enum/gameState';
|
||||
|
||||
@Component({
|
||||
selector: 'app-game-result',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CurrencyPipe],
|
||||
template: `
|
||||
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
|
||||
<div class="modal-card" [@cardAnimation]>
|
||||
<h2 class="modal-heading" [class]="getResultClass()">{{ getResultTitle() }}</h2>
|
||||
<p class="py-2 text-text-secondary mb-4">{{ getResultMessage() }}</p>
|
||||
|
||||
<div
|
||||
class="bg-deep-blue-light/50 rounded-lg p-5 mb-6 shadow-inner border border-deep-blue-light/30"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="text-text-secondary">Einsatz:</div>
|
||||
<div class="font-medium text-right">{{ amount }} €</div>
|
||||
|
||||
<div class="text-text-secondary">
|
||||
{{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }}
|
||||
</div>
|
||||
<div
|
||||
class="font-medium text-right"
|
||||
[ngClass]="{
|
||||
'text-emerald': isWin,
|
||||
'text-accent-red': isLoss,
|
||||
'text-yellow-400': isDraw,
|
||||
}"
|
||||
>
|
||||
{{ isLoss ? '-' : '+' }}{{ amount }} €
|
||||
</div>
|
||||
|
||||
<div class="text-text-secondary border-t border-text-secondary/20 pt-3 font-medium">
|
||||
Gesamt:
|
||||
</div>
|
||||
<div
|
||||
class="font-medium text-right border-t border-text-secondary/20 pt-3"
|
||||
[ngClass]="{
|
||||
'text-emerald': isWin,
|
||||
'text-accent-red': isLoss,
|
||||
'text-white': isDraw,
|
||||
}"
|
||||
>
|
||||
{{ isWin ? '+' : isLoss ? '-' : '' }}{{ amount }} €
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" (click)="closeDialog()" class="button-primary w-full py-2">
|
||||
Verstanden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('fadeInOut', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('150ms ease-out', style({ opacity: 1 })),
|
||||
]),
|
||||
transition(':leave', [animate('150ms ease-in', style({ opacity: 0 }))]),
|
||||
]),
|
||||
trigger('cardAnimation', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0, transform: 'scale(0.95)' }),
|
||||
animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' })),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class GameResultComponent {
|
||||
@Input() gameState: GameState = GameState.IN_PROGRESS;
|
||||
@Input() amount = 0;
|
||||
@Input() set show(value: boolean) {
|
||||
console.log('GameResultComponent show input changed:', value, 'gameState:', this.gameState);
|
||||
this.visible = value;
|
||||
}
|
||||
|
||||
@Output() gameResultClosed = new EventEmitter<void>();
|
||||
|
||||
visible = false;
|
||||
|
||||
get isWin(): boolean {
|
||||
return this.gameState === GameState.PLAYER_WON || this.gameState === GameState.PLAYER_BLACKJACK;
|
||||
}
|
||||
|
||||
get isLoss(): boolean {
|
||||
return this.gameState === GameState.PLAYER_LOST;
|
||||
}
|
||||
|
||||
get isDraw(): boolean {
|
||||
return this.gameState === GameState.DRAW;
|
||||
}
|
||||
|
||||
getResultTitle(): string {
|
||||
if (this.gameState === GameState.PLAYER_BLACKJACK) return 'Blackjack!';
|
||||
if (this.isWin) return 'Gewonnen!';
|
||||
if (this.isLoss) return 'Verloren!';
|
||||
if (this.isDraw) return 'Unentschieden!';
|
||||
return '';
|
||||
}
|
||||
|
||||
getResultMessage(): string {
|
||||
if (this.gameState === GameState.PLAYER_BLACKJACK)
|
||||
return 'Glückwunsch! Du hast mit einem Blackjack gewonnen!';
|
||||
if (this.isWin) return 'Glückwunsch! Du hast diese Runde gewonnen.';
|
||||
if (this.isLoss) return 'Schade! Du hast diese Runde verloren.';
|
||||
if (this.isDraw) return 'Diese Runde endet unentschieden. Dein Einsatz wurde zurückgegeben.';
|
||||
return '';
|
||||
}
|
||||
|
||||
getResultClass(): string {
|
||||
if (this.gameState === GameState.PLAYER_BLACKJACK) return 'text-emerald font-bold';
|
||||
if (this.isWin) return 'text-emerald';
|
||||
if (this.isLoss) return 'text-accent-red';
|
||||
if (this.isDraw) return 'text-yellow-400';
|
||||
return '';
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.visible = false;
|
||||
this.gameResultClosed.emit();
|
||||
console.log('Dialog closed by user');
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||
import { Card } from '../../models/blackjack.model';
|
||||
import { Card } from '@blackjack/models/blackjack.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-player-hand',
|
||||
|
@ -15,11 +15,12 @@ import { Card } from '../../models/blackjack.model';
|
|||
class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg"
|
||||
>
|
||||
@if (cards.length > 0) {
|
||||
@for (card of cards; track card) {
|
||||
@for (card of cardsWithState; track card.id) {
|
||||
<app-playing-card
|
||||
[rank]="card.rank"
|
||||
[suit]="card.suit"
|
||||
[hidden]="card.hidden"
|
||||
[isNew]="card.isNew"
|
||||
></app-playing-card>
|
||||
}
|
||||
} @else {
|
||||
|
@ -33,6 +34,31 @@ import { Card } from '../../models/blackjack.model';
|
|||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PlayerHandComponent {
|
||||
export class PlayerHandComponent implements OnChanges {
|
||||
@Input() cards: Card[] = [];
|
||||
cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
|
||||
|
||||
private lastCardCount = 0;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['cards']) {
|
||||
this.updateCardsWithState();
|
||||
}
|
||||
}
|
||||
|
||||
private updateCardsWithState(): void {
|
||||
const newCards = this.cards.length > this.lastCardCount;
|
||||
|
||||
this.cardsWithState = this.cards.map((card, index) => {
|
||||
const isNew = newCards && index >= this.lastCardCount;
|
||||
|
||||
return {
|
||||
...card,
|
||||
isNew,
|
||||
id: `${card.suit}-${card.rank}-${index}`,
|
||||
};
|
||||
});
|
||||
|
||||
this.lastCardCount = this.cards.length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
AfterViewInit,
|
||||
ElementRef,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { suitSymbols, Suit } from '../../models/blackjack.model';
|
||||
import { gsap } from 'gsap';
|
||||
import { Suit, suitSymbols } from '@blackjack/models/blackjack.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playing-card',
|
||||
|
@ -8,31 +17,96 @@ import { suitSymbols, Suit } from '../../models/blackjack.model';
|
|||
imports: [CommonModule],
|
||||
template: `
|
||||
<div
|
||||
class="w-24 h-36 rounded-lg p-2 relative flex flex-col justify-between shadow-lg"
|
||||
#cardElement
|
||||
class="w-24 h-36 rounded-lg p-2 relative flex flex-col justify-between shadow-lg card-element"
|
||||
[class]="hidden ? 'bg-red-800' : 'bg-white'"
|
||||
>
|
||||
@if (!hidden) {
|
||||
<span class="text-xl font-bold text-accent-red">{{ getDisplayRank(rank) }}</span>
|
||||
<span class="text-xl font-bold" [class]="isRedSuit ? 'text-accent-red' : 'text-black'">{{
|
||||
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"
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl"
|
||||
[class]="isRedSuit ? 'text-accent-red' : 'text-black'"
|
||||
>{{ getSuitSymbol(suit) }}</span
|
||||
>
|
||||
}
|
||||
@if (!hidden) {
|
||||
<span class="text-xl font-bold text-accent-red self-end rotate-180">{{
|
||||
getDisplayRank(rank)
|
||||
}}</span>
|
||||
<span
|
||||
class="text-xl font-bold self-end rotate-180"
|
||||
[class]="isRedSuit ? 'text-accent-red' : 'text-black'"
|
||||
>{{ getDisplayRank(rank) }}</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.card-element {
|
||||
transform-style: preserve-3d;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PlayingCardComponent {
|
||||
export class PlayingCardComponent implements AfterViewInit, OnChanges {
|
||||
@Input({ required: true }) rank!: string;
|
||||
@Input({ required: true }) suit!: Suit;
|
||||
@Input({ required: true }) hidden!: boolean;
|
||||
@Input() isNew = false;
|
||||
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
|
||||
get isRedSuit(): boolean {
|
||||
return this.suit === 'HEARTS' || this.suit === 'DIAMONDS';
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.isNew) {
|
||||
this.animateNewCard();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['hidden'] && !changes['hidden'].firstChange) {
|
||||
this.animateCardFlip();
|
||||
}
|
||||
}
|
||||
|
||||
private animateNewCard(): void {
|
||||
const cardElement = this.elementRef.nativeElement.querySelector('.card-element');
|
||||
gsap.fromTo(
|
||||
cardElement,
|
||||
{
|
||||
y: -100,
|
||||
opacity: 0,
|
||||
rotation: -10,
|
||||
scale: 0.7,
|
||||
},
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
rotation: 0,
|
||||
scale: 1,
|
||||
duration: 0.5,
|
||||
ease: 'power2.out',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private animateCardFlip(): void {
|
||||
const cardElement = this.elementRef.nativeElement.querySelector('.card-element');
|
||||
gsap.to(cardElement, {
|
||||
rotationY: 180,
|
||||
duration: 0.3,
|
||||
onComplete: () => {
|
||||
gsap.set(cardElement, { rotationY: 0 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected getSuitSymbol(suit: Suit): string {
|
||||
return suitSymbols[suit];
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export enum GameState {
|
||||
PLAYER_WON = 'PLAYER_WON',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
PLAYER_LOST = 'PLAYER_LOST',
|
||||
DRAW = 'DRAW',
|
||||
PLAYER_BLACKJACK = 'PLAYER_BLACKJACK',
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, catchError } from 'rxjs';
|
||||
import { BlackjackGame } from '../models/blackjack.model';
|
||||
import { BlackjackGame } from '@blackjack/models/blackjack.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -41,4 +41,26 @@ export class BlackjackService {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
doubleDown(gameId: number): Observable<BlackjackGame> {
|
||||
return this.http
|
||||
.post<BlackjackGame>(`/backend/blackjack/${gameId}/doubleDown`, {}, { responseType: 'json' })
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
console.error('Double Down error:', error);
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getGame(gameId: number): Observable<BlackjackGame> {
|
||||
return this.http
|
||||
.get<BlackjackGame>(`/backend/blackjack/${gameId}`, { responseType: 'json' })
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
console.error('Get game error:', error);
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Card } from '../models/blackjack.model';
|
||||
import { GameState } from '../enum/gameState';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GameControlsService {
|
||||
calculateHandValue(cards: Card[]): number {
|
||||
let sum = 0;
|
||||
let aceCount = 0;
|
||||
|
||||
const rankValues: Record<string, number> = {
|
||||
TWO: 2,
|
||||
THREE: 3,
|
||||
FOUR: 4,
|
||||
FIVE: 5,
|
||||
SIX: 6,
|
||||
SEVEN: 7,
|
||||
EIGHT: 8,
|
||||
NINE: 9,
|
||||
TEN: 10,
|
||||
JACK: 10,
|
||||
QUEEN: 10,
|
||||
KING: 10,
|
||||
ACE: 11,
|
||||
};
|
||||
|
||||
for (const card of cards) {
|
||||
if (!card.hidden) {
|
||||
const value = rankValues[card.rank] || 0;
|
||||
sum += value;
|
||||
if (card.rank === 'ACE') {
|
||||
aceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (sum > 21 && aceCount > 0) {
|
||||
sum -= 10;
|
||||
aceCount--;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
getStatusText(state: GameState): string {
|
||||
switch (state) {
|
||||
case GameState.IN_PROGRESS:
|
||||
return 'Spiel läuft';
|
||||
case GameState.PLAYER_WON:
|
||||
return 'Gewonnen!';
|
||||
case GameState.PLAYER_LOST:
|
||||
return 'Verloren!';
|
||||
case GameState.DRAW:
|
||||
return 'Unentschieden!';
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusClass(state: GameState): string {
|
||||
switch (state) {
|
||||
case GameState.PLAYER_WON:
|
||||
return 'text-emerald';
|
||||
case GameState.PLAYER_LOST:
|
||||
return 'text-accent-red';
|
||||
case GameState.DRAW:
|
||||
return 'text-yellow-400';
|
||||
default:
|
||||
return 'text-white';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { NavbarComponent } from '../../shared/components/navbar/navbar.component';
|
||||
import { CurrencyPipe, NgFor } from '@angular/common';
|
||||
import { Game } from '../../model/Game';
|
||||
import { Transaction } from '../../model/Transaction';
|
||||
import { DepositComponent } from '../deposit/deposit.component';
|
||||
import { ConfirmationComponent } from '../../shared/components/confirmation/confirmation.component';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ConfirmationComponent } from '@shared/components/confirmation/confirmation.component';
|
||||
import { Transaction } from 'app/model/Transaction';
|
||||
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||
import { Game } from 'app/model/Game';
|
||||
|
||||
@Component({
|
||||
selector: 'app-homepage',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { NavbarComponent } from '../../shared/components/navbar/navbar.component';
|
||||
import { NgFor } from '@angular/common';
|
||||
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-page',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
||||
import { UserService } from '../../service/user.service';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserService } from '@service/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-success',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { KeycloakProfile } from 'keycloak-js';
|
||||
import { catchError, EMPTY, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs';
|
||||
import { User } from '../model/User';
|
||||
|
||||
@Injectable({
|
||||
|
@ -9,20 +9,39 @@ import { User } from '../model/User';
|
|||
})
|
||||
export class UserService {
|
||||
private http: HttpClient = inject(HttpClient);
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
|
||||
constructor() {
|
||||
// Initialize with current user data
|
||||
this.getCurrentUser().subscribe();
|
||||
}
|
||||
|
||||
public getUser(id: string): Observable<User | null> {
|
||||
return this.http.get<User | null>(`/backend/user/${id}`).pipe(catchError(() => EMPTY));
|
||||
return this.http.get<User | null>(`/backend/user/${id}`).pipe(
|
||||
catchError(() => EMPTY),
|
||||
tap((user) => this.currentUserSubject.next(user))
|
||||
);
|
||||
}
|
||||
|
||||
public getCurrentUser(): Observable<User | null> {
|
||||
return this.http.get<User | null>('/backend/user').pipe(catchError(() => EMPTY));
|
||||
return this.http.get<User | null>('/backend/user').pipe(
|
||||
catchError(() => EMPTY),
|
||||
tap((user) => this.currentUserSubject.next(user))
|
||||
);
|
||||
}
|
||||
|
||||
public refreshCurrentUser(): void {
|
||||
this.getCurrentUser().subscribe();
|
||||
}
|
||||
|
||||
public createUser(id: string, username: string): Observable<User> {
|
||||
return this.http.post<User>('/backend/user', {
|
||||
keycloakId: id,
|
||||
username: username,
|
||||
});
|
||||
return this.http
|
||||
.post<User>('/backend/user', {
|
||||
keycloakId: id,
|
||||
username: username,
|
||||
})
|
||||
.pipe(tap((user) => this.currentUserSubject.next(user)));
|
||||
}
|
||||
|
||||
public async getOrCreateUser(userProfile: KeycloakProfile) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
AfterViewInit,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { ModalAnimationService } from '../../services/modal-animation.service';
|
||||
import { ModalAnimationService } from '@shared/services/modal-animation.service';
|
||||
import gsap from 'gsap';
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div
|
||||
class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
|
||||
>
|
||||
<span>Guthaben: {{ balance() | currency: 'EUR' : 'symbol' : '1.2-2' }}</span>
|
||||
<span>{{ balance() | currency: 'EUR' : 'symbol' : '1.2-2' }}</span>
|
||||
</div>
|
||||
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
|
||||
}
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { UserService } from '../../../service/user.service';
|
||||
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { UserService } from '@service/user.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
templateUrl: './navbar.component.html',
|
||||
|
@ -11,22 +19,27 @@ import { CurrencyPipe } from '@angular/common';
|
|||
imports: [RouterModule, CurrencyPipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NavbarComponent implements OnInit {
|
||||
export class NavbarComponent implements OnInit, OnDestroy {
|
||||
isMenuOpen = false;
|
||||
private keycloakService: KeycloakService = inject(KeycloakService);
|
||||
isLoggedIn = this.keycloakService.isLoggedIn();
|
||||
|
||||
private userService = inject(UserService);
|
||||
private user = this.userService.getCurrentUser();
|
||||
|
||||
private userSubscription: Subscription | undefined;
|
||||
public balance = signal(0);
|
||||
|
||||
ngOnInit() {
|
||||
this.user.subscribe((user) => {
|
||||
this.userSubscription = this.userService.currentUser$.subscribe((user) => {
|
||||
this.balance.set(user?.balance ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.userSubscription) {
|
||||
this.userSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
login() {
|
||||
try {
|
||||
const baseUrl = window.location.origin;
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@service/*": ["app/service/*"],
|
||||
"@environments/*": ["environments/*"],
|
||||
"@shared/*": ["app/shared/*"],
|
||||
"@blackjack/*": ["app/feature/game/blackjack/*"]
|
||||
},
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
|
|
Loading…
Add table
Reference in a new issue