feat: add double down feature to blackjack game

This commit is contained in:
Jan-Marlon Leibl 2025-03-27 15:40:26 +01:00
parent d90fcdcf1e
commit d2b22b561d
Signed by: jleibl
GPG key ID: 300B2F906DC6F1D5
7 changed files with 190 additions and 61 deletions

View file

@ -77,6 +77,23 @@ public class BlackJackGameController {
return ResponseEntity.ok(blackJackService.stand(game)); 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") @PostMapping("/blackjack/start")
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) { public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token); Optional<UserEntity> optionalUser = userService.getCurrentUser(token);

View file

@ -14,17 +14,15 @@ import java.util.Random;
public class BlackJackService { public class BlackJackService {
private final BlackJackGameRepository blackJackGameRepository; private final BlackJackGameRepository blackJackGameRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final Random random = new Random();
public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) { public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) {
this.blackJackGameRepository = blackJackGameRepository; this.blackJackGameRepository = blackJackGameRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
} }
private final Random random = new Random();
public BlackJackGameEntity getBlackJackGame(Long id) { public BlackJackGameEntity getBlackJackGame(Long id) {
Optional<BlackJackGameEntity> optionalBlackJackGame = blackJackGameRepository.findById(id); return blackJackGameRepository.findById(id).orElse(null);
return optionalBlackJackGame.orElse(null);
} }
@Transactional @Transactional
@ -32,88 +30,146 @@ public class BlackJackService {
BlackJackGameEntity game = new BlackJackGameEntity(); BlackJackGameEntity game = new BlackJackGameEntity();
game.setUser(user); game.setUser(user);
game.setBet(betAmount); game.setBet(betAmount);
initializeDeck(game); initializeDeck(game);
dealInitialCards(game);
for (int i = 0; i < 2; i++) {
CardEntity playerCard = drawCardFromDeck(game); game.setState(getState(game));
playerCard.setCardType(CardType.PLAYER); deductBetFromBalance(user, betAmount);
game.getPlayerCards().add(playerCard);
}
CardEntity dealerCard = drawCardFromDeck(game);
dealerCard.setCardType(CardType.DEALER);
game.getDealerCards().add(dealerCard);
BlackJackState state = handleState(game, user);
game.setState(state);
userRepository.save(user);
return blackJackGameRepository.save(game); return blackJackGameRepository.save(game);
} }
@Transactional @Transactional
public BlackJackGameEntity hit(BlackJackGameEntity game) { public BlackJackGameEntity hit(BlackJackGameEntity game) {
game = blackJackGameRepository.findById(game.getId()).orElse(game); game = refreshGameState(game);
if (game.getState() != BlackJackState.IN_PROGRESS) { if (game.getState() != BlackJackState.IN_PROGRESS) {
return game; return game;
} }
CardEntity drawnCard = drawCardFromDeck(game); dealCardToPlayer(game);
drawnCard.setCardType(CardType.PLAYER); updateGameStateAndBalance(game);
game.getPlayerCards().add(drawnCard);
game.setState(handleState(game, game.getUser()));
if (game.getState() == BlackJackState.PLAYER_WON) {
updateUserBalance(game, true);
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
updateUserBalance(game, false);
}
return blackJackGameRepository.save(game); return blackJackGameRepository.save(game);
} }
@Transactional @Transactional
public BlackJackGameEntity stand(BlackJackGameEntity game) { public BlackJackGameEntity stand(BlackJackGameEntity game) {
game = blackJackGameRepository.findById(game.getId()).orElse(game); game = refreshGameState(game);
if (game.getState() != BlackJackState.IN_PROGRESS) { if (game.getState() != BlackJackState.IN_PROGRESS) {
return game; return game;
} }
while (calculateHandValue(game.getDealerCards()) < 17) { dealCardsToDealerUntilMinimumScore(game);
CardEntity dealerCard = drawCardFromDeck(game); determineWinnerAndUpdateBalance(game);
dealerCard.setCardType(CardType.DEALER);
game.getDealerCards().add(dealerCard); 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;
}
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 playerValue = calculateHandValue(game.getPlayerCards());
int dealerValue = calculateHandValue(game.getDealerCards()); int dealerValue = calculateHandValue(game.getDealerCards());
if (dealerValue > 21 || playerValue > dealerValue) { if (dealerValue > 21 || playerValue > dealerValue) {
game.setState(BlackJackState.PLAYER_WON); game.setState(BlackJackState.PLAYER_WON);
updateUserBalance(game, true); updateUserBalance(game, true);
} else if (playerValue < dealerValue) {
game.setState(BlackJackState.PLAYER_LOST);
updateUserBalance(game, false);
} else { } else {
game.setState(BlackJackState.DRAW); game.setState(BlackJackState.DRAW);
updateUserBalance(game, false); updateUserBalance(game, false); // For draw, player gets their bet back
} }
}
return blackJackGameRepository.save(game);
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
user.setBalance(user.getBalance().subtract(betAmount));
userRepository.save(user);
} }
@Transactional @Transactional
private void updateUserBalance(BlackJackGameEntity game, boolean isWin) { private void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
UserEntity user = game.getUser(); UserEntity user = getUserWithFreshData(game.getUser());
user = userRepository.findById(user.getId()).orElse(user);
BigDecimal balance = user.getBalance();
BigDecimal betAmount = game.getBet(); BigDecimal betAmount = game.getBet();
BigDecimal balance = user.getBalance();
if (isWin) { if (isWin) {
balance = balance.add(betAmount.multiply(BigDecimal.valueOf(2))); balance = balance.add(betAmount.multiply(BigDecimal.valueOf(2)));
} } else if (game.getState() == BlackJackState.DRAW) {
else if (game.getState() == BlackJackState.DRAW) {
balance = balance.add(betAmount); balance = balance.add(betAmount);
} }
@ -144,7 +200,7 @@ public class BlackJackService {
return game.getDeck().removeFirst(); return game.getDeck().removeFirst();
} }
private BlackJackState handleState(BlackJackGameEntity game, UserEntity user) { private BlackJackState getState(BlackJackGameEntity game) {
int playerHandValue = calculateHandValue(game.getPlayerCards()); int playerHandValue = calculateHandValue(game.getPlayerCards());
if (playerHandValue == 21) { if (playerHandValue == 21) {
@ -155,14 +211,14 @@ public class BlackJackService {
int dealerHandValue = calculateHandValue(game.getDealerCards()); int dealerHandValue = calculateHandValue(game.getDealerCards());
if (dealerHandValue == 21) { if (dealerHandValue == 21) {
return BlackJackState.STANDOFF; return BlackJackState.DRAW;
} else { } else {
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5")); BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
UserEntity user = getUserWithFreshData(game.getUser());
user.setBalance(user.getBalance().add(blackjackWinnings)); user.setBalance(user.getBalance().add(blackjackWinnings));
return BlackJackState.PLAYER_BLACKJACK; return BlackJackState.PLAYER_BLACKJACK;
} }
} else if (playerHandValue > 21) { } else if (playerHandValue > 21) {
user.setBalance(user.getBalance().subtract(game.getBet()));
return BlackJackState.PLAYER_LOST; return BlackJackState.PLAYER_LOST;
} }

View file

@ -11,6 +11,7 @@
[gameState]="gameState()" [gameState]="gameState()"
(hit)="onHit()" (hit)="onHit()"
(stand)="onStand()" (stand)="onStand()"
(doubleDown)="onDoubleDown()"
(leave)="leaveGame()" (leave)="leaveGame()"
></app-game-controls> ></app-game-controls>
} }
@ -32,4 +33,5 @@
[gameState]="gameState()" [gameState]="gameState()"
[amount]="currentBet()" [amount]="currentBet()"
[show]="showGameResult()" [show]="showGameResult()"
(close)="onCloseGameResult()"
></app-game-result> ></app-game-result>

View file

@ -60,10 +60,13 @@ export default class BlackjackComponent {
this.gameInProgress.set(game.state === 'IN_PROGRESS'); this.gameInProgress.set(game.state === 'IN_PROGRESS');
this.gameState.set(game.state); this.gameState.set(game.state);
// When game ends, make sure all dealer cards are visible
const isGameOver = game.state !== 'IN_PROGRESS';
this.dealerCards.set( this.dealerCards.set(
game.dealerCards.map((card, index) => ({ game.dealerCards.map((card, index) => ({
...card, ...card,
hidden: index === 1 && game.state === 'IN_PROGRESS', hidden: !isGameOver && index === 1 && game.state === 'IN_PROGRESS',
})) }))
); );
@ -74,11 +77,14 @@ export default class BlackjackComponent {
})) }))
); );
if (game.state !== 'IN_PROGRESS') { // Only refresh and show game result if the game has ended
if (isGameOver) {
console.log('Game is over, state:', game.state);
this.refreshUserBalance(); this.refreshUserBalance();
setTimeout(() => {
this.showGameResult.set(true); // Show result immediately without resetting first
}, 1000); this.showGameResult.set(true);
console.log('Game result dialog should be shown now');
} }
} }
@ -127,7 +133,26 @@ export default class BlackjackComponent {
}); });
} }
onDoubleDown(): void {
if (!this.currentGameId()) return;
if (this.gameState() !== 'IN_PROGRESS' || this.playerCards().length !== 2) {
return;
}
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => {
this.updateGameState(game);
},
error: (error) => {
console.error('Failed to double down:', error);
this.handleGameError(error);
},
});
}
onCloseGameResult(): void { onCloseGameResult(): void {
console.log('Closing game result dialog');
this.showGameResult.set(false); this.showGameResult.set(false);
} }

