feat: implement slots api route

This commit is contained in:
Phan Huy Tran 2025-04-24 11:00:32 +02:00 committed by Phan Huy Tran
parent 934e60e80d
commit 7a0dd0593b
10 changed files with 326 additions and 42 deletions

View file

@ -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<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);
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<String, String> 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<String, String> 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()));
}
}

View file

@ -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;
}

View file

@ -7,6 +7,8 @@ import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;
@Service
@ -54,7 +56,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);

View 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;
}

View file

@ -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);
}
}

View file

@ -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.handleSpinResult(
betDto.getBetAmount(),
user
);
return ResponseEntity.ok(spinResult);
}
}

View file

@ -0,0 +1,127 @@
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 = 60;
private static final int SEVEN_COUNT = 4;
private static final int BAR_COUNT = 20;
private static final int CHERRY_COUNT = 36;
private final List<Symbol> FIRST_REEL = new ArrayList<>(REEL_LENGTH);
private final List<Symbol> SECOND_REEL = new ArrayList<>(REEL_LENGTH);
private final List<Symbol> THIRD_REEL = new ArrayList<>(REEL_LENGTH);
private final BalanceService balanceService;
public SlotService(BalanceService balanceService) {
this.balanceService = balanceService;
initReels();
}
public SpinResult handleSpinResult(
BigDecimal betAmount,
UserEntity user
) {
Random random = new Random();
int index1 = random.nextInt(REEL_LENGTH);
int index2 = random.nextInt(REEL_LENGTH);
int index3 = random.nextInt(REEL_LENGTH);
Symbol symbol1 = FIRST_REEL.get(index1);
Symbol symbol2 = SECOND_REEL.get(index2);
Symbol symbol3 = THIRD_REEL.get(index3);
boolean isWin = symbol1.equals(symbol2) && symbol1.equals(symbol3);
SpinResult spinResult = new SpinResult();
if (isWin) {
BigDecimal winAmount = betAmount.multiply(symbol1.getPayoutMultiplier());
this.balanceService.addFunds(user, winAmount);
spinResult.setStatus("win");
spinResult.setAmount(winAmount);
spinResult.setWin(true);
} else {
this.balanceService.subtractFunds(user, betAmount);
spinResult.setStatus("lose");
spinResult.setAmount(betAmount);
spinResult.setWin(false);
}
buildResultMatrix(spinResult, index1, index2, index3);
return spinResult;
}
public 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.get(0).add(getSymbolAt(FIRST_REEL, index1 - 1));
resultMatrix.get(1).add(getSymbolAt(FIRST_REEL, index1));
resultMatrix.get(2).add(getSymbolAt(FIRST_REEL, index1 + 1));
resultMatrix.get(0).add(getSymbolAt(SECOND_REEL, index2 - 1));
resultMatrix.get(1).add(getSymbolAt(SECOND_REEL, index2));
resultMatrix.get(2).add(getSymbolAt(SECOND_REEL, index2 + 1));
resultMatrix.get(0).add(getSymbolAt(THIRD_REEL, index3 - 1));
resultMatrix.get(1).add(getSymbolAt(THIRD_REEL, index3));
resultMatrix.get(2).add(getSymbolAt(THIRD_REEL, index3 + 1));
spinResult.setResultMatrix(resultMatrix);
}
private void initReels() {
List<Symbol> reelStrip = new ArrayList<>(REEL_LENGTH);
for (int i = 0; i < CHERRY_COUNT; i++) {
Symbol CHERRY = new Symbol("cherry", new BigDecimal(5));
reelStrip.add(CHERRY);
}
for (int i = 0; i < BAR_COUNT; i++) {
Symbol BAR = new Symbol("bar", new BigDecimal(10));
reelStrip.add(BAR);
}
for (int i = 0; i < SEVEN_COUNT; i++) {
Symbol SEVEN = new Symbol("seven", new BigDecimal(85));
reelStrip.add(SEVEN);
}
FIRST_REEL.addAll(reelStrip);
SECOND_REEL.addAll(reelStrip);
THIRD_REEL.addAll(reelStrip);
Collections.shuffle(FIRST_REEL);
Collections.shuffle(SECOND_REEL);
Collections.shuffle(THIRD_REEL);
}
private Symbol getSymbolAt(List<Symbol> reel, int index) {
int effectiveIndex = index % REEL_LENGTH;
if (effectiveIndex < 0) {
effectiveIndex += REEL_LENGTH;
}
return reel.get(effectiveIndex);
}
}

View 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;
}

View 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;
}
}

View file

@ -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);
}
}