diff --git a/backend/pom.xml b/backend/pom.xml index f5c137d..7efc523 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -17,6 +17,7 @@ 17 1.5.3.Final + 1.18.26 @@ -44,6 +45,7 @@ org.projectlombok lombok + ${org.projectlombok.version} true @@ -56,10 +58,16 @@ commons-lang3 3.12.0 + + com.google.guava + guava + 31.1-jre + provided + com.h2database h2 - 2.1.214 + 2.2.220 runtime @@ -67,6 +75,11 @@ mapstruct ${org.mapstruct.version} + + org.yaml + snakeyaml + 2.0 + org.springframework.boot spring-boot-starter-test @@ -108,6 +121,16 @@ mapstruct-processor ${org.mapstruct.version} + + org.projectlombok + lombok + ${org.projectlombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/controller/GameController.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/controller/GameController.java index e094020..31dcaac 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/controller/GameController.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/controller/GameController.java @@ -1,34 +1,23 @@ package tw.waterballsa.gaas.unoflip.controller; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import tw.waterballsa.gaas.unoflip.presenter.GameJoinPresenter; -import tw.waterballsa.gaas.unoflip.service.SseService; import tw.waterballsa.gaas.unoflip.usecase.GameJoinUseCase; -import tw.waterballsa.gaas.unoflip.vo.GameJoinResult; import tw.waterballsa.gaas.unoflip.vo.JoinRequest; -import tw.waterballsa.gaas.unoflip.vo.JoinResult; -import tw.waterballsa.gaas.unoflip.vo.Response; +import tw.waterballsa.gaas.unoflip.response.JoinResult; +import tw.waterballsa.gaas.unoflip.response.Response; @RestController +@RequiredArgsConstructor public class GameController { private final GameJoinUseCase gameJoinUseCase; - private final GameJoinPresenter gameJoinPresenter; - private final SseService sseService; - - public GameController(GameJoinUseCase gameJoinUseCase, GameJoinPresenter gameJoinPresenter, SseService sseService) { - this.gameJoinUseCase = gameJoinUseCase; - this.gameJoinPresenter = gameJoinPresenter; - this.sseService = sseService; - } @PostMapping("join/{playerId}") public Response join(@PathVariable String playerId, @RequestBody JoinRequest joinRequest) { - GameJoinResult gameJoinResult = gameJoinUseCase.join(playerId, joinRequest.playerName()); - sseService.sendMessage(gameJoinPresenter.broadcastEvent(playerId, gameJoinResult)); - return gameJoinPresenter.response(playerId, gameJoinResult); + return gameJoinUseCase.join(playerId, joinRequest.playerName()); } } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/DealResult.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/DealResult.java new file mode 100644 index 0000000..b1f9264 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/DealResult.java @@ -0,0 +1,8 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; + +import java.util.List; + +record DealResult(List playersHandCard, Card discardCard, List drawPileCards) { +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Dealer.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Dealer.java new file mode 100644 index 0000000..3fe435c --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Dealer.java @@ -0,0 +1,36 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; + +class Dealer { + public static DealResult deal() { + List cardNumbers = Card.getAllIds(); + Collections.shuffle(cardNumbers); + + List playersHandCards = new ArrayList<>(); + + playersHandCards.add(createHandCard(0, 7, cardNumbers)); + playersHandCards.add(createHandCard(7, 14, cardNumbers)); + playersHandCards.add(createHandCard(14, 21, cardNumbers)); + playersHandCards.add(createHandCard(21, 28, cardNumbers)); + + Card discardCard = Card.getLightInstance(cardNumbers.get(28)); + + List drawPileCards = getRandomCardList(29, 112, cardNumbers); + + return new DealResult(playersHandCards, discardCard, drawPileCards); + } + + private static HandCard createHandCard(int startInclusive, int endExclusive, List cardNumbers) { + return new HandCard(getRandomCardList(startInclusive, endExclusive, cardNumbers)); + } + + private static List getRandomCardList(int startInclusive, int endExclusive, List cardNumbers) { + return IntStream.range(startInclusive, endExclusive).mapToObj(i -> Card.getLightInstance(cardNumbers.get(i))).toList(); + } +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/HandCard.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/HandCard.java new file mode 100644 index 0000000..713e149 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/HandCard.java @@ -0,0 +1,26 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; + +import java.util.Collections; +import java.util.List; + +public class HandCard { + private final List cards; + + public HandCard(List cards) { + this.cards = cards; + } + + public List toCardIds() { + return cards.stream().map(Card::getId).toList(); + } + + int size() { + return cards.size(); + } + + List getCards() { + return Collections.unmodifiableList(cards); + } +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Player.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Player.java new file mode 100644 index 0000000..8f41852 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Player.java @@ -0,0 +1,24 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public class Player { + + private final PlayerInfo playerInfo; + @Setter + private HandCard handCard; + + public Player(PlayerInfo playerInfo) { + this.playerInfo = playerInfo; + } + + public int getPosition() { + return playerInfo.position(); + } + + public String getId() { + return playerInfo.id(); + } +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/PlayerInfo.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/PlayerInfo.java new file mode 100644 index 0000000..5d24be5 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/PlayerInfo.java @@ -0,0 +1,4 @@ +package tw.waterballsa.gaas.unoflip.domain; + +public record PlayerInfo(String id, String name, Integer position) { +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Players.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Players.java new file mode 100644 index 0000000..1555178 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/Players.java @@ -0,0 +1,49 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class Players { + private final Map playerMap = new HashMap<>(); + + public boolean exists(String playerId) { + return playerMap.get(playerId) != null; + } + + public void add(PlayerInfo playerInfo) { + playerMap.put(playerInfo.id(), new Player(playerInfo)); + } + + public List toInfoList() { + return playerMap.values().stream().map(Player::getPlayerInfo).toList(); + } + + public HandCard getPlayerHandCard(String playerId) { + return Optional.ofNullable(playerMap.get(playerId)) + .map(Player::getHandCard) + .orElseThrow(() -> new IllegalArgumentException("player %s not exists".formatted(playerId))); + } + + public void setHandCard(String playerId, HandCard handCard) { + Player player = Optional.ofNullable(playerMap.get(playerId)).orElseThrow(() -> new IllegalArgumentException("player %s not exists".formatted(playerId))); + player.setHandCard(handCard); + } + + public int size() { + return playerMap.size(); + } + + public List getIds() { + return playerMap.values().stream().map(Player::getId).toList(); + } + + public String getPlayerId(int position) { + return playerMap.values().stream() + .filter(player -> position == player.getPosition()) + .findFirst() + .map(Player::getId) + .orElseThrow(() -> new IllegalArgumentException("position %d not exists".formatted(position))); + } +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGame.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGame.java index c78f24d..abea1a3 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGame.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGame.java @@ -1,6 +1,10 @@ package tw.waterballsa.gaas.unoflip.domain; -import tw.waterballsa.gaas.unoflip.vo.PlayerInfo; +import lombok.Getter; +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; +import tw.waterballsa.gaas.unoflip.domain.eumns.Direction; +import tw.waterballsa.gaas.unoflip.domain.eumns.GameMode; +import tw.waterballsa.gaas.unoflip.domain.eumns.GameStatus; import java.util.ArrayList; import java.util.Collections; @@ -8,42 +12,85 @@ public class UnoFlipGame { private static final int MAX_PLAYER_NUMBER = 4; + private final List drawPileList = new ArrayList<>(); + private final List discardPileList = new ArrayList<>(); + + @Getter + private final Players players = new Players(); + @Getter private final int tableId; - private final List playerInfoList = new ArrayList<>(); + @Getter + private String actionPlayerId; + @Getter + private GameStatus status; + @Getter + private Direction direction; + @Getter + private GameMode mode; public UnoFlipGame(int tableId) { this.tableId = tableId; + this.status = GameStatus.WAITING; + this.direction = Direction.RIGHT; + this.mode = GameMode.LIGHT; } - public int getTableId() { - return tableId; + public boolean isFull() { + return players.size() >= MAX_PLAYER_NUMBER; } - public void join(String playerId, String playerName) { - if (isPlayerAlreadyInGame(playerId)) { - throw new RuntimeException("player already in game"); - } + public List getPlayerInfoList() { + return players.toInfoList(); + } - playerInfoList.add(new PlayerInfo(playerId, playerName, getAvailablePosition())); + public List getDrawPile() { + return Collections.unmodifiableList(drawPileList); } - public List getPlayerInfoList() { - return Collections.unmodifiableList(playerInfoList); + public List getDiscardPile() { + return Collections.unmodifiableList(discardPileList); } - public boolean isFull() { - return playerInfoList.size() >= MAX_PLAYER_NUMBER; + public void join(String playerId, String playerName) { + if (isPlayerAlreadyInGame(playerId)) { + throw new RuntimeException("player already in game"); + } + + players.add(new PlayerInfo(playerId, playerName, getAvailablePosition())); } private boolean isPlayerAlreadyInGame(String playerId) { - return playerInfoList.stream().anyMatch(playerInfo -> playerId.equals(playerInfo.playerId())); + return players.exists(playerId); } private int getAvailablePosition() { if (isFull()) { - throw new RuntimeException("game is full"); + throw new IllegalStateException("game is full"); } - return playerInfoList.size() + 1; + return players.size() + 1; + } + + public void start() { + status = GameStatus.STARTED; + actionPlayerId = players.getPlayerId(getInitPosition()); + + DealResult dealResult = Dealer.deal(); + + setPlayersHandCard(dealResult); + discardPileList.add(dealResult.discardCard()); + drawPileList.addAll(dealResult.drawPileCards()); } + + private int getInitPosition() { + return (int) (Math.random() * MAX_PLAYER_NUMBER) + 1; + } + + private void setPlayersHandCard(DealResult dealResult) { + int handCardListIdx = 0; + for (String playerId : players.getIds()) { + players.setHandCard(playerId, dealResult.playersHandCard().get(handCardListIdx++)); + } + } + } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/Card.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/Card.java new file mode 100644 index 0000000..ac76fc5 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/Card.java @@ -0,0 +1,144 @@ +package tw.waterballsa.gaas.unoflip.domain.eumns; + +import lombok.Getter; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Getter +public enum Card { + CARD_1(1, LightCard.RED_4, DarkCard.ORANGE_DRAW_5), + CARD_2(2, LightCard.YELLOW_DRAW_1, DarkCard.PINK_4), + CARD_3(3, LightCard.RED_FLIP, DarkCard.ORANGE_7), + CARD_4(4, LightCard.GREEN_8, DarkCard.TEAL_SKIP_EVERYONE), + CARD_5(5, LightCard.YELLOW_SKIP, DarkCard.ORANGE_5), + CARD_6(6, LightCard.YELLOW_2, DarkCard.PINK_6), + CARD_7(7, LightCard.RED_REVERSE, DarkCard.TEAL_4), + CARD_8(8, LightCard.YELLOW_7, DarkCard.TEAL_1), + CARD_9(9, LightCard.GREEN_5, DarkCard.WILD), + CARD_10(10, LightCard.BLUE_DRAW_1, DarkCard.PINK_8), + CARD_11(11, LightCard.BLUE_4, DarkCard.ORANGE_FLIP), + CARD_12(12, LightCard.RED_6, DarkCard.WILD), + CARD_13(13, LightCard.RED_5, DarkCard.PURPLE_4), + CARD_14(14, LightCard.BLUE_SKIP, DarkCard.TEAL_7), + CARD_15(15, LightCard.GREEN_SKIP, DarkCard.PINK_DRAW_5), + CARD_16(16, LightCard.RED_DRAW_1, DarkCard.PURPLE_1), + CARD_17(17, LightCard.BLUE_5, DarkCard.ORANGE_DRAW_5), + CARD_18(18, LightCard.BLUE_DRAW_1, DarkCard.ORANGE_5), + CARD_19(19, LightCard.BLUE_9, DarkCard.PURPLE_5), + CARD_20(20, LightCard.RED_9, DarkCard.PURPLE_9), + CARD_21(21, LightCard.RED_7, DarkCard.PINK_9), + CARD_22(22, LightCard.YELLOW_REVERSE, DarkCard.ORANGE_8), + CARD_23(23, LightCard.GREEN_4, DarkCard.PINK_8), + CARD_24(24, LightCard.WILD, DarkCard.ORANGE_7), + CARD_25(25, LightCard.RED_1, DarkCard.PURPLE_3), + CARD_26(26, LightCard.BLUE_2, DarkCard.ORANGE_8), + CARD_27(27, LightCard.YELLOW_3, DarkCard.ORANGE_3), + CARD_28(28, LightCard.BLUE_2, DarkCard.WILD_DRAW_COLOR), + CARD_29(29, LightCard.GREEN_2, DarkCard.PINK_6), + CARD_30(30, LightCard.GREEN_REVERSE, DarkCard.PURPLE_REVERSE), + CARD_31(31, LightCard.YELLOW_9, DarkCard.PINK_1), + CARD_32(32, LightCard.BLUE_SKIP, DarkCard.PURPLE_FLIP), + CARD_33(33, LightCard.GREEN_1, DarkCard.PINK_7), + CARD_34(34, LightCard.RED_9, DarkCard.ORANGE_9), + CARD_35(35, LightCard.BLUE_8, DarkCard.PINK_SKIP_EVERYONE), + CARD_36(36, LightCard.GREEN_SKIP, DarkCard.TEAL_2), + CARD_37(37, LightCard.WILD, DarkCard.PURPLE_1), + CARD_38(38, LightCard.BLUE_3, DarkCard.PINK_1), + CARD_39(39, LightCard.RED_FLIP, DarkCard.PINK_REVERSE), + CARD_40(40, LightCard.YELLOW_4, DarkCard.TEAL_8), + CARD_41(41, LightCard.YELLOW_9, DarkCard.PURPLE_SKIP_EVERYONE), + CARD_42(42, LightCard.YELLOW_8, DarkCard.TEAL_6), + CARD_43(43, LightCard.GREEN_7, DarkCard.ORANGE_6), + CARD_44(44, LightCard.YELLOW_REVERSE, DarkCard.PURPLE_7), + CARD_45(45, LightCard.GREEN_1, DarkCard.TEAL_7), + CARD_46(46, LightCard.RED_8, DarkCard.PINK_3), + CARD_47(47, LightCard.RED_SKIP, DarkCard.TEAL_REVERSE), + CARD_48(48, LightCard.WILD_DRAW_2, DarkCard.TEAL_8), + CARD_49(49, LightCard.YELLOW_6, DarkCard.PINK_DRAW_5), + CARD_50(50, LightCard.GREEN_8, DarkCard.PINK_REVERSE), + CARD_51(51, LightCard.YELLOW_SKIP, DarkCard.PURPLE_SKIP_EVERYONE), + CARD_52(52, LightCard.BLUE_4, DarkCard.PINK_REVERSE), + CARD_53(53, LightCard.BLUE_9, DarkCard.PURPLE_6), + CARD_54(54, LightCard.YELLOW_FLIP, DarkCard.PURPLE_5), + CARD_55(55, LightCard.RED_5, DarkCard.PINK_FLIP), + CARD_56(56, LightCard.GREEN_4, DarkCard.PURPLE_3), + CARD_57(57, LightCard.BLUE_3, DarkCard.TEAL_REVERSE), + CARD_58(58, LightCard.RED_1, DarkCard.PINK_2), + CARD_59(59, LightCard.RED_2, DarkCard.PURPLE_2), + CARD_60(60, LightCard.YELLOW_3, DarkCard.TEAL_FLIP), + CARD_61(61, LightCard.BLUE_8, DarkCard.TEAL_FLIP), + CARD_62(62, LightCard.BLUE_FLIP, DarkCard.ORANGE_9), + CARD_63(63, LightCard.YELLOW_1, DarkCard.PURPLE_6), + CARD_64(64, LightCard.WILD_DRAW_2, DarkCard.ORANGE_FLIP), + CARD_65(65, LightCard.GREEN_9, DarkCard.ORANGE_REVERSE), + CARD_66(66, LightCard.GREEN_FLIP, DarkCard.ORANGE_1), + CARD_67(67, LightCard.GREEN_DRAW_1, DarkCard.TEAL_DRAW_5), + CARD_68(68, LightCard.YELLOW_5, DarkCard.PURPLE_2), + CARD_69(69, LightCard.GREEN_2, DarkCard.ORANGE_2), + CARD_70(70, LightCard.GREEN_7, DarkCard.WILD), + CARD_71(71, LightCard.WILD_DRAW_2, DarkCard.TEAL_5), + CARD_72(72, LightCard.BLUE_7, DarkCard.PURPLE_SKIP_EVERYONE), + CARD_73(73, LightCard.RED_6, DarkCard.PURPLE_8), + CARD_74(74, LightCard.BLUE_7, DarkCard.PINK_3), + CARD_75(75, LightCard.BLUE_5, DarkCard.ORANGE_3), + CARD_76(76, LightCard.YELLOW_FLIP, DarkCard.TEAL_5), + CARD_77(77, LightCard.GREEN_9, DarkCard.TEAL_1), + CARD_78(78, LightCard.GREEN_REVERSE, DarkCard.TEAL_4), + CARD_79(79, LightCard.RED_8, DarkCard.ORANGE_SKIP_EVERYONE), + CARD_80(80, LightCard.WILD, DarkCard.PURPLE_7), + CARD_81(81, LightCard.GREEN_FLIP, DarkCard.TEAL_9), + CARD_82(82, LightCard.WILD, DarkCard.ORANGE_4), + CARD_83(83, LightCard.RED_3, DarkCard.PINK_4), + CARD_84(84, LightCard.YELLOW_6, DarkCard.WILD), + CARD_85(85, LightCard.YELLOW_4, DarkCard.TEAL_6), + CARD_86(86, LightCard.WILD_DRAW_2, DarkCard.WILD_DRAW_COLOR), + CARD_87(87, LightCard.BLUE_FLIP, DarkCard.PURPLE_8), + CARD_88(88, LightCard.RED_2, DarkCard.ORANGE_1), + CARD_89(89, LightCard.BLUE_6, DarkCard.PINK_5), + CARD_90(90, LightCard.BLUE_REVERSE, DarkCard.ORANGE_2), + CARD_91(91, LightCard.GREEN_3, DarkCard.PINK_9), + CARD_92(92, LightCard.BLUE_1, DarkCard.WILD_DRAW_COLOR), + CARD_93(93, LightCard.YELLOW_DRAW_1, DarkCard.PURPLE_9), + CARD_94(94, LightCard.GREEN_6, DarkCard.ORANGE_REVERSE), + CARD_95(95, LightCard.BLUE_REVERSE, DarkCard.TEAL_DRAW_5), + CARD_96(96, LightCard.GREEN_DRAW_1, DarkCard.PURPLE_DRAW_5), + CARD_97(97, LightCard.BLUE_6, DarkCard.PURPLE_4), + CARD_98(98, LightCard.BLUE_1, DarkCard.TEAL_9), + CARD_99(99, LightCard.YELLOW_7, DarkCard.PURPLE_DRAW_5), + CARD_100(100, LightCard.YELLOW_8, DarkCard.PINK_2), + CARD_101(101, LightCard.YELLOW_1, DarkCard.PURPLE_FLIP), + CARD_102(102, LightCard.YELLOW_2, DarkCard.ORANGE_4), + CARD_103(103, LightCard.RED_DRAW_1, DarkCard.TEAL_2), + CARD_104(104, LightCard.GREEN_3, DarkCard.WILD_DRAW_COLOR), + CARD_105(105, LightCard.YELLOW_5, DarkCard.ORANGE_6), + CARD_106(106, LightCard.GREEN_5, DarkCard.PINK_FLIP), + CARD_107(107, LightCard.RED_3, DarkCard.TEAL_3), + CARD_108(108, LightCard.RED_SKIP, DarkCard.ORANGE_SKIP_EVERYONE), + CARD_109(109, LightCard.RED_REVERSE, DarkCard.TEAL_3), + CARD_110(110, LightCard.GREEN_6, DarkCard.PINK_7), + CARD_111(111, LightCard.RED_4, DarkCard.TEAL_SKIP_EVERYONE), + CARD_112(112, LightCard.RED_7, DarkCard.PINK_5); + + private final int id; + private final LightCard lightCard; + private final DarkCard darkCard; + + private static final Map enumsByValue = Arrays.stream(values()).collect(Collectors.toMap(Card::getId, Function.identity())); + + Card(int id, LightCard lightCard, DarkCard darkCard) { + this.id = id; + this.lightCard = lightCard; + this.darkCard = darkCard; + } + + public static Card getLightInstance(int id) { + return Optional.ofNullable(enumsByValue.get(id)).orElseThrow(() -> new IllegalArgumentException("unsupported id %d".formatted(id))); + } + + public static List getAllIds() { + return new ArrayList<>(enumsByValue.keySet()); + } + +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/DarkCard.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/DarkCard.java new file mode 100644 index 0000000..a7f395f --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/DarkCard.java @@ -0,0 +1,63 @@ +package tw.waterballsa.gaas.unoflip.domain.eumns; + +public enum DarkCard { + ORANGE_1, + ORANGE_2, + ORANGE_3, + ORANGE_4, + ORANGE_5, + ORANGE_6, + ORANGE_7, + ORANGE_8, + ORANGE_9, + ORANGE_DRAW_5, + ORANGE_SKIP_EVERYONE, + ORANGE_REVERSE, + ORANGE_FLIP, + + PINK_1, + PINK_2, + PINK_3, + PINK_4, + PINK_5, + PINK_6, + PINK_7, + PINK_8, + PINK_9, + PINK_DRAW_5, + PINK_SKIP_EVERYONE, + PINK_REVERSE, + PINK_FLIP, + + TEAL_1, + TEAL_2, + TEAL_3, + TEAL_4, + TEAL_5, + TEAL_6, + TEAL_7, + TEAL_8, + TEAL_9, + TEAL_DRAW_5, + TEAL_SKIP_EVERYONE, + TEAL_REVERSE, + TEAL_FLIP, + + PURPLE_1, + PURPLE_2, + PURPLE_3, + PURPLE_4, + PURPLE_5, + PURPLE_6, + PURPLE_7, + PURPLE_8, + PURPLE_9, + PURPLE_DRAW_5, + PURPLE_SKIP_EVERYONE, + PURPLE_REVERSE, + PURPLE_FLIP, + + WILD, + WILD_DRAW_COLOR + +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/Direction.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/Direction.java new file mode 100644 index 0000000..465875a --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/Direction.java @@ -0,0 +1,16 @@ +package tw.waterballsa.gaas.unoflip.domain.eumns; + +import lombok.Getter; + +@Getter +public enum Direction { + LEFT(1), + RIGHT(2); + + private final int code; + + Direction(int code) { + this.code = code; + } + +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/GameMode.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/GameMode.java new file mode 100644 index 0000000..540926d --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/GameMode.java @@ -0,0 +1,5 @@ +package tw.waterballsa.gaas.unoflip.domain.eumns; + +public enum GameMode { + LIGHT, DARK +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/GameStatus.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/GameStatus.java new file mode 100644 index 0000000..7e9166f --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/GameStatus.java @@ -0,0 +1,5 @@ +package tw.waterballsa.gaas.unoflip.domain.eumns; + +public enum GameStatus { + WAITING, STARTED +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/LightCard.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/LightCard.java new file mode 100644 index 0000000..d06b711 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/domain/eumns/LightCard.java @@ -0,0 +1,62 @@ +package tw.waterballsa.gaas.unoflip.domain.eumns; + +public enum LightCard { + RED_1, + RED_2, + RED_3, + RED_4, + RED_5, + RED_6, + RED_7, + RED_8, + RED_9, + RED_DRAW_1, + RED_SKIP, + RED_REVERSE, + RED_FLIP, + + YELLOW_1, + YELLOW_2, + YELLOW_3, + YELLOW_4, + YELLOW_5, + YELLOW_6, + YELLOW_7, + YELLOW_8, + YELLOW_9, + YELLOW_DRAW_1, + YELLOW_SKIP, + YELLOW_REVERSE, + YELLOW_FLIP, + + BLUE_1, + BLUE_2, + BLUE_3, + BLUE_4, + BLUE_5, + BLUE_6, + BLUE_7, + BLUE_8, + BLUE_9, + BLUE_DRAW_1, + BLUE_SKIP, + BLUE_REVERSE, + BLUE_FLIP, + + GREEN_1, + GREEN_2, + GREEN_3, + GREEN_4, + GREEN_5, + GREEN_6, + GREEN_7, + GREEN_8, + GREEN_9, + GREEN_DRAW_1, + GREEN_SKIP, + GREEN_REVERSE, + GREEN_FLIP, + + WILD, + WILD_DRAW_2 +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/BroadcastEvent.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/BroadcastEvent.java similarity index 69% rename from backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/BroadcastEvent.java rename to backend/src/main/java/tw/waterballsa/gaas/unoflip/event/BroadcastEvent.java index 9dccd22..432d6df 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/BroadcastEvent.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/BroadcastEvent.java @@ -1,4 +1,4 @@ -package tw.waterballsa.gaas.unoflip.vo; +package tw.waterballsa.gaas.unoflip.event; import java.util.List; diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/EventType.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/EventType.java new file mode 100644 index 0000000..b33df65 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/EventType.java @@ -0,0 +1,22 @@ +package tw.waterballsa.gaas.unoflip.event; + +import lombok.Getter; + +@Getter +public enum EventType { + JOIN(1), + STARTED(2), + HAND_CARD(3), + DRAW(4), + PLAY(5), + COLOR(6), + UNO(7), + CATCH(8), + ENDED(9); + + private final int code; + + EventType(int code) { + this.code = code; + } +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/JoinBroadcastEvent.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/JoinBroadcastEvent.java new file mode 100644 index 0000000..65d91e2 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/JoinBroadcastEvent.java @@ -0,0 +1,4 @@ +package tw.waterballsa.gaas.unoflip.event; + +public record JoinBroadcastEvent(int eventType, String playerId, String playerName, int position) { +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/PersonalEvent.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/PersonalEvent.java new file mode 100644 index 0000000..b7d2e67 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/PersonalEvent.java @@ -0,0 +1,4 @@ +package tw.waterballsa.gaas.unoflip.event; + +public record PersonalEvent (String playerId, Object eventBody){ +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/StartedBroadcastEvent.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/StartedBroadcastEvent.java new file mode 100644 index 0000000..f7dfe54 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/StartedBroadcastEvent.java @@ -0,0 +1,7 @@ +package tw.waterballsa.gaas.unoflip.event; + +import java.util.List; + +public record StartedBroadcastEvent(int eventType, String initPlayerId, int direction, int initDiscardCard, List drawPile) { + +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/StartedPersonalEvent.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/StartedPersonalEvent.java new file mode 100644 index 0000000..2336692 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/event/StartedPersonalEvent.java @@ -0,0 +1,7 @@ +package tw.waterballsa.gaas.unoflip.event; + +import java.util.List; + +public record StartedPersonalEvent(int eventType, List handCards) { + +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenter.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenter.java index 35516bc..0e35eb0 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenter.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenter.java @@ -1,6 +1,12 @@ package tw.waterballsa.gaas.unoflip.presenter; import org.springframework.stereotype.Service; +import tw.waterballsa.gaas.unoflip.domain.PlayerInfo; +import tw.waterballsa.gaas.unoflip.event.BroadcastEvent; +import tw.waterballsa.gaas.unoflip.event.EventType; +import tw.waterballsa.gaas.unoflip.event.JoinBroadcastEvent; +import tw.waterballsa.gaas.unoflip.response.JoinResult; +import tw.waterballsa.gaas.unoflip.response.Response; import tw.waterballsa.gaas.unoflip.vo.*; import java.util.List; @@ -22,24 +28,24 @@ public BroadcastEvent broadcastEvent(String targetPlayerId, GameJoinResult gameJ List playerInfoList = gameJoinResult.playerInfoList(); PlayerInfo playerInfo = getTargetPlayerInfo(playerInfoList, targetPlayerId); - JoinEvent joinEvent = new JoinEvent(targetPlayerId, playerInfo.playerName(), playerInfo.position()); + JoinBroadcastEvent joinBroadcastEvent = new JoinBroadcastEvent(EventType.JOIN.getCode(), targetPlayerId, playerInfo.name(), playerInfo.position()); - return new BroadcastEvent(getPlayerIds(playerInfoList), joinEvent); + return new BroadcastEvent(getPlayerIds(playerInfoList), joinBroadcastEvent); } private List getOtherPlayerInfoList(String targetPlayerId, List playerInfoList) { return playerInfoList.stream() - .filter(playerInfo -> !targetPlayerId.equals(playerInfo.playerId())) + .filter(playerInfo -> !targetPlayerId.equals(playerInfo.id())) .collect(Collectors.toList()); } private PlayerInfo getTargetPlayerInfo(List gameJoinResult, String targetPlayerId) { return gameJoinResult.stream() - .filter(p -> targetPlayerId.equals(p.playerId())).findFirst() - .orElseThrow(() -> new RuntimeException("should not happened, player %s not in game join result".formatted(targetPlayerId))); + .filter(p -> targetPlayerId.equals(p.id())).findFirst() + .orElseThrow(() -> new IllegalArgumentException("should not happened, player %s not in game join result".formatted(targetPlayerId))); } private List getPlayerIds(List playerInfoList) { - return playerInfoList.stream().map(PlayerInfo::playerId).collect(Collectors.toList()); + return playerInfoList.stream().map(PlayerInfo::id).collect(Collectors.toList()); } } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameStartPresenter.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameStartPresenter.java new file mode 100644 index 0000000..89fef69 --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/GameStartPresenter.java @@ -0,0 +1,35 @@ +package tw.waterballsa.gaas.unoflip.presenter; + +import org.springframework.stereotype.Service; +import tw.waterballsa.gaas.unoflip.domain.Players; +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; +import tw.waterballsa.gaas.unoflip.event.*; +import tw.waterballsa.gaas.unoflip.vo.*; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class GameStartPresenter { + public BroadcastEvent broadcastEvent(GameStartResult gameStartResult) { + + StartedBroadcastEvent startedBroadcastEvent = new StartedBroadcastEvent( + EventType.STARTED.getCode(), + gameStartResult.currentPlayerId(), + gameStartResult.direction().getCode(), + gameStartResult.discardPileCards().get(0).getId(), + gameStartResult.drawPileCards().stream().map(Card::getId).toList()); + + return new BroadcastEvent(gameStartResult.players().getIds(), startedBroadcastEvent); + } + + public List personalEvents(GameStartResult gameStartResult) { + List personalEventList = new ArrayList<>(); + Players players = gameStartResult.players(); + for (String id : players.getIds()) { + StartedPersonalEvent event = new StartedPersonalEvent(EventType.HAND_CARD.getCode(), players.getPlayerHandCard(id).toCardIds()); + personalEventList.add(new PersonalEvent(id, event)); + } + return personalEventList; + } +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/StatusCode.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/StatusCode.java index 6729ed3..bfdd12d 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/StatusCode.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/presenter/StatusCode.java @@ -1,5 +1,8 @@ package tw.waterballsa.gaas.unoflip.presenter; +import lombok.Getter; + +@Getter public enum StatusCode { OK(0); @@ -9,7 +12,4 @@ public enum StatusCode { this.code = code; } - public int getCode() { - return code; - } } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/JoinResult.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/response/JoinResult.java similarity index 54% rename from backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/JoinResult.java rename to backend/src/main/java/tw/waterballsa/gaas/unoflip/response/JoinResult.java index 860ace0..0afa754 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/JoinResult.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/response/JoinResult.java @@ -1,4 +1,6 @@ -package tw.waterballsa.gaas.unoflip.vo; +package tw.waterballsa.gaas.unoflip.response; + +import tw.waterballsa.gaas.unoflip.domain.PlayerInfo; import java.util.List; diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/Response.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/response/Response.java similarity index 59% rename from backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/Response.java rename to backend/src/main/java/tw/waterballsa/gaas/unoflip/response/Response.java index 92dacf9..0c2a40e 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/Response.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/response/Response.java @@ -1,4 +1,4 @@ -package tw.waterballsa.gaas.unoflip.vo; +package tw.waterballsa.gaas.unoflip.response; public record Response(int code, String message, T payload) { } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/service/SseService.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/service/SseService.java index 3b168e6..a16a88a 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/service/SseService.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/service/SseService.java @@ -2,11 +2,13 @@ import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import tw.waterballsa.gaas.unoflip.vo.BroadcastEvent; +import tw.waterballsa.gaas.unoflip.event.BroadcastEvent; +import tw.waterballsa.gaas.unoflip.event.PersonalEvent; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Optional; @Service public class SseService { @@ -26,17 +28,27 @@ public void sendMessage(BroadcastEvent broadcastEvent) { if (null == broadcastEvent.playerIds() || broadcastEvent.playerIds().isEmpty()) { return; } + for (String playerId : broadcastEvent.playerIds()) { - SseEmitter emitter = emitterMap.get(playerId); - if (emitter == null) { - return; - } - System.out.printf("[%s] send event to %s, data: %s%n", playerId, playerId, broadcastEvent.eventBody()); + Optional.ofNullable(emitterMap.get(playerId)).ifPresent(emitter -> { + System.out.printf("[broadcast] send eventBody to %s, data: %s%n", playerId, broadcastEvent.eventBody()); + try { + emitter.send(broadcastEvent.eventBody()); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + } + + public void sendMessage(PersonalEvent personalEvent) { + Optional.ofNullable(emitterMap.get(personalEvent.playerId())).ifPresent(emitter -> { + System.out.printf("[personal] send eventBody to %s, data: %s%n", personalEvent.playerId(), personalEvent.eventBody()); try { - emitter.send(broadcastEvent.eventBody()); + emitter.send(personalEvent.eventBody()); } catch (IOException e) { e.printStackTrace(); } - } + }); } } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCase.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCase.java index 5ecee8a..a6db288 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCase.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCase.java @@ -1,23 +1,59 @@ package tw.waterballsa.gaas.unoflip.usecase; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import tw.waterballsa.gaas.unoflip.domain.UnoFlipGame; +import tw.waterballsa.gaas.unoflip.presenter.GameJoinPresenter; +import tw.waterballsa.gaas.unoflip.presenter.GameStartPresenter; import tw.waterballsa.gaas.unoflip.repository.GameRepo; +import tw.waterballsa.gaas.unoflip.service.SseService; import tw.waterballsa.gaas.unoflip.vo.GameJoinResult; +import tw.waterballsa.gaas.unoflip.vo.GameStartResult; +import tw.waterballsa.gaas.unoflip.response.JoinResult; +import tw.waterballsa.gaas.unoflip.response.Response; + +import java.util.Optional; @Service +@RequiredArgsConstructor public class GameJoinUseCase { private final GameRepo gameRepo; + private final GameJoinPresenter gameJoinPresenter; + private final GameStartPresenter gameStartPresenter; + private final SseService sseService; - public GameJoinUseCase(GameRepo gameRepo) { - this.gameRepo = gameRepo; - } - - public GameJoinResult join(String playerId, String playerName) { + public synchronized Response join(String playerId, String playerName) { UnoFlipGame availableGame = gameRepo.getAvailable().orElseGet(() -> new UnoFlipGame(gameRepo.generateTableId())); availableGame.join(playerId, playerName); + + Optional gameStartResult = tryStartGame(availableGame); + gameRepo.save(availableGame); - return new GameJoinResult(availableGame.getTableId(), availableGame.getPlayerInfoList()); + GameJoinResult gameJoinResult = new GameJoinResult(availableGame.getTableId(), availableGame.getPlayerInfoList()); + sseService.sendMessage(gameJoinPresenter.broadcastEvent(playerId, gameJoinResult)); + gameStartResult.ifPresent(this::sendStartedEvents); + + return gameJoinPresenter.response(playerId, gameJoinResult); + } + + private Optional tryStartGame(UnoFlipGame availableGame) { + if (!availableGame.isFull()) { + return Optional.empty(); + } + + availableGame.start(); + + return Optional.of(new GameStartResult( + availableGame.getActionPlayerId(), + availableGame.getDirection(), + availableGame.getPlayers(), + availableGame.getDiscardPile(), + availableGame.getDrawPile())); + } + + private void sendStartedEvents(GameStartResult gameStartResult) { + sseService.sendMessage(gameStartPresenter.broadcastEvent(gameStartResult)); + gameStartPresenter.personalEvents(gameStartResult).forEach(sseService::sendMessage); } } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameJoinResult.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameJoinResult.java index f7f6613..74ec2ff 100644 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameJoinResult.java +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameJoinResult.java @@ -1,6 +1,8 @@ package tw.waterballsa.gaas.unoflip.vo; +import tw.waterballsa.gaas.unoflip.domain.PlayerInfo; + import java.util.List; -public record GameJoinResult (int tableId, List playerInfoList){ +public record GameJoinResult(int tableId, List playerInfoList) { } diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameStartResult.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameStartResult.java new file mode 100644 index 0000000..189c56a --- /dev/null +++ b/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/GameStartResult.java @@ -0,0 +1,11 @@ +package tw.waterballsa.gaas.unoflip.vo; + +import tw.waterballsa.gaas.unoflip.domain.Players; +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; +import tw.waterballsa.gaas.unoflip.domain.eumns.Direction; + +import java.util.List; + +public record GameStartResult(String currentPlayerId, Direction direction, Players players, + List discardPileCards, List drawPileCards) { +} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/JoinEvent.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/JoinEvent.java deleted file mode 100644 index ff4063f..0000000 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/JoinEvent.java +++ /dev/null @@ -1,4 +0,0 @@ -package tw.waterballsa.gaas.unoflip.vo; - -public record JoinEvent (String playerId, String playerName, int position){ -} diff --git a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/PlayerInfo.java b/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/PlayerInfo.java deleted file mode 100644 index 23da47b..0000000 --- a/backend/src/main/java/tw/waterballsa/gaas/unoflip/vo/PlayerInfo.java +++ /dev/null @@ -1,4 +0,0 @@ -package tw.waterballsa.gaas.unoflip.vo; - -public record PlayerInfo(String playerId, String playerName, Integer position) { -} diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/DealerTest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/DealerTest.java new file mode 100644 index 0000000..4eab61f --- /dev/null +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/DealerTest.java @@ -0,0 +1,66 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import org.junit.jupiter.api.Test; +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class DealerTest { + + private DealResult dealResult; + + @Test + void deal() { + when_deal(); + + should_provide_different_hand_cards_for_four_players(); + should_has_discard_card(); + should_has_83_cards_in_draw_pile_cards(); + should_has_total_112_non_duplicate_cards_in_draw_result(); + } + + @Test + void should_be_different_in_each_deal() { + DealResult firstDealResult = Dealer.deal(); + DealResult secondDealResult = Dealer.deal(); + + should_be_different(firstDealResult, secondDealResult); + } + + private void should_be_different(DealResult firstDealResult, DealResult secondDealResult) { + assertThat(firstDealResult.discardCard()).isNotEqualTo(secondDealResult.discardCard()); + assertThat(firstDealResult.drawPileCards()).isNotEqualTo(secondDealResult.drawPileCards()); + for (int i = 0; i < 4; i++) { + assertThat(firstDealResult.playersHandCard().get(i)).isNotEqualTo(secondDealResult.playersHandCard().get(i)); + } + } + + private void should_has_83_cards_in_draw_pile_cards() { + assertThat(dealResult.drawPileCards()).hasSize(83); + } + + private void should_has_discard_card() { + assertThat(dealResult.discardCard()).isNotNull(); + } + + private void should_provide_different_hand_cards_for_four_players() { + assertThat(dealResult.playersHandCard()).hasSize(4); + } + + private void should_has_total_112_non_duplicate_cards_in_draw_result() { + List cards = new ArrayList<>(); + cards.add(dealResult.discardCard()); + cards.addAll(dealResult.drawPileCards()); + dealResult.playersHandCard().stream().map(HandCard::getCards).flatMap(List::stream).forEach(cards::add); + + assertThat(new HashSet<>(cards)).hasSize(112); + } + + private void when_deal() { + dealResult = Dealer.deal(); + } +} \ No newline at end of file diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/PlayersTest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/PlayersTest.java new file mode 100644 index 0000000..df29b61 --- /dev/null +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/PlayersTest.java @@ -0,0 +1,113 @@ +package tw.waterballsa.gaas.unoflip.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +class PlayersTest { + + private Players sut; + + @BeforeEach + void setUp() { + sut = new Players(); + } + + @Test + void add() { + PlayerInfo playerInfo = given_player_info("player1111", "playerA"); + + sut.add(playerInfo); + + Assertions.assertThat(sut.exists("player1111")).isTrue(); + } + + @Test + void get_player_info_list() { + PlayerInfo playerInfo = given_player_list_has_one_player_info(); + + Assertions.assertThat(sut.toInfoList()) + .hasSize(1) + .containsExactly(playerInfo); + } + + @Test + void get_player_hand_card() { + HandCard handCard = given_hand_card_for_player1111_in_player_list(); + + Assertions.assertThat(sut.getPlayerHandCard("player1111")).isEqualTo(handCard); + } + + @Test + void player_hand_card_not_exists() { + Assertions.assertThatThrownBy(() -> sut.getPlayerHandCard("notExistsPlayerId")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void size() { + given_player_list_has_one_player_info(); + + Assertions.assertThat(sut.size()).isEqualTo(1); + } + + @Test + void get_player_ids() { + given_player1111_in_player_list(); + + Assertions.assertThat(sut.getIds()) + .hasSize(1) + .containsExactly("player1111"); + } + + @Test + void get_player_id() { + given_player_list_has_player_info("player1111", 1); + given_player_list_has_player_info("player2222", 2); + given_player_list_has_player_info("player3333", 3); + + Assertions.assertThat(sut.getPlayerId(2)).isEqualTo("player2222"); + } + + @Test + void position_not_exists() { + Assertions.assertThatThrownBy(() -> sut.getPlayerId(-1)) + .isInstanceOf(IllegalArgumentException.class); + } + + private HandCard given_hand_card_for_player1111_in_player_list() { + given_player1111_in_player_list(); + HandCard handCard = new HandCard(Collections.emptyList()); + + sut.setHandCard("player1111", handCard); + + return handCard; + } + + private void given_player_list_has_player_info(String playerId, int position) { + PlayerInfo playerInfo = new PlayerInfo(playerId, "NO_CARE", position); + + sut.add(playerInfo); + } + + + private void given_player1111_in_player_list() { + PlayerInfo playerInfo = given_player_info("player1111", "NO_CARE"); + + sut.add(playerInfo); + } + + private PlayerInfo given_player_list_has_one_player_info() { + PlayerInfo playerInfo = given_player_info("NO_CARE", "NO_CARE"); + + sut.add(playerInfo); + + return playerInfo; + } + + private PlayerInfo given_player_info(String playerId, String playerName) { + return new PlayerInfo(playerId, playerName, 1); + } +} \ No newline at end of file diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGameTest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGameTest.java index 0a21517..74595bb 100644 --- a/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGameTest.java +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/domain/UnoFlipGameTest.java @@ -3,9 +3,9 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import tw.waterballsa.gaas.unoflip.vo.PlayerInfo; +import tw.waterballsa.gaas.unoflip.domain.eumns.GameStatus; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; class UnoFlipGameTest { @@ -18,9 +18,7 @@ void setUp() { @Test void get_table_id() { - UnoFlipGame game = new UnoFlipGame(123); - - Assertions.assertThat(game.getTableId()).isEqualTo(123); + Assertions.assertThat(new UnoFlipGame(123).getTableId()).isEqualTo(123); } @Test @@ -55,10 +53,53 @@ void game_is_full() { Assertions.assertThat(sut.isFull()).isTrue(); } + @Test + void init_game_status() { + assertThat(new UnoFlipGame(123).getStatus()).isEqualTo(GameStatus.WAITING); + } + + @Test + void start_game_when_status_is_waiting() { + given_already_four_players_in_the_game(); + + sut.start(); + + then_game_status_should_be_started(); + then_should_assign_init_player(); + then_each_player_should_has_7_cards(); + then_discard_pile_should_has_one_card(); + then_draw_pile_should_has_83_cards(); + } + + private void then_should_assign_init_player() { + assertThat(sut.getActionPlayerId()).isNotEmpty(); + } + + private void then_discard_pile_should_has_one_card() { + assertThat(sut.getDiscardPile()).hasSize(1); + } + + private void then_draw_pile_should_has_83_cards() { + assertThat(sut.getDrawPile()).hasSize(83); + } + + private void then_game_status_should_be_started() { + assertThat(sut.getStatus()).isEqualTo(GameStatus.STARTED); + } + + private void then_each_player_should_has_7_cards() { + Players players = sut.getPlayers(); + assertThat(players.getPlayerHandCard("player1111").size()).isEqualTo(7); + assertThat(players.getPlayerHandCard("player2222").size()).isEqualTo(7); + assertThat(players.getPlayerHandCard("player3333").size()).isEqualTo(7); + assertThat(players.getPlayerHandCard("player4444").size()).isEqualTo(7); + } + private void given_already_four_players_in_the_game() { - sut.join("playerId1", "player1"); - sut.join("playerId2", "player2"); - sut.join("playerId3", "player3"); - sut.join("playerId4", "player4"); + sut.join("player1111", "playerA"); + sut.join("player2222", "playerB"); + sut.join("player3333", "playerC"); + sut.join("player4444", "playerD"); } + } \ No newline at end of file diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameJoinE2ETest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameJoinE2ETest.java index 729784d..0d2472c 100644 --- a/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameJoinE2ETest.java +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameJoinE2ETest.java @@ -10,13 +10,14 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import tw.waterballsa.gaas.unoflip.presenter.StatusCode; import tw.waterballsa.gaas.unoflip.vo.JoinRequest; -import tw.waterballsa.gaas.unoflip.vo.JoinResult; -import tw.waterballsa.gaas.unoflip.vo.PlayerInfo; -import tw.waterballsa.gaas.unoflip.vo.Response; +import tw.waterballsa.gaas.unoflip.response.JoinResult; +import tw.waterballsa.gaas.unoflip.domain.PlayerInfo; +import tw.waterballsa.gaas.unoflip.response.Response; import java.time.Duration; import java.util.ArrayList; @@ -31,10 +32,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) @AutoConfigureMockMvc public class GameJoinE2ETest { private final String PLAYER_A_ID = "playerA123"; + @Value(value = "${local.server.port}") private int port; @@ -42,6 +45,7 @@ public class GameJoinE2ETest { private ObjectMapper mapper; @Autowired private MockMvc mockMvc; + private WebTestClient client; private ExecutorService executor; private List responseList; @@ -77,8 +81,8 @@ void join_game() throws Exception { private void playerA_should_received_two_join_broadcasts() throws InterruptedException { countDownLatch.await(); - assertThat(responseList).containsExactly("{\"playerId\":\"playerA123\",\"playerName\":\"PlayerA\",\"position\":1}", - "{\"playerId\":\"playerB456\",\"playerName\":\"PlayerB\",\"position\":2}"); + assertThat(responseList).containsExactly("{\"eventType\":1,\"playerId\":\"playerA123\",\"playerName\":\"PlayerA\",\"position\":1}", + "{\"eventType\":1,\"playerId\":\"playerB456\",\"playerName\":\"PlayerB\",\"position\":2}"); } private void register_sse_client_for_playerA123() { @@ -95,7 +99,7 @@ private void register_sse_client_for_playerA123() { private void playerA_and_playerB_should_in_the_same_game(Response responseOfPlayerA, Response responseOfPlayerB) { assertThat(responseOfPlayerA.payload().tableId()).isEqualTo(responseOfPlayerB.payload().tableId()); - assertThat(responseOfPlayerB.payload().otherPlayerInfo().stream().map(PlayerInfo::playerId).anyMatch(PLAYER_A_ID::equals)).isTrue(); + assertThat(responseOfPlayerB.payload().otherPlayerInfo().stream().map(PlayerInfo::id).anyMatch(PLAYER_A_ID::equals)).isTrue(); } private void then_join_success(Response responseOfPlayerA) { diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameStartE2ETest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameStartE2ETest.java new file mode 100644 index 0000000..eeeb2ce --- /dev/null +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/e2e/GameStartE2ETest.java @@ -0,0 +1,195 @@ +package tw.waterballsa.gaas.unoflip.e2e; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.MockMvc; +import tw.waterballsa.gaas.unoflip.domain.UnoFlipGame; +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; +import tw.waterballsa.gaas.unoflip.domain.eumns.GameStatus; +import tw.waterballsa.gaas.unoflip.event.StartedBroadcastEvent; +import tw.waterballsa.gaas.unoflip.event.StartedPersonalEvent; +import tw.waterballsa.gaas.unoflip.event.EventType; +import tw.waterballsa.gaas.unoflip.repository.GameRepo; +import tw.waterballsa.gaas.unoflip.vo.JoinRequest; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@AutoConfigureMockMvc +public class GameStartE2ETest { + private final Map> sseMessagesOfPlayer = new HashMap<>(); + private final Map sseMessageCountDownLatchOfPlayer = new HashMap<>(); + + @Value(value = "${local.server.port}") + private int port; + + @Autowired + private ObjectMapper mapper; + @Autowired + private MockMvc mockMvc; + @Autowired + private GameRepo gameRepo; + + private ExecutorService executor; + private WebTestClient client; + private UnoFlipGame game; + + @BeforeEach + void setUp() { + executor = Executors.newFixedThreadPool(4); + client = WebTestClient.bindToServer().responseTimeout(Duration.ofMinutes(5)).baseUrl("http://localhost:" + port).build(); + } + + @AfterEach + void tearDown() { + executor.shutdown(); + } + + @Test + void game_start() throws Exception { + given_player_join_game("player111", "playerA"); + given_player_join_game("player222", "playerB"); + given_player_join_game("player333", "playerC"); + given_player_join_game("player444", "playerD"); + + when_start_game(); + + then_game_exists(); + then_game_is_started(); + then_draw_pile_has_83_cards(); + then_discard_pile_has_1_card(); + + should_send_personal_message_for("player111"); + should_send_personal_message_for("player222"); + should_send_personal_message_for("player333"); + should_send_personal_message_for("player444"); + + should_send_same_broadcast_message_for("player111", "player222", "player333", "player444"); + } + + private void given_player_join_game(String playerId, String playerA) throws Exception { + register_sse_client_for(playerId); + TimeUnit.MILLISECONDS.sleep(1000); + send_join_game(playerId, playerA); + } + + private void register_sse_client_for(String playerId) { + sseMessagesOfPlayer.put(playerId, new ArrayList<>()); + sseMessageCountDownLatchOfPlayer.put(playerId, new CountDownLatch(2)); + + executor.submit(() -> client.get().uri("/sse/%s".formatted(playerId)).accept(MediaType.TEXT_EVENT_STREAM).exchange() + .expectStatus().isOk().returnResult(String.class).getResponseBody().toStream() + .forEach(response -> { + sseMessagesOfPlayer.get(playerId).add(response); + sseMessageCountDownLatchOfPlayer.get(playerId).countDown(); + })); + } + + private void should_send_same_broadcast_message_for(String... playerIds) throws JsonProcessingException { + StartedBroadcastEvent expected = new StartedBroadcastEvent( + EventType.STARTED.getCode(), + game.getActionPlayerId(), + game.getDirection().getCode(), + game.getDiscardPile().get(0).getId(), + game.getDrawPile().stream().map(Card::getId).toList()); + + for (String playerId : playerIds) { + Optional message = getTargetMessage(playerId, EventType.STARTED); + + Assertions.assertThat(message) + .isPresent() + .map(this::toStartedBroadcastEvent) + .hasValue(expected); + } + } + + private StartedBroadcastEvent toStartedBroadcastEvent(String message) { + try { + return mapper.readValue(message, StartedBroadcastEvent.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private void should_send_personal_message_for(String playerId) throws JsonProcessingException { + List excepted = game.getPlayers().getPlayerHandCard(playerId).toCardIds(); + + Optional message = getTargetMessage(playerId, EventType.HAND_CARD); + + Assertions.assertThat(message) + .isPresent() + .map(this::toStartedPersonalEvent) + .hasValue(excepted); + } + + private List toStartedPersonalEvent(String message) { + try { + return mapper.readValue(message, StartedPersonalEvent.class).handCards(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private Optional getTargetMessage(String playerId, EventType eventType) throws JsonProcessingException { + List messages = sseMessagesOfPlayer.get(playerId); + for (String message : messages) { + JsonNode eventTypeId = mapper.readTree(message).get("eventType"); + if (null != eventTypeId && eventType.getCode() == eventTypeId.asInt()) { + return Optional.of(message); + } + } + + return Optional.empty(); + } + + private void then_discard_pile_has_1_card() { + Assertions.assertThat(game.getDiscardPile()).hasSize(1); + } + + private void then_draw_pile_has_83_cards() { + Assertions.assertThat(game.getDrawPile()).hasSize(83); + } + + private void then_game_is_started() { + Assertions.assertThat(game.getStatus()).isEqualTo(GameStatus.STARTED); + } + + private void then_game_exists() { + Optional game = gameRepo.get(1); + Assertions.assertThat(game).isPresent(); + this.game = game.get(); + } + + private void when_start_game() { + // do nothing, due to game will auto start when player number achieved 4 + } + + private void send_join_game(String playerId, String playerName) throws Exception { + mockMvc.perform(post("http://localhost:" + port + "/join/" + playerId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(new JoinRequest(playerName)))) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + } +} diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenterTest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenterTest.java index d5e9c3f..4e2882d 100644 --- a/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenterTest.java +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameJoinPresenterTest.java @@ -3,6 +3,12 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tw.waterballsa.gaas.unoflip.domain.PlayerInfo; +import tw.waterballsa.gaas.unoflip.event.BroadcastEvent; +import tw.waterballsa.gaas.unoflip.event.EventType; +import tw.waterballsa.gaas.unoflip.event.JoinBroadcastEvent; +import tw.waterballsa.gaas.unoflip.response.JoinResult; +import tw.waterballsa.gaas.unoflip.response.Response; import tw.waterballsa.gaas.unoflip.vo.*; import java.util.Arrays; @@ -52,8 +58,8 @@ void broadcast_event() { } private void verify_broadcast(BroadcastEvent actual) { - JoinEvent targetJoinEvent = new JoinEvent("targetPlayerId", "targetPlayerName", 2); - BroadcastEvent expected = new BroadcastEvent(Arrays.asList("otherPlayerId", "targetPlayerId"), targetJoinEvent); + JoinBroadcastEvent targetJoinBroadcastEvent = new JoinBroadcastEvent(EventType.JOIN.getCode(), "targetPlayerId", "targetPlayerName", 2); + BroadcastEvent expected = new BroadcastEvent(Arrays.asList("otherPlayerId", "targetPlayerId"), targetJoinBroadcastEvent); Assertions.assertThat(actual).isEqualTo(expected); } diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameStartPresenterTest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameStartPresenterTest.java new file mode 100644 index 0000000..e55814c --- /dev/null +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/presenter/GameStartPresenterTest.java @@ -0,0 +1,91 @@ +package tw.waterballsa.gaas.unoflip.presenter; + +import com.google.common.collect.ImmutableList; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tw.waterballsa.gaas.unoflip.domain.HandCard; +import tw.waterballsa.gaas.unoflip.domain.Players; +import tw.waterballsa.gaas.unoflip.domain.eumns.Card; +import tw.waterballsa.gaas.unoflip.domain.eumns.Direction; +import tw.waterballsa.gaas.unoflip.event.*; +import tw.waterballsa.gaas.unoflip.vo.*; + +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GameStartPresenterTest { + + private GameStartPresenter sut; + + @BeforeEach + void setUp() { + sut = new GameStartPresenter(); + } + + @Test + void broadcast_event() { + GameStartResult gameStartResult = new GameStartResult( + "player1111", + Direction.RIGHT, + given_players("player1111", "player2222", "player3333", "player4444"), + Collections.singletonList(Card.CARD_1), + Collections.singletonList(Card.CARD_2)); + + Assertions.assertThat(sut.broadcastEvent(gameStartResult)).isEqualTo( + new BroadcastEvent(ImmutableList.of("player1111", "player2222", "player3333", "player4444"), + new StartedBroadcastEvent( + EventType.STARTED.getCode(), + "player1111", + Direction.RIGHT.getCode(), + Card.CARD_1.getId(), + Collections.singletonList(Card.CARD_2.getId())))); + } + + @Test + void personal_events() { + Players players = given_players_are("player1111", "player2222", "player3333", "player4444"); + HandCard handCardForPlayer1111 = given_player_hand_card(players, "player1111", Card.CARD_1); + HandCard handCardForPlayer2222 = given_player_hand_card(players, "player2222", Card.CARD_2); + HandCard handCardForPlayer3333 = given_player_hand_card(players, "player3333", Card.CARD_3); + HandCard handCardForPlayer4444 = given_player_hand_card(players, "player4444", Card.CARD_4); + + GameStartResult gameStartResult = new GameStartResult("NO_CARE", + Direction.RIGHT, + players, + Collections.emptyList(), + Collections.emptyList()); + + Assertions.assertThat(sut.personalEvents(gameStartResult)).containsExactly( + new PersonalEvent("player1111", new StartedPersonalEvent(EventType.HAND_CARD.getCode(), handCardForPlayer1111.toCardIds())), + new PersonalEvent("player2222", new StartedPersonalEvent(EventType.HAND_CARD.getCode(), handCardForPlayer2222.toCardIds())), + new PersonalEvent("player3333", new StartedPersonalEvent(EventType.HAND_CARD.getCode(), handCardForPlayer3333.toCardIds())), + new PersonalEvent("player4444", new StartedPersonalEvent(EventType.HAND_CARD.getCode(), handCardForPlayer4444.toCardIds()))); + } + + private Players given_players_are(String... playerIds) { + Players players = mock(Players.class); + when(players.getIds()).thenReturn(Arrays.asList(playerIds)); + return players; + } + + private HandCard given_player_hand_card(Players players, String playerId, Card card) { + HandCard handCard = given_hand_card_for_player(card); + when(players.getPlayerHandCard(playerId)).thenReturn(handCard); + return handCard; + } + + private HandCard given_hand_card_for_player(Card card) { + return new HandCard(Collections.singletonList(card)); + } + + private Players given_players(String... playerIds) { + Players players = mock(Players.class); + when(players.getIds()).thenReturn(Arrays.asList(playerIds)); + return players; + } + +} \ No newline at end of file diff --git a/backend/src/test/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCaseTest.java b/backend/src/test/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCaseTest.java index 46ec736..a5c8a85 100644 --- a/backend/src/test/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCaseTest.java +++ b/backend/src/test/java/tw/waterballsa/gaas/unoflip/usecase/GameJoinUseCaseTest.java @@ -1,14 +1,25 @@ package tw.waterballsa.gaas.unoflip.usecase; +import com.google.common.collect.ImmutableList; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import tw.waterballsa.gaas.unoflip.domain.PlayerInfo; import tw.waterballsa.gaas.unoflip.domain.UnoFlipGame; +import tw.waterballsa.gaas.unoflip.event.BroadcastEvent; +import tw.waterballsa.gaas.unoflip.event.PersonalEvent; +import tw.waterballsa.gaas.unoflip.presenter.GameJoinPresenter; +import tw.waterballsa.gaas.unoflip.presenter.GameStartPresenter; +import tw.waterballsa.gaas.unoflip.presenter.StatusCode; import tw.waterballsa.gaas.unoflip.repository.GameRepo; +import tw.waterballsa.gaas.unoflip.service.SseService; import tw.waterballsa.gaas.unoflip.vo.GameJoinResult; -import tw.waterballsa.gaas.unoflip.vo.PlayerInfo; +import tw.waterballsa.gaas.unoflip.vo.GameStartResult; +import tw.waterballsa.gaas.unoflip.response.JoinResult; +import tw.waterballsa.gaas.unoflip.response.Response; import java.util.ArrayList; import java.util.List; @@ -21,55 +32,76 @@ class GameJoinUseCaseTest { private static final String MAX_PLAYER_ID = "max456"; private static final String SHADOW_PLAYER_ID = "shadow123"; - private final String MAX_NAME = "Max"; - private final String SHADOW_NAME = "Shadow"; @Mock private GameRepo gameRepo; @Mock + private SseService sseService; + @Mock + private GameJoinPresenter gameJoinPresenter; + @Mock + private GameStartPresenter gameStartPresenter; + @Mock private UnoFlipGame unoFlipGame; @Mock private UnoFlipGame unoFlipGame1; @Mock private UnoFlipGame unoFlipGame2; - private GameJoinUseCase sut; + @Mock + private BroadcastEvent gameStartedBroadcastEvent; + private List playerInfoList; - private GameJoinResult shadowJoinResult; - private GameJoinResult maxJoinResult; private List playerInfoList1; - private List playerInfoList2; + private Response joinTableResponse; + private Response joinTable1Response; + private Response joinTable2Response; + private GameJoinUseCase sut; + private UnoFlipGame spyGame; + @BeforeEach void setUp() { - sut = new GameJoinUseCase(gameRepo); + sut = new GameJoinUseCase(gameRepo, gameJoinPresenter, gameStartPresenter, sseService); } + @Test void should_create_new_game_when_no_available_game() { given_no_available_game(); - given_next_table_id_is_123(); + given_new_game_table_id_is_123(); + + when_someone_join(); - when_join_game_then_table_id_is_123(); + then_table_id_is_123(); } @Test void should_save_game() { - when(gameRepo.getAvailable()).thenReturn(Optional.of(unoFlipGame)); + given_one_available_game(); - sut.join(SHADOW_PLAYER_ID, SHADOW_NAME); + when_someone_join(); verify(gameRepo).save(unoFlipGame); } + @Test + void should_send_join_event() { + BroadcastEvent joinBroadcastEvent = given_broadcast_event_for_shadow(); + + when_shadow_join(); + + verify(sseService).sendMessage(joinBroadcastEvent); + } + @Test void two_player_join_different_game() { init_two_games(); - given_shadow_join_table_1(); - given_max_join_table_2(); + given_shadow_join_game_1(); + given_max_join_game_2(); when_shadow_join(); - then_shadow_should_at_position(1); + then_shadow_should_at_position_1(); when_max_join(); then_max_should_at_position(1); @@ -84,7 +116,7 @@ void two_player_join_the_same_game() { given_max_is_the_second_player_of_the_game(); when_shadow_join(); - then_shadow_should_at_position(1); + then_shadow_should_at_position_1(); when_max_join(); then_max_should_at_position(2); @@ -92,11 +124,61 @@ void two_player_join_the_same_game() { then_shadow_and_max_should_in_the_same_game(); } + @Test + void should_start_game_when_game_full_after_player_join() { + given_already_three_players_in_game(); + BroadcastEvent joinBroadcastEvent = given_broadcast_event_for_shadow(); + given_started_broadcast_event(); + given_started_personal_events(); + + when_shadow_join(); + + should_start_game(); + should_send_events_in_order(joinBroadcastEvent); + } + + private void should_send_events_in_order(BroadcastEvent joinBroadcastEvent) { + InOrder inOrder = inOrder(sseService); + inOrder.verify(sseService).sendMessage(joinBroadcastEvent); + inOrder.verify(sseService).sendMessage(gameStartedBroadcastEvent); + inOrder.verify(sseService, times(4)).sendMessage(isA(PersonalEvent.class)); + } + + private void given_started_personal_events() { + when(gameStartPresenter.personalEvents(isA(GameStartResult.class))).thenReturn(ImmutableList.of( + mock(PersonalEvent.class), + mock(PersonalEvent.class), + mock(PersonalEvent.class), + mock(PersonalEvent.class))); + } + + private void given_started_broadcast_event() { + when(gameStartPresenter.broadcastEvent(isA(GameStartResult.class))).thenReturn(gameStartedBroadcastEvent); + } + + private void given_already_three_players_in_game() { + spyGame = spy(new UnoFlipGame(1)); + when(gameRepo.getAvailable()).thenReturn(Optional.of(spyGame)); + doNothing().when(spyGame).start(); + + spyGame.join("player1111", "playerA"); + spyGame.join("player2222", "playerB"); + spyGame.join("player3333", "playerC"); + } + + private void should_start_game() { + verify(spyGame).start(); + } + + private void when_someone_join() { + joinTableResponse = sut.join("SOMEONE", "NO_CARE"); + } + private void init_game() { playerInfoList = new ArrayList<>(); when(unoFlipGame.getPlayerInfoList()).thenReturn(playerInfoList); lenient().when(unoFlipGame.getTableId()).thenReturn(1); - when(gameRepo.getAvailable()).thenReturn(Optional.of(unoFlipGame)); + given_one_available_game(); } private void init_two_games() { @@ -115,61 +197,82 @@ private void given_no_available_game() { when(gameRepo.getAvailable()).thenReturn(Optional.empty()); } + private void given_one_available_game() { + when(gameRepo.getAvailable()).thenReturn(Optional.of(unoFlipGame)); + } + private void given_shadow_is_the_first_player_of_the_game() { - playerInfoList.add(new PlayerInfo(SHADOW_PLAYER_ID, SHADOW_NAME, 1)); + Response response = given_response(1, 1); + when(gameJoinPresenter.response(SHADOW_PLAYER_ID, new GameJoinResult(1, playerInfoList))).thenReturn(response); } private void given_max_is_the_second_player_of_the_game() { - playerInfoList.add(new PlayerInfo(MAX_PLAYER_ID, MAX_NAME, 2)); + Response response = given_response(1, 2); + when(gameJoinPresenter.response(MAX_PLAYER_ID, new GameJoinResult(1, playerInfoList))).thenReturn(response); } - private void given_shadow_join_table_1() { - playerInfoList1.add(new PlayerInfo(SHADOW_PLAYER_ID, SHADOW_NAME, 1)); + private void given_shadow_join_game_1() { + Response response = given_response(1, 1); + when(gameJoinPresenter.response(SHADOW_PLAYER_ID, new GameJoinResult(1, playerInfoList1))).thenReturn(response); } - private void given_next_table_id_is_123() { + private void given_max_join_game_2() { + Response response = given_response(2, 1); + when(gameJoinPresenter.response(MAX_PLAYER_ID, new GameJoinResult(2, playerInfoList2))).thenReturn(response); + } + + private Response given_response(int tableId, int position) { + JoinResult joinResult = given_join_result(tableId, position); + return new Response<>(StatusCode.OK.getCode(), "join successfully", joinResult); + } + + private void given_new_game_table_id_is_123() { when(gameRepo.generateTableId()).thenReturn(123); + + JoinResult joinResult = mock(JoinResult.class); + when(joinResult.tableId()).thenReturn(123); + Response response = new Response<>(StatusCode.OK.getCode(), "join successfully", joinResult); + when(gameJoinPresenter.response(eq("SOMEONE"), isA(GameJoinResult.class))).thenReturn(response); } - private void given_max_join_table_2() { - playerInfoList2.add(new PlayerInfo(MAX_PLAYER_ID, MAX_NAME, 1)); + private JoinResult given_join_result(int tableId, int position) { + JoinResult joinResult = mock(JoinResult.class); + when(joinResult.tableId()).thenReturn(tableId); + when(joinResult.position()).thenReturn(position); + return joinResult; } - private void when_join_game_then_table_id_is_123() { - assertThat(sut.join(SHADOW_PLAYER_ID, SHADOW_NAME).tableId()).isEqualTo(123); + private BroadcastEvent given_broadcast_event_for_shadow() { + BroadcastEvent event = mock(BroadcastEvent.class); + when(gameJoinPresenter.broadcastEvent(eq(SHADOW_PLAYER_ID), isA(GameJoinResult.class))).thenReturn(event); + return event; } private void when_shadow_join() { - shadowJoinResult = sut.join(SHADOW_PLAYER_ID, SHADOW_NAME); + joinTable1Response = sut.join(SHADOW_PLAYER_ID, "Shadow"); } private void when_max_join() { - maxJoinResult = sut.join(MAX_PLAYER_ID, MAX_NAME); + joinTable2Response = sut.join(MAX_PLAYER_ID, "Max"); } - private void then_shadow_should_at_position(int expectedPosition) { - Integer shadowPosition = shadowJoinResult.playerInfoList().stream() - .filter(playerInfo -> SHADOW_PLAYER_ID.equals(playerInfo.playerId())) - .map(PlayerInfo::position) - .findFirst() - .orElseThrow(() -> new RuntimeException("player %s not int game".formatted(SHADOW_PLAYER_ID))); - assertThat(shadowPosition).isEqualTo(expectedPosition); + private void then_table_id_is_123() { + assertThat(joinTableResponse.payload().tableId()).isEqualTo(123); } - private void then_shadow_and_max_should_in_different_game() { - assertThat(shadowJoinResult.tableId()).isNotEqualTo(maxJoinResult.tableId()); + private void then_shadow_should_at_position_1() { + assertThat(joinTable1Response.payload().position()).isEqualTo(1); } - private void then_shadow_and_max_should_in_the_same_game() { - assertThat(shadowJoinResult.tableId()).isEqualTo(maxJoinResult.tableId()); + private void then_max_should_at_position(int exceptedPosition) { + assertThat(joinTable2Response.payload().position()).isEqualTo(exceptedPosition); } - private void then_max_should_at_position(int exceptedPosition) { - Integer maxPosition = maxJoinResult.playerInfoList().stream() - .filter(playerInfo -> MAX_PLAYER_ID.equals(playerInfo.playerId())) - .map(PlayerInfo::position) - .findFirst() - .orElseThrow(() -> new RuntimeException("player %s not int game".formatted(MAX_PLAYER_ID))); - assertThat(maxPosition).isEqualTo(exceptedPosition); + private void then_shadow_and_max_should_in_different_game() { + assertThat(joinTable1Response.payload().tableId()).isNotEqualTo(joinTable2Response.payload().tableId()); + } + + private void then_shadow_and_max_should_in_the_same_game() { + assertThat(joinTable1Response.payload().tableId()).isEqualTo(joinTable2Response.payload().tableId()); } } \ No newline at end of file