diff --git a/README.md b/README.md new file mode 100644 index 00000000..3f8dc7f0 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# 자바 사다리 게임 + +## 주요 기능 + +* 쉼표로 구분하여 여러 플레이어 이름 입력 +* 사다리 끝에 위치할 보상 입력 +* 사다리 높이 설정 +* 가로 라인이 무작위로 생성되는 사다리 +* 최종 생성된 사다리 모양 출력 +* 각 플레이어의 최종 결과(보상) 표시 +* 전체 플레이어 또는 특정 플레이어의 결과 조회 기능 + +## 코드 설명 +* **`Application.java`**: 프로그램의 시작점입니다. 전체 게임의 흐름을 제어합니다. + +* **`controller` 패키지**: + * `Controller.java`: 사용자의 입력을 받아 Model을 업데이트하고, 결과를 View에 전달하는 등 게임의 핵심 로직을 관리합니다. + +* **`model` 패키지**: 게임의 데이터와 비즈니스 로직을 담당합니다. + * `Player.java`: 게임에 참여하는 플레이어들의 정보를 관리합니다. + * `Rewards.java`: 사다리 게임의 결과로 주어지는 보상 정보를 관리합니다. + * `Bridge.java`: 전체 사다리의 구조를 나타냅니다. 가로 줄(`BridgeStep`)들의 리스트로 구성됩니다. + * `BridgeStep.java`: 사다리의 한 칸이 가로 줄을 가지고 있는지 여부(`EXIST` 또는 `EMPTY`)를 나타내는 Enum입니다. + * `LadderDescentService.java`: 플레이어가 사다리를 내려가는 과정을 처리하는 서비스 클래스입니다. + * `SplitMethod.java`: 사용자 입력을 쉼표 기준으로 분리하는 유틸리티 클래스입니다. + +* **`view` 패키지**: 사용자 인터페이스(입력/출력)를 담당합니다. + * `InputView.java`: 사용자로부터 플레이어 이름, 보상, 사다리 높이 등의 입력을 받습니다. + * `OutputView.java`: 게임 진행 상황, 사다리 모양, 최종 결과 등을 콘솔에 출력합니다. diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..ba6c418b --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,8 @@ +import Controller.Controller; + +public class Application { + public static void main(String[] args) { + Controller controller = new Controller(); + controller.startLadder(); + } +} diff --git a/src/main/java/Controller/Controller.java b/src/main/java/Controller/Controller.java new file mode 100644 index 00000000..bd706c7e --- /dev/null +++ b/src/main/java/Controller/Controller.java @@ -0,0 +1,78 @@ +package Controller; + +import Model.Bridge; +import Model.Player; +import Model.Rewards; +import view.InputView; +import view.OutputView; + +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import Model.LadderDescentService; +import Model.LadderResult; + +public class Controller { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + + public void startLadder() { + int height; + + Player players = getPlayer(); + Rewards rewards = getRewards(); + + outputView.askLadderHeight(); + height = inputView.getWidthAndHeight(); + + outputView.printPlayersAndRewards(players.getPlayers()); + Bridge bridge = new Bridge(height, players.getPlayersNumber() - 1); + outputView.printBridge(bridge); + + outputView.printPlayersAndRewards(rewards.getRewards()); + + LadderDescentService descentService = new LadderDescentService(bridge); + LadderResult ladderResult = descentService.calculateAllResults(players.getPlayersNumber()); + + inputView.getScanner().nextLine(); + + while (true) { + outputView.askResults(); + String string = inputView.getScanner().nextLine(); + + if (string.trim().equals("all")) { + outputView.printAllResults(players, rewards, ladderResult); + break; + } + outputView.printSpecificResults(players, rewards, ladderResult, string); + } + } + + private Rewards getRewards() { + outputView.askRewards(); + String rewardsInputs = inputView.getScanner().nextLine(); + return new Rewards(split(rewardsInputs)); + } + + private Player getPlayer() { + outputView.askPlayers(); + String playerInputs = inputView.getScanner().nextLine(); + return new Player(split(playerInputs)); + } + + private static List split(String input) { + + final Pattern DELIMITER = Pattern.compile(","); + + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + return DELIMITER.splitAsStream(input) + .map(String::trim) // 양쪽 공백 제거 + .filter(name -> !name.isEmpty()) // 빈 값 제거 (예: "neo,,brie") + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/Model/Bridge.java b/src/main/java/Model/Bridge.java new file mode 100644 index 00000000..cf56a24e --- /dev/null +++ b/src/main/java/Model/Bridge.java @@ -0,0 +1,32 @@ +package Model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Collections; + +public class Bridge { + + private final List rows; + private final Random random = new Random(); + + public Bridge(int height, int width) { + this.rows = new ArrayList<>(); + initializeBridgeRows(height, width); + } + + public Bridge(List rows) { + this.rows = rows; + } + + + private void initializeBridgeRows(int height, int width) { + for (int i = 0; i < height; i++) { + this.rows.add(new BridgeRow(width, this.random)); + } + } + + public List getRows() { + return Collections.unmodifiableList(this.rows); + } +} diff --git a/src/main/java/Model/BridgeRow.java b/src/main/java/Model/BridgeRow.java new file mode 100644 index 00000000..8e142dc4 --- /dev/null +++ b/src/main/java/Model/BridgeRow.java @@ -0,0 +1,57 @@ +package Model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Collections; + +public class BridgeRow { + + private final List steps; + + public BridgeRow(int width, Random random) { + this.steps = createOneRow(width, random); + } + + private List createOneRow(int width, Random random) { + List row = generateRandomRow(width, random); + ensureNoSideBySideSteps(row, random); + return row; + } + + private List generateRandomRow(int width, Random random) { + List row = new ArrayList<>(); + for (int i = 0; i < width; i++) { + row.add(BridgeStep.createRandomStep(random)); + } + return row; + } + + private void ensureNoSideBySideSteps(List row, Random random) { + for (int i = 0; i < row.size() - 1; i++) { + fixSideBySideStepIfNeeded(row, i, random); + } + } + + private void fixSideBySideStepIfNeeded(List row, int index, Random random) { + if (areBothStepsExisting(row, index)) { + setOneToNoneRandomly(row, index, random); + } + } + + private boolean areBothStepsExisting(List row, int index) { + return row.get(index).isExist() && row.get(index + 1).isExist(); + } + + private void setOneToNoneRandomly(List row, int index, Random random) { + if (random.nextBoolean()) { + row.set(index, BridgeStep.NONE); + return; + } + row.set(index + 1, BridgeStep.NONE); + } + + public List getSteps() { + return Collections.unmodifiableList(this.steps); + } +} diff --git a/src/main/java/Model/BridgeStep.java b/src/main/java/Model/BridgeStep.java new file mode 100644 index 00000000..926b4daa --- /dev/null +++ b/src/main/java/Model/BridgeStep.java @@ -0,0 +1,26 @@ +// BridgeStep.java +package Model; + +import java.util.Random; + +public enum BridgeStep { + NONE(0), // 다리 없음 + EXIST(1); // 다리 있음 + + private final int value; + + BridgeStep(int value) { + this.value = value; + } + + public static BridgeStep createRandomStep(Random random) { + if (random.nextBoolean()) { + return EXIST; + } + return NONE; + } + + public boolean isExist() { + return this == EXIST; + } +} diff --git a/src/main/java/Model/LadderDescentService.java b/src/main/java/Model/LadderDescentService.java new file mode 100644 index 00000000..aa68bebc --- /dev/null +++ b/src/main/java/Model/LadderDescentService.java @@ -0,0 +1,55 @@ +package Model; + +import java.util.List; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class LadderDescentService { + private final Bridge bridge; + + public LadderDescentService(Bridge bridge) { + this.bridge = bridge; + } + + public LadderResult calculateAllResults(int numberOfPlayers) { + Map resultMap = IntStream.range(0, numberOfPlayers) + .boxed() + .collect(Collectors.toMap(i -> i, this::descent)); + return new LadderResult(resultMap); + } + + private int descent(int curPos){ + int pos = curPos; + for (BridgeRow row : bridge.getRows()) { + List steps = row.getSteps(); + pos += checkRightBridge(steps,pos) + checkLeftBridge(steps,pos); + } + return pos; + } + + + private int checkRightBridge(List row, int pos) { + if (pos >= row.size()) { + return 0; + } + if (row.get(pos) == BridgeStep.EXIST) { + return 1; + } + return 0; + } + + private int checkLeftBridge(List row, int pos) { + if (pos == 0) { + return 0; + } + if (row.get(pos - 1) == BridgeStep.EXIST) { + return -1; + } + return 0; + } + + +} diff --git a/src/main/java/Model/LadderResult.java b/src/main/java/Model/LadderResult.java new file mode 100644 index 00000000..0048e7e6 --- /dev/null +++ b/src/main/java/Model/LadderResult.java @@ -0,0 +1,20 @@ +package Model; + +import java.util.Collections; +import java.util.Map; + +public class LadderResult { + private final Map resultMap; + + public LadderResult(Map resultMap) { + this.resultMap = resultMap; + } + + public int getDestinationIndex(int startIndex) { + return resultMap.get(startIndex); + } + + public Map getResultMap() { + return Collections.unmodifiableMap(resultMap); + } +} diff --git a/src/main/java/Model/Player.java b/src/main/java/Model/Player.java new file mode 100644 index 00000000..57120fb2 --- /dev/null +++ b/src/main/java/Model/Player.java @@ -0,0 +1,29 @@ +package Model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Player { + List players; + + public Player(List players) { + this.players = players; + } + + public List getPlayers() { + return Collections.unmodifiableList(players); + } + + public int getPlayersNumber() { + return players.size(); + } + + public String getPlayerName(int index) { + return players.get(index); + } + + public int getPlayerIndex(String name) { + return players.indexOf(name); + } +} diff --git a/src/main/java/Model/Rewards.java b/src/main/java/Model/Rewards.java new file mode 100644 index 00000000..c0fc1efa --- /dev/null +++ b/src/main/java/Model/Rewards.java @@ -0,0 +1,20 @@ +package Model; + +import java.util.Collections; +import java.util.List; + +public class Rewards { + private final List rewards; + + public Rewards(List rewards) { + this.rewards = rewards; + } + + public String getReward(int index) { + return rewards.get(index); + } + + public List getRewards() { + return Collections.unmodifiableList(rewards); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..aaf069ba --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,15 @@ +package view; + +import java.util.Scanner; + +public class InputView { + Scanner scanner = new Scanner(System.in); + + public int getWidthAndHeight() { + return scanner.nextInt(); + } + + public Scanner getScanner() { + return scanner; + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..4e7ed251 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,77 @@ +package view; + +import Model.*; + +import java.util.Arrays; +import java.util.List; + +public class OutputView { + + public void printBridge(Bridge bridge) { + for (BridgeRow row : bridge.getRows()) { + printBridgeRow(row); + System.out.print("|"); + System.out.println(); + } + } + + private void printBridgeRow(BridgeRow row) { + row.getSteps().forEach(bridgeStep -> { + System.out.print("|"); + printBridgeStep(bridgeStep); + }); + } + + public void printPlayersAndRewards(List strings) { + strings.forEach(string -> System.out.printf("%-6s", string)); + System.out.println(); + } + + private void printBridgeStep(BridgeStep bridgeStep) { + if (bridgeStep == BridgeStep.EXIST) { + System.out.print("-----"); + } else { + System.out.print(" "); + } + } + + public void askResults() { + System.out.println("결과를 보고 싶은 사람은?"); + } + + public void printAllResults(Player players, Rewards rewards, LadderResult ladderResult) { + for (int i = 0; i < players.getPlayersNumber(); i++) { + String playerName = players.getPlayerName(i); + int destinationIndex = ladderResult.getDestinationIndex(i); + String reward = rewards.getReward(destinationIndex); + System.out.println(playerName + " : " + reward); + } + } + + public void printSpecificResults(Player players, Rewards rewards, LadderResult ladderResult, String specificNames) { + Arrays.stream(specificNames.split(",")) + .map(String::trim) + .forEach(trimmedName -> { + try { + int playerIndex = players.getPlayerIndex(trimmedName); + int destinationIndex = ladderResult.getDestinationIndex(playerIndex); + String reward = rewards.getReward(destinationIndex); + System.out.println(trimmedName + " : " + reward); + } catch (Exception e) { + System.out.println("[ERROR] 존재하지 않는 플레이어 이름입니다: " + trimmedName); + } + }); + } + + public void askPlayers() { + System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"); + } + + public void askRewards() { + System.out.println("실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)"); + } + + public void askLadderHeight() { + System.out.println("사다리의 높이는 몇 개인가요?"); + } +} diff --git a/src/test/java/model/LadderGameModelTest.java b/src/test/java/model/LadderGameModelTest.java new file mode 100644 index 00000000..e975639e --- /dev/null +++ b/src/test/java/model/LadderGameModelTest.java @@ -0,0 +1,111 @@ +package model; + +import Model.Bridge; +import Model.BridgeRow; +import Model.BridgeStep; +import Model.LadderDescentService; +import Model.LadderResult; +import Model.Player; +import Model.Rewards; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("사다리 게임 모델 테스트") +public class LadderGameModelTest { + + @Test + @DisplayName("플레이어 이름은 쉼표로 구분되어 생성된다") + void playerNamesAreSplit() { + // given + String inputNames = "pobi,crong,honux"; + List names = Arrays.asList(inputNames.split(",")); + + // when + Player players = new Player(names); + List playerList = players.getPlayers(); + + // then + assertThat(playerList).containsExactly("pobi", "crong", "honux"); + } + + @Test + @DisplayName("보상 목록은 쉼표로 구분되어 생성된다") + void rewardsAreSplit() { + // given + String inputRewards = "꽝,5000,꽝"; + List rewardNames = Arrays.asList(inputRewards.split(",")); + + // when + Rewards rewards = new Rewards(rewardNames); + + // then + assertEquals("꽝", rewards.getReward(0)); + assertEquals("5000", rewards.getReward(1)); + } + + private Bridge createTestBridge(List steps) { + return new Bridge(Collections.singletonList(new BridgeRow(steps.size(), new Random()) { + @Override + public List getSteps() { + return steps; + } + })); + } + + @Test + @DisplayName("사다리 결과 계산 - 연결선이 없으면 그대로 내려간다") + void calculateResultStaysWhenNoConnection() { + // given + Bridge bridge = createTestBridge(Arrays.asList(BridgeStep.NONE, BridgeStep.NONE, BridgeStep.NONE)); + LadderDescentService descentService = new LadderDescentService(bridge); + + // when + LadderResult result = descentService.calculateAllResults(3); + + // then + assertEquals(0, result.getDestinationIndex(0)); + assertEquals(1, result.getDestinationIndex(1)); + assertEquals(2, result.getDestinationIndex(2)); + } + + @Test + @DisplayName("사다리 결과 계산 - 오른쪽으로 이동") + void calculateResultMovesRight() { + // given + Bridge bridge = createTestBridge(Arrays.asList(BridgeStep.EXIST, BridgeStep.NONE, BridgeStep.NONE)); + LadderDescentService descentService = new LadderDescentService(bridge); + + // when + LadderResult result = descentService.calculateAllResults(3); + + // then + assertEquals(1, result.getDestinationIndex(0)); + assertEquals(0, result.getDestinationIndex(1)); + assertEquals(2, result.getDestinationIndex(2)); + } + + @Test + @DisplayName("사다리 결과 계산 - 왼쪽으로 이동") + void calculateResultMovesLeft() { + // given + Bridge bridge = createTestBridge(Arrays.asList(BridgeStep.NONE, BridgeStep.EXIST, BridgeStep.NONE)); + LadderDescentService descentService = new LadderDescentService(bridge); + + // when + LadderResult result = descentService.calculateAllResults(3); + + // then + assertEquals(0, result.getDestinationIndex(0)); + assertEquals(2, result.getDestinationIndex(1)); + assertEquals(1, result.getDestinationIndex(2)); + } +} \ No newline at end of file diff --git a/src/test/java/view/OutputViewTest.java b/src/test/java/view/OutputViewTest.java new file mode 100644 index 00000000..5f3fa24a --- /dev/null +++ b/src/test/java/view/OutputViewTest.java @@ -0,0 +1,138 @@ +package view; + +import Model.LadderResult; +import Model.Player; +import Model.Rewards; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutputViewTest { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + } + + private Player createTestPlayer() { + return new Player(Arrays.asList("pobi", "crong", "honux")); + } + + private Rewards createTestRewards() { + return new Rewards(Arrays.asList("꽝", "5000", "꽝")); + } + + private LadderResult createTestLadderResult() { + Map resultMap = new HashMap<>(); + resultMap.put(0, 1); // pobi -> 5000 + resultMap.put(1, 2); // crong -> 꽝 + resultMap.put(2, 0); // honux -> 꽝 + return new LadderResult(resultMap); + } + + @Test + @DisplayName("'all' 입력 시 모든 결과를 출력한다") + void printAllResultsTest() { + // given + OutputView outputView = new OutputView(); + Player players = createTestPlayer(); + Rewards rewards = createTestRewards(); + LadderResult ladderResult = createTestLadderResult(); + + // when + outputView.printAllResults(players, rewards, ladderResult); + + // then + String expectedOutput = "pobi : 5000" + System.lineSeparator() + + "crong : 꽝" + System.lineSeparator() + + "honux : 꽝" + System.lineSeparator(); + assertThat(outContent.toString()).isEqualTo(expectedOutput); + } + + @Test + @DisplayName("특정 플레이어 한 명의 결과를 출력한다") + void printSinglePlayerResult() { + // given + OutputView outputView = new OutputView(); + Player players = createTestPlayer(); + Rewards rewards = createTestRewards(); + LadderResult ladderResult = createTestLadderResult(); + + // when + outputView.printSpecificResults(players, rewards, ladderResult, "pobi"); + + // then + String expectedOutput = "pobi : 5000" + System.lineSeparator(); + assertThat(outContent.toString()).isEqualTo(expectedOutput); + } + + @Test + @DisplayName("여러 명의 결과를 쉼표로 구분하여 출력한다") + void printMultiplePlayerResults() { + // given + OutputView outputView = new OutputView(); + Player players = createTestPlayer(); + Rewards rewards = createTestRewards(); + LadderResult ladderResult = createTestLadderResult(); + + // when + outputView.printSpecificResults(players, rewards, ladderResult, "pobi,honux"); + + // then + String expectedOutput = "pobi : 5000" + System.lineSeparator() + + "honux : 꽝" + System.lineSeparator(); + assertThat(outContent.toString()).isEqualTo(expectedOutput); + } + + @Test + @DisplayName("입력값에 공백이 있어도 정상 처리한다") + void printResultWithWhitespace() { + // given + OutputView outputView = new OutputView(); + Player players = createTestPlayer(); + Rewards rewards = createTestRewards(); + LadderResult ladderResult = createTestLadderResult(); + + // when + outputView.printSpecificResults(players, rewards, ladderResult, " crong, honux "); + + // then + String expectedOutput = "crong : 꽝" + System.lineSeparator() + + "honux : 꽝" + System.lineSeparator(); + assertThat(outContent.toString()).isEqualTo(expectedOutput); + } + + @Test + @DisplayName("존재하지 않는 플레이어 이름 입력 시 에러 메시지를 출력한다") + void printResultForInvalidPlayer() { + // given + OutputView outputView = new OutputView(); + Player players = createTestPlayer(); + Rewards rewards = createTestRewards(); + LadderResult ladderResult = createTestLadderResult(); + + // when + outputView.printSpecificResults(players, rewards, ladderResult, "unknown"); + + // then + String expectedOutput = "[ERROR] 존재하지 않는 플레이어 이름입니다: unknown" + System.lineSeparator(); + assertThat(outContent.toString()).isEqualTo(expectedOutput); + } +}