Merge pull request 'feat: implement api route for slots (CAS-4 CAS-58)' (!140) from feature-slots into main
Reviewed-on: #140 Reviewed-by: Jan K9f <jan@kjan.email>
This commit is contained in:
commit
ef069d7d18
10 changed files with 337 additions and 42 deletions
|
@ -1,6 +1,7 @@
|
||||||
package de.szut.casino.blackjack;
|
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.UserEntity;
|
||||||
import de.szut.casino.user.UserService;
|
import de.szut.casino.user.UserService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
@ -9,19 +10,18 @@ import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
public class BlackJackGameController {
|
public class BlackJackGameController {
|
||||||
|
|
||||||
|
private final BalanceService balanceService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final BlackJackService blackJackService;
|
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.blackJackService = blackJackService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ public class BlackJackGameController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/blackjack/start")
|
@PostMapping("/blackjack/start")
|
||||||
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) {
|
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
if (optionalUser.isEmpty()) {
|
||||||
|
@ -120,21 +120,11 @@ public class BlackJackGameController {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
UserEntity user = optionalUser.get();
|
||||||
BigDecimal balance = user.getBalance();
|
|
||||||
BigDecimal betAmount = createBlackJackGameDto.getBetAmount();
|
|
||||||
|
|
||||||
if (betAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
if (!this.balanceService.hasFunds(user, betDto)) {
|
||||||
Map<String, String> errorResponse = new HashMap<>();
|
return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds"));
|
||||||
errorResponse.put("error", "Invalid bet amount");
|
|
||||||
return ResponseEntity.badRequest().body(errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (betAmount.compareTo(balance) > 0) {
|
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betDto.getBetAmount()));
|
||||||
Map<String, String> errorResponse = new HashMap<>();
|
|
||||||
errorResponse.put("error", "Insufficient funds");
|
|
||||||
return ResponseEntity.badRequest().body(errorResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -7,6 +7,7 @@ import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserRepository;
|
import de.szut.casino.user.UserRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -54,7 +55,7 @@ public class TransactionService {
|
||||||
UserEntity user = transaction.getUser();
|
UserEntity user = transaction.getUser();
|
||||||
Long amountTotal = checkoutSession.getAmountTotal();
|
Long amountTotal = checkoutSession.getAmountTotal();
|
||||||
if (amountTotal != null) {
|
if (amountTotal != null) {
|
||||||
user.addBalance(amountTotal);
|
user.addBalance(BigDecimal.valueOf(amountTotal).movePointLeft(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
18
backend/src/main/java/de/szut/casino/shared/dto/BetDto.java
Normal file
18
backend/src/main/java/de/szut/casino/shared/dto/BetDto.java
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Object> spinSlots(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) {
|
||||||
|
Optional<UserEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
139
backend/src/main/java/de/szut/casino/slots/SlotService.java
Normal file
139
backend/src/main/java/de/szut/casino/slots/SlotService.java
Normal file
|
@ -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<Symbol> firstReel;
|
||||||
|
private final List<Symbol> secondReel;
|
||||||
|
private final List<Symbol> thirdReel;
|
||||||
|
|
||||||
|
private final Random random;
|
||||||
|
private final BalanceService balanceService;
|
||||||
|
|
||||||
|
public SlotService(BalanceService balanceService) {
|
||||||
|
this.random = new Random();
|
||||||
|
this.balanceService = balanceService;
|
||||||
|
|
||||||
|
List<Symbol> 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<List<Symbol>> 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<Symbol> shuffleReel(List<Symbol> reelStrip) {
|
||||||
|
Collections.shuffle(reelStrip, this.random);
|
||||||
|
|
||||||
|
return reelStrip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Symbol> createReelStrip() {
|
||||||
|
List<Symbol> 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<Symbol> strip, Symbol symbol, int count) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
strip.add(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Symbol getSymbolAt(List<Symbol> reel, int index) {
|
||||||
|
int effectiveIndex = index % REEL_LENGTH;
|
||||||
|
|
||||||
|
if (effectiveIndex < 0) {
|
||||||
|
effectiveIndex += REEL_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reel.get(effectiveIndex);
|
||||||
|
}
|
||||||
|
}
|
24
backend/src/main/java/de/szut/casino/slots/SpinResult.java
Normal file
24
backend/src/main/java/de/szut/casino/slots/SpinResult.java
Normal file
|
@ -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<List<Symbol>> resultMatrix;
|
||||||
|
}
|
35
backend/src/main/java/de/szut/casino/slots/Symbol.java
Normal file
35
backend/src/main/java/de/szut/casino/slots/Symbol.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,13 +31,31 @@ public class UserEntity {
|
||||||
this.balance = balance;
|
this.balance = balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addBalance(long amountInCents) {
|
public void addBalance(BigDecimal amountToAdd) {
|
||||||
BigDecimal amountToAdd = BigDecimal.valueOf(amountInCents).movePointLeft(2);
|
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.balance == null) {
|
if (this.balance == null) {
|
||||||
this.balance = amountToAdd;
|
this.balance = BigDecimal.ZERO;
|
||||||
} else {
|
|
||||||
this.balance = this.balance.add(amountToAdd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue