diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java index c89f3ef..c9d5c26 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java @@ -1,6 +1,7 @@ package de.szut.casino.blackjack; -import de.szut.casino.blackjack.dto.CreateBlackJackGameDto; +import de.szut.casino.shared.dto.BetDto; +import de.szut.casino.shared.service.BalanceService; import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserService; import jakarta.validation.Valid; @@ -9,19 +10,18 @@ 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; +import java.util.*; @Slf4j @RestController public class BlackJackGameController { + private final BalanceService balanceService; private final UserService userService; private final BlackJackService blackJackService; - public BlackJackGameController(UserService userService, BlackJackService blackJackService) { + public BlackJackGameController(BalanceService balanceService, UserService userService, BlackJackService blackJackService) { + this.balanceService = balanceService; this.blackJackService = blackJackService; this.userService = userService; } @@ -112,7 +112,7 @@ public class BlackJackGameController { } @PostMapping("/blackjack/start") - public ResponseEntity createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) { + public ResponseEntity createBlackJackGame(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { @@ -120,21 +120,11 @@ public class BlackJackGameController { } UserEntity user = optionalUser.get(); - BigDecimal balance = user.getBalance(); - BigDecimal betAmount = createBlackJackGameDto.getBetAmount(); - if (betAmount.compareTo(BigDecimal.ZERO) <= 0) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Invalid bet amount"); - return ResponseEntity.badRequest().body(errorResponse); + if (!this.balanceService.hasFunds(user, betDto)) { + return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds")); } - if (betAmount.compareTo(balance) > 0) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Insufficient funds"); - return ResponseEntity.badRequest().body(errorResponse); - } - - return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount)); + return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betDto.getBetAmount())); } } diff --git a/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java b/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java deleted file mode 100644 index e5b0c97..0000000 --- a/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java +++ /dev/null @@ -1,16 +0,0 @@ -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; -} diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionService.java b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java index 2cfc19e..fceb27b 100644 --- a/backend/src/main/java/de/szut/casino/deposit/TransactionService.java +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java @@ -7,6 +7,7 @@ import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserRepository; import org.springframework.stereotype.Service; +import java.math.BigDecimal; import java.util.Optional; @Service @@ -54,7 +55,7 @@ public class TransactionService { UserEntity user = transaction.getUser(); Long amountTotal = checkoutSession.getAmountTotal(); if (amountTotal != null) { - user.addBalance(amountTotal); + user.addBalance(BigDecimal.valueOf(amountTotal).movePointLeft(2)); } userRepository.save(user); diff --git a/backend/src/main/java/de/szut/casino/shared/dto/BetDto.java b/backend/src/main/java/de/szut/casino/shared/dto/BetDto.java new file mode 100644 index 0000000..a910a03 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/shared/dto/BetDto.java @@ -0,0 +1,18 @@ +package de.szut.casino.shared.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +@Getter +@Setter +@AllArgsConstructor +public class BetDto { + @NotNull(message = "Bet amount cannot be null") + @Positive(message = "Bet amount must be positive") + private BigDecimal betAmount; +} diff --git a/backend/src/main/java/de/szut/casino/shared/service/BalanceService.java b/backend/src/main/java/de/szut/casino/shared/service/BalanceService.java new file mode 100644 index 0000000..40e6caa --- /dev/null +++ b/backend/src/main/java/de/szut/casino/shared/service/BalanceService.java @@ -0,0 +1,36 @@ +package de.szut.casino.shared.service; + +import de.szut.casino.shared.dto.BetDto; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class BalanceService { + private UserRepository userRepository; + + public BalanceService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public boolean hasFunds(UserEntity user, BetDto betDto) { + BigDecimal balance = user.getBalance(); + BigDecimal betAmount = betDto.getBetAmount(); + + return betAmount.compareTo(balance) <= 0; + } + + public void addFunds(UserEntity user, BigDecimal amount) { + user.addBalance(amount); + + this.userRepository.save(user); + } + + public void subtractFunds(UserEntity user, BigDecimal amount) { + user.subtractBalance(amount); + + this.userRepository.save(user); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SlotController.java b/backend/src/main/java/de/szut/casino/slots/SlotController.java new file mode 100644 index 0000000..8f98b1d --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/SlotController.java @@ -0,0 +1,50 @@ +package de.szut.casino.slots; + +import de.szut.casino.shared.dto.BetDto; +import de.szut.casino.shared.service.BalanceService; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.Optional; + +@RestController +public class SlotController { + private final UserService userService; + private final BalanceService balanceService; + private final SlotService slotService; + + public SlotController(UserService userService, BalanceService balanceService, SlotService slotService) { + this.userService = userService; + this.balanceService = balanceService; + this.slotService = slotService; + } + + @PostMapping("/slots/spin") + public ResponseEntity spinSlots(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); + + if (optionalUser.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + UserEntity user = optionalUser.get(); + + if (!this.balanceService.hasFunds(user, betDto)) { + return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds")); + } + + SpinResult spinResult = this.slotService.spin( + betDto.getBetAmount(), + user + ); + + return ResponseEntity.ok(spinResult); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SlotService.java b/backend/src/main/java/de/szut/casino/slots/SlotService.java new file mode 100644 index 0000000..39ebeab --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/SlotService.java @@ -0,0 +1,139 @@ +package de.szut.casino.slots; + +import de.szut.casino.shared.service.BalanceService; +import de.szut.casino.user.UserEntity; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +@Service +public class SlotService { + private final int REEL_LENGTH = 32; + + // 98% RTP + private final int SEVEN_COUNT = 1; + private final int BAR_COUNT = 4; + private final int BELL_COUNT = 7; + private final int CHERRY_COUNT = 10; + private final int BLANK_COUNT = 10; + + private final Symbol SEVEN = new Symbol("seven", new BigDecimal("1000")); + private final Symbol BAR = new Symbol("bar", new BigDecimal("85")); + private final Symbol BELL = new Symbol("bell", new BigDecimal("40")); + private final Symbol CHERRY = new Symbol("cherry", new BigDecimal("10")); + private final Symbol BLANK = new Symbol("blank", new BigDecimal("0")); + + private final List firstReel; + private final List secondReel; + private final List thirdReel; + + private final Random random; + private final BalanceService balanceService; + + public SlotService(BalanceService balanceService) { + this.random = new Random(); + this.balanceService = balanceService; + + List reelStrip = createReelStrip(); + this.firstReel = shuffleReel(reelStrip); + this.secondReel = shuffleReel(reelStrip); + this.thirdReel = shuffleReel(reelStrip); + } + + public SpinResult spin(BigDecimal betAmount, UserEntity user) { + int index1 = this.random.nextInt(REEL_LENGTH); + int index2 = this.random.nextInt(REEL_LENGTH); + int index3 = this.random.nextInt(REEL_LENGTH); + + Symbol symbol1 = getSymbolAt(this.firstReel, index1); + Symbol symbol2 = getSymbolAt(this.secondReel, index2); + Symbol symbol3 = getSymbolAt(this.thirdReel, index3); + + boolean isWin = symbol1.equals(symbol2) && symbol1.equals(symbol3); + + SpinResult spinResult = processResult(betAmount, user, isWin, symbol1); + buildResultMatrix(spinResult, index1, index2, index3); + + return spinResult; + } + + private SpinResult processResult(BigDecimal betAmount, UserEntity user, boolean isWin, Symbol winSymbol) { + BigDecimal resultAmount; + String status; + + if (isWin) { + resultAmount = betAmount.multiply(winSymbol.getPayoutMultiplier()); + status = "win"; + this.balanceService.addFunds(user, resultAmount); + } else { + resultAmount = betAmount; + status = "lose"; + this.balanceService.subtractFunds(user, betAmount); + } + + SpinResult spinResult = new SpinResult(); + spinResult.setStatus(status); + spinResult.setAmount(resultAmount); + spinResult.setWin(isWin); + + return spinResult; + } + + private void buildResultMatrix(SpinResult spinResult, int index1, int index2, int index3) { + List> resultMatrix = new ArrayList<>(3); + + for (int i = 0; i < 3; i++) { + resultMatrix.add(new ArrayList<>(3)); + } + + resultMatrix.getFirst().add(getSymbolAt(this.firstReel, index1 - 1)); + resultMatrix.getFirst().add(getSymbolAt(this.secondReel, index2 - 1)); + resultMatrix.getFirst().add(getSymbolAt(this.thirdReel, index3 - 1)); + + resultMatrix.get(1).add(getSymbolAt(this.firstReel, index1)); + resultMatrix.get(1).add(getSymbolAt(this.secondReel, index2)); + resultMatrix.get(1).add(getSymbolAt(this.thirdReel, index3)); + + resultMatrix.getLast().add(getSymbolAt(this.firstReel, index1 + 1)); + resultMatrix.getLast().add(getSymbolAt(this.secondReel, index2 + 1)); + resultMatrix.getLast().add(getSymbolAt(this.thirdReel, index3 + 1)); + + spinResult.setResultMatrix(resultMatrix); + } + + private List shuffleReel(List reelStrip) { + Collections.shuffle(reelStrip, this.random); + + return reelStrip; + } + + private List createReelStrip() { + List reelStrip = new ArrayList<>(REEL_LENGTH); + addSymbolsToStrip(reelStrip, CHERRY, CHERRY_COUNT); + addSymbolsToStrip(reelStrip, BELL, BELL_COUNT); + addSymbolsToStrip(reelStrip, BAR, BAR_COUNT); + addSymbolsToStrip(reelStrip, SEVEN, SEVEN_COUNT); + addSymbolsToStrip(reelStrip, BLANK, BLANK_COUNT); + return reelStrip; + } + + private void addSymbolsToStrip(List strip, Symbol symbol, int count) { + for (int i = 0; i < count; i++) { + strip.add(symbol); + } + } + + private Symbol getSymbolAt(List reel, int index) { + int effectiveIndex = index % REEL_LENGTH; + + if (effectiveIndex < 0) { + effectiveIndex += REEL_LENGTH; + } + + return reel.get(effectiveIndex); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SpinResult.java b/backend/src/main/java/de/szut/casino/slots/SpinResult.java new file mode 100644 index 0000000..6263e9b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/SpinResult.java @@ -0,0 +1,24 @@ +package de.szut.casino.slots; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class SpinResult { + public SpinResult(String status, BigDecimal amount, boolean isWin) { + this.status = status; + this.amount = amount; + this.isWin = isWin; + } + + private String status; + private BigDecimal amount; + private boolean isWin; + private List> resultMatrix; +} diff --git a/backend/src/main/java/de/szut/casino/slots/Symbol.java b/backend/src/main/java/de/szut/casino/slots/Symbol.java new file mode 100644 index 0000000..806d14b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/Symbol.java @@ -0,0 +1,35 @@ +package de.szut.casino.slots; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Getter +@Setter +@AllArgsConstructor +public class Symbol { + private String name; + private BigDecimal payoutMultiplier; + + @Override + public boolean equals(Object other) { + if (!(other instanceof Symbol that)) { + return false; + } + + return this.name.equals(that.name) && this.payoutMultiplier.equals(that.payoutMultiplier); + } + + @Override + public int hashCode() { + int hashCode = 1; + + hashCode = hashCode * 37 + this.name.hashCode(); + hashCode = hashCode * 37 + this.payoutMultiplier.hashCode(); + + return hashCode; + } +} diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index 67fc1ae..03f4a34 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -31,13 +31,31 @@ public class UserEntity { this.balance = balance; } - public void addBalance(long amountInCents) { - BigDecimal amountToAdd = BigDecimal.valueOf(amountInCents).movePointLeft(2); + public void addBalance(BigDecimal amountToAdd) { + if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) { + return; + } if (this.balance == null) { - this.balance = amountToAdd; - } else { - this.balance = this.balance.add(amountToAdd); + this.balance = BigDecimal.ZERO; } + + this.balance = this.balance.add(amountToAdd); + } + + public void subtractBalance(BigDecimal amountToSubtract) { + if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Amount to subtract must be positive."); + } + + if (this.balance == null) { + this.balance = BigDecimal.ZERO; + } + + if (this.balance.compareTo(amountToSubtract) < 0) { + throw new IllegalStateException("Insufficient funds to subtract " + amountToSubtract); + } + + this.balance = this.balance.subtract(amountToSubtract); } }