diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java b/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java index 1c4a18e..6ae7ae2 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java @@ -68,7 +68,7 @@ private Room newRoom( String hostActionToken, String hostNickname ) { - RoomTemplateSpec spec = RoomTemplateSpec.from(template); + RoomTemplateSpec spec = RoomTemplateSpecFactory.from(template); return Room.createFromTemplate( generateCode(), hostLeaderId, diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/RoomTemplateSpecFactory.java b/src/main/java/com/naminhyeok/fantazzk/room/application/RoomTemplateSpecFactory.java new file mode 100644 index 0000000..a7661f9 --- /dev/null +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/RoomTemplateSpecFactory.java @@ -0,0 +1,70 @@ +package com.naminhyeok.fantazzk.room.application; + +import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; +import com.naminhyeok.fantazzk.room.domain.RoomMode; +import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; +import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; +import com.naminhyeok.fantazzk.template.TemplateCatalog; +import java.util.List; +import java.util.Objects; + +final class RoomTemplateSpecFactory { + private RoomTemplateSpecFactory() { + } + + static RoomTemplateSpec from(TemplateCatalog.TemplateBlueprint template) { + Objects.requireNonNull(template, "template must not be null"); + List players = template.players().stream() + .map(RoomTemplateSpecFactory::playerFrom) + .toList(); + int requiredPlayerCount = template.teamCount() * (template.teamSize() - 1); + if (players.size() != requiredPlayerCount) { + throw new IllegalArgumentException("선수 수는 정확히 " + requiredPlayerCount + "명이어야 합니다"); + } + return new RoomTemplateSpec( + template.gameType(), + roomModeFrom(template.mode()), + template.teamCount(), + template.teamSize(), + template.budget(), + requirePickBanTime(template.pickBanTime()), + template.minBidUnit(), + draftOrderStrategyFrom(template.draftOrderStrategy()), + players + ); + } + + private static RoomMode roomModeFrom(TemplateCatalog.Mode mode) { + return switch (Objects.requireNonNull(mode, "mode must not be null")) { + case AUCTION -> RoomMode.AUCTION; + case DRAFT -> RoomMode.DRAFT; + }; + } + + private static DraftOrderStrategy draftOrderStrategyFrom(TemplateCatalog.DraftOrderStrategy strategy) { + if (strategy == null) { + return null; + } + return switch (strategy) { + case SNAKE -> DraftOrderStrategy.SNAKE; + case FIXED -> DraftOrderStrategy.FIXED; + }; + } + + private static RoomTemplateSpec.Player playerFrom(TemplateCatalog.PlayerBlueprint player) { + Objects.requireNonNull(player, "player must not be null"); + return new RoomTemplateSpec.Player( + new RoomPlayerId(player.playerIndex()), + player.name(), + player.position(), + player.playerIndex() + ); + } + + private static int requirePickBanTime(Integer pickBanTime) { + if (pickBanTime == null) { + throw new IllegalArgumentException("방 생성 명세에는 픽밴 시간이 필요합니다"); + } + return pickBanTime; + } +} diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/DraftOrderStrategy.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/DraftOrderStrategy.java index 492440f..bd58dd4 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/domain/DraftOrderStrategy.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/domain/DraftOrderStrategy.java @@ -1,18 +1,6 @@ package com.naminhyeok.fantazzk.room.domain; -import com.naminhyeok.fantazzk.template.TemplateCatalog; - public enum DraftOrderStrategy { SNAKE, - FIXED; - - public static DraftOrderStrategy from(TemplateCatalog.DraftOrderStrategy strategy) { - if (strategy == null) { - return null; - } - return switch (strategy) { - case SNAKE -> SNAKE; - case FIXED -> FIXED; - }; - } + FIXED } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java index b9111db..9bfbb7e 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java @@ -216,26 +216,7 @@ public StartedGameSnapshot start(TeamLeaderId callerLeaderId, GameId gameId, Ins startedAt = Objects.requireNonNull(now, "now must not be null"); status = RoomStatus.STARTED; - return new StartedGameSnapshot( - id, - code, - startedGameId, - startedAt, - gameType, - mode, - mode == RoomMode.AUCTION - ? GameRules.auction(teamCount, teamSize, budget, pickBanTime, minBidUnit) - : GameRules.draft(teamCount, teamSize, pickBanTime, draftOrderStrategy), - leaders.stream() - .map(leader -> mode == RoomMode.AUCTION - ? GameParticipant.auction(leader.getId(), leader.getNickname(), leader.getRemainingBudget()) - : GameParticipant.draft(leader.getId(), leader.getNickname(), leader.getDraftPosition())) - .toList(), - players.stream() - .sorted(Comparator.comparingInt(RoomPlayer::getDisplayOrder)) - .map(player -> new GamePlayer(player.getId(), player.getName(), player.getPosition(), player.getDisplayOrder())) - .toList() - ); + return StartedGameSnapshotFactory.from(this); } private void validateDraftPositionChange(int draftPosition) { diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomMode.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomMode.java index 6951643..a2bae49 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomMode.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomMode.java @@ -1,16 +1,6 @@ package com.naminhyeok.fantazzk.room.domain; -import com.naminhyeok.fantazzk.template.TemplateCatalog; -import java.util.Objects; - public enum RoomMode { AUCTION, - DRAFT; - - public static RoomMode from(TemplateCatalog.Mode mode) { - return switch (Objects.requireNonNull(mode, "mode must not be null")) { - case AUCTION -> AUCTION; - case DRAFT -> DRAFT; - }; - } + DRAFT } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpec.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpec.java index e744156..477ff48 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpec.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpec.java @@ -1,6 +1,5 @@ package com.naminhyeok.fantazzk.room.domain; -import com.naminhyeok.fantazzk.template.TemplateCatalog; import java.util.List; import java.util.Objects; @@ -53,42 +52,6 @@ public record RoomTemplateSpec( } } - public static RoomTemplateSpec from(TemplateCatalog.TemplateBlueprint template) { - Objects.requireNonNull(template, "template must not be null"); - List players = template.players().stream().map(Player::from).toList(); - int requiredPlayerCount = template.teamCount() * (template.teamSize() - 1); - if (players.size() != requiredPlayerCount) { - throw new IllegalArgumentException("선수 수는 정확히 " + requiredPlayerCount + "명이어야 합니다"); - } - return new RoomTemplateSpec( - template.gameType(), - RoomMode.from(template.mode()), - template.teamCount(), - template.teamSize(), - template.budget(), - requirePickBanTime(template.pickBanTime()), - template.minBidUnit(), - DraftOrderStrategy.from(template.draftOrderStrategy()), - players - ); - } - public record Player(RoomPlayerId id, String name, String position, int displayOrder) { - public static Player from(TemplateCatalog.PlayerBlueprint player) { - Objects.requireNonNull(player, "player must not be null"); - return new Player( - new RoomPlayerId(player.playerIndex()), - player.name(), - player.position(), - player.playerIndex() - ); - } - } - - private static int requirePickBanTime(Integer pickBanTime) { - if (pickBanTime == null) { - throw new IllegalArgumentException("방 생성 명세에는 픽밴 시간이 필요합니다"); - } - return pickBanTime; } } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/StartedGameSnapshotFactory.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/StartedGameSnapshotFactory.java new file mode 100644 index 0000000..1a51de7 --- /dev/null +++ b/src/main/java/com/naminhyeok/fantazzk/room/domain/StartedGameSnapshotFactory.java @@ -0,0 +1,47 @@ +package com.naminhyeok.fantazzk.room.domain; + +import java.util.Comparator; + +final class StartedGameSnapshotFactory { + private StartedGameSnapshotFactory() { + } + + static StartedGameSnapshot from(Room room) { + return new StartedGameSnapshot( + room.getId(), + room.getCode(), + room.getStartedGameId(), + room.getStartedAt(), + room.getGameType(), + room.getMode(), + rulesOf(room), + room.getLeaders().stream() + .map(leader -> room.getMode() == RoomMode.AUCTION + ? GameParticipant.auction(leader.getId(), leader.getNickname(), leader.getRemainingBudget()) + : GameParticipant.draft(leader.getId(), leader.getNickname(), leader.getDraftPosition())) + .toList(), + room.getPlayers().stream() + .sorted(Comparator.comparingInt(RoomPlayer::getDisplayOrder)) + .map(player -> new GamePlayer(player.getId(), player.getName(), player.getPosition(), player.getDisplayOrder())) + .toList() + ); + } + + private static GameRules rulesOf(Room room) { + if (room.getMode() == RoomMode.AUCTION) { + return GameRules.auction( + room.getTeamCount(), + room.getTeamSize(), + room.getBudget(), + room.getPickBanTime(), + room.getMinBidUnit() + ); + } + return GameRules.draft( + room.getTeamCount(), + room.getTeamSize(), + room.getPickBanTime(), + room.getDraftOrderStrategy() + ); + } +} diff --git a/src/test/java/com/naminhyeok/fantazzk/architecture/RoomInternalPackageIsolationTest.java b/src/test/java/com/naminhyeok/fantazzk/architecture/RoomInternalPackageIsolationTest.java index 6c724b2..f7f8a4a 100644 --- a/src/test/java/com/naminhyeok/fantazzk/architecture/RoomInternalPackageIsolationTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/architecture/RoomInternalPackageIsolationTest.java @@ -43,6 +43,15 @@ class RoomInternalPackageIsolationTest { "..room.infrastructure.." ); + @ArchTest + static final ArchRule room_domain은_template_모듈에_의존하지_않는다 = + noClasses() + .that() + .resideInAnyPackage("..room.domain..") + .should() + .dependOnClassesThat() + .resideInAnyPackage("..template.."); + @ArchTest static final ArchRule room_query는_web이나_infrastructure에_의존하지_않는다 = noClasses() diff --git a/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java b/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java index 9b2a76d..795caa5 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java @@ -11,20 +11,11 @@ import com.naminhyeok.fantazzk.room.application.SelectDraftPosition; import com.naminhyeok.fantazzk.room.application.SettleAuctionAttempt; import com.naminhyeok.fantazzk.room.domain.AuctionGame; -import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; import com.naminhyeok.fantazzk.room.domain.Game; -import com.naminhyeok.fantazzk.room.domain.GameFactory; import com.naminhyeok.fantazzk.room.domain.GameId; -import com.naminhyeok.fantazzk.room.domain.GameParticipant; -import com.naminhyeok.fantazzk.room.domain.GamePlayer; -import com.naminhyeok.fantazzk.room.domain.GameRules; import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; import com.naminhyeok.fantazzk.room.domain.RoomId; -import com.naminhyeok.fantazzk.room.domain.RoomMode; -import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; -import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; -import com.naminhyeok.fantazzk.room.domain.StartedGameSnapshot; import com.naminhyeok.fantazzk.room.domain.StartedRoomSnapshot; import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomRealtimeEvent; @@ -32,7 +23,7 @@ import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomUpdatedEvent; import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; -import java.nio.charset.StandardCharsets; +import com.naminhyeok.fantazzk.room.support.RoomFixtureBuilder; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; @@ -43,16 +34,12 @@ import org.springframework.orm.ObjectOptimisticLockingFailureException; class RoomRealtimePublishingTest { - private static final Instant CREATED_AT = Instant.parse("2024-01-01T10:00:00Z"); + private static final Instant CREATED_AT = RoomFixtureBuilder.CREATED_AT; private static final Instant PUBLISHED_AT = Instant.parse("2024-01-01T10:00:30Z"); - private static final String HOST_ID = "leader-host"; - private static final String HOST_ACTION_TOKEN = "host-token"; - private static final String GUEST_ID = "leader-guest"; - private static final String GUEST_ACTION_TOKEN = "guest-token"; @Test void 방_참가_저장_충돌은_동시_수정_오류로_번역하고_이벤트를_발행하지_않는다() { - Room room = waitingAuctionRoom(); + Room room = RoomFixtureBuilder.auction().buildRoom(); RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); JoinRoom joinRoom = new JoinRoom(new OptimisticLockFailureRooms(room), publisher); @@ -63,29 +50,29 @@ class RoomRealtimePublishingTest { @Test void 드래프트_자리_선택은_로비_스냅샷을_발행한다() { - Room room = waitingDraftRoomForPositionChange(); + Room room = RoomFixtureBuilder.draft().joined().buildRoom(); RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); SelectDraftPosition selectDraftPosition = new SelectDraftPosition(new InMemoryRooms(room), new RoomActionAuthorizer(), publisher); - selectDraftPosition.select(room.getCode(), HOST_ACTION_TOKEN, 1); + selectDraftPosition.select(room.getCode(), RoomFixtureBuilder.HOST_TOKEN, 1); assertThat(publisher.events).hasSize(1); assertThat(publisher.events.getFirst()).isInstanceOf(RoomUpdatedEvent.class); RoomUpdatedEvent event = (RoomUpdatedEvent) publisher.events.getFirst(); assertThat(event.roomCode()).isEqualTo(room.getCode()); - assertThat(event.room().draftOrder().slots().getFirst().leaderId()).isEqualTo(HOST_ID); + assertThat(event.room().draftOrder().slots().getFirst().leaderId()).isEqualTo(RoomFixtureBuilder.HOST_ID); } @Test void 드래프트_자리_취소는_로비_스냅샷을_발행한다() { - Room room = waitingDraftRoomForPositionChange(); - room.selectDraftPosition(new TeamLeaderId(HOST_ID), 1); + Room room = RoomFixtureBuilder.draft().joined().buildRoom(); + room.selectDraftPosition(new TeamLeaderId(RoomFixtureBuilder.HOST_ID), 1); RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); ClearDraftPosition clearDraftPosition = new ClearDraftPosition(new InMemoryRooms(room), new RoomActionAuthorizer(), publisher); - clearDraftPosition.clear(room.getCode(), HOST_ACTION_TOKEN); + clearDraftPosition.clear(room.getCode(), RoomFixtureBuilder.HOST_TOKEN); assertThat(publisher.events).hasSize(1); assertThat(publisher.events.getFirst()).isInstanceOf(RoomUpdatedEvent.class); @@ -95,8 +82,8 @@ class RoomRealtimePublishingTest { @Test void 마감_전_정산은_스냅샷을_발행하지_않는다() { - Room room = startedAuctionRoom(); - AuctionGame game = (AuctionGame) gameFor(room); + Room room = RoomFixtureBuilder.auction().started().buildRoom(); + AuctionGame game = (AuctionGame) RoomFixtureBuilder.gameFor(room); RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); InMemoryRooms rooms = new InMemoryRooms(room); InMemoryGames games = new InMemoryGames(game); @@ -117,109 +104,6 @@ private static void assertRoomError(CoreException exception, RoomErrorType error assertThat(exception.getError()).isSameAs(errorType); } - private static Room waitingAuctionRoom() { - return Room.createFromTemplate( - "ROOM01", - new TeamLeaderId(HOST_ID), - "호스트", - HOST_ACTION_TOKEN, - new RoomTemplateSpec( - "LEAGUE_OF_LEGENDS", - RoomMode.AUCTION, - 2, - 2, - 300, - 30, - 10, - null, - List.of( - new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), - new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1) - ) - ), - CREATED_AT - ); - } - - private static Room joinedAuctionRoom() { - Room room = waitingAuctionRoom(); - room.join(new TeamLeaderId(GUEST_ID), "게스트", GUEST_ACTION_TOKEN); - return room; - } - - private static Room startedAuctionRoom() { - Room room = joinedAuctionRoom(); - room.start(new TeamLeaderId(HOST_ID), deterministicGameId(room), CREATED_AT); - return room; - } - - private static Room waitingDraftRoomForPositionChange() { - Room room = - Room.createFromTemplate( - "DRF001", - new TeamLeaderId(HOST_ID), - "호스트", - HOST_ACTION_TOKEN, - new RoomTemplateSpec( - "LEAGUE_OF_LEGENDS", - RoomMode.DRAFT, - 2, - 2, - null, - 30, - null, - DraftOrderStrategy.SNAKE, - List.of( - new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), - new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1) - ) - ), - CREATED_AT - ); - room.join(new TeamLeaderId(GUEST_ID), "게스트", GUEST_ACTION_TOKEN); - return room; - } - - private static Game gameFor(Room room) { - return new GameFactory().create( - new StartedGameSnapshot( - room.getId(), - room.getCode(), - room.getStartedGameId(), - room.getStartedAt(), - room.getGameType(), - room.getMode(), - room.getMode() == RoomMode.AUCTION - ? GameRules.auction( - room.getTeamCount(), - room.getTeamSize(), - room.getBudget(), - room.getPickBanTime(), - room.getMinBidUnit() - ) - : GameRules.draft( - room.getTeamCount(), - room.getTeamSize(), - room.getPickBanTime(), - room.getDraftOrderStrategy() - ), - room.getLeaders().stream() - .map(leader -> room.getMode() == RoomMode.AUCTION - ? GameParticipant.auction(leader.getId(), leader.getNickname(), leader.getRemainingBudget()) - : GameParticipant.draft(leader.getId(), leader.getNickname(), leader.getDraftPosition())) - .toList(), - room.getPlayers().stream() - .map(player -> new GamePlayer(player.getId(), player.getName(), player.getPosition(), player.getDisplayOrder())) - .toList() - ) - ); - } - - private static GameId deterministicGameId(Room room) { - String source = "game:%s".formatted(room.getId().roomId()); - return new GameId(java.util.UUID.nameUUIDFromBytes(source.getBytes(StandardCharsets.UTF_8))); - } - private static final class RecordingRoomRealtimeEventPublisher implements RoomRealtimeEventPublisher { private final List events = new ArrayList<>(); diff --git a/src/test/java/com/naminhyeok/fantazzk/room/application/RoomTemplateSpecFactoryTest.java b/src/test/java/com/naminhyeok/fantazzk/room/application/RoomTemplateSpecFactoryTest.java new file mode 100644 index 0000000..b98e794 --- /dev/null +++ b/src/test/java/com/naminhyeok/fantazzk/room/application/RoomTemplateSpecFactoryTest.java @@ -0,0 +1,101 @@ +package com.naminhyeok.fantazzk.room.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; + +import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; +import com.naminhyeok.fantazzk.room.domain.RoomMode; +import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; +import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; +import com.naminhyeok.fantazzk.template.TemplateCatalog; +import java.util.List; +import org.junit.jupiter.api.Test; + +class RoomTemplateSpecFactoryTest { + @Test + void 템플릿_계약을_room_생성_명세로_한_번만_정제한다() { + RoomTemplateSpec spec = + RoomTemplateSpecFactory.from( + new TemplateCatalog.TemplateBlueprint( + "LEAGUE_OF_LEGENDS", + TemplateCatalog.Mode.DRAFT, + 2, + 3, + null, + 30, + null, + TemplateCatalog.DraftOrderStrategy.SNAKE, + List.of( + new TemplateCatalog.PlayerBlueprint("선수1", "TOP", 0), + new TemplateCatalog.PlayerBlueprint("선수2", "JUNGLE", 1), + new TemplateCatalog.PlayerBlueprint("선수3", "MID", 2), + new TemplateCatalog.PlayerBlueprint("선수4", "ADC", 3) + ) + ) + ); + + assertThat(spec.gameType()).isEqualTo("LEAGUE_OF_LEGENDS"); + assertThat(spec.mode()).isEqualTo(RoomMode.DRAFT); + assertThat(spec.teamCount()).isEqualTo(2); + assertThat(spec.teamSize()).isEqualTo(3); + assertThat(spec.pickBanTime()).isEqualTo(30); + assertThat(spec.draftOrderStrategy()).isEqualTo(DraftOrderStrategy.SNAKE); + assertThat(spec.players()) + .extracting(RoomTemplateSpec.Player::id, RoomTemplateSpec.Player::name, RoomTemplateSpec.Player::position, RoomTemplateSpec.Player::displayOrder) + .containsExactly( + tuple(new RoomPlayerId(0), "선수1", "TOP", 0), + tuple(new RoomPlayerId(1), "선수2", "JUNGLE", 1), + tuple(new RoomPlayerId(2), "선수3", "MID", 2), + tuple(new RoomPlayerId(3), "선수4", "ADC", 3) + ); + } + + @Test + void 드래프트_계약에_순서_전략이_없으면_변환을_거부한다() { + assertThatThrownBy( + () -> RoomTemplateSpecFactory.from( + new TemplateCatalog.TemplateBlueprint( + "LEAGUE_OF_LEGENDS", + TemplateCatalog.Mode.DRAFT, + 2, + 2, + null, + 30, + null, + null, + List.of( + new TemplateCatalog.PlayerBlueprint("선수1", "TOP", 0), + new TemplateCatalog.PlayerBlueprint("선수2", "JUNGLE", 1) + ) + ) + ) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("드래프트 방 생성 명세에는 순서 전략이 필요합니다"); + } + + @Test + void 픽밴_시간이_없으면_변환을_거부한다() { + assertThatThrownBy( + () -> RoomTemplateSpecFactory.from( + new TemplateCatalog.TemplateBlueprint( + "LEAGUE_OF_LEGENDS", + TemplateCatalog.Mode.AUCTION, + 2, + 2, + 300, + null, + 10, + null, + List.of( + new TemplateCatalog.PlayerBlueprint("선수1", "TOP", 0), + new TemplateCatalog.PlayerBlueprint("선수2", "JUNGLE", 1) + ) + ) + ) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("방 생성 명세에는 픽밴 시간이 필요합니다"); + } +} diff --git a/src/test/java/com/naminhyeok/fantazzk/room/application/StartedGameContextLoaderTest.java b/src/test/java/com/naminhyeok/fantazzk/room/application/StartedGameContextLoaderTest.java index 44c0d44..d96194c 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/application/StartedGameContextLoaderTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/application/StartedGameContextLoaderTest.java @@ -4,52 +4,37 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.naminhyeok.fantazzk.CoreException; -import com.naminhyeok.fantazzk.room.application.RoomActionAuthorizer; -import com.naminhyeok.fantazzk.room.application.StartedGameActionContext; -import com.naminhyeok.fantazzk.room.application.StartedGameContextLoader; import com.naminhyeok.fantazzk.room.domain.Game; -import com.naminhyeok.fantazzk.room.domain.GameFactory; import com.naminhyeok.fantazzk.room.domain.GameId; -import com.naminhyeok.fantazzk.room.domain.GameParticipant; -import com.naminhyeok.fantazzk.room.domain.GamePlayer; -import com.naminhyeok.fantazzk.room.domain.GameRules; import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; import com.naminhyeok.fantazzk.room.domain.RoomId; -import com.naminhyeok.fantazzk.room.domain.RoomMode; -import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; -import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; -import com.naminhyeok.fantazzk.room.domain.StartedGameSnapshot; import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; -import java.nio.charset.StandardCharsets; -import java.time.Instant; +import com.naminhyeok.fantazzk.room.support.RoomFixtureBuilder; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; class StartedGameContextLoaderTest { - private static final Instant CREATED_AT = Instant.parse("2026-04-18T00:00:00Z"); - @Test void 시작된_게임_행위는_방의_액션_토큰으로_인증한다() { - Room room = startedAuctionRoom(); - Game game = startedGameOf(room); + Room room = RoomFixtureBuilder.auction().started().buildRoom(); + Game game = RoomFixtureBuilder.gameFor(room); StartedGameContextLoader loader = new StartedGameContextLoader( new InMemoryRooms(Map.of(room.getId(), room)), new InMemoryGames(Map.of(game.getId(), game)), new RoomActionAuthorizer() ); - StartedGameActionContext action = loader.authenticate(game.getId().gameId(), "host-action-token"); + StartedGameActionContext action = loader.authenticate(game.getId().gameId(), RoomFixtureBuilder.HOST_TOKEN); assertThat(action.room().getId()).isEqualTo(room.getId()); assertThat(action.game().getId()).isEqualTo(game.getId()); - assertThat(action.caller().getId()).isEqualTo(new TeamLeaderId("host-1")); + assertThat(action.caller().getId()).isEqualTo(new TeamLeaderId(RoomFixtureBuilder.HOST_ID)); } @Test @@ -66,8 +51,8 @@ class StartedGameContextLoaderTest { @Test void 게임의_방이_없어도_GAME_NOT_FOUND를_반환한다() { - Room room = startedAuctionRoom(); - Game game = startedGameOf(room); + Room room = RoomFixtureBuilder.auction().started().buildRoom(); + Game game = RoomFixtureBuilder.gameFor(room); StartedGameContextLoader loader = new StartedGameContextLoader(new InMemoryRooms(Map.of()), new InMemoryGames(Map.of(game.getId(), game)), new RoomActionAuthorizer()); @@ -80,8 +65,8 @@ class StartedGameContextLoaderTest { @Test void 액션_토큰이_유효하지_않으면_ROOM_ACTION_TOKEN_INVALID를_반환한다() { - Room room = startedAuctionRoom(); - Game game = startedGameOf(room); + Room room = RoomFixtureBuilder.auction().started().buildRoom(); + Game game = RoomFixtureBuilder.gameFor(room); StartedGameContextLoader loader = new StartedGameContextLoader( new InMemoryRooms(Map.of(room.getId(), room)), new InMemoryGames(Map.of(game.getId(), game)), @@ -95,61 +80,6 @@ class StartedGameContextLoaderTest { ); } - private Room startedAuctionRoom() { - Room room = - Room.createFromTemplate( - "ROOM01", - new TeamLeaderId("host-1"), - "호스트", - "host-action-token", - new RoomTemplateSpec( - "LEAGUE_OF_LEGENDS", - RoomMode.AUCTION, - 2, - 2, - 300, - 45, - 10, - null, - List.of( - new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), - new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1) - ) - ), - CREATED_AT - ); - room.join(new TeamLeaderId("guest-1"), "게스트", "guest-action-token"); - room.start(new TeamLeaderId("host-1"), deterministicGameId(room), CREATED_AT); - return room; - } - - private Game startedGameOf(Room room) { - return new GameFactory().create( - new StartedGameSnapshot( - room.getId(), - room.getCode(), - room.getStartedGameId(), - room.getStartedAt(), - room.getGameType(), - room.getMode(), - GameRules.auction(room.getTeamCount(), room.getTeamSize(), room.getBudget(), room.getPickBanTime(), room.getMinBidUnit()), - List.of( - GameParticipant.auction(new TeamLeaderId("host-1"), "호스트", 300), - GameParticipant.auction(new TeamLeaderId("guest-1"), "게스트", 300) - ), - List.of( - new GamePlayer(new RoomPlayerId(0), "선수1", "TOP", 0), - new GamePlayer(new RoomPlayerId(1), "선수2", "JUNGLE", 1) - ) - ) - ); - } - - private static GameId deterministicGameId(Room room) { - String source = "game:%s".formatted(room.getId().roomId()); - return new GameId(UUID.nameUUIDFromBytes(source.getBytes(StandardCharsets.UTF_8))); - } - private static final class InMemoryRooms implements Rooms { private final Map byId; diff --git a/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpecTest.java b/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpecTest.java index fe7a24f..7f8bf43 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpecTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomTemplateSpecTest.java @@ -1,74 +1,24 @@ package com.naminhyeok.fantazzk.room.domain; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; -import com.naminhyeok.fantazzk.room.domain.RoomMode; -import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; -import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; -import com.naminhyeok.fantazzk.template.TemplateCatalog; import java.util.List; -import java.util.UUID; import org.junit.jupiter.api.Test; class RoomTemplateSpecTest { @Test - void 템플릿_계약을_room_생성_명세로_한_번만_정제한다() { - RoomTemplateSpec spec = - RoomTemplateSpec.from( - new TemplateCatalog.TemplateBlueprint( - "LEAGUE_OF_LEGENDS", - TemplateCatalog.Mode.DRAFT, - 2, - 3, - null, - 30, - null, - TemplateCatalog.DraftOrderStrategy.SNAKE, - List.of( - new TemplateCatalog.PlayerBlueprint("선수1", "TOP", 0), - new TemplateCatalog.PlayerBlueprint("선수2", "JUNGLE", 1), - new TemplateCatalog.PlayerBlueprint("선수3", "MID", 2), - new TemplateCatalog.PlayerBlueprint("선수4", "ADC", 3) - ) - ) - ); - - assertThat(spec.gameType()).isEqualTo("LEAGUE_OF_LEGENDS"); - assertThat(spec.mode()).isEqualTo(RoomMode.DRAFT); - assertThat(spec.teamCount()).isEqualTo(2); - assertThat(spec.teamSize()).isEqualTo(3); - assertThat(spec.pickBanTime()).isEqualTo(30); - assertThat(spec.draftOrderStrategy()).isEqualTo(DraftOrderStrategy.SNAKE); - assertThat(spec.players()) - .extracting(RoomTemplateSpec.Player::id, RoomTemplateSpec.Player::name, RoomTemplateSpec.Player::position, RoomTemplateSpec.Player::displayOrder) - .containsExactly( - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(0), "선수1", "TOP", 0), - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(1), "선수2", "JUNGLE", 1), - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(2), "선수3", "MID", 2), - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(3), "선수4", "ADC", 3) - ); - } - - @Test - void 드래프트_계약에_순서_전략이_없으면_변환을_거부한다() { + void 드래프트_명세에는_순서_전략이_필요하다() { assertThatThrownBy( - () -> RoomTemplateSpec.from( - new TemplateCatalog.TemplateBlueprint( - "LEAGUE_OF_LEGENDS", - TemplateCatalog.Mode.DRAFT, - 2, - 2, - null, - 30, - null, - null, - List.of( - new TemplateCatalog.PlayerBlueprint("선수1", "TOP", 0), - new TemplateCatalog.PlayerBlueprint("선수2", "JUNGLE", 1) - ) - ) + () -> new RoomTemplateSpec( + "LEAGUE_OF_LEGENDS", + RoomMode.DRAFT, + 2, + 2, + null, + 30, + null, + null, + players() ) ) .isInstanceOf(IllegalArgumentException.class) @@ -76,26 +26,28 @@ class RoomTemplateSpecTest { } @Test - void 픽밴_시간이_없으면_변환을_거부한다() { + void 경매_명세에는_예산이_필요하다() { assertThatThrownBy( - () -> RoomTemplateSpec.from( - new TemplateCatalog.TemplateBlueprint( - "LEAGUE_OF_LEGENDS", - TemplateCatalog.Mode.AUCTION, - 2, - 2, - 300, - null, - 10, - null, - List.of( - new TemplateCatalog.PlayerBlueprint("선수1", "TOP", 0), - new TemplateCatalog.PlayerBlueprint("선수2", "JUNGLE", 1) - ) - ) + () -> new RoomTemplateSpec( + "LEAGUE_OF_LEGENDS", + RoomMode.AUCTION, + 2, + 2, + null, + 30, + 10, + null, + players() ) ) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("방 생성 명세에는 픽밴 시간이 필요합니다"); + .hasMessage("경매 방 생성 명세에는 예산이 필요합니다"); + } + + private List players() { + return List.of( + new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), + new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1) + ); } } diff --git a/src/test/java/com/naminhyeok/fantazzk/room/support/RoomFixtureBuilder.java b/src/test/java/com/naminhyeok/fantazzk/room/support/RoomFixtureBuilder.java new file mode 100644 index 0000000..2e2b56a --- /dev/null +++ b/src/test/java/com/naminhyeok/fantazzk/room/support/RoomFixtureBuilder.java @@ -0,0 +1,217 @@ +package com.naminhyeok.fantazzk.room.support; + +import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; +import com.naminhyeok.fantazzk.room.domain.Game; +import com.naminhyeok.fantazzk.room.domain.GameFactory; +import com.naminhyeok.fantazzk.room.domain.GameId; +import com.naminhyeok.fantazzk.room.domain.GameParticipant; +import com.naminhyeok.fantazzk.room.domain.GamePlayer; +import com.naminhyeok.fantazzk.room.domain.GameRules; +import com.naminhyeok.fantazzk.room.domain.Room; +import com.naminhyeok.fantazzk.room.domain.RoomMode; +import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; +import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; +import com.naminhyeok.fantazzk.room.domain.StartedGameSnapshot; +import com.naminhyeok.fantazzk.room.domain.StartedRoomSnapshot; +import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public final class RoomFixtureBuilder { + public static final Instant CREATED_AT = Instant.parse("2024-01-01T10:00:00Z"); + public static final String ROOM_CODE = "ROOM01"; + public static final String HOST_ID = "leader-host"; + public static final String HOST_TOKEN = "host-token"; + public static final String GUEST_ID = "leader-guest"; + public static final String GUEST_TOKEN = "guest-token"; + + private static final String GAME_TYPE = "LEAGUE_OF_LEGENDS"; + private static final String HOST_NICKNAME = "호스트"; + private static final String GUEST_NICKNAME = "게스트"; + + private final RoomMode mode; + private String code = ROOM_CODE; + private Instant createdAt = CREATED_AT; + private int teamCount = 2; + private int teamSize = 2; + private Integer budget; + private int pickBanTime; + private Integer minBidUnit; + private DraftOrderStrategy draftOrderStrategy; + private boolean joined; + private boolean draftPositionsSelected; + private boolean started; + private List players; + + private RoomFixtureBuilder(RoomMode mode) { + this.mode = mode; + if (mode == RoomMode.AUCTION) { + this.budget = 300; + this.pickBanTime = 30; + this.minBidUnit = 10; + this.draftOrderStrategy = null; + } else { + this.budget = null; + this.pickBanTime = 30; + this.minBidUnit = null; + this.draftOrderStrategy = DraftOrderStrategy.SNAKE; + } + } + + public static RoomFixtureBuilder auction() { + return new RoomFixtureBuilder(RoomMode.AUCTION); + } + + public static RoomFixtureBuilder draft() { + return new RoomFixtureBuilder(RoomMode.DRAFT); + } + + public RoomFixtureBuilder code(String code) { + this.code = code; + return this; + } + + public RoomFixtureBuilder createdAt(Instant createdAt) { + this.createdAt = createdAt; + return this; + } + + public RoomFixtureBuilder teamSize(int teamSize) { + this.teamSize = teamSize; + this.players = null; + return this; + } + + public RoomFixtureBuilder pickBanTime(int pickBanTime) { + this.pickBanTime = pickBanTime; + return this; + } + + public RoomFixtureBuilder joined() { + this.joined = true; + return this; + } + + public RoomFixtureBuilder draftPositionsSelected() { + this.joined = true; + this.draftPositionsSelected = true; + return this; + } + + public RoomFixtureBuilder started() { + this.joined = true; + this.started = true; + if (mode == RoomMode.DRAFT) { + this.draftPositionsSelected = true; + } + return this; + } + + public RoomFixtureBuilder players(List players) { + this.players = List.copyOf(players); + return this; + } + + public Room buildRoom() { + Room room = Room.createFromTemplate( + code, + new TeamLeaderId(HOST_ID), + HOST_NICKNAME, + HOST_TOKEN, + spec(), + createdAt + ); + if (joined) { + room.join(new TeamLeaderId(GUEST_ID), GUEST_NICKNAME, GUEST_TOKEN); + } + if (draftPositionsSelected) { + room.selectDraftPosition(new TeamLeaderId(HOST_ID), 1); + room.selectDraftPosition(new TeamLeaderId(GUEST_ID), 2); + } + if (started) { + room.start(new TeamLeaderId(HOST_ID), deterministicGameId(room), createdAt); + } + return room; + } + + public StartedRoomSnapshot buildStartedDetails() { + Room room = started().buildRoom(); + return new StartedRoomSnapshot(room, gameFor(room)); + } + + public static Game gameFor(Room room) { + return new GameFactory().create(startedGameSnapshotOf(room)); + } + + public static GameId deterministicGameId(Room room) { + String source = "game:%s".formatted(room.getId().roomId()); + return new GameId(UUID.nameUUIDFromBytes(source.getBytes(StandardCharsets.UTF_8))); + } + + private RoomTemplateSpec spec() { + return new RoomTemplateSpec( + GAME_TYPE, + mode, + teamCount, + teamSize, + budget, + pickBanTime, + minBidUnit, + draftOrderStrategy, + players == null ? defaultPlayers() : players + ); + } + + private List defaultPlayers() { + List defaultPlayers = new ArrayList<>(); + int playerCount = teamCount * (teamSize - 1); + String[] positions = {"TOP", "JUNGLE", "MID", "ADC", "SUPPORT"}; + for (int index = 0; index < playerCount; index++) { + defaultPlayers.add( + new RoomTemplateSpec.Player( + new RoomPlayerId(index), + "선수" + (index + 1), + positions[index % positions.length], + index + ) + ); + } + return defaultPlayers; + } + + private static StartedGameSnapshot startedGameSnapshotOf(Room room) { + return new StartedGameSnapshot( + room.getId(), + room.getCode(), + room.getStartedGameId(), + room.getStartedAt(), + room.getGameType(), + room.getMode(), + room.getMode() == RoomMode.AUCTION + ? GameRules.auction( + room.getTeamCount(), + room.getTeamSize(), + room.getBudget(), + room.getPickBanTime(), + room.getMinBidUnit() + ) + : GameRules.draft( + room.getTeamCount(), + room.getTeamSize(), + room.getPickBanTime(), + room.getDraftOrderStrategy() + ), + room.getLeaders().stream() + .map(leader -> room.getMode() == RoomMode.AUCTION + ? GameParticipant.auction(leader.getId(), leader.getNickname(), leader.getRemainingBudget()) + : GameParticipant.draft(leader.getId(), leader.getNickname(), leader.getDraftPosition())) + .toList(), + room.getPlayers().stream() + .map(player -> new GamePlayer(player.getId(), player.getName(), player.getPosition(), player.getDisplayOrder())) + .toList() + ); + } +}