Merge branch 'main' into feature/authentik
Some checks failed
Some checks failed
This commit is contained in:
commit
d7fe0e3965
56 changed files with 2782 additions and 602 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
An online gaming platform offering various casino-style games with virtual currency support. This project features a modern tech stack with Angular frontend, Spring Boot backend, and complete user authentication.
|
An online gaming platform offering various casino-style games with virtual currency support. This project features a modern tech stack with Angular frontend, Spring Boot backend, and complete user authentication.
|
||||||
|
|
||||||
Please refer to our [Style Guide](/frontend/README.md#style-guide) for design guidelines and component standards.
|
Please refer to our [Style Guide](https://git.kjan.de/SZUT/casino/wiki/Frontend#design-system) for design guidelines and component standards.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -67,6 +67,11 @@ cd backend
|
||||||
./gradlew bootRun
|
./gradlew bootRun
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You may optionally install [watchexec](https://github.com/watchexec/watchexec?tab=readme-ov-file) and use this command to autorecompile the backend on file changes:
|
||||||
|
```bash
|
||||||
|
watchexec -r -e java ./gradlew :bootRun
|
||||||
|
```
|
||||||
|
|
||||||
The backend will be available at:
|
The backend will be available at:
|
||||||
- API endpoint: http://localhost:8080
|
- API endpoint: http://localhost:8080
|
||||||
- Swagger documentation: http://localhost:8080/swagger
|
- Swagger documentation: http://localhost:8080/swagger
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("org.springframework.boot") version "3.4.3"
|
id("org.springframework.boot") version "3.4.4"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
id("checkstyle")
|
id("checkstyle")
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.stripe:stripe-java:20.136.0")
|
implementation("com.stripe:stripe-java:29.0.0")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
@ -47,10 +47,10 @@ dependencies {
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.4.4")
|
||||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.4")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5")
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
|
|
20
backend/requests/blackjack.http
Normal file
20
backend/requests/blackjack.http
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
POST http://localhost:8080/blackjack/start
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"betAmount": 1.01
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
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}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
POST http://localhost:9090/realms/LF12/protocol/openid-connect/token
|
POST http://localhost:9090/realms/LF12/protocol/openid-connect/token
|
||||||
Content-Type: application/x-www-form-urlencoded
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
grant_type=password&client_id=lf12&username=lf12_test_user&password=secret
|
grant_type=password&client_id=lf12&username=lf12_test_user&password=secret&scope=openid
|
||||||
|
|
||||||
> {% client.global.set("token", response.body.access_token); %}
|
> {% client.global.set("token", response.body.access_token); %}
|
9
backend/requests/lootboxes.http
Normal file
9
backend/requests/lootboxes.http
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
GET http://localhost:8080/lootboxes
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8080/lootboxes/2
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
|
@ -1,10 +1,20 @@
|
||||||
package de.szut.casino;
|
package de.szut.casino;
|
||||||
|
|
||||||
|
import de.szut.casino.lootboxes.LootBoxEntity;
|
||||||
|
import de.szut.casino.lootboxes.LootBoxRepository;
|
||||||
|
import de.szut.casino.lootboxes.RewardEntity;
|
||||||
|
import de.szut.casino.lootboxes.RewardRepository;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class CasinoApplication {
|
public class CasinoApplication {
|
||||||
|
|
||||||
|
@ -16,4 +26,65 @@ public class CasinoApplication {
|
||||||
public static RestTemplate restTemplate() {
|
public static RestTemplate restTemplate() {
|
||||||
return new RestTemplate();
|
return new RestTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CommandLineRunner initData(LootBoxRepository lootBoxRepository, RewardRepository rewardRepository) {
|
||||||
|
return _ -> {
|
||||||
|
if (lootBoxRepository.count() == 0) {
|
||||||
|
LootBoxEntity basicLootBox = new LootBoxEntity();
|
||||||
|
basicLootBox.setName("Basic LootBox");
|
||||||
|
basicLootBox.setPrice(new BigDecimal("2"));
|
||||||
|
basicLootBox.setRewards(new ArrayList<>()); // Initialize the list
|
||||||
|
|
||||||
|
LootBoxEntity premiumLootBox = new LootBoxEntity();
|
||||||
|
premiumLootBox.setName("Premium LootBox");
|
||||||
|
premiumLootBox.setPrice(new BigDecimal("5"));
|
||||||
|
premiumLootBox.setRewards(new ArrayList<>()); // Initialize the list
|
||||||
|
|
||||||
|
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
|
||||||
|
|
||||||
|
RewardEntity commonReward = new RewardEntity();
|
||||||
|
commonReward.setValue(new BigDecimal("0.50"));
|
||||||
|
commonReward.setProbability(new BigDecimal("0.7"));
|
||||||
|
|
||||||
|
RewardEntity rareReward = new RewardEntity();
|
||||||
|
rareReward.setValue(new BigDecimal("2.00"));
|
||||||
|
rareReward.setProbability(new BigDecimal("0.25"));
|
||||||
|
|
||||||
|
RewardEntity epicReward = new RewardEntity();
|
||||||
|
epicReward.setValue(new BigDecimal("5.00"));
|
||||||
|
epicReward.setProbability(new BigDecimal("0.5"));
|
||||||
|
|
||||||
|
RewardEntity premiumCommon = new RewardEntity();
|
||||||
|
premiumCommon.setValue(new BigDecimal("2.00"));
|
||||||
|
premiumCommon.setProbability(new BigDecimal("0.6"));
|
||||||
|
|
||||||
|
RewardEntity premiumRare = new RewardEntity();
|
||||||
|
premiumRare.setValue(new BigDecimal("5.00"));
|
||||||
|
premiumRare.setProbability(new BigDecimal("0.3"));
|
||||||
|
|
||||||
|
RewardEntity legendaryReward = new RewardEntity();
|
||||||
|
legendaryReward.setValue(new BigDecimal("15.00"));
|
||||||
|
legendaryReward.setProbability(new BigDecimal("0.10"));
|
||||||
|
|
||||||
|
rewardRepository.saveAll(Arrays.asList(
|
||||||
|
commonReward, rareReward, epicReward,
|
||||||
|
premiumCommon, premiumRare, legendaryReward
|
||||||
|
));
|
||||||
|
|
||||||
|
basicLootBox.getRewards().add(commonReward);
|
||||||
|
basicLootBox.getRewards().add(premiumRare);
|
||||||
|
|
||||||
|
premiumLootBox.getRewards().add(premiumCommon);
|
||||||
|
premiumLootBox.getRewards().add(premiumRare);
|
||||||
|
premiumLootBox.getRewards().add(legendaryReward);
|
||||||
|
|
||||||
|
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
|
||||||
|
|
||||||
|
System.out.println("Initial LootBoxes and rewards created successfully");
|
||||||
|
} else {
|
||||||
|
System.out.println("LootBoxes already exist, skipping initialization");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
import de.szut.casino.blackjack.dto.CreateBlackJackGameDto;
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import de.szut.casino.user.UserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
public class BlackJackGameController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
private final BlackJackService blackJackService;
|
||||||
|
|
||||||
|
public BlackJackGameController(UserService userService, BlackJackService blackJackService) {
|
||||||
|
this.blackJackService = blackJackService;
|
||||||
|
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);
|
||||||
|
|
||||||
|
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.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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/{id}/split")
|
||||||
|
public ResponseEntity<Object> split(@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.split(game));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/blackjack/start")
|
||||||
|
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) {
|
||||||
|
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||||
|
|
||||||
|
if (optionalUser.isEmpty()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntity user = optionalUser.get();
|
||||||
|
BigDecimal balance = user.getBalance();
|
||||||
|
BigDecimal betAmount = createBlackJackGameDto.getBetAmount();
|
||||||
|
|
||||||
|
if (betAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
Map<String, String> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("error", "Invalid bet amount");
|
||||||
|
return ResponseEntity.badRequest().body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (betAmount.compareTo(balance) > 0) {
|
||||||
|
Map<String, String> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("error", "Insufficient funds");
|
||||||
|
return ResponseEntity.badRequest().body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class BlackJackGameEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
@JsonIgnore
|
||||||
|
private UserEntity user;
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return user != null ? user.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private BlackJackState state;
|
||||||
|
private BigDecimal bet;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
@JsonIgnore
|
||||||
|
@SQLRestriction("card_type = 'DECK'")
|
||||||
|
private List<CardEntity> deck = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
@JsonManagedReference
|
||||||
|
@SQLRestriction("card_type = 'PLAYER'")
|
||||||
|
private List<CardEntity> playerCards = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
@JsonManagedReference
|
||||||
|
@SQLRestriction("card_type = 'DEALER'")
|
||||||
|
private List<CardEntity> dealerCards = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
@JsonManagedReference
|
||||||
|
@SQLRestriction("card_type = 'PLAYER_SPLIT'")
|
||||||
|
private List<CardEntity> playerSplitCards = new ArrayList<>();
|
||||||
|
|
||||||
|
@Column(name = "split_bet")
|
||||||
|
private BigDecimal splitBet;
|
||||||
|
|
||||||
|
@Column(name = "is_split")
|
||||||
|
private boolean isSplit;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public interface BlackJackGameRepository extends JpaRepository<BlackJackGameEntity, Long> {
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
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;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlackJackGameEntity getBlackJackGame(Long id) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public BlackJackGameEntity hit(BlackJackGameEntity game) {
|
||||||
|
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
dealCardToPlayer(game);
|
||||||
|
|
||||||
|
updateGameStateAndBalance(game);
|
||||||
|
|
||||||
|
return blackJackGameRepository.save(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public BlackJackGameEntity stand(BlackJackGameEntity game) {
|
||||||
|
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
dealCardsToDealerUntilMinimumScore(game);
|
||||||
|
determineWinnerAndUpdateBalance(game);
|
||||||
|
|
||||||
|
return blackJackGameRepository.save(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public BlackJackGameEntity doubleDown(BlackJackGameEntity game) {
|
||||||
|
if (game.getState() != BlackJackState.IN_PROGRESS || game.getPlayerCards().size() != 2) {
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntity user = getUserWithFreshData(game.getUser());
|
||||||
|
BigDecimal additionalBet = game.getBet();
|
||||||
|
|
||||||
|
deductBetFromBalance(user, additionalBet);
|
||||||
|
game.setBet(game.getBet().add(additionalBet));
|
||||||
|
|
||||||
|
dealCardToPlayer(game);
|
||||||
|
updateGameStateAndBalance(game);
|
||||||
|
|
||||||
|
if (game.getState() == BlackJackState.IN_PROGRESS) {
|
||||||
|
return stand(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public BlackJackGameEntity split(BlackJackGameEntity game) {
|
||||||
|
if (game.getState() != BlackJackState.IN_PROGRESS ||
|
||||||
|
game.getPlayerCards().size() != 2 ||
|
||||||
|
game.isSplit() ||
|
||||||
|
!game.getPlayerCards().get(0).getRank().equals(game.getPlayerCards().get(1).getRank())) {
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntity user = getUserWithFreshData(game.getUser());
|
||||||
|
BigDecimal splitBet = game.getBet();
|
||||||
|
|
||||||
|
if (user.getBalance().compareTo(splitBet) < 0) {
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
deductBetFromBalance(user, splitBet);
|
||||||
|
game.setSplitBet(splitBet);
|
||||||
|
game.setSplit(true);
|
||||||
|
|
||||||
|
CardEntity card = game.getPlayerCards().remove(1);
|
||||||
|
card.setCardType(CardType.PLAYER_SPLIT);
|
||||||
|
game.getPlayerSplitCards().add(card);
|
||||||
|
|
||||||
|
dealCardToPlayer(game);
|
||||||
|
dealCardToSplitHand(game);
|
||||||
|
|
||||||
|
return blackJackGameRepository.save(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 dealCardToSplitHand(BlackJackGameEntity game) {
|
||||||
|
CardEntity card = drawCardFromDeck(game);
|
||||||
|
card.setCardType(CardType.PLAYER_SPLIT);
|
||||||
|
game.getPlayerSplitCards().add(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateGameStateAndBalance(BlackJackGameEntity game) {
|
||||||
|
if (game.isSplit()) {
|
||||||
|
int mainHandValue = calculateHandValue(game.getPlayerCards());
|
||||||
|
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
|
||||||
|
|
||||||
|
if (mainHandValue > 21 && splitHandValue > 21) {
|
||||||
|
game.setState(BlackJackState.PLAYER_LOST);
|
||||||
|
updateUserBalance(game, false);
|
||||||
|
} else if (mainHandValue <= 21 && splitHandValue <= 21) {
|
||||||
|
game.setState(BlackJackState.IN_PROGRESS);
|
||||||
|
} else {
|
||||||
|
game.setState(BlackJackState.IN_PROGRESS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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());
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
|
||||||
|
user.setBalance(user.getBalance().subtract(betAmount));
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
private void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
|
||||||
|
UserEntity user = getUserWithFreshData(game.getUser());
|
||||||
|
BigDecimal totalBet = game.getBet();
|
||||||
|
BigDecimal balance = user.getBalance();
|
||||||
|
|
||||||
|
if (game.isSplit()) {
|
||||||
|
totalBet = totalBet.add(game.getSplitBet());
|
||||||
|
|
||||||
|
if (isWin) {
|
||||||
|
int mainHandValue = calculateHandValue(game.getPlayerCards());
|
||||||
|
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
|
||||||
|
int dealerValue = calculateHandValue(game.getDealerCards());
|
||||||
|
|
||||||
|
if (mainHandValue <= 21 && (dealerValue > 21 || mainHandValue > dealerValue)) {
|
||||||
|
balance = balance.add(game.getBet().multiply(BigDecimal.valueOf(2)));
|
||||||
|
} else if (mainHandValue == dealerValue) {
|
||||||
|
balance = balance.add(game.getBet());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splitHandValue <= 21 && (dealerValue > 21 || splitHandValue > dealerValue)) {
|
||||||
|
balance = balance.add(game.getSplitBet().multiply(BigDecimal.valueOf(2)));
|
||||||
|
} else if (splitHandValue == dealerValue) {
|
||||||
|
balance = balance.add(game.getSplitBet());
|
||||||
|
}
|
||||||
|
} else if (game.getState() == BlackJackState.DRAW) {
|
||||||
|
balance = balance.add(totalBet);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isWin) {
|
||||||
|
balance = balance.add(totalBet.multiply(BigDecimal.valueOf(2)));
|
||||||
|
} else if (game.getState() == BlackJackState.DRAW) {
|
||||||
|
balance = balance.add(totalBet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setBalance(balance);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeDeck(BlackJackGameEntity game) {
|
||||||
|
for (Suit suit : Suit.values()) {
|
||||||
|
for (Rank rank : Rank.values()) {
|
||||||
|
CardEntity card = new CardEntity();
|
||||||
|
card.setGame(game);
|
||||||
|
card.setSuit(suit);
|
||||||
|
card.setRank(rank);
|
||||||
|
card.setCardType(CardType.DECK);
|
||||||
|
game.getDeck().add(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Collections.shuffle(game.getDeck(), random);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardEntity drawCardFromDeck(BlackJackGameEntity game) {
|
||||||
|
if (game.getDeck().isEmpty()) {
|
||||||
|
throw new IllegalStateException("Deck is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
return game.getDeck().removeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlackJackState getState(BlackJackGameEntity game) {
|
||||||
|
int playerHandValue = calculateHandValue(game.getPlayerCards());
|
||||||
|
|
||||||
|
if (playerHandValue == 21) {
|
||||||
|
CardEntity hole = drawCardFromDeck(game);
|
||||||
|
hole.setCardType(CardType.DEALER);
|
||||||
|
game.getDealerCards().add(hole);
|
||||||
|
|
||||||
|
int dealerHandValue = calculateHandValue(game.getDealerCards());
|
||||||
|
|
||||||
|
if (dealerHandValue == 21) {
|
||||||
|
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) {
|
||||||
|
return BlackJackState.PLAYER_LOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlackJackState.IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateHandValue(List<CardEntity> hand) {
|
||||||
|
int sum = 0;
|
||||||
|
int aceCount = 0;
|
||||||
|
for (CardEntity card : hand) {
|
||||||
|
sum += card.getRank().getValue();
|
||||||
|
if (card.getRank() == Rank.ACE) {
|
||||||
|
aceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (sum > 21 && aceCount > 0) {
|
||||||
|
sum -= 10;
|
||||||
|
aceCount--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
public enum BlackJackState {
|
||||||
|
IN_PROGRESS,
|
||||||
|
PLAYER_BLACKJACK,
|
||||||
|
PLAYER_LOST,
|
||||||
|
PLAYER_WON,
|
||||||
|
DRAW,
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonBackReference;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CardEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@JsonIgnore
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "game_id", nullable = false)
|
||||||
|
@JsonBackReference
|
||||||
|
private BlackJackGameEntity game;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Suit suit;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Rank rank;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@JsonIgnore
|
||||||
|
private CardType cardType;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CardType {
|
||||||
|
DECK, PLAYER, DEALER, PLAYER_SPLIT
|
||||||
|
}
|
31
backend/src/main/java/de/szut/casino/blackjack/Rank.java
Normal file
31
backend/src/main/java/de/szut/casino/blackjack/Rank.java
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum Rank {
|
||||||
|
TWO("2", "Two", 2),
|
||||||
|
THREE("3", "Three", 3),
|
||||||
|
FOUR("4", "Four", 4),
|
||||||
|
FIVE("5", "Five", 5),
|
||||||
|
SIX("6", "Six", 6),
|
||||||
|
SEVEN("7", "Seven", 7),
|
||||||
|
EIGHT("8", "Eight", 8),
|
||||||
|
NINE("9", "Nine", 9),
|
||||||
|
TEN("10", "Ten", 10),
|
||||||
|
JACK("J", "Jack", 10),
|
||||||
|
QUEEN("Q", "Queen", 10),
|
||||||
|
KING("K", "King", 10),
|
||||||
|
ACE("A", "Ace", 11);
|
||||||
|
|
||||||
|
private final String symbol;
|
||||||
|
private final String displayName;
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
Rank(String symbol, String displayName, int value) {
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
backend/src/main/java/de/szut/casino/blackjack/Suit.java
Normal file
20
backend/src/main/java/de/szut/casino/blackjack/Suit.java
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum Suit {
|
||||||
|
HEARTS("H", "Hearts"),
|
||||||
|
DIAMONDS("D", "Diamonds"),
|
||||||
|
CLUBS("C", "Clubs"),
|
||||||
|
SPADES("S", "Spades");
|
||||||
|
|
||||||
|
private final String symbol;
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
Suit(String symbol, String displayName) {
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package de.szut.casino.blackjack.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CreateBlackJackGameDto {
|
||||||
|
private BigDecimal betAmount;
|
||||||
|
}
|
|
@ -52,11 +52,15 @@ public class DepositController {
|
||||||
|
|
||||||
SessionCreateParams params = SessionCreateParams.builder()
|
SessionCreateParams params = SessionCreateParams.builder()
|
||||||
.addLineItem(SessionCreateParams.LineItem.builder()
|
.addLineItem(SessionCreateParams.LineItem.builder()
|
||||||
.setAmount((long) amountDto.getAmount() * 100)
|
.setPriceData(SessionCreateParams.LineItem.PriceData.builder()
|
||||||
.setCurrency("EUR")
|
.setCurrency("EUR")
|
||||||
.setQuantity(1L)
|
.setUnitAmount((long) amountDto.getAmount() * 100)
|
||||||
|
.setProductData(SessionCreateParams.LineItem.PriceData.ProductData.builder()
|
||||||
.setName("Einzahlung")
|
.setName("Einzahlung")
|
||||||
.build())
|
.build())
|
||||||
|
.build())
|
||||||
|
.setQuantity(1L)
|
||||||
|
.build())
|
||||||
.setSuccessUrl(frontendHost+"/home?success=true")
|
.setSuccessUrl(frontendHost+"/home?success=true")
|
||||||
.setCancelUrl(frontendHost+"/home?success=false")
|
.setCancelUrl(frontendHost+"/home?success=false")
|
||||||
.setMode(SessionCreateParams.Mode.PAYMENT)
|
.setMode(SessionCreateParams.Mode.PAYMENT)
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class TransactionService {
|
||||||
.build();
|
.build();
|
||||||
Session checkoutSession = Session.retrieve(sessionID, params, null);
|
Session checkoutSession = Session.retrieve(sessionID, params, null);
|
||||||
|
|
||||||
if (!Objects.equals(checkoutSession.getPaymentStatus(), "paid")) {
|
if (!"paid".equals(checkoutSession.getPaymentStatus())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,10 +53,12 @@ public class TransactionService {
|
||||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
||||||
|
|
||||||
UserEntity user = transaction.getUser();
|
UserEntity user = transaction.getUser();
|
||||||
user.addBalance(checkoutSession.getAmountTotal());
|
Long amountTotal = checkoutSession.getAmountTotal();
|
||||||
|
if (amountTotal != null) {
|
||||||
|
user.addBalance(amountTotal);
|
||||||
|
}
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
transactionRepository.save(transaction);
|
transactionRepository.save(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,12 +51,18 @@ public class WebhookController {
|
||||||
switch (event.getType()) {
|
switch (event.getType()) {
|
||||||
case "checkout.session.completed":
|
case "checkout.session.completed":
|
||||||
case "checkout.session.async_payment_succeeded":
|
case "checkout.session.async_payment_succeeded":
|
||||||
Session session = (Session) event.getData().getObject();
|
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
|
||||||
|
|
||||||
|
if (dataObjectDeserializer.getObject().isPresent()) {
|
||||||
|
Session session = (Session) dataObjectDeserializer.getObject().get();
|
||||||
this.transactionService.fulfillCheckout(session.getId());
|
this.transactionService.fulfillCheckout(session.getId());
|
||||||
|
} else {
|
||||||
|
logger.error("Failed to deserialize webhook event data");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// No action needed for other event types
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok().body(null);
|
return ResponseEntity.ok().body(null);
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import de.szut.casino.user.UserRepository;
|
||||||
|
import de.szut.casino.user.UserService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class LootBoxController {
|
||||||
|
private final LootBoxRepository lootBoxRepository;
|
||||||
|
private final UserService userService;
|
||||||
|
private final LootBoxService lootBoxService;
|
||||||
|
|
||||||
|
public LootBoxController(LootBoxRepository lootBoxRepository, UserRepository userRepository, UserService userService, LootBoxService lootBoxService) {
|
||||||
|
this.lootBoxRepository = lootBoxRepository;
|
||||||
|
this.userService = userService;
|
||||||
|
this.lootBoxService = lootBoxService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/lootboxes")
|
||||||
|
public List<LootBoxEntity> getAllLootBoxes() {
|
||||||
|
return lootBoxRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/lootboxes/{id}")
|
||||||
|
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id, @RequestHeader("Authorization") String token) {
|
||||||
|
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
||||||
|
if (optionalLootBox.isEmpty()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
LootBoxEntity lootBox = optionalLootBox.get();
|
||||||
|
|
||||||
|
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||||
|
if (optionalUser.isEmpty()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntity user = optionalUser.get();
|
||||||
|
|
||||||
|
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
|
||||||
|
Map<String, String> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("error", "Insufficient balance");
|
||||||
|
return ResponseEntity.badRequest().body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
RewardEntity reward = lootBoxService.determineReward(lootBox);
|
||||||
|
lootBoxService.handleBalance(user, lootBox, reward);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(reward);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonBackReference;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||||
|
import de.szut.casino.blackjack.CardEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class LootBoxEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(precision = 19, scale = 2)
|
||||||
|
private BigDecimal price;
|
||||||
|
|
||||||
|
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
||||||
|
@JoinTable(
|
||||||
|
name = "lootbox_reward",
|
||||||
|
joinColumns = @JoinColumn(name = "lootbox_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "reward_id")
|
||||||
|
)
|
||||||
|
private List<RewardEntity> rewards = new ArrayList<>();
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public interface LootBoxRepository extends JpaRepository<LootBoxEntity, Long> {
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import de.szut.casino.user.UserRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LootBoxService {
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public LootBoxService(UserRepository userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
|
||||||
|
return user.getBalance().compareTo(price) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RewardEntity determineReward(LootBoxEntity lootBox) {
|
||||||
|
double randomValue = Math.random();
|
||||||
|
BigDecimal cumulativeProbability = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (RewardEntity reward : lootBox.getRewards()) {
|
||||||
|
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
|
||||||
|
if (randomValue <= cumulativeProbability.doubleValue()) {
|
||||||
|
return reward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lootBox.getRewards().getLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
|
||||||
|
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
|
||||||
|
user.setBalance(user.getBalance().add(reward.getValue()));
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonBackReference;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
public class RewardEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(precision = 19, scale = 2)
|
||||||
|
private BigDecimal value;
|
||||||
|
|
||||||
|
@Column(precision = 5, scale = 2)
|
||||||
|
private BigDecimal probability;
|
||||||
|
|
||||||
|
@ManyToMany(mappedBy = "rewards")
|
||||||
|
@JsonBackReference
|
||||||
|
private List<LootBoxEntity> lootBoxes = new ArrayList<>();
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public interface RewardRepository extends JpaRepository<RewardEntity, Long> {
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ public class UserController {
|
||||||
|
|
||||||
@GetMapping("/user")
|
@GetMapping("/user")
|
||||||
public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) {
|
public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) {
|
||||||
GetUserDto userData = userService.getCurrentUser(token);
|
GetUserDto userData = userService.getCurrentUserAsDto(token);
|
||||||
|
|
||||||
if (userData == null) {
|
if (userData == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class UserService {
|
||||||
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetUserDto getCurrentUser(String token) {
|
public GetUserDto getCurrentUserAsDto(String token) {
|
||||||
KeycloakUserDto userData = getAuthentikUserInfo(token);
|
KeycloakUserDto userData = getAuthentikUserInfo(token);
|
||||||
|
|
||||||
if (userData == null) {
|
if (userData == null) {
|
||||||
|
@ -48,6 +48,15 @@ public class UserService {
|
||||||
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<UserEntity> getCurrentUser(String token) {
|
||||||
|
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
||||||
|
|
||||||
|
if (userData == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return this.userRepository.findOneByKeycloakId(userData.getSub());
|
||||||
|
}
|
||||||
|
|
||||||
private KeycloakUserDto getAuthentikUserInfo(String token) {
|
private KeycloakUserDto getAuthentikUserInfo(String token) {
|
||||||
try {
|
try {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
|
|
@ -2,7 +2,7 @@ spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/postgresdb
|
||||||
spring.datasource.username=postgres_user
|
spring.datasource.username=postgres_user
|
||||||
spring.datasource.password=postgres_pass
|
spring.datasource.password=postgres_pass
|
||||||
server.port=8080
|
server.port=8080
|
||||||
spring.jpa.hibernate.ddl-auto=create-drop
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
|
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
|
||||||
stripe.webhook.secret=whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15
|
stripe.webhook.secret=whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15
|
||||||
app.frontend-host=http://localhost:4200
|
app.frontend-host=http://localhost:4200
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,38 +9,42 @@
|
||||||
"test": "bunx @angular/cli test",
|
"test": "bunx @angular/cli test",
|
||||||
"format": "prettier --write \"src/**/*.{ts,html,css,scss}\"",
|
"format": "prettier --write \"src/**/*.{ts,html,css,scss}\"",
|
||||||
"format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"",
|
"format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"",
|
||||||
"lint": "ng lint"
|
"lint": "bunx @angular/cli lint"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^18.2.0",
|
"@angular/animations": "^19.0.0",
|
||||||
"@angular/cdk": "~18.2.14",
|
"@angular/cdk": "~19.2.0",
|
||||||
"@angular/common": "^18.2.0",
|
"@angular/common": "^19.0.0",
|
||||||
"@angular/compiler": "^18.2.0",
|
"@angular/compiler": "^19.2.4",
|
||||||
"@angular/core": "^18.2.0",
|
"@angular/core": "^19.0.0",
|
||||||
"@angular/forms": "^18.2.0",
|
"@angular/forms": "^19.0.0",
|
||||||
"@angular/platform-browser": "^18.2.0",
|
"@angular/platform-browser": "^19.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||||
"@angular/router": "^18.2.0",
|
"@angular/router": "^19.0.0",
|
||||||
"@fortawesome/angular-fontawesome": "^1.0.0",
|
"@fortawesome/angular-fontawesome": "^1.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@stripe/stripe-js": "^5.6.0",
|
"@stripe/stripe-js": "^7.0.0",
|
||||||
"@tailwindcss/postcss": "^4.0.3",
|
"@tailwindcss/postcss": "^4.0.3",
|
||||||
|
"ajv": "8.17.1",
|
||||||
|
"ajv-formats": "3.0.1",
|
||||||
"gsap": "^3.12.7",
|
"gsap": "^3.12.7",
|
||||||
"angular-oauth2-oidc": "^19.0.0",
|
"angular-oauth2-oidc": "^19.0.0",
|
||||||
|
"keycloak-angular": "^19.0.0",
|
||||||
|
"keycloak-js": "^26.0.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.2",
|
||||||
"tailwindcss": "^4.0.3",
|
"tailwindcss": "^4.0.3",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^18.2.2",
|
"@angular-devkit/build-angular": "^19.0.0",
|
||||||
"@angular/cli": "^18.2.2",
|
"@angular/cli": "^19.2.5",
|
||||||
"@angular/compiler-cli": "^18.2.0",
|
"@angular/compiler-cli": "^19.0.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"angular-eslint": "19.2.1",
|
"angular-eslint": "19.3.0",
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.0",
|
||||||
"jasmine-core": "~5.6.0",
|
"jasmine-core": "~5.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
|
@ -49,7 +53,7 @@
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "~5.5.0",
|
"typescript": "~5.8.0",
|
||||||
"typescript-eslint": "8.26.1"
|
"typescript-eslint": "8.29.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,9 @@ export const routes: Routes = [
|
||||||
loadComponent: () => import('./feature/home/home.component'),
|
loadComponent: () => import('./feature/home/home.component'),
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'game/blackjack',
|
||||||
|
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -16,12 +16,12 @@ import {
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||||
import { DepositService } from '../../service/deposit.service';
|
|
||||||
import { debounceTime } from 'rxjs';
|
import { debounceTime } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { NgIf } from '@angular/common';
|
import { NgIf } from '@angular/common';
|
||||||
import { ModalAnimationService } from '../../shared/services/modal-animation.service';
|
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
|
import { DepositService } from '@service/deposit.service';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
import { ModalAnimationService } from '@shared/services/modal-animation.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-deposit',
|
selector: 'app-deposit',
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<app-navbar></app-navbar>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
@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>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-1 space-y-6">
|
||||||
|
<app-game-info
|
||||||
|
[balance]="balance()"
|
||||||
|
[currentBet]="currentBet()"
|
||||||
|
[gameInProgress]="gameInProgress()"
|
||||||
|
[isActionInProgress]="isActionInProgress()"
|
||||||
|
(newGame)="onNewGame($event)"
|
||||||
|
></app-game-info>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-game-result
|
||||||
|
[gameState]="gameState()"
|
||||||
|
[amount]="currentBet()"
|
||||||
|
[balance]="balance()"
|
||||||
|
[show]="showGameResult()"
|
||||||
|
(gameResultClosed)="onCloseGameResult()"
|
||||||
|
></app-game-result>
|
||||||
|
|
||||||
|
<app-debt-dialog
|
||||||
|
[amount]="debtAmount()"
|
||||||
|
[show]="showDebtDialog()"
|
||||||
|
(dialogClosed)="onCloseDebtDialog()"
|
||||||
|
></app-debt-dialog>
|
231
frontend/src/app/feature/game/blackjack/blackjack.component.ts
Normal file
231
frontend/src/app/feature/game/blackjack/blackjack.component.ts
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import { ChangeDetectionStrategy, Component, inject, signal, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
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 '@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';
|
||||||
|
import { timer } from 'rxjs';
|
||||||
|
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-blackjack',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NavbarComponent,
|
||||||
|
PlayingCardComponent,
|
||||||
|
DealerHandComponent,
|
||||||
|
PlayerHandComponent,
|
||||||
|
GameControlsComponent,
|
||||||
|
GameInfoComponent,
|
||||||
|
GameResultComponent,
|
||||||
|
DebtDialogComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './blackjack.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export default class BlackjackComponent implements OnInit {
|
||||||
|
private router = inject(Router);
|
||||||
|
private userService = inject(UserService);
|
||||||
|
private blackjackService = inject(BlackjackService);
|
||||||
|
|
||||||
|
dealerCards = signal<Card[]>([]);
|
||||||
|
playerCards = signal<Card[]>([]);
|
||||||
|
currentBet = signal(0);
|
||||||
|
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>('');
|
||||||
|
|
||||||
|
showDebtDialog = signal(false);
|
||||||
|
debtAmount = signal(0);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.userService.currentUser$.subscribe((user) => {
|
||||||
|
if (user) {
|
||||||
|
this.balance.set(user.balance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === 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: !isGameOver && index === 1 && game.state === GameState.IN_PROGRESS,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.playerCards.set(
|
||||||
|
game.playerCards.map((card) => ({
|
||||||
|
...card,
|
||||||
|
hidden: false,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isGameOver) {
|
||||||
|
console.log('Game is over, state:', game.state);
|
||||||
|
this.userService.refreshCurrentUser();
|
||||||
|
timer(1500).subscribe(() => {
|
||||||
|
this.showGameResult.set(true);
|
||||||
|
console.log('Game result dialog shown after delay');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() || 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() || 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.getCurrentUser().subscribe((user) => {
|
||||||
|
if (user && user.balance < 0) {
|
||||||
|
this.debtAmount.set(Math.abs(user.balance));
|
||||||
|
this.showDebtDialog.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
this.userService.refreshCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseDebtDialog(): void {
|
||||||
|
this.showDebtDialog.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.userService.refreshCurrentUser();
|
||||||
|
} else if (error.status === 500) {
|
||||||
|
console.log('Server error occurred. The game may have been updated in another session.');
|
||||||
|
this.gameInProgress.set(false);
|
||||||
|
this.userService.refreshCurrentUser();
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveGame(): void {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
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 { GameControlsService } from '@blackjack/services/game-controls.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dealer-hand',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, PlayingCardComponent],
|
||||||
|
template: `
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="section-heading text-2xl">Dealer's Karten</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="text-text-secondary">Punkte:</div>
|
||||||
|
<div class="text-xl font-bold text-accent-red">
|
||||||
|
{{ gameControlsService.calculateHandValue(cards) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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 cardsWithState; track card.id) {
|
||||||
|
<app-playing-card
|
||||||
|
[rank]="card.rank"
|
||||||
|
[suit]="card.suit"
|
||||||
|
[hidden]="card.hidden"
|
||||||
|
[isNew]="card.isNew"
|
||||||
|
></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>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class DealerHandComponent implements OnChanges {
|
||||||
|
@Input() cards: Card[] = [];
|
||||||
|
cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
|
||||||
|
|
||||||
|
private lastCardCount = 0;
|
||||||
|
|
||||||
|
constructor(protected gameControlsService: GameControlsService) {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
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 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) {}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||||
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { BettingService } from '@blackjack/services/betting.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-game-info',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, CurrencyPipe, ReactiveFormsModule],
|
||||||
|
template: `
|
||||||
|
<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">Aktuelle Wette:</span>
|
||||||
|
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'">
|
||||||
|
{{ currentBet | currency: 'EUR' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@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 disabled:opacity-50"
|
||||||
|
[min]="1"
|
||||||
|
[max]="balance"
|
||||||
|
step="0.01"
|
||||||
|
[disabled]="gameInProgress || isActionInProgress"
|
||||||
|
[placeholder]="balance | currency: 'EUR'"
|
||||||
|
/>
|
||||||
|
@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 relative"
|
||||||
|
[disabled]="!betForm.valid || gameInProgress || isActionInProgress"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class GameInfoComponent implements OnChanges {
|
||||||
|
@Input() set balance(value: number) {
|
||||||
|
this._balance.set(value);
|
||||||
|
}
|
||||||
|
get balance() {
|
||||||
|
return this._balance();
|
||||||
|
}
|
||||||
|
private _balance = signal(0);
|
||||||
|
|
||||||
|
@Input() currentBet = 0;
|
||||||
|
@Input() gameInProgress = false;
|
||||||
|
@Input() isActionInProgress = false;
|
||||||
|
@Output() newGame = new EventEmitter<number>();
|
||||||
|
|
||||||
|
betForm: FormGroup;
|
||||||
|
|
||||||
|
constructor(private bettingService: BettingService) {
|
||||||
|
this.betForm = this.bettingService.createBetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['balance']) {
|
||||||
|
this.bettingService.updateBetFormValidators(this.betForm, this.balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBetAmount(percentage: number) {
|
||||||
|
const betAmount = this.bettingService.calculateBetAmount(this.balance, percentage);
|
||||||
|
if (this.bettingService.isValidBet(betAmount, this.balance)) {
|
||||||
|
this.betForm.patchValue({ bet: betAmount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
if (this.betForm.valid) {
|
||||||
|
const betAmount = parseFloat(this.betForm.value.bet);
|
||||||
|
if (this.bettingService.isValidBet(betAmount, this.balance)) {
|
||||||
|
this.newGame.emit(betAmount);
|
||||||
|
this.betForm.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
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 | currency: 'EUR' }}</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 ? '-' : '+' }}{{ isWin ? amount * 2 : (amount | currency: 'EUR') }}
|
||||||
|
<div *ngIf="isWin" class="text-xs text-text-secondary">
|
||||||
|
(Einsatz {{ amount | currency: 'EUR' }} × 2)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-text-secondary border-t border-text-secondary/20 pt-3 font-medium">
|
||||||
|
Kontostand:
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-right border-t border-text-secondary/20 pt-3">
|
||||||
|
{{ balance | currency: 'EUR' }}
|
||||||
|
</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() balance = 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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
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 '@blackjack/models/blackjack.model';
|
||||||
|
import { GameControlsService } from '@blackjack/services/game-controls.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-player-hand',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, PlayingCardComponent],
|
||||||
|
template: `
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="section-heading text-2xl">Deine Karten</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="text-text-secondary">Punkte:</div>
|
||||||
|
<div class="text-xl font-bold text-emerald">
|
||||||
|
{{ gameControlsService.calculateHandValue(cards) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-6 !bg-emerald">
|
||||||
|
<div
|
||||||
|
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 cardsWithState; track card.id) {
|
||||||
|
<app-playing-card
|
||||||
|
[rank]="card.rank"
|
||||||
|
[suit]="card.suit"
|
||||||
|
[hidden]="card.hidden"
|
||||||
|
[isNew]="card.isNew"
|
||||||
|
></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>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class PlayerHandComponent implements OnChanges {
|
||||||
|
@Input() cards: Card[] = [];
|
||||||
|
cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
|
||||||
|
|
||||||
|
private lastCardCount = 0;
|
||||||
|
|
||||||
|
constructor(protected gameControlsService: GameControlsService) {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
AfterViewInit,
|
||||||
|
ElementRef,
|
||||||
|
OnChanges,
|
||||||
|
SimpleChanges,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { gsap } from 'gsap';
|
||||||
|
import { Suit, suitSymbols } from '@blackjack/models/blackjack.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-playing-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
template: `
|
||||||
|
<div
|
||||||
|
#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" [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"
|
||||||
|
[class]="isRedSuit ? 'text-accent-red' : 'text-black'"
|
||||||
|
>{{ getSuitSymbol(suit) }}</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@if (!hidden) {
|
||||||
|
<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 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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',
|
||||||
|
}
|
|
@ -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: '♠',
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface Card {
|
||||||
|
value: string;
|
||||||
|
suit: string;
|
||||||
|
hidden: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class BettingService {
|
||||||
|
constructor(private fb: FormBuilder) {}
|
||||||
|
|
||||||
|
createBetForm(): FormGroup {
|
||||||
|
return this.fb.group({
|
||||||
|
bet: ['', [Validators.required, Validators.min(1)]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBetFormValidators(form: FormGroup, balance: number): void {
|
||||||
|
form.reset();
|
||||||
|
form
|
||||||
|
.get('bet')
|
||||||
|
?.setValidators([Validators.required, Validators.min(1), Validators.max(balance)]);
|
||||||
|
form.get('bet')?.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBetAmount(balance: number, percentage: number): number {
|
||||||
|
return Math.floor(balance * percentage * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidBet(betAmount: number, balance: number): boolean {
|
||||||
|
return betAmount >= 1 && betAmount <= balance;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable, catchError } from 'rxjs';
|
||||||
|
import { BlackjackGame } from '@blackjack/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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,9 @@
|
||||||
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
|
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
|
||||||
>
|
>
|
||||||
<h4 class="game-heading">{{ game.name }}</h4>
|
<h4 class="game-heading">{{ game.name }}</h4>
|
||||||
<button class="button-primary w-full py-2">Jetzt Spielen</button>
|
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
|
||||||
|
Jetzt Spielen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +61,9 @@
|
||||||
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
|
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
|
||||||
>
|
>
|
||||||
<h4 class="game-heading">{{ game.name }}</h4>
|
<h4 class="game-heading">{{ game.name }}</h4>
|
||||||
<button class="button-primary w-full py-2">Jetzt Spielen</button>
|
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
|
||||||
|
Jetzt Spielen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { NavbarComponent } from '../../shared/components/navbar/navbar.component';
|
|
||||||
import { CurrencyPipe, NgFor } from '@angular/common';
|
import { CurrencyPipe, NgFor } from '@angular/common';
|
||||||
import { Game } from '../../model/Game';
|
|
||||||
import { Transaction } from '../../model/Transaction';
|
|
||||||
import { DepositComponent } from '../deposit/deposit.component';
|
import { DepositComponent } from '../deposit/deposit.component';
|
||||||
import { ConfirmationComponent } from '../../shared/components/confirmation/confirmation.component';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
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({
|
@Component({
|
||||||
selector: 'app-homepage',
|
selector: 'app-homepage',
|
||||||
|
@ -38,31 +38,37 @@ export default class HomeComponent implements OnInit {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Poker',
|
name: 'Poker',
|
||||||
image: '/poker.webp',
|
image: '/poker.webp',
|
||||||
|
route: '/game/poker',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
name: 'Blackjack',
|
name: 'Blackjack',
|
||||||
image: '/blackjack.webp',
|
image: '/blackjack.webp',
|
||||||
|
route: '/game/blackjack',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
name: 'Slots',
|
name: 'Slots',
|
||||||
image: '/slots.webp',
|
image: '/slots.webp',
|
||||||
|
route: '/game/slots',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
name: 'Plinko',
|
name: 'Plinko',
|
||||||
image: '/plinko.webp',
|
image: '/plinko.webp',
|
||||||
|
route: '/game/plinko',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
name: 'Liars Dice',
|
name: 'Liars Dice',
|
||||||
image: '/liars-dice.webp',
|
image: '/liars-dice.webp',
|
||||||
|
route: '/game/liars-dice',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '6',
|
id: '6',
|
||||||
name: 'Lootboxen',
|
name: 'Lootboxen',
|
||||||
image: '/lootbox.webp',
|
image: '/lootbox.webp',
|
||||||
|
route: '/game/lootbox',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -83,4 +89,8 @@ export default class HomeComponent implements OnInit {
|
||||||
closeDepositConfirmationModal() {
|
closeDepositConfirmationModal() {
|
||||||
this.isDepositSuccessful = false;
|
this.isDepositSuccessful = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToGame(route: string) {
|
||||||
|
this.router.navigate([route]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { NavbarComponent } from '../../shared/components/navbar/navbar.component';
|
|
||||||
import { NgFor } from '@angular/common';
|
import { NgFor } from '@angular/common';
|
||||||
|
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-landing-page',
|
selector: 'app-landing-page',
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../../service/auth.service';
|
import { AuthService } from '../../service/auth.service';
|
||||||
import { User } from '../../model/User';
|
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
|
@ -2,4 +2,5 @@ export interface Game {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { catchError, EMPTY, Observable } from 'rxjs';
|
import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs';
|
||||||
import { User } from '../model/User';
|
import { User } from '../model/User';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -8,16 +8,37 @@ import { User } from '../model/User';
|
||||||
})
|
})
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private http: HttpClient = inject(HttpClient);
|
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),
|
||||||
|
tap((user) => this.currentUserSubject.next(user))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public getCurrentUser(): Observable<User | null> {
|
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> {
|
public createUser(id: string, username: string): Observable<User> {
|
||||||
return this.http.post<User>('/backend/user', {
|
return this.http.post<User>('/backend/user', {
|
||||||
authentikId: id,
|
authentikId: id,
|
||||||
username: username,
|
username: username,
|
||||||
});
|
}).pipe(tap((user) => this.currentUserSubject.next(user)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOrCreateUser(profile: any): Observable<User> {
|
public getOrCreateUser(profile: any): Observable<User> {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ModalAnimationService } from '../../services/modal-animation.service';
|
import { ModalAnimationService } from '@shared/services/modal-animation.service';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
|
import { interval, Subscription, takeWhile } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-debt-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
template: `
|
||||||
|
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
|
||||||
|
<div class="modal-card" [@cardAnimation]>
|
||||||
|
<h2 class="modal-heading text-accent-red">WARNUNG!</h2>
|
||||||
|
<p class="py-2 text-text-secondary mb-4">
|
||||||
|
Du hast nicht genug Geld für den Double Down. Du bist jetzt im Minus und schuldest uns
|
||||||
|
{{ amount | currency: 'EUR' }}.
|
||||||
|
</p>
|
||||||
|
<p class="py-2 text-accent-red mb-4 font-bold">
|
||||||
|
Liefer das Geld sofort an den Dead Drop oder es wird unangenehme Konsequenzen geben!
|
||||||
|
</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">Schulden:</div>
|
||||||
|
<div class="font-medium text-right text-accent-red">{{ amount | currency: 'EUR' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div
|
||||||
|
class="text-8xl font-bold text-accent-red"
|
||||||
|
[class.animate-pulse]="timeLeft() <= 10"
|
||||||
|
[class.animate-bounce]="timeLeft() <= 5"
|
||||||
|
[@countdown]="timeLeft()"
|
||||||
|
>
|
||||||
|
{{ timeLeft() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-text-secondary mt-2">Sekunden verbleibend</div>
|
||||||
|
</div>
|
||||||
|
@if (timeLeft() === 0) {
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 bg-accent-red/20 blur-xl rounded-full"></div>
|
||||||
|
<div
|
||||||
|
class="relative bg-gradient-to-b from-accent-red to-red-900 p-8 rounded-lg border-2 border-accent-red shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center gap-4 mb-4">
|
||||||
|
<svg
|
||||||
|
class="w-12 h-12 text-accent-red animate-[spin_2s_linear_infinite]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="text-4xl font-black tracking-wider text-white animate-[pulse_1s_ease-in-out_infinite]"
|
||||||
|
>
|
||||||
|
ZEIT ABGELAUFEN
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="w-12 h-12 text-accent-red animate-[spin_2s_linear_infinite]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold text-white/90 tracking-wider animate-[pulse_1s_ease-in-out_infinite]"
|
||||||
|
>
|
||||||
|
KONSEQUENZEN FOLGEN
|
||||||
|
</div>
|
||||||
|
</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)' })),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
trigger('countdown', [
|
||||||
|
transition('* => *', [
|
||||||
|
style({ transform: 'scale(1.2)' }),
|
||||||
|
animate('100ms ease-out', style({ transform: 'scale(1)' })),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DebtDialogComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() amount = 0;
|
||||||
|
@Input() set show(value: boolean) {
|
||||||
|
this.visible = value;
|
||||||
|
if (value) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output() dialogClosed = new EventEmitter<void>();
|
||||||
|
|
||||||
|
visible = false;
|
||||||
|
timeLeft = signal(30);
|
||||||
|
private timerSubscription: Subscription | undefined;
|
||||||
|
private warningSound = new Audio('assets/sounds/warning.mp3');
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.visible) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer() {
|
||||||
|
this.timeLeft.set(30);
|
||||||
|
this.timerSubscription = interval(1000)
|
||||||
|
.pipe(takeWhile(() => this.timeLeft() > 0))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.timeLeft.update((value) => value - 1);
|
||||||
|
if (this.timeLeft() <= 5) {
|
||||||
|
this.warningSound.play();
|
||||||
|
}
|
||||||
|
if (this.timeLeft() === 0) {
|
||||||
|
setTimeout(() => this.closeDialog(), 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTimer() {
|
||||||
|
if (this.timerSubscription) {
|
||||||
|
this.timerSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog(): void {
|
||||||
|
this.stopTimer();
|
||||||
|
this.visible = false;
|
||||||
|
this.dialogClosed.emit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,11 @@
|
||||||
@if (isLoggedIn) {
|
@if (isLoggedIn) {
|
||||||
<div
|
<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"
|
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"
|
||||||
|
routerLink="/home"
|
||||||
>
|
>
|
||||||
<span>Balance: {{ balance() | currency: 'EUR' : 'symbol' : '1.2-2' }}</span>
|
<span [class]="balance() < 0 ? 'text-accent-red' : ''">{{
|
||||||
|
balance() | currency: 'EUR' : 'symbol' : '1.2-2'
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
|
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
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 { RouterModule } from '@angular/router';
|
||||||
import { UserService } from '../../../service/user.service';
|
|
||||||
import { CurrencyPipe } from '@angular/common';
|
|
||||||
import { AuthService } from '../../../service/auth.service';
|
import { AuthService } from '../../../service/auth.service';
|
||||||
|
import { CurrencyPipe } from '@angular/common';
|
||||||
|
import { UserService } from '@service/user.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
|
@ -11,22 +19,27 @@ import { AuthService } from '../../../service/auth.service';
|
||||||
imports: [RouterModule, CurrencyPipe],
|
imports: [RouterModule, CurrencyPipe],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class NavbarComponent implements OnInit {
|
export class NavbarComponent implements OnInit, OnDestroy {
|
||||||
isMenuOpen = false;
|
isMenuOpen = false;
|
||||||
private authService: AuthService = inject(AuthService);
|
private authService: AuthService = inject(AuthService);
|
||||||
isLoggedIn = this.authService.isLoggedIn();
|
isLoggedIn = this.authService.isLoggedIn();
|
||||||
|
|
||||||
private userService = inject(UserService);
|
private userService = inject(UserService);
|
||||||
private user = this.userService.getCurrentUser();
|
private userSubscription: Subscription | undefined;
|
||||||
|
|
||||||
public balance = signal(0);
|
public balance = signal(0);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.user.subscribe((user) => {
|
this.userSubscription = this.userService.currentUser$.subscribe((user) => {
|
||||||
this.balance.set(user?.balance ?? 0);
|
this.balance.set(user?.balance ?? 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.userSubscription) {
|
||||||
|
this.userSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
try {
|
try {
|
||||||
this.authService.login();
|
this.authService.login();
|
||||||
|
|
|
@ -3,6 +3,13 @@
|
||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"@service/*": ["app/service/*"],
|
||||||
|
"@environments/*": ["environments/*"],
|
||||||
|
"@shared/*": ["app/shared/*"],
|
||||||
|
"@blackjack/*": ["app/feature/game/blackjack/*"]
|
||||||
|
},
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue