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