diff --git a/README.md b/README.md new file mode 100644 index 00000000..1bd5d5c5 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# 사다리 게임(Ladder Game) + +## 🎯 게임 개요 + +이 게임은 참여 인원과 실행 결과를 입력하여 개인별 결과를 출력해주는 사다리 게임입니다! + +## 📌 게임 진행 방식 + +- **참여 인원 입력**: 참여 인원의 이름을 입력해주세요. (이름은 쉼표(,)를 기준으로 구분하고 최대 5글자) +- **실행 결과 입력**: 원하는 실행 결과를 입력해주세요. (쉼표(,)를 기준으로 구분, 예시-꽝, 5000 등) +- **사다리 높이 입력**: 원하는 사다리 높이를 입력해주세요. +- **전체 결과 발표**: 전체 사다리 결과가 나옵니다. +- **지목 결과 발표**: 지목한 사람의 실행 결과를 확인합니다. (예-tommy or all) + +## 🚀 실행 방법 + +1. 프로그램을 실행합니다. +2. 참여 인원과 실행 결과를 입력합니다. +3. 사다리 높이를 입력해 사다리를 만듭니다. +4. 궁금한 사람의 이름을 지목하여 그 결과를 확인하세요! 🎉 + +## 🛠 주요 기능 + +- **사다리 생성**: 원하는 높이의 사다리 생성. +- **결과 계산**: 각 인원의 사다리 타기 결과 계산. + +## ✅ 체크리스트 + +### 요구사항 +- [v] 게임 개요 및 진행 방식 정리 +- [v] 필요한 기능 도출 (입력, 번호 생성, 결과 처리) + +### 리팩토링 +- [v] enum 클래스 이용 +- [v] 매직 넘버 제거 +- [v] indent depth 1까지 허용 +- [v] 일급 컬렉션 사용 +- [v] 함수형 문법 사용 +- [v] 모든 엔티티 작게 유지 + +### 테스트 +- [v] 정상 입력값에 대한 동작 확인 +- [v] 잘못된 입력값 처리 테스트 +- [v] 사다리 생성 동작 확인 +- [v] 결과값 확인 + diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..0b20749e --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,15 @@ +import controller.LadderController; +import util.RandomLadderGenerator; +import view.InputView; +import view.OutputView; + +public class Application { + + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + RandomLadderGenerator generator = new RandomLadderGenerator(); + LadderController controller = new LadderController(inputView, outputView, generator); + controller.run(); + } +} diff --git a/src/main/java/controller/LadderController.java b/src/main/java/controller/LadderController.java new file mode 100644 index 00000000..36aa4629 --- /dev/null +++ b/src/main/java/controller/LadderController.java @@ -0,0 +1,100 @@ +package controller; + +import static view.InputView.INPUT_EXCEPTION_MESSAGE; + +import java.util.Map; +import java.util.stream.Collectors; +import model.goal.Goal; +import model.goal.Goals; +import model.ladder.Ladder; +import model.LadderGame; +import model.ladder.LadderHeight; +import java.util.ArrayList; +import java.util.List; +import model.player.Player; +import model.player.PlayerName; +import model.player.Players; +import model.player.Position; +import util.LadderGenerator; +import view.InputView; +import view.OutputView; + +public class LadderController { + + private final InputView inputView; + private final OutputView outputView; + private final LadderGenerator generator; + + public LadderController(InputView inputView, OutputView outputView, LadderGenerator generator) { + this.inputView = inputView; + this.outputView = outputView; + this.generator = generator; + } + + public void run() { + Players players = createPlayers(); + Goals goals = createGoals(players); + LadderHeight height = createLadderHeight(players.size()); + Ladder ladder = Ladder.of(players, height, generator); + LadderGame game = new LadderGame(ladder); + Map results = game.play(players, goals); + outputView.printLadder(ladder, players, goals); + showResults(players, results); + } + + private Players createPlayers() { + try { + List rawNames = inputView.inputPlayers(); + return Players.from(rawNames); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return createPlayers(); + } + } + + private Goals createGoals(Players players) { + try { + List rawGoals = inputView.inputGoals(); + return Goals.from(rawGoals, players.size()); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return createGoals(players); + } + } + + private LadderHeight createLadderHeight(int playerCount) { + try { + int height = inputView.inputLadderHeight(); + return new LadderHeight(height, playerCount); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return createLadderHeight(playerCount); + } + } + + private void showResults(Players players, Map results) { + String input = inputView.inputPlayerForResult(); + while (!input.isBlank()) { + input = processResultInput(input, players, results); + } + } + + private String processResultInput(String input, Players players, Map results) { + try { + selectResult(input, players, results); + return inputView.inputPlayerForResult(); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return inputView.inputPlayerForResult(); + } + } + + private void selectResult(String input, Players players, Map results) { + if (input.equals("all")) { + outputView.printAllResults(results); + return; + } + players.validateContainsPlayer(input); + outputView.printSingleResult(input, players, results); + } +} diff --git a/src/main/java/model/LadderGame.java b/src/main/java/model/LadderGame.java new file mode 100644 index 00000000..dbadd5be --- /dev/null +++ b/src/main/java/model/LadderGame.java @@ -0,0 +1,29 @@ +package model; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import model.ladder.Ladder; +import model.player.Position; +import model.goal.Goal; +import model.goal.Goals; +import model.player.Player; +import model.player.Players; + +public class LadderGame { + + private final Ladder ladder; + + public LadderGame(Ladder ladder) { + this.ladder = ladder; + } + + public Map play(Players players, Goals goals) { + return IntStream.range(0, players.size()) + .boxed() + .collect(Collectors.toMap( + players::getPlayerAt, + index -> goals.getGoalAt(ladder.getGoalsPosition(new Position(index)).getValue()) + )); + } +} diff --git a/src/main/java/model/goal/Goal.java b/src/main/java/model/goal/Goal.java new file mode 100644 index 00000000..037f768e --- /dev/null +++ b/src/main/java/model/goal/Goal.java @@ -0,0 +1,23 @@ +package model.goal; + +public class Goal { + + private final int MAXINUM_GOAL_LENGTH = 5; + private final String goal; + + public Goal(String goal) { + validateGoal(goal); + this.goal = goal; + } + + private void validateGoal(String goal) { + if (goal == null || goal.isBlank() || goal.length() > MAXINUM_GOAL_LENGTH) { + throw new IllegalArgumentException("실행 결과는 1~5글자여야 합니다."); + } + + } + + public String getGoal() { + return goal; + } +} diff --git a/src/main/java/model/goal/Goals.java b/src/main/java/model/goal/Goals.java new file mode 100644 index 00000000..8503c9c7 --- /dev/null +++ b/src/main/java/model/goal/Goals.java @@ -0,0 +1,38 @@ +package model.goal; + +import java.util.List; + +public class Goals { + + private final List goals; + + public Goals(List goals, int playerCount) { + validateGoals(goals, playerCount); + this.goals = List.copyOf(goals); + + } + + public static Goals from(List rawGoals, int playerCount) { + List goalList = rawGoals.stream() + .map(Goal::new) + .toList(); + return new Goals(goalList, playerCount); + } + + private void validateGoals(List goals, int playerCount) { + if (goals.size() != playerCount) { + throw new IllegalArgumentException("실행 결과 수와 참여자 수는 같아야 합니다."); + } + } + + public Goal getGoalAt(int index) { + if (index < 0 || index >= goals.size()) { + throw new IllegalArgumentException("유효하지 않은 인덱스입니다: " + index); + } + return goals.get(index); + } + + public List getGoals() { + return List.copyOf(goals); + } +} diff --git a/src/main/java/model/ladder/Direction.java b/src/main/java/model/ladder/Direction.java new file mode 100644 index 00000000..5ddce6c0 --- /dev/null +++ b/src/main/java/model/ladder/Direction.java @@ -0,0 +1,51 @@ +package model.ladder; + +import java.util.List; +import model.player.Position; + +public enum Direction { + + // 현재 위치의 오른쪽에 사다리 연결이 있을 경우 오른쪽 이동 + RIGHT( + (position, points) -> position.getValue() < points.size() + && points.get(position.getValue()).isConnected(), + Position::moveToRight + ), + // 현재 위치의 왼쪽에 사다리 연결이 있을 경우 왼쪽 이동 + LEFT( + (position, points) -> position.getValue() > 0 + && points.get(position.getValue() - 1).isConnected(), + Position::moveToLeft + ), + // 그 외의 경우, 현재 위치에 그대로 머무름 + STAY( + (position, points) -> true, + position -> position + ); + + private final MoveStrategy strategy; + private final PositionChanger changer; + + Direction(MoveStrategy strategy, PositionChanger changer) { + this.strategy = strategy; + this.changer = changer; + } + + public boolean isMovable(Position position, List points) { + return strategy.isMovable(position, points); + } + + public Position move(Position position) { + return changer.move(position); + } + + @FunctionalInterface + interface MoveStrategy { // 지금 이 방향으로 움직일 수 있는가? + boolean isMovable(Position position, List points); + } + + @FunctionalInterface + interface PositionChanger { // 이동했을 때 Position 어디인가? + Position move(Position position); + } +} diff --git a/src/main/java/model/ladder/Ladder.java b/src/main/java/model/ladder/Ladder.java new file mode 100644 index 00000000..b68f63e1 --- /dev/null +++ b/src/main/java/model/ladder/Ladder.java @@ -0,0 +1,88 @@ +package model.ladder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import model.player.Players; +import model.player.Position; +import util.LadderGenerator; + +public class Ladder { + + private final LadderHeight height; + private final List lines; + + public Ladder(LadderHeight height, List lines) { + this.height = height; + this.lines = lines; + } + + public static Ladder of(Players players, LadderHeight height, LadderGenerator ladderGenerator) { + List lines = new ArrayList<>(); + Ladder ladder = new Ladder(height, lines); + ladder.addLines(players.size(), ladderGenerator); + return ladder; + } + + private void addLines(int playersCount, LadderGenerator ladderGenerator) { + for (int i = 0; i < getLadderHeight(); i++) { + lines.add(Line.create(playersCount, ladderGenerator)); + } + if (isLadderNotConnected(playersCount)) { + lines.clear(); + addLines(playersCount, ladderGenerator); + } + } + + // Line이 최소 한 번 연결되었는지 검증 + private boolean isLadderNotConnected(int playersCount) { + Boolean[] isConnectedAt = getLadderConnectionStatus(playersCount); + return Arrays.stream(isConnectedAt) + .collect(Collectors.toSet()) + .size() != 1; + } + + // 전체 Line별 연결 상태 확인 + private Boolean[] getLadderConnectionStatus(int playersCount) { + int width = playersCount - 1; + Boolean[] isConnectedAt = new Boolean[width]; + Arrays.fill(isConnectedAt, false); + lines.forEach(line -> getLineConnectionStatus(isConnectedAt, line)); + return isConnectedAt; + } + + // 특정 Line의 연결 상태 확인 + private void getLineConnectionStatus(Boolean[] isConnectedAt, Line line) { + IntStream.range(0, line.getLadderWidth()) + .forEach(i -> getPointConnectionStatus(isConnectedAt, line, i)); + } + + // 특정 Line의 특정 Point 연결 상태 확인 + private void getPointConnectionStatus(Boolean[] isConnectedAt, Line line, int i) { + if (line.getPoints().get(i).isConnected()) { + isConnectedAt[i] = true; + } + } + + public int getLadderHeight() { + return height.getLadderHeight(); + } + + public int getLadderWidth(Players players) { + return players.size(); + } + + public Position getGoalsPosition(Position start) { + Position current = start; + for (Line line : lines) { + current = line.move(current); + } + return current; + } + + public List getLines() { + return List.copyOf(lines); + } +} diff --git a/src/main/java/model/ladder/LadderHeight.java b/src/main/java/model/ladder/LadderHeight.java new file mode 100644 index 00000000..766675ad --- /dev/null +++ b/src/main/java/model/ladder/LadderHeight.java @@ -0,0 +1,23 @@ +package model.ladder; + +public class LadderHeight { + + private static final int MAXIMUM_LADDER_HEIGHT = 20; + + private final int height; + + public LadderHeight(int height, int playerCount) { + validateLadderHeight(height, playerCount); + this.height = height; + } + + private void validateLadderHeight(int height, int playerCount) { + if (height < playerCount - 1 || height > MAXIMUM_LADDER_HEIGHT) { + throw new IllegalArgumentException("사다리의 높이는 (플레이어 수 - 1) 이상, 20 이하의 숫자만 가능합니다."); + } + } + + public int getLadderHeight() { + return height; + } +} diff --git a/src/main/java/model/ladder/Line.java b/src/main/java/model/ladder/Line.java new file mode 100644 index 00000000..c25c972f --- /dev/null +++ b/src/main/java/model/ladder/Line.java @@ -0,0 +1,52 @@ +package model.ladder; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import model.player.Position; +import util.LadderGenerator; + +public class Line { + + private final List points; + + public Line(List points) { + this.points = List.copyOf(points); + } + + public static Line create(int playerCount, LadderGenerator ladderGenerator) { + List points = new ArrayList<>(); + IntStream.range(0, playerCount - 1) + .forEach(i -> makeLine(points, ladderGenerator)); + return new Line(points); + } + + private static void makeLine(List points, LadderGenerator ladderGenerator) { + if (points.isEmpty()) { // 비어 있을 땐, 랜덤 생성 + points.add(Point.from(ladderGenerator.generate())); + return; + } + if (points.get(points.size() - 1).isConnected()) { // 연속 연결 방지 + points.add(Point.DISCONNECTED); + return; + } + points.add(Point.from(ladderGenerator.generate())); // 다시 랜덤 생성 + } + + public Position move(Position position) { + return Stream.of(Direction.RIGHT, Direction.LEFT, Direction.STAY) + .filter(direction -> direction.isMovable(position, points)) + .findFirst() + .map(direction -> direction.move(position)) + .orElse(position); + } + + public List getPoints() { + return List.copyOf(points); + } + + public int getLadderWidth() { + return points.size(); + } +} diff --git a/src/main/java/model/ladder/Point.java b/src/main/java/model/ladder/Point.java new file mode 100644 index 00000000..46470ef4 --- /dev/null +++ b/src/main/java/model/ladder/Point.java @@ -0,0 +1,17 @@ +package model.ladder; + +public enum Point { + CONNECTED, + DISCONNECTED; + + public static Point from(boolean status) { + if (status) { + return CONNECTED; + } + return DISCONNECTED; + } + + public boolean isConnected() { + return this.equals(CONNECTED); + } +} diff --git a/src/main/java/model/player/Player.java b/src/main/java/model/player/Player.java new file mode 100644 index 00000000..2b44c943 --- /dev/null +++ b/src/main/java/model/player/Player.java @@ -0,0 +1,20 @@ +package model.player; + +public class Player { + + private final PlayerName name; + private final Position position; + + public Player(String name, int position) { + this.name = new PlayerName(name); + this.position = new Position(position); + } + + public PlayerName getName() { + return name; + } + + public Position getPosition() { + return position; + } +} diff --git a/src/main/java/model/player/PlayerName.java b/src/main/java/model/player/PlayerName.java new file mode 100644 index 00000000..a2586bc1 --- /dev/null +++ b/src/main/java/model/player/PlayerName.java @@ -0,0 +1,49 @@ +package model.player; + +import java.util.Objects; + +public class PlayerName { + + private final String name; + private final String CANNOT_NAME = "all"; + private final int MAXINUM_NAME_LENGTH = 5; + + public PlayerName(String name) { + validateName(name); + validateNaming(name); + this.name = name; + } + + private void validateName(String name) { + if (name == null || name.isBlank() || name.length() > MAXINUM_NAME_LENGTH) { + throw new IllegalArgumentException("플레이어의 이름은 1~5글자여야 합니다."); + } + } + + private void validateNaming(String name) { + if (name.equalsIgnoreCase(CANNOT_NAME)) { + throw new IllegalArgumentException("플레이어 이름으로 'all'은 사용할 수 없습니다."); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlayerName that = (PlayerName) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/model/player/Players.java b/src/main/java/model/player/Players.java new file mode 100644 index 00000000..7a8b6472 --- /dev/null +++ b/src/main/java/model/player/Players.java @@ -0,0 +1,66 @@ +package model.player; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Players { + + private final List players; + + public Players(List players) { + validatePlayerCount(players); + validateDuplicate(players); + this.players = List.copyOf(players); + ; + } + + public static Players from(List names) { + List players = IntStream.range(0, names.size()) + .mapToObj(i -> new Player(names.get(i), i)) + .toList(); + return new Players(players); + } + + public Player getPlayerAt(int index) { + if (index < 0 || index >= players.size()) { + throw new IllegalArgumentException("유효하지 않은 인덱스입니다: " + index); + } + return players.get(index); + } + + private void validatePlayerCount(List players) { + if (players.size() < 2) { + throw new IllegalArgumentException("플레이어는 최소 2명 이상이어야 합니다."); + } + } + + private void validateDuplicate(List players) { + Set names = players.stream() + .map(Player::getName) + .collect(Collectors.toSet()); + if (names.size() != players.size()) { + throw new IllegalArgumentException("플레이어의 이름은 중복될 수 없습니다."); + } + } + + public void validateContainsPlayer(String input) { + if (!this.containsPlayer(input)) { + throw new IllegalArgumentException("해당 이름의 플레이어가 존재하지 않습니다: " + input); + } + } + + public boolean containsPlayer(String name) { + return players.stream() + .anyMatch(player -> player.getName().getName().equals(name)); + } + + public int size() { + return players.size(); + } + + public List getPlayers() { + return List.copyOf(players); + } +} diff --git a/src/main/java/model/player/Position.java b/src/main/java/model/player/Position.java new file mode 100644 index 00000000..a30b2f46 --- /dev/null +++ b/src/main/java/model/player/Position.java @@ -0,0 +1,26 @@ +package model.player; + +public class Position { + + public static final int MINIMUM_POSITION = 0; + private final int value; + + public Position(int value) { + this.value = value; + } + + public Position moveToRight() { + return new Position(value + 1); + } + + public Position moveToLeft() { + if (value <= MINIMUM_POSITION) { + throw new IllegalArgumentException("Position의 값은 음수가 될 수 없습니다."); + } + return new Position(value - 1); + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/util/LadderGenerator.java b/src/main/java/util/LadderGenerator.java new file mode 100644 index 00000000..741c336f --- /dev/null +++ b/src/main/java/util/LadderGenerator.java @@ -0,0 +1,6 @@ +package util; + +public interface LadderGenerator { + + boolean generate(); +} diff --git a/src/main/java/util/RandomLadderGenerator.java b/src/main/java/util/RandomLadderGenerator.java new file mode 100644 index 00000000..fe61dd32 --- /dev/null +++ b/src/main/java/util/RandomLadderGenerator.java @@ -0,0 +1,12 @@ +package util; + +import java.util.Random; + +public class RandomLadderGenerator implements LadderGenerator { + + private final Random random = new Random(); + + public boolean generate() { + return random.nextBoolean(); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..4c721cb1 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,54 @@ +package view; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.Scanner; +import model.player.Players; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + public static final String INPUT_EXCEPTION_MESSAGE = "올바른 입력 형식이 아닙니다. 숫자로 입력해주세요."; + private static final String LADDER_HEIGHT_MESSAGE = "최대 사다리 높이는 몇 개인가요?"; + private static final String EXECUTION_GOAL_MESSAGE = "실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)"; + private static final String EXECUTION_NAME_MESSAGE = "참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"; + private static final String PLAYER_FOR_RESULT_MESSAGE = "결과를 보고 싶은 사람은?"; + private static final String INPUT_DELIMITER = ","; + private static final String NEW_LINE = "\n"; + + public int inputLadderHeight() { + System.out.println(NEW_LINE + LADDER_HEIGHT_MESSAGE); + String rawLadderHeight = scanner.nextLine(); + return parseInt(rawLadderHeight); + } + + private int parseInt(String rawLadderHeight) { + try { + return Integer.parseInt(rawLadderHeight); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INPUT_EXCEPTION_MESSAGE); + } + } + + public List inputPlayers() { + System.out.println(NEW_LINE + EXECUTION_NAME_MESSAGE); + String rawPlayerName = scanner.nextLine(); + return Arrays.stream(rawPlayerName.split(INPUT_DELIMITER)) + .map(String::trim) + .collect(Collectors.toList()); + } + + public List inputGoals() { + System.out.println(NEW_LINE + EXECUTION_GOAL_MESSAGE); + String rawGoals = scanner.nextLine(); + return Arrays.stream(rawGoals.split(INPUT_DELIMITER)) + .map(String::trim) + .collect(Collectors.toList()); + } + + public String inputPlayerForResult() { + System.out.println(NEW_LINE + PLAYER_FOR_RESULT_MESSAGE); + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..ec61b360 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,93 @@ +package view; + +import java.util.List; +import model.goal.Goal; +import model.goal.Goals; +import model.ladder.Ladder; +import model.player.Player; +import model.player.Players; +import model.ladder.Point; +import java.util.Map; + +public class OutputView { + + private static final int NAME_WIDTH = 6; + private static final String EXECUTION_RESULT = "\n실행 결과"; + private static final String LADDER_RESULT_HEADER = "\n사다리 결과\n"; + private static final String CONNECTED_LINE = "-----|"; + private static final String DISCONNECTED_LINE = " |"; + private static final String EDGE_OF_POINT = "|"; + private static final String START_PADDING = " "; + private static final String SINGLE_SPACE = " "; + + public void printLadder(Ladder ladder, Players players, Goals goals) { + System.out.println(LADDER_RESULT_HEADER); + printPlayerNamers(players); + printLadderLines(ladder); + printGoals(goals); + } + + private void printPlayerNamers(Players players) { + for (Player player : players.getPlayers()) { + String name = player.getName().getName(); + int padding = NAME_WIDTH - 1 - name.length(); + System.out.print(SINGLE_SPACE.repeat(padding) + name + SINGLE_SPACE); + } + System.out.println(); + } + + private void printLadderLines(Ladder ladder) { + for (var line : ladder.getLines()) { + printLine(line.getPoints()); + } + } + + private void printLine(List points) { + System.out.print(START_PADDING + EDGE_OF_POINT); + for (var point : points) { + printPoint(point); + } + System.out.println(); + } + + private void printPoint(Point point) { + if (point.isConnected()) { + System.out.print(CONNECTED_LINE); + return; + } + System.out.print(DISCONNECTED_LINE); + } + + private void printGoals(Goals goals) { + for (Goal goal : goals.getGoals()) { + String text = goal.getGoal(); + int padding = NAME_WIDTH - 1 - text.length(); + System.out.print(SINGLE_SPACE.repeat(padding) + text + SINGLE_SPACE); + } + System.out.println(); + } + + public void printAllResults(Map results) { + System.out.println(EXECUTION_RESULT); + for (var entry : results.entrySet()) { + System.out.println(entry.getKey().getName().getName() + " : " + entry.getValue().getGoal()); + } + } + + public void printSingleResult(String name, Players players, Map results) { + System.out.println(EXECUTION_RESULT); + for (Player player : players.getPlayers()) { + printPlayerResult(name, player, results); + } + } + + private void printPlayerResult(String name, Player player, Map results) { + if (player.getName().getName().equals(name)) { + System.out.println(results.get(player).getGoal()); + } + } + + public void printErrorMessage(String message) { + System.out.println(message); + } +} diff --git a/src/test/java/model/TestLadderGenerator.java b/src/test/java/model/TestLadderGenerator.java new file mode 100644 index 00000000..720de780 --- /dev/null +++ b/src/test/java/model/TestLadderGenerator.java @@ -0,0 +1,24 @@ +package model; + +import java.util.List; +import util.LadderGenerator; + +public class TestLadderGenerator implements LadderGenerator { + + private final List values; + private int index = 0; + + public TestLadderGenerator(List values) { + this.values = values; + } + + @Override + public boolean generate() { + boolean value = values.get(index); + index++; + if (index >= values.size()) { + index = 0; + } + return value; + } +} diff --git a/src/test/java/model/goal/GoalTest.java b/src/test/java/model/goal/GoalTest.java new file mode 100644 index 00000000..f3205736 --- /dev/null +++ b/src/test/java/model/goal/GoalTest.java @@ -0,0 +1,27 @@ +package model.goal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class GoalTest { + + @DisplayName("정상적으로 Goal 객체를 생성한다") + @Test + void createGoal() { + assertDoesNotThrow(() -> new Goal("꽝")); + } + + @ParameterizedTest + @ValueSource(strings = {"결과가너무너무길때", " "}) + @DisplayName("플레이어 이름이 공백이거나 6글자 이상이면 예외가 발생한다") + void throwExceptionWhenExceedsFive(String goal) { + assertThatThrownBy(() -> new Goal(goal)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("실행 결과는 1~5글자여야 합니다."); + } +} diff --git a/src/test/java/model/goal/GoalsTest.java b/src/test/java/model/goal/GoalsTest.java new file mode 100644 index 00000000..cd79d728 --- /dev/null +++ b/src/test/java/model/goal/GoalsTest.java @@ -0,0 +1,23 @@ +package model.goal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class GoalsTest { + + @Test + @DisplayName("플레이어의 수와 다른 갯수의 결과가 입력되면 예외를 반환한다") + void throwExceptionWhenNotMatches() { + // Given + List goals = List.of(new Goal("당첨"), new Goal("꽝")); + int playerCount = 3; + + // When & Then + assertThatThrownBy(() -> new Goals(goals, playerCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("실행 결과 수와 참여자 수는 같아야 합니다."); + } +} diff --git a/src/test/java/model/ladder/DirectionTest.java b/src/test/java/model/ladder/DirectionTest.java new file mode 100644 index 00000000..448e2802 --- /dev/null +++ b/src/test/java/model/ladder/DirectionTest.java @@ -0,0 +1,55 @@ +package model.ladder; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import model.player.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DirectionTest { + + @Test + @DisplayName("오른쪽 이동이 가능한 경우") + void rightDirectionTest() { + // Given + Position position = new Position(0); + List points = List.of(Point.CONNECTED); + + // When + boolean result = Direction.RIGHT.isMovable(position, points); + + // Then + assertTrue(result); + } + + @Test + @DisplayName("왼쪽 이동이 가능한 경우") + void leftDirectionTest() { + // Given + Position position = new Position(1); + List points = List.of(Point.CONNECTED); + + // When + boolean result = Direction.LEFT.isMovable(position, points); + + // Then + assertTrue(result); + } + + @Test + @DisplayName("왼쪽 이동이 가능한 경우") + void stayDirectionTest() { + // Given + Position position = new Position(1); + List points = List.of(Point.DISCONNECTED); + + // When + boolean movable = Direction.STAY.isMovable(position, points); + Position moved = Direction.STAY.move(position); + + // Then + assertTrue(movable); + assertEquals(1, moved.getValue()); + } +} diff --git a/src/test/java/model/ladder/LadderHeightTest.java b/src/test/java/model/ladder/LadderHeightTest.java new file mode 100644 index 00000000..bc502be0 --- /dev/null +++ b/src/test/java/model/ladder/LadderHeightTest.java @@ -0,0 +1,37 @@ +package model.ladder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LadderHeightTest { + + @Test + @DisplayName("정상적으로 LadderHeight 객체가 생성된다") + void createLadderHeight() { + // Given + int height = 5; + int player = 5; + + // When + LadderHeight ladderHeight = new LadderHeight(height, player); + + // Then + assertThat(ladderHeight.getLadderHeight()).isEqualTo(height); + } + + @Test + @DisplayName("사다리 높이가 (플레이어 - 1) 작거나 20보다 크면 예외가 발생한다") + void throwExceptionHeightWhenIsBetween() { + // Given + int invalidHeight = 10; + int playerCount = 15; + + // When & Then + assertThatThrownBy(() -> new LadderHeight(invalidHeight, playerCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사다리의 높이는 (플레이어 수 - 1) 이상, 20 이하의 숫자만 가능합니다."); + } +} diff --git a/src/test/java/model/ladder/LadderTest.java b/src/test/java/model/ladder/LadderTest.java new file mode 100644 index 00000000..8382740e --- /dev/null +++ b/src/test/java/model/ladder/LadderTest.java @@ -0,0 +1,86 @@ +package model.ladder; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import model.TestLadderGenerator; +import model.player.Player; +import model.player.PlayerName; +import model.player.Players; +import model.player.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import util.RandomLadderGenerator; + +class LadderTest { + + @Test + @DisplayName("Ladder는 올바른 높이와 플레이어 수에 맞는 라인을 생성한다") + void createLadder() { + // Given + int playerCount = 4; + LadderHeight height = new LadderHeight(5, playerCount); + + List playersList = List.of( + new Player("a", 0), + new Player("b", 1), + new Player("c", 2), + new Player("d", 3) + ); + Players players = new Players(playersList); + + // When + Ladder ladder = Ladder.of(players, height, new RandomLadderGenerator()); + + // Then + assertThat(ladder.getLadderHeight()).isEqualTo(5); + assertThat(ladder.getLadderWidth(players)).isEqualTo(4); + assertThat(ladder.getLines().size()).isEqualTo(5); + } + + @Test + @DisplayName("플레이어가 다르다면 서로 다른 결과 위치를 반환해주어야 한다") + void getDifferentStartEndPosition() { + // Given + LadderHeight height = new LadderHeight(2, 3); + List playersList = List.of( + new Player("a", 0), + new Player("b", 1), + new Player("c", 2) + ); + Players players = new Players(playersList); + Ladder ladder = Ladder.of(players, height, new RandomLadderGenerator()); + + // When + Position first = ladder.getGoalsPosition(new Position(0)); + Position second = ladder.getGoalsPosition(new Position(1)); + + // Then + assertThat(first).isNotEqualTo(second); + } + + @ParameterizedTest + @CsvSource(value = {"0:3", "1:1", "2:2", "3:0"}, delimiter = ':') + @DisplayName("사다리의 결과에 맞는 시작 위치, 도착 위치를 반환해주어야 한다") + void getAppropriatePosition(int start, int end) { + // Given + List orderList = List.of(true, true, false, true, true, true); + LadderHeight height = new LadderHeight(3, 4); + List playersList = List.of( + new Player("a", 0), + new Player("b", 1), + new Player("c", 2), + new Player("d", 3) + ); + Players players = new Players(playersList); + Ladder ladder = Ladder.of(players, height, new TestLadderGenerator(orderList)); + + // When + Position results = ladder.getGoalsPosition(new Position(start)); + + // Then + assertThat(results.getValue()).isEqualTo(end); + } +} diff --git a/src/test/java/model/ladder/LineTest.java b/src/test/java/model/ladder/LineTest.java new file mode 100644 index 00000000..b7061a5e --- /dev/null +++ b/src/test/java/model/ladder/LineTest.java @@ -0,0 +1,45 @@ +package model.ladder; + +import static model.ladder.Point.CONNECTED; +import static model.ladder.Point.DISCONNECTED; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import model.TestLadderGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import util.RandomLadderGenerator; + +class LineTest { + + @Test + @DisplayName("인자값으로 받은 playerCount -1 개 만큼의 Point를 가진다") + void createLine() { + // Given + int playerCount = 3; + + // When + Line line = Line.create(3, new RandomLadderGenerator()); + List points = line.getPoints(); + + // Then + assertThat(points.size()).isEqualTo(playerCount - 1); + } + + @Test + @DisplayName("Point 들은 연속된 다리를 가질 수 없다") + void pointCanNotHaveDuplicate() { + int playerCount = 4; + List orderList = List.of( + true, + true, + true + ); + + Line line = Line.create(playerCount, new TestLadderGenerator(orderList)); + List points = line.getPoints(); + + assertThat(points).containsExactly(CONNECTED, DISCONNECTED, CONNECTED); + } + +} diff --git a/src/test/java/model/ladder/PositionTest.java b/src/test/java/model/ladder/PositionTest.java new file mode 100644 index 00000000..01e88a65 --- /dev/null +++ b/src/test/java/model/ladder/PositionTest.java @@ -0,0 +1,33 @@ +package model.ladder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import model.player.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PositionTest { + + @Test + @DisplayName("정상적으로 객체를 가진다") + void createPosition() { + // Given + Position position = new Position(5); + + // When & Then + assertThat(position.getValue()).isEqualTo(5); + } + + @Test + @DisplayName("position은 값이 0 이하로 내려갈 수 없다") + void throwExceptionWhenPositionIsNegative() { + // Given + Position position = new Position(-0); + + // When & Then + assertThatThrownBy(() -> position.moveToLeft()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Position의 값은 음수가 될 수 없습니다."); + } +} diff --git a/src/test/java/model/player/PlayerNameTest.java b/src/test/java/model/player/PlayerNameTest.java new file mode 100644 index 00000000..a58003ba --- /dev/null +++ b/src/test/java/model/player/PlayerNameTest.java @@ -0,0 +1,36 @@ +package model.player; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PlayerNameTest { + + @ParameterizedTest + @ValueSource(strings = {"a", "abc", "abcde"}) + @DisplayName("플레이어 이름이 1글자에서 5글자 사이면 정상적으로 생성된다") + void createWhenPlayerNameIsBetween(String name) { + assertThatNoException().isThrownBy(() -> new PlayerName(name)); + } + + @ParameterizedTest + @ValueSource(strings = {"abcdef", "hahahaha", " "}) + @DisplayName("플레이어 이름이 공백이거나 6글자 이상이면 예외가 발생한다") + void throwExceptionWhenExceedsFive(String name) { + assertThatThrownBy(() -> new PlayerName(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("플레이어의 이름은 1~5글자여야 합니다."); + } + + @ParameterizedTest + @ValueSource(strings = {"all", "ALL", "aLl"}) + @DisplayName("플레이어 이름이 'all'이면 예외가 발생한다") + void throwExceptionWhenNameIsAll(String name) { + assertThatThrownBy(() -> new PlayerName(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("플레이어 이름으로 'all'은 사용할 수 없습니다."); + } +} diff --git a/src/test/java/model/player/PlayerTest.java b/src/test/java/model/player/PlayerTest.java new file mode 100644 index 00000000..76966bb8 --- /dev/null +++ b/src/test/java/model/player/PlayerTest.java @@ -0,0 +1,20 @@ +package model.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayerTest { + + @Test + @DisplayName("정상적으로 Player 객체를 생성한다") + void playerShouldHaveNameAndPosition() { + // Given + Player player = new Player("abc", 5); + + // When & Then + assertThat(player.getName()).isEqualTo(new PlayerName("abc")); + assertThat(player.getPosition()).isEqualTo(new Position(5)); + } +} diff --git a/src/test/java/model/player/PlayersTest.java b/src/test/java/model/player/PlayersTest.java new file mode 100644 index 00000000..b06268d4 --- /dev/null +++ b/src/test/java/model/player/PlayersTest.java @@ -0,0 +1,50 @@ +package model.player; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayersTest { + + @Test + @DisplayName("플레이어가 2명 미만일 경우 예외가 발생한다") + void throwExceptionWhenLessThanTwo() { + // Given + List players = List.of(new Player("abc", 2)); + + // When & Then + assertThatThrownBy(() -> new Players(players)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("플레이어는 최소 2명 이상이어야 합니다."); + } + + @Test + @DisplayName("플레이어 이름이 중복되면 예외가 발생한다") + void throwExceptionWhenDuplicate() { + // Given + Player player1 = new Player("abc",1); + Player player2 = new Player("abc",2); + List players = List.of(player1, player2); + + // When & Then + assertThatThrownBy(() -> new Players(players)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("플레이어의 이름은 중복될 수 없습니다."); + } + + @Test + @DisplayName("존재하지 않는 플레이어 이름 입력 시 예외가 발생한다") + void throwExceptionWhenInvalidName() { + // Given + Player player1 = new Player("a",0); + Player player2 = new Player("b",1); + Players players = new Players(List.of(player1, player2)); + + // When & Then + assertThatThrownBy(() -> players.validateContainsPlayer("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("해당 이름의 플레이어가 존재하지 않습니다: invalid"); + } +}