diff --git a/README.md b/README.md new file mode 100644 index 00000000..0a14083d --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# 4단계 - 게임 실행 + +## 기능 요구사항 +- 사다리 게임에 참여하는 사람에 이름을 최대 5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다. +- 사람 이름은 쉼표(,)를 기준으로 구분한다. +- 개인별 이름을 입력하면 개인별 결과를 출력하고, "all"을 입력하면 전체 참여자의 실행 결과를 출력한다. + +```text +참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요) +neo,brown,brie,tommy + +실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요) +꽝,5000,꽝,3000 + +최대 사다리 높이는 몇 개인가요? +5 + +사다리 결과 + + neo brown brie tommy + |-----| |-----| + | |-----| | + |-----| | | + | |-----| | + |-----| |-----| + 꽝 5000 꽝 3000 + +결과를 보고 싶은 사람은? +neo + +실행 결과 +꽝 + +결과를 보고 싶은 사람은? +all + +실행 결과 +neo : 꽝 +brown : 3000 +brie : 꽝 +tommy : 5000 + +``` + +# 3단계 - 사다리 타기 + +## 기능 요구사항 +- 사다리의 시작 지점과 도착 지점을 출력한다. + +```text +사다리의 넓이는 몇 개인가요? +4 + +사다리의 높이는 몇 개인가요? +5 + +실행결과 + + |-----| |-----| + | |-----| | + |-----| | | + | |-----| | + |-----| |-----| + +0 -> 0 +1 -> 3 +2 -> 2 +3 -> 1 +``` + +# 2단계 - 사다리 생성 + +## 기능 요구사항 +- 사다리는 크기를 입력 받아 생성할 수 있다. + +```text +사다리의 넓이는 몇 개인가요? +4 + +사다리의 높이는 몇 개인가요? +5 + +실행결과 + + |-----| |-----| + | |-----| | + |-----| | | + | |-----| | + |-----| |-----| +``` + +# 1단계 - 사다리 출력 + +## 기능 요구사항 + +- 네이버 사다리 게임을 참고하여 도메인을 분석하여 구현한다. +- 사다리는 4X4 크기로 고정되고, 연결 여부는 랜덤으로 결정한다. +- 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다. + +### 실행 결과 +- 프로그램을 실행한 결과는 다음과 같다. + +```text +실행결과 + + |-----| |-----| + | |-----| | + |-----| | | + | |-----| | +``` + +## 추가된 요구 사항 +- 모든 엔티티를 작게 유지한다. +- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..e948443b --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,12 @@ +import controller.LadderController; +import view.InputView; +import view.OutputView; + +public class Application { + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + LadderController controller = new LadderController(inputView, outputView); + controller.run(); + } +} diff --git a/src/main/java/controller/LadderController.java b/src/main/java/controller/LadderController.java new file mode 100644 index 00000000..e3c434b9 --- /dev/null +++ b/src/main/java/controller/LadderController.java @@ -0,0 +1,89 @@ +package controller; + +import domain.LadderBoard; +import domain.LadderPath; +import domain.Participants; +import domain.PlayerResults; +import dto.LadderBuildResponse; +import dto.LadderResultResponse; +import view.InputView; +import view.OutputView; + +import java.util.List; + +public class LadderController { + + private static final String ALL_COMMAND = "all"; + + private final InputView inputView; + private final OutputView outputView; + + public LadderController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + Participants participants = readParticipants(); + List resultLabels = readResultLabels(); + int ladderHeight = readLadderHeight(); + + LadderBuildResponse ladder = buildLadder(participants.getCount(), ladderHeight); + printLadderBoard(participants.getNames(), resultLabels, ladder); + + PlayerResults playerResults = mapPlayerResults(participants, resultLabels, ladder); + handleResultRequest(playerResults); + } + + private Participants readParticipants() { + outputView.printParticipantPrompt(); + List names = inputView.readParticipantNames(); + return Participants.of(names); + } + + private List readResultLabels() { + outputView.printResultPrompt(); + return inputView.readResultLabels(); + } + + private int readLadderHeight() { + outputView.printHeightPrompt(); + return inputView.readLadderHeight(); + } + + private LadderBuildResponse buildLadder(int columnCount, int rowCount) { + LadderBoard ladderBoard = LadderBoard.build(columnCount, rowCount); + return LadderBuildResponse.from(ladderBoard); + } + + private void printLadderBoard(List participants, List results, LadderBuildResponse ladder) { + outputView.printLadderTitle(); + outputView.printParticipantNames(participants); + outputView.printBridgeLines(ladder); + outputView.printResultLabels(results); + } + + private PlayerResults mapPlayerResults(Participants participants, List results, LadderBuildResponse ladder) { + LadderPath ladderPath = new LadderPath(ladder.lines(), ladder.columnCount()); + LadderResultResponse resultMapping = new LadderResultResponse(ladderPath.mapStartToEndIndex()); + return PlayerResults.from(participants, results, resultMapping.positionMap()); + } + + private void handleResultRequest(PlayerResults playerResults) { + outputView.printResultSelectionPrompt(); + String name = inputView.readResultRequest(); + + if (ALL_COMMAND.equals(name)) { + outputView.printLadderTitle(); + outputView.printAllResults(playerResults.allResults()); + return; + } + + if (playerResults.hasPlayer(name)) { + outputView.printSingleResultWithTitle(playerResults.resultOf(name)); + return; + } + + outputView.printNameNotFound(); + } +} diff --git a/src/main/java/domain/BridgeLine.java b/src/main/java/domain/BridgeLine.java new file mode 100644 index 00000000..ee621da2 --- /dev/null +++ b/src/main/java/domain/BridgeLine.java @@ -0,0 +1,45 @@ +package domain; + +import java.util.Collections; +import java.util.List; + +public class BridgeLine { + + private static final String ERROR_INVALID_CONNECTIONS = "[ERROR] 가로줄 연결 상태는 null이거나 비어 있을 수 없습니다."; + + private final List horizontalConnections; + + public BridgeLine(List horizontalConnections) { + validate(horizontalConnections); + this.horizontalConnections = Collections.unmodifiableList(horizontalConnections); + } + + public boolean isConnectedAt(int index) { + if (index < 0 || index >= horizontalConnections.size()) { + return false; + } + return horizontalConnections.get(index); + } + + public int width() { + return horizontalConnections.size(); + } + + public int nextPositionFrom(int position) { + if (position > 0 && isConnectedAt(position - 1)) { + return position - 1; + } + + if (position < width() && isConnectedAt(position)) { + return position + 1; + } + + return position; + } + + private void validate(List connections) { + if (connections == null || connections.isEmpty()) { + throw new IllegalArgumentException(ERROR_INVALID_CONNECTIONS); + } + } +} diff --git a/src/main/java/domain/LadderBoard.java b/src/main/java/domain/LadderBoard.java new file mode 100644 index 00000000..2e878191 --- /dev/null +++ b/src/main/java/domain/LadderBoard.java @@ -0,0 +1,108 @@ +package domain; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class LadderBoard { + + private static final Random RANDOM = new Random(); + private static final int MAX_ATTEMPTS = 100; + private static final int MIN_PARTICIPANTS = 2; + private static final int MIN_HEIGHT = 1; + + private static final String ERROR_TOO_FEW_PARTICIPANTS = "[ERROR] 사다리는 최소 2명 이상의 참가자가 필요합니다."; + private static final String ERROR_INVALID_HEIGHT = "[ERROR] 사다리 높이는 1 이상이어야 합니다."; + private static final String ERROR_GENERATION_FAILED = "[ERROR] 사다리를 생성하지 못했습니다. 참가자 수나 높이를 확인해 주세요."; + + private final int columnCount; + private final List lines; + + private LadderBoard(int columnCount, List lines) { + this.columnCount = columnCount; + this.lines = lines; + } + + public static LadderBoard build(int columnCount, int rowCount) { + validate(columnCount, rowCount); + int centerColumnIndex = (columnCount - 1) / 2; + + List bridgeLines = tryGenerateBridgeLines(columnCount, rowCount, centerColumnIndex); + return new LadderBoard(columnCount, bridgeLines); + } + + public int getColumnCount() { + return columnCount; + } + + public List getLines() { + return lines; + } + + private static void validate(int columnCount, int rowCount) { + if (columnCount < MIN_PARTICIPANTS) { + throw new IllegalArgumentException(ERROR_TOO_FEW_PARTICIPANTS); + } + if (rowCount < MIN_HEIGHT) { + throw new IllegalArgumentException(ERROR_INVALID_HEIGHT); + } + } + + private static List tryGenerateBridgeLines(int participantCount, int ladderHeight, int centerColumnIndex) { + return IntStream.range(0, MAX_ATTEMPTS) + .mapToObj(i -> generateAttempt(participantCount, ladderHeight)) + .filter(lines -> isValidBridgeLines(lines, participantCount, centerColumnIndex)) + .findFirst() + .orElseThrow(() -> new IllegalStateException(ERROR_GENERATION_FAILED)); + } + + private static List generateAttempt(int participantCount, int ladderHeight) { + return IntStream.range(0, ladderHeight) + .mapToObj(i -> new BridgeLine(generateConnectionStates(participantCount))) + .collect(Collectors.toList()); + } + + private static boolean isValidBridgeLines(List lines, int participantCount, int centerColumnIndex) { + return hasCenterBridge(lines, centerColumnIndex) && + hasAllColumnConnections(lines, participantCount - 1); + } + + private static boolean hasCenterBridge(List lines, int centerIndex) { + return lines.stream().anyMatch(line -> isConnectedAtCenter(line, centerIndex)); + } + + private static boolean hasAllColumnConnections(List lines, int expectedCount) { + Set connectedIndices = new HashSet<>(); + lines.forEach(line -> recordConnectedIndices(connectedIndices, getConnectionStates(line))); + return connectedIndices.size() == expectedCount; + } + + private static List getConnectionStates(BridgeLine line) { + return IntStream.range(0, line.width()) + .mapToObj(line::isConnectedAt) + .collect(Collectors.toList()); + } + + private static List generateConnectionStates(int participantCount) { + return IntStream.range(0, participantCount - 1) + .boxed() + .collect(ArrayList::new, (list, i) -> { + boolean canConnect = i == 0 || !list.get(i - 1); + list.add(canConnect && RANDOM.nextBoolean()); + }, List::addAll); + } + + private static boolean isConnectedAtCenter(BridgeLine line, int centerIndex) { + return centerIndex < line.width() && line.isConnectedAt(centerIndex); + } + + private static void recordConnectedIndices(Set connectedColumnIndices, List connectionStates) { + IntStream.range(0, connectionStates.size()) + .filter(i -> connectionStates.get(i)) + .forEach(connectedColumnIndices::add); + } +} diff --git a/src/main/java/domain/LadderPath.java b/src/main/java/domain/LadderPath.java new file mode 100644 index 00000000..e98437fb --- /dev/null +++ b/src/main/java/domain/LadderPath.java @@ -0,0 +1,37 @@ +package domain; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +public class LadderPath { + + private final List bridgeLines; + private final int numberOfColumns; + + public LadderPath(List bridgeLines, int numberOfColumns) { + this.bridgeLines = bridgeLines; + this.numberOfColumns = numberOfColumns; + } + + public Map mapStartToEndIndex() { + return IntStream.range(0, numberOfColumns) + .boxed() + .collect( + LinkedHashMap::new, + (map, i) -> map.put(i, tracePathFrom(i)), + Map::putAll + ); + } + + private int tracePathFrom(int startColumnIndex) { + int currentPosition = startColumnIndex; + + for (BridgeLine bridgeLine : bridgeLines) { + currentPosition = bridgeLine.nextPositionFrom(currentPosition); + } + + return currentPosition; + } +} diff --git a/src/main/java/domain/Participants.java b/src/main/java/domain/Participants.java new file mode 100644 index 00000000..80ef5cd9 --- /dev/null +++ b/src/main/java/domain/Participants.java @@ -0,0 +1,59 @@ +package domain; + +import java.util.List; + +public class Participants { + + private static final int MAX_NAME_LENGTH = 5; + private static final String RESERVED_KEYWORD = "all"; + + private static final String ERROR_NO_PARTICIPANTS = "[ERROR] 참여자는 한 명 이상이어야 합니다."; + private static final String ERROR_NAME_TOO_LONG = "[ERROR] 참여자 이름은 5자 이하만 가능합니다: "; + private static final String ERROR_RESERVED_NAME = "[ERROR] 'all'은 사용할 수 없는 이름입니다."; + + private final List names; + + private Participants(List names) { + this.names = names; + } + + public static Participants of(List names) { + validate(names); + return new Participants(names); + } + + public List getNames() { + return names; + } + + public int getCount() { + return names.size(); + } + + private static void validate(List names) { + validateNotEmpty(names); + validateNameLength(names); + validateReservedKeyword(names); + } + + private static void validateNotEmpty(List names) { + if (names.isEmpty()) { + throw new IllegalArgumentException(ERROR_NO_PARTICIPANTS); + } + } + + private static void validateNameLength(List names) { + names.stream() + .filter(name -> name.length() > MAX_NAME_LENGTH) + .findFirst() + .ifPresent(name -> { + throw new IllegalArgumentException(ERROR_NAME_TOO_LONG + name); + }); + } + + private static void validateReservedKeyword(List names) { + if (names.contains(RESERVED_KEYWORD)) { + throw new IllegalArgumentException(ERROR_RESERVED_NAME); + } + } +} diff --git a/src/main/java/domain/PlayerResults.java b/src/main/java/domain/PlayerResults.java new file mode 100644 index 00000000..4da46a53 --- /dev/null +++ b/src/main/java/domain/PlayerResults.java @@ -0,0 +1,48 @@ +package domain; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +public class PlayerResults { + + private static final String ERROR_RESULT_COUNT_MISMATCH = "[ERROR] 실행 결과 수가 참여자 수와 일치해야 합니다."; + + private final Map resultByPlayerName; + + private PlayerResults(Map resultByPlayerName) { + this.resultByPlayerName = resultByPlayerName; + } + + public static PlayerResults from(Participants participants, List outcomeLabels, Map startToEndIndexMap) { + validateSizeMatch(participants, outcomeLabels); + Map mappedResults = mapResults(participants.getNames(), outcomeLabels, startToEndIndexMap); + return new PlayerResults(mappedResults); + } + + public String resultOf(String playerName) { + return resultByPlayerName.get(playerName); + } + + public Map allResults() { + return resultByPlayerName; + } + + public boolean hasPlayer(String playerName) { + return resultByPlayerName.containsKey(playerName); + } + + private static void validateSizeMatch(Participants participants, List outcomeLabels) { + if (participants.getCount() != outcomeLabels.size()) { + throw new IllegalArgumentException(ERROR_RESULT_COUNT_MISMATCH); + } + } + + private static Map mapResults(List names, List labels, Map indexMap) { + return IntStream.range(0, names.size()) + .collect(LinkedHashMap::new, + (map, i) -> map.put(names.get(i), labels.get(indexMap.get(i))), + Map::putAll); + } +} diff --git a/src/main/java/dto/LadderBuildRequest.java b/src/main/java/dto/LadderBuildRequest.java new file mode 100644 index 00000000..27c4c24a --- /dev/null +++ b/src/main/java/dto/LadderBuildRequest.java @@ -0,0 +1,4 @@ +package dto; + +public record LadderBuildRequest(int columnCount, int rowCount) { +} diff --git a/src/main/java/dto/LadderBuildResponse.java b/src/main/java/dto/LadderBuildResponse.java new file mode 100644 index 00000000..f17596e5 --- /dev/null +++ b/src/main/java/dto/LadderBuildResponse.java @@ -0,0 +1,13 @@ +package dto; + +import domain.BridgeLine; +import domain.LadderBoard; + +import java.util.List; + +public record LadderBuildResponse(int columnCount, List lines) { + public static LadderBuildResponse from(LadderBoard board) { + return new LadderBuildResponse(board.getColumnCount(), board.getLines()); + } +} + diff --git a/src/main/java/dto/LadderResultResponse.java b/src/main/java/dto/LadderResultResponse.java new file mode 100644 index 00000000..d58069ad --- /dev/null +++ b/src/main/java/dto/LadderResultResponse.java @@ -0,0 +1,6 @@ +package dto; + +import java.util.Map; + +public record LadderResultResponse(Map positionMap) { +} diff --git a/src/main/java/util/NumericParser.java b/src/main/java/util/NumericParser.java new file mode 100644 index 00000000..8c36c5e4 --- /dev/null +++ b/src/main/java/util/NumericParser.java @@ -0,0 +1,14 @@ +package util; + +public class NumericParser { + + private static final String ERROR_INVALID_NUMBER = "[ERROR] 숫자를 입력해 주세요."; + + public static int parse(String input) { + try { + return Integer.parseInt(input.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ERROR_INVALID_NUMBER); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..6cb0a30b --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,32 @@ +package view; + +import util.NumericParser; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + + public List readParticipantNames() { + return Arrays.asList(readLine().split(",")); + } + + public List readResultLabels() { + return Arrays.asList(readLine().split(",")); + } + + public int readLadderHeight() { + return NumericParser.parse(readLine()); + } + + public String readResultRequest() { + return readLine(); + } + + private String readLine() { + return scanner.nextLine().trim(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..83196195 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,106 @@ +package view; + +import domain.BridgeLine; +import dto.LadderBuildResponse; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class OutputView { + + private static final String PARTICIPANT_PROMPT = "참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"; + private static final String RESULT_PROMPT = "실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)"; + private static final String HEIGHT_PROMPT = "사다리의 높이는 몇 칸인가요?"; + private static final String RESULT_SELECTION_PROMPT = "결과를 보고 싶은 사람은?"; + private static final String LADDER_RESULT_TITLE = "실행 결과"; + private static final String NAME_NOT_FOUND_MESSAGE = "해당 이름은 존재하지 않습니다."; + + private static final int DISPLAY_CELL_WIDTH = 6; + private static final String CELL_FORMAT = "%-" + DISPLAY_CELL_WIDTH + "s"; + private static final String RESULT_FORMAT = "%s : %s"; + + private static final String VERTICAL_BAR = "|"; + private static final String CONNECTED_LINE = "-----"; + private static final String EMPTY_LINE = " "; + private static final String INDENT = " "; + + public void printParticipantPrompt() { + System.out.println(PARTICIPANT_PROMPT); + } + + public void printResultPrompt() { + System.out.println(RESULT_PROMPT); + } + + public void printHeightPrompt() { + System.out.println(HEIGHT_PROMPT); + } + + public void printResultSelectionPrompt() { + System.out.println(RESULT_SELECTION_PROMPT); + } + + public void printLadderTitle() { + System.out.println(LADDER_RESULT_TITLE); + } + + public void printNameNotFound() { + System.out.println(NAME_NOT_FOUND_MESSAGE); + } + + public void printParticipantNames(List participantNames) { + System.out.println(joinAligned(participantNames)); + } + + public void printResultLabels(List resultLabels) { + System.out.println(joinAligned(resultLabels)); + } + + public void printBridgeLines(LadderBuildResponse response) { + int columnCount = response.columnCount(); + List lines = response.lines(); + + lines.forEach(line -> System.out.println(INDENT + formatBridgeLine(line, columnCount))); + } + + public void printAllResults(Map participantResults) { + participantResults.forEach((name, result) -> + System.out.println(formatResult(name, result))); + } + + public void printSingleResult(String resultValue) { + System.out.println(resultValue); + } + + public void printSingleResultWithTitle(String resultValue) { + printLadderTitle(); + printSingleResult(resultValue); + } + + private String formatBridgeLine(BridgeLine line, int columnCount) { + return IntStream.range(0, columnCount) + .mapToObj(i -> VERTICAL_BAR + bridgeRepresentation(line, i)) + .collect(Collectors.joining()); + } + + private String bridgeRepresentation(BridgeLine line, int index) { + if (line.isConnectedAt(index)) return CONNECTED_LINE; + return EMPTY_LINE; + } + + private String joinAligned(List values) { + return values.stream() + .map(this::formatCell) + .collect(Collectors.joining()); + } + + private String formatCell(String value) { + return String.format(CELL_FORMAT, value); + } + + private String formatResult(String name, String result) { + return String.format(RESULT_FORMAT, name, result); + } +} diff --git a/src/test/java/domain/BridgeLineTest.java b/src/test/java/domain/BridgeLineTest.java new file mode 100644 index 00000000..337f518a --- /dev/null +++ b/src/test/java/domain/BridgeLineTest.java @@ -0,0 +1,57 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class BridgeLineTest { + + @ParameterizedTest(name = "index {0}는 연결 상태가 {1}이다") + @MethodSource("connectionCases") + @DisplayName("isConnectedAt은 연결 여부를 반환한다") + void isConnectedAt_returnsExpected(int index, boolean expected) { + BridgeLine line = new BridgeLine(List.of(false, true, false)); + assertThat(line.isConnectedAt(index)).isEqualTo(expected); + } + + @Test + @DisplayName("width는 연결 수를 반환한다") + void width_returnsConnectionSize() { + BridgeLine line = new BridgeLine(List.of(true, false, true)); + assertThat(line.width()).isEqualTo(3); + } + + @ParameterizedTest(name = "위치 {0}에서 다음 위치는 {1}이다") + @MethodSource("nextPositionCases") + @DisplayName("nextPositionFrom은 연결에 따라 위치를 이동한다") + void nextPositionFrom_returnsExpected(int current, int expected) { + BridgeLine line = new BridgeLine(List.of(true, false, true)); + assertThat(line.nextPositionFrom(current)).isEqualTo(expected); + } + + private static Stream connectionCases() { + return Stream.of( + Arguments.of(0, false), + Arguments.of(1, true), + Arguments.of(2, false), + Arguments.of(-1, false), + Arguments.of(3, false) + ); + } + + private static Stream nextPositionCases() { + return Stream.of( + Arguments.of(0, 1), + Arguments.of(1, 0), + Arguments.of(2, 3), + Arguments.of(3, 2) + ); + } +} diff --git a/src/test/java/domain/LadderBoardTest.java b/src/test/java/domain/LadderBoardTest.java new file mode 100644 index 00000000..b909e6b2 --- /dev/null +++ b/src/test/java/domain/LadderBoardTest.java @@ -0,0 +1,56 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class LadderBoardTest { + + @Test + @DisplayName("요청에 따라 사다리를 생성하고 높이와 열 수가 일치해야 한다") + void build_createsValidLadderStructure() { + int columnCount = 4; + int rowCount = 5; + + LadderBoard board = LadderBoard.build(columnCount, rowCount); + + assertThat(board.getColumnCount()).isEqualTo(columnCount); + assertThat(board.getLines()).hasSize(rowCount); + } + + @Test + @DisplayName("생성된 사다리는 가운데 열에 적어도 하나의 연결이 포함되어야 한다") + void build_containsAtLeastOneCenterConnection() { + int columnCount = 5; + int rowCount = 10; + int centerIndex = (columnCount - 1) / 2; + + LadderBoard board = LadderBoard.build(columnCount, rowCount); + List lines = board.getLines(); + + boolean hasCenterConnection = lines.stream() + .anyMatch(line -> line.isConnectedAt(centerIndex)); + + assertThat(hasCenterConnection).isTrue(); + } + + @DisplayName("참가자 수가 2명 미만이면 예외를 던진다") + @Test + void throwsIfParticipantsLessThanTwo() { + assertThatThrownBy(() -> LadderBoard.build(1, 5)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 사다리는 최소 2명 이상의 참가자가 필요합니다."); + } + + @DisplayName("사다리 높이가 1 미만이면 예외를 던진다") + @Test + void throwsIfHeightLessThanOne() { + assertThatThrownBy(() -> LadderBoard.build(4, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 사다리 높이는 1 이상이어야 합니다."); + } +} diff --git a/src/test/java/domain/LadderPathTest.java b/src/test/java/domain/LadderPathTest.java new file mode 100644 index 00000000..00f162a1 --- /dev/null +++ b/src/test/java/domain/LadderPathTest.java @@ -0,0 +1,35 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; + +import static org.assertj.core.api.Assertions.assertThat; + +class LadderPathTest { + + @Test + @DisplayName("3줄 사다리에서 각 참가자의 최종 위치를 정확히 계산한다") + void mapStartToEndIndex_correctlyMapsPath() { + List bridgeLines = List.of( + new BridgeLine(List.of(true, false, true)), + new BridgeLine(List.of(false, true, false)), + new BridgeLine(List.of(true, false, false)) + ); + + LadderPath path = new LadderPath(bridgeLines, 4); + + Map result = path.mapStartToEndIndex(); + + Map expected = new LinkedHashMap<>(); + expected.put(0, 2); + expected.put(1, 1); + expected.put(2, 3); + expected.put(3, 0); + + assertThat(result).containsExactlyEntriesOf(expected); + } +} diff --git a/src/test/java/domain/ParticipantsTest.java b/src/test/java/domain/ParticipantsTest.java new file mode 100644 index 00000000..3ba604e7 --- /dev/null +++ b/src/test/java/domain/ParticipantsTest.java @@ -0,0 +1,35 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ParticipantsTest { + + @Test + @DisplayName("참가자가 없을 경우 예외 발생") + void throwsIfEmptyParticipants() { + List empty = List.of(); + assertThrows(IllegalArgumentException.class, () -> + Participants.of(empty)); + } + + @Test + @DisplayName("이름이 5자를 초과할 경우 예외 발생") + void throwsIfNameTooLong() { + List names = List.of("toolongname"); + assertThrows(IllegalArgumentException.class, () -> + Participants.of(names)); + } + + @Test + @DisplayName("이름으로 예약어 'all'이 포함되면 예외 발생") + void throwsIfContainsReservedKeyword() { + List names = List.of("neo", "all", "brie"); + assertThrows(IllegalArgumentException.class, () -> + Participants.of(names)); + } +} diff --git a/src/test/java/domain/PlayerResultsTest.java b/src/test/java/domain/PlayerResultsTest.java new file mode 100644 index 00000000..d22d809a --- /dev/null +++ b/src/test/java/domain/PlayerResultsTest.java @@ -0,0 +1,46 @@ +package domain; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; + +class PlayerResultsTest { + + @Test + @DisplayName("참가자 이름, 결과, 인덱스 맵으로 결과를 생성한다") + void from_createsCorrectMapping() { + Participants participants = Participants.of(List.of("neo", "brown", "brie", "tommy")); + List outcomeLabels = List.of("꽝", "5000", "꽝", "3000"); + + Map startToEndIndexMap = Map.of( + 0, 1, + 1, 3, + 2, 0, + 3, 2 + ); + + PlayerResults results = PlayerResults.from(participants, outcomeLabels, startToEndIndexMap); + + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(results.resultOf("neo")).isEqualTo("5000"); + softly.assertThat(results.resultOf("brown")).isEqualTo("3000"); + softly.assertThat(results.resultOf("brie")).isEqualTo("꽝"); + softly.assertThat(results.resultOf("tommy")).isEqualTo("꽝"); + + softly.assertThat(results.hasPlayer("neo")).isTrue(); + softly.assertThat(results.hasPlayer("noname")).isFalse(); + + Map expected = new LinkedHashMap<>(); + expected.put("neo", "5000"); + expected.put("brown", "3000"); + expected.put("brie", "꽝"); + expected.put("tommy", "꽝"); + + softly.assertThat(results.allResults()).containsExactlyEntriesOf(expected); + softly.assertAll(); + } +} diff --git a/src/test/java/util/NumericParserTest.java b/src/test/java/util/NumericParserTest.java new file mode 100644 index 00000000..7b6bcd7d --- /dev/null +++ b/src/test/java/util/NumericParserTest.java @@ -0,0 +1,24 @@ +package util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class NumericParserTest { + + @Test + @DisplayName("정수 문자열을 숫자로 파싱한다") + void parsesValidInteger() { + int result = NumericParser.parse("42"); + assertThat(result).isEqualTo(42); + } + + @Test + @DisplayName("숫자가 아닌 문자열을 파싱하면 예외를 던진다") + void throwsIfNotNumber() { + assertThatThrownBy(() -> NumericParser.parse("abc")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 숫자를 입력해 주세요."); + } +}