View file

@ -31,6 +31,13 @@ import { Card } from '../../models/blackjack.model';
> >
Halten Halten
</button> </button>
<button
(click)="doubleDown.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px]"
[disabled]="gameState !== 'IN_PROGRESS' || playerCards.length !== 2"
>
Verdoppeln
</button>
<button <button
(click)="leave.emit()" (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" 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"
@ -48,6 +55,7 @@ export class GameControlsComponent {
@Output() hit = new EventEmitter<void>(); @Output() hit = new EventEmitter<void>();
@Output() stand = new EventEmitter<void>(); @Output() stand = new EventEmitter<void>();
@Output() doubleDown = new EventEmitter<void>();
@Output() leave = new EventEmitter<void>(); @Output() leave = new EventEmitter<void>();
calculateHandValue(cards: Card[]): number { calculateHandValue(cards: Card[]): number {

View file

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
@ -11,6 +11,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
*ngIf="visible" *ngIf="visible"
[@fadeInOut] [@fadeInOut]
class="modal-bg" class="modal-bg"
style="z-index: 1000; position: fixed;"
> >
<div <div
class="modal-card" class="modal-card"
@ -52,7 +53,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
<button <button
type="button" type="button"
(click)="visible = false" (click)="closeDialog()"
class="button-primary w-full py-2" class="button-primary w-full py-2"
> >
Verstanden Verstanden
@ -66,16 +67,16 @@ import { animate, style, transition, trigger } from '@angular/animations';
trigger('fadeInOut', [ trigger('fadeInOut', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0 }), style({ opacity: 0 }),
animate('300ms ease-out', style({ opacity: 1 })) animate('150ms ease-out', style({ opacity: 1 }))
]), ]),
transition(':leave', [ transition(':leave', [
animate('200ms ease-in', style({ opacity: 0 })) animate('150ms ease-in', style({ opacity: 0 }))
]) ])
]), ]),
trigger('cardAnimation', [ trigger('cardAnimation', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0, transform: 'scale(0.8)' }), style({ opacity: 0, transform: 'scale(0.95)' }),
animate('350ms ease-out', style({ opacity: 1, transform: 'scale(1)' })) animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' }))
]) ])
]) ])
] ]
@ -84,9 +85,12 @@ export class GameResultComponent {
@Input() gameState: string = ''; @Input() gameState: string = '';
@Input() amount: number = 0; @Input() amount: number = 0;
@Input() set show(value: boolean) { @Input() set show(value: boolean) {
console.log('GameResultComponent show input changed:', value, 'gameState:', this.gameState);
this.visible = value; this.visible = value;
} }
@Output() close = new EventEmitter<void>();
visible = false; visible = false;
get isWin(): boolean { get isWin(): boolean {
@ -121,4 +125,10 @@ export class GameResultComponent {
if (this.isDraw) return 'text-yellow-400'; if (this.isDraw) return 'text-yellow-400';
return ''; return '';
} }
closeDialog(): void {
this.visible = false;
this.close.emit();
console.log('Dialog closed by user');
}
} }

View file

@ -42,6 +42,17 @@ 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> { getGame(gameId: number): Observable<BlackjackGame> {
return this.http return this.http
.get<BlackjackGame>(`/backend/blackjack/${gameId}`, { responseType: 'json' }) .get<BlackjackGame>(`/backend/blackjack/${gameId}`, { responseType: 'json' })