diff --git a/README.md b/README.md new file mode 100644 index 00000000..2e8305bc --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# java-ladder : 사다리 - 함수형 프로그래밍 + +## Level1. 사다리 출력 + +### - 요구사항 + +- [x] 네이버 사다리 게임을 참고하여 도메인을 분석하여 구현한다. +- [x] 사다리는 4x4 크기로 고정되고, 연결 여부는 랜덤으로 결정한다. +- [x] 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다. +- [x] 모든 엔티티를 작게 유지한다. +- [x] 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. + +## Level2. 사다리 생성 + +### - 요구사항 + +- [x] 사다리는 크기를 입력 받아 생성할 수 있다. (넓이 & 높이) + +## Level3. 사다리 타기 + +### - 요구사항 + +- [x] 사다리의 시작 지점과 도착 지점을 출력한다. + +## Level4. 게임 실행 + +### - 요구사항 + +- [x] 사다리 게임에 참여하는 사람에 이름을 최대 5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다. +- [x] 사람 이름은 쉼표(,)를 기준으로 구분한다. +- [x] 개인별 이름을 입력하면 개인별 결과를 출력하고, "all"을 입력하면 전체 참여자의 실행 결과를 출력한다. + +## Level5. 리팩토링 + +### - 요구사항 + +- [x] 학습 테스트를 통해 학습한 내용을 반영한다. 자바에서 제공하는 함수형 문법을 적용해보고, 어떠한 차이가 있는지 경험한다. diff --git a/src/main/java/ladder/controller/Application.java b/src/main/java/ladder/controller/Application.java new file mode 100644 index 00000000..4c8a16e4 --- /dev/null +++ b/src/main/java/ladder/controller/Application.java @@ -0,0 +1,13 @@ +package ladder.controller; + +import ladder.view.InputView; +import ladder.view.OutputView; + +public class Application { + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + LadderGame game = new LadderGame(inputView, outputView); + game.play(); + } +} diff --git a/src/main/java/ladder/controller/LadderGame.java b/src/main/java/ladder/controller/LadderGame.java new file mode 100644 index 00000000..fdcbdcfc --- /dev/null +++ b/src/main/java/ladder/controller/LadderGame.java @@ -0,0 +1,56 @@ +package ladder.controller; + +import java.util.List; +import java.util.stream.Collectors; +import ladder.model.Ladder; +import ladder.model.LadderResult; +import ladder.model.Participants; +import ladder.model.Point; +import ladder.model.Result; +import ladder.view.InputView; +import ladder.view.OutputView; + +public class LadderGame { + + private final InputView inputView; + private final OutputView outputView; + + public LadderGame(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void play() { + Participants participants = inputView.inputParticipants(); + List results = inputView.inputResults(); + int height = inputView.inputHeight(); + + Ladder ladder = Ladder.create(participants.size(), height); + outputView.printLadder(ladder, participants, results); + + String resultRequest = inputView.inputGameResult(); + processResult(ladder, participants, results, resultRequest); + } + + private void processResult(Ladder ladder, Participants participants, List results, + String resultRequest) { + List ladderResults = ladder.result(); + List mappedResults = mapResults(results, ladderResults); + LadderResult ladderResult = new LadderResult(participants, mappedResults); + printGameResults(resultRequest, ladderResult); + } + + private void printGameResults(String result, LadderResult gameLadderResult) { + if ("all".equals(result)) { + outputView.printAllResults(gameLadderResult); + return; + } + outputView.printSingleResult(gameLadderResult.getResult(result)); + } + + private List mapResults(List inputResults, List ladderResults) { + return ladderResults.stream() + .map(point -> inputResults.get(point.getEnd())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/ladder/model/Ladder.java b/src/main/java/ladder/model/Ladder.java new file mode 100644 index 00000000..c37b698b --- /dev/null +++ b/src/main/java/ladder/model/Ladder.java @@ -0,0 +1,63 @@ +package ladder.model; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Ladder { + + private final List lines; + + private Ladder(List lines) { + this.lines = lines; + } + + public static Ladder create(int width, int height) { + return new Ladder( + IntStream.range(0, height) + .mapToObj(i -> Line.create(width)) + .collect(Collectors.toList()) + ); + } + + public List getLines() { + return lines; + } + + public List result() { + int width = lines.get(0).getPoints().size() + 1; + return IntStream.range(0, width) + .mapToObj(start -> new Point(start, getEndPoint(start))) // 출발점과 도착점을 Point로 묶음 + .collect(Collectors.toList()); + } + + private int getEndPoint(int start) { + int position = start; + + for (Line line : lines) { + position = move(position, line.getPoints()); + } + + return position; + } + + private int move(int position, List points) { + if (canMoveLeft(position, points)) { + return position - 1; + } + + if (canMoveRight(position, points)) { + return position + 1; + } + + return position; + } + + private boolean canMoveLeft(int position, List points) { + return position > 0 && points.get(position - 1); + } + + private boolean canMoveRight(int position, List points) { + return position < points.size() && points.get(position); + } +} diff --git a/src/main/java/ladder/model/LadderResult.java b/src/main/java/ladder/model/LadderResult.java new file mode 100644 index 00000000..b0d41b59 --- /dev/null +++ b/src/main/java/ladder/model/LadderResult.java @@ -0,0 +1,31 @@ +package ladder.model; + +import java.util.ArrayList; +import java.util.List; + +public class LadderResult { + + private final Participants participants; + private final List results; + + public LadderResult(Participants participants, List results) { + this.participants = participants; + this.results = results; + } + + public List getAll() { + return new ArrayList<>(results); + } + + public List getParticipants() { + return participants.getParticipantsNameList(); + } + + public Result getResult(String participant) { + return participants.getParticipantsNameList().stream() + .filter(name -> name.matches(participant)) + .findFirst() + .map(name -> results.get(participants.getParticipantsNameList().indexOf(name))) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 참가자입니다.")); + } +} diff --git a/src/main/java/ladder/model/Line.java b/src/main/java/ladder/model/Line.java new file mode 100644 index 00000000..a29039e6 --- /dev/null +++ b/src/main/java/ladder/model/Line.java @@ -0,0 +1,40 @@ +package ladder.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class Line { + + private static final Random random = new Random(); + private final List points; + + private Line(List points) { + this.points = points; + } + + public List getPoints() { + return new ArrayList<>(points); + } + + public static Line create(int width) { + List connections = new ArrayList<>(); + for (int i = 0; i < width - 1; i++) { + // 새로운 라인 연결 여부 + boolean shouldConnect = isConnectable(i, connections); + connections.add(shouldConnect); + } + return new Line(connections); + } + + private static boolean isConnectable(int currentIndex, List connections) { + // 첫 번째 위치라면? 바로 Random 값 설정 + if (currentIndex == 0) { + return random.nextBoolean(); + } + + // 바로 이전 연결 여부를 확인 -> 연속된 라인(true) 방지 + boolean previousConnected = connections.get(currentIndex - 1); + return !previousConnected && new Random().nextBoolean(); + } +} diff --git a/src/main/java/ladder/model/Name.java b/src/main/java/ladder/model/Name.java new file mode 100644 index 00000000..6268998a --- /dev/null +++ b/src/main/java/ladder/model/Name.java @@ -0,0 +1,42 @@ +package ladder.model; + +import java.util.Objects; + +public class Name { + + private static final int MAX_LENGTH = 5; + private final String value; + + public Name(String value) { + if (value.length() > MAX_LENGTH) { + throw new IllegalArgumentException("이름은 최대 5글자까지 가능합니다."); + } + this.value = value; + } + + public boolean matches(String other) { + return value.equals(other); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Name name = (Name) o; + return Objects.equals(value, name.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/ladder/model/Participants.java b/src/main/java/ladder/model/Participants.java new file mode 100644 index 00000000..5e3f694b --- /dev/null +++ b/src/main/java/ladder/model/Participants.java @@ -0,0 +1,25 @@ +package ladder.model; + +import java.util.ArrayList; +import java.util.List; + +public class Participants { + + private final List names; + + private Participants(List names) { + this.names = names; + } + + public static Participants of(List names) { + return new Participants(new ArrayList<>(names)); + } + + public int size() { + return names.size(); + } + + public List getParticipantsNameList() { + return new ArrayList<>(names); + } +} diff --git a/src/main/java/ladder/model/Point.java b/src/main/java/ladder/model/Point.java new file mode 100644 index 00000000..51c3d3c7 --- /dev/null +++ b/src/main/java/ladder/model/Point.java @@ -0,0 +1,44 @@ +package ladder.model; + +import java.util.Objects; + +public class Point { + + private final int start; + private final int end; + + public Point(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Point point = (Point) o; + return start == point.start && end == point.end; + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + @Override + public String toString() { + return "Point{" + "start=" + start + ", end=" + end + '}'; + } +} diff --git a/src/main/java/ladder/model/Result.java b/src/main/java/ladder/model/Result.java new file mode 100644 index 00000000..d30fdff1 --- /dev/null +++ b/src/main/java/ladder/model/Result.java @@ -0,0 +1,31 @@ +package ladder.model; + +public class Result { + + private final String value; + + public Result(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("결과 값은 비어있을 수 없습니다."); + } + this.value = value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Result result = (Result) o; + return value.equals(result.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/ladder/view/InputView.java b/src/main/java/ladder/view/InputView.java new file mode 100644 index 00000000..00dc549f --- /dev/null +++ b/src/main/java/ladder/view/InputView.java @@ -0,0 +1,43 @@ +package ladder.view; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; +import ladder.model.Name; +import ladder.model.Participants; +import ladder.model.Result; + +public class InputView { + + private final Scanner scanner = new Scanner(System.in); + + public Participants inputParticipants() { + System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"); + String input = scanner.nextLine(); + List names = Arrays.stream(input.split(",")) + .map(String::trim) + .map(Name::new) + .collect(Collectors.toList()); + return Participants.of(names); + } + + public List inputResults() { + System.out.println("\n실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)"); + String input = scanner.nextLine(); + return Arrays.stream(input.split(",")) + .map(String::trim) + .map(Result::new) + .collect(Collectors.toList()); + } + + public int inputHeight() { + System.out.println("\n최대 사다리 높이는 몇 개인가요?"); + return Integer.parseInt(scanner.nextLine()); + } + + public String inputGameResult() { + System.out.println("\n결과를 보고 싶은 사람은?"); + return scanner.nextLine(); + } +} diff --git a/src/main/java/ladder/view/OutputView.java b/src/main/java/ladder/view/OutputView.java new file mode 100644 index 00000000..98f2af03 --- /dev/null +++ b/src/main/java/ladder/view/OutputView.java @@ -0,0 +1,69 @@ +package ladder.view; + +import java.util.List; +import java.util.stream.IntStream; +import ladder.model.LadderResult; +import ladder.model.Ladder; +import ladder.model.Line; +import ladder.model.Name; +import ladder.model.Participants; +import ladder.model.Result; + +public class OutputView { + + public void printLadder(Ladder ladder, Participants participants, List results) { + System.out.println("\n사다리 결과\n"); + printParticipants(participants); + printLadderLines(ladder); + printResults(results); + } + + private void printParticipants(Participants participants) { + for (Name name : participants.getParticipantsNameList()) { + System.out.printf("%-6s", name); + } + System.out.println(); + } + + private void printLadderLines(Ladder ladder) { + for (Line line : ladder.getLines()) { + printLine(line); + } + } + + private void printLine(Line line) { + for (Boolean point : line.getPoints()) { + System.out.print("|"); + printLineSegment(point); + } + System.out.println("|"); + } + + private void printLineSegment(Boolean connected) { + if (connected) { + System.out.print("-----"); + return; + } + System.out.print(" "); + } + + public void printResults(List results) { + for (Result result : results) { + System.out.printf("%-6s", result); + } + System.out.println("\n"); + } + + public void printSingleResult(Result result) { + System.out.println("\n실행 결과"); + System.out.println(result); + } + + public void printAllResults(LadderResult results) { + System.out.println("\n실행 결과"); + IntStream.range(0, results.getParticipants().size()) + .forEach(i -> System.out.printf("%s : %s%n", + results.getParticipants().get(i), + results.getAll().get(i))); + } +} diff --git a/src/test/java/ladder/LadderTest.java b/src/test/java/ladder/LadderTest.java new file mode 100644 index 00000000..fe58bc91 --- /dev/null +++ b/src/test/java/ladder/LadderTest.java @@ -0,0 +1,51 @@ +package ladder; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.List; +import ladder.model.Ladder; +import ladder.model.Point; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LadderTest { + + @Test + @DisplayName("입력받은 넓이와 높이대로 사다리가 생성되어야한다.") + void generateLadder() { + // given + int width = 4; // 넓이 + int height = 5; // 높이 + + // when + Ladder ladder = Ladder.create(width, height); + + // then + assertThat(ladder.getLines()).hasSize(height); // 높이에 맞게 생성되었는지 확인 + + // 각 Line의 너비 확인 (width - 1개의 Boolean 리스트여야 함) + ladder.getLines().forEach(line -> + assertThat(line.getPoints()).hasSize(width - 1) + ); + + } + + @Test + @DisplayName("사다리 게임의 결과를 확인한다.") + void getLadderResult() { + // given + Ladder ladder = Ladder.create(4, 5); + + // when + List results = ladder.result(); + + // then + assertThat(results).hasSize(4); // 참가자 수 = 결과가 나와야 한다. + + // 각 결과의 출발점과 도착점 검사 + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).getStart()).isEqualTo(i); // 출발점이 i와 같음 + assertThat(results.get(i).getEnd()).isBetween(0, 3); // 도착점은 [0, 3](넓이 값) 사이 + } + } +} diff --git a/src/test/java/ladder/LineTest.java b/src/test/java/ladder/LineTest.java new file mode 100644 index 00000000..2dad6ead --- /dev/null +++ b/src/test/java/ladder/LineTest.java @@ -0,0 +1,41 @@ +package ladder; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.List; +import ladder.model.Line; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LineTest { + + @Test + @DisplayName("라인이 넓이에 따라 정확히 생성돼야한다") + void generateLineWithWidth() { + int width = 5; + Line line = Line.create(width); + + List points = line.getPoints(); + assertThat(points).hasSize(width - 1); // 연결 여부는 width-1 사이즈여야 함 + } + + @Test + @DisplayName("라인을 연결할 때 연속된 true가 나오지 않는다") + void validateNoConsecutiveTrueConnections() { + + /* + * 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다. + * |-----|-----| 모양과 같이 가로 라인이 겹치는 경우 + * 어느 방향으로 이동할지 결정할 수 없다. + * */ + + int width = 5; + Line line = Line.create(width); + + List points = line.getPoints(); + for (int i = 0; i < points.size() - 1; i++) { + assertThat(points.get(i) && points.get(i + 1)) + .isFalse(); + } + } +} diff --git a/src/test/java/ladder/NameTest.java b/src/test/java/ladder/NameTest.java new file mode 100644 index 00000000..50dd27cd --- /dev/null +++ b/src/test/java/ladder/NameTest.java @@ -0,0 +1,25 @@ +package ladder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import ladder.model.Name; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class NameTest { + + @Test + @DisplayName("이름 정상 입력") + void generateName() { + assertThatNoException().isThrownBy(() -> new Name("pobi")); + } + + @Test + @DisplayName("이름 5글자 초과시 예외발생") + void overLengthName() { + assertThatThrownBy(() -> new Name("pobiii")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 최대 5글자까지 가능합니다."); + } +} diff --git a/src/test/java/ladder/ParticipantsTest.java b/src/test/java/ladder/ParticipantsTest.java new file mode 100644 index 00000000..4258af11 --- /dev/null +++ b/src/test/java/ladder/ParticipantsTest.java @@ -0,0 +1,30 @@ +package ladder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.util.Arrays; +import java.util.List; +import ladder.model.Name; +import ladder.model.Participants; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ParticipantsTest { + + @Test + @DisplayName("참가자 List을 통해서 Participants를 생성해야한다.") + void generateParticipants() { + List names = Arrays.asList( + new Name("pobi"), + new Name("jason"), + new Name("brown") + ); + Participants participants = Participants.of(names); + + assertThat(participants.size()).isEqualTo(3); + assertThat(participants.getParticipantsNameList().get(0).matches("pobi")).isTrue(); + assertThat(participants.getParticipantsNameList().get(1).matches("jason")).isTrue(); + assertThat(participants.getParticipantsNameList().get(2).matches("brown")).isTrue(); + + } +}