diff --git a/src/main/java/bowling/App.java b/src/main/java/bowling/App.java new file mode 100644 index 0000000000..5a3f7de79e --- /dev/null +++ b/src/main/java/bowling/App.java @@ -0,0 +1,22 @@ +package bowling; + +import bowling.domain.Hit; +import bowling.domain.Name; +import bowling.domain.Round; +import bowling.view.InputView; +import bowling.view.ResultView; + +public class App { + + public static void main(String[] args) { + Name name = InputView.inputName(); + + Round round = new Round(); + ResultView.printRound(name, round); + while (!round.isEnd()) { + Hit hit = InputView.inputPins(round.getCurrentFrameNumber()); + round.hit(hit); + ResultView.printRound(name, round); + } + } +} diff --git a/src/main/java/bowling/domain/Frame.java b/src/main/java/bowling/domain/Frame.java new file mode 100644 index 0000000000..c0550618e1 --- /dev/null +++ b/src/main/java/bowling/domain/Frame.java @@ -0,0 +1,16 @@ +package bowling.domain; + +public interface Frame { + + void play(Hit hit); + + FrameStatus getFirstStatus(); + FrameStatus getSecondStatus(); + FrameStatus getThirdStatus(); + + Hit getFirstHit(); + Hit getSecondHit(); + Hit getThirdHit(); + + boolean isEnd(); +} diff --git a/src/main/java/bowling/domain/FrameStatus.java b/src/main/java/bowling/domain/FrameStatus.java new file mode 100644 index 0000000000..18608e195e --- /dev/null +++ b/src/main/java/bowling/domain/FrameStatus.java @@ -0,0 +1,11 @@ +package bowling.domain; + +public enum FrameStatus { + BEFORE, + PLAYING, + SKIP, + STRIKE, + SPARE, + MISS, + GUTTER; +} diff --git a/src/main/java/bowling/domain/Hit.java b/src/main/java/bowling/domain/Hit.java new file mode 100644 index 0000000000..4d568b99b4 --- /dev/null +++ b/src/main/java/bowling/domain/Hit.java @@ -0,0 +1,43 @@ +package bowling.domain; + +public class Hit { + public static final int MIN = 0; + public static final int MAX = 10; + private final int hit; + + + public Hit(int hit) { + validate(hit); + this.hit = hit; + } + + private void validate(int hit) { + if (hit < MIN || hit > MAX) { + throw new IllegalArgumentException("범위를 벗어나는 점수가 입력되었습니다."); + } + } + + public Hit plus(Hit target) { + return new Hit(hit + target.hit); + } + + public boolean isMin() { + return hit == MIN; + } + + public boolean isMax() { + return hit == MAX; + } + + public int getScore() { + return hit; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Hit)) { + return false; + } + return hit == ((Hit) obj).hit; + } +} diff --git a/src/main/java/bowling/domain/LastFrame.java b/src/main/java/bowling/domain/LastFrame.java new file mode 100644 index 0000000000..b2a516026a --- /dev/null +++ b/src/main/java/bowling/domain/LastFrame.java @@ -0,0 +1,122 @@ +package bowling.domain; + +public class LastFrame implements Frame { + private Hit first; + private Hit second; + private Hit third; + + @Override + public void play(Hit hit) { + if (getFirstStatus() == FrameStatus.BEFORE) { + firstThrow(hit); + return; + } + if (getSecondStatus() == FrameStatus.BEFORE) { + secondThrow(hit); + return; + } + thirdThrow(hit); + } + + private void firstThrow(Hit hit) { + this.first = hit; + } + + private void secondThrow(Hit hit) { + this.second = hit; + } + + private void thirdThrow(Hit hit) { + this.third = hit; + } + + @Override + public FrameStatus getFirstStatus() { + return getNormalStatus(first); + } + + @Override + public FrameStatus getSecondStatus() { + if (first == null || second == null) { + return FrameStatus.BEFORE; + } + if (first.isMax()) { + return getNormalStatus(second); + } + Hit sum = first.plus(second); + if (sum.isMax()) { + return FrameStatus.SPARE; + } + if (second.isMin()) { + return FrameStatus.GUTTER; + } + return FrameStatus.MISS; + } + + @Override + public FrameStatus getThirdStatus() { + if (first == null || second == null || third == null) { + return FrameStatus.BEFORE; + } + if (!canPlayThird()) { + return FrameStatus.SKIP; + } + if (third.isMax()) { + return FrameStatus.STRIKE; + } + if (third.isMin()) { + return FrameStatus.GUTTER; + } + if (second.isMax()) { + return FrameStatus.MISS; + } + Hit sum = second.plus(third); + if (sum.isMax()) { + return FrameStatus.SPARE; + } + return FrameStatus.MISS; + } + + private FrameStatus getNormalStatus(Hit hit) { + if (hit == null) { + return FrameStatus.BEFORE; + } + if (hit.isMax()) { + return FrameStatus.STRIKE; + } + if (hit.isMin()) { + return FrameStatus.GUTTER; + } + return FrameStatus.MISS; + } + + @Override + public Hit getFirstHit() { + return first; + } + + @Override + public Hit getSecondHit() { + return second; + } + + @Override + public Hit getThirdHit() { + return third; + } + + @Override + public boolean isEnd() { + if (first == null || second == null) { + return false; + } + if (third != null) { + return true; + } + return !canPlayThird(); + } + + private boolean canPlayThird() { + return getFirstStatus() == FrameStatus.STRIKE || getSecondStatus() == FrameStatus.SPARE; + } +} diff --git a/src/main/java/bowling/domain/Name.java b/src/main/java/bowling/domain/Name.java new file mode 100644 index 0000000000..c48ae4a40f --- /dev/null +++ b/src/main/java/bowling/domain/Name.java @@ -0,0 +1,21 @@ +package bowling.domain; + +public class Name { + private final String name; + + + public Name(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + if (name.length() != 3) { + throw new IllegalArgumentException("이름은 3글자로 입력되어야 합니다"); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/bowling/domain/NormalFrame.java b/src/main/java/bowling/domain/NormalFrame.java new file mode 100644 index 0000000000..6812863df9 --- /dev/null +++ b/src/main/java/bowling/domain/NormalFrame.java @@ -0,0 +1,83 @@ +package bowling.domain; + +public class NormalFrame implements Frame { + private Hit first; + private Hit second; + + @Override + public void play(Hit hit) { + if (getFirstStatus() == FrameStatus.BEFORE) { + firstThrow(hit); + return; + } + secondThrow(hit); + } + + private void firstThrow(Hit hit) { + this.first = hit; + if (hit.isMax()) { + second = new Hit(0); + } + } + + private void secondThrow(Hit hit) { + this.second = hit; + } + + @Override + public FrameStatus getFirstStatus() { + if (first == null) { + return FrameStatus.BEFORE; + } + if (first.isMax()) { + return FrameStatus.STRIKE; + } + if (first.isMin()) { + return FrameStatus.GUTTER; + } + return FrameStatus.MISS; + } + + @Override + public FrameStatus getSecondStatus() { + if (first == null || second == null) { + return FrameStatus.BEFORE; + } + if (first.isMax()) { + return FrameStatus.SKIP; + } + Hit sum = first.plus(second); + if (sum.isMax()) { + return FrameStatus.SPARE; + } + if (second.isMin()) { + return FrameStatus.GUTTER; + } + return FrameStatus.MISS; + } + + @Override + public FrameStatus getThirdStatus() { + return FrameStatus.SKIP; + } + + @Override + public Hit getFirstHit() { + return first; + } + + @Override + public Hit getSecondHit() { + return second; + } + + @Override + public Hit getThirdHit() { + return new Hit(0); + } + + @Override + public boolean isEnd() { + return first != null && second != null; + } +} diff --git a/src/main/java/bowling/domain/Round.java b/src/main/java/bowling/domain/Round.java new file mode 100644 index 0000000000..c78659c8d8 --- /dev/null +++ b/src/main/java/bowling/domain/Round.java @@ -0,0 +1,50 @@ +package bowling.domain; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Round { + private static final int ROUND_COUNT = 10; + private final List frames; + + public Round() { + frames = IntStream.range(0, ROUND_COUNT - 1) + .mapToObj(i -> new NormalFrame()) + .collect(Collectors.toList()); + frames.add(new LastFrame()); + } + + public void hit(Hit hit) { + Frame last = getLastPlayingFrame() + .orElseThrow(() -> new IllegalStateException("모든 프레임이 종료되었습니다.")); + last.play(hit); + } + + public boolean isEnd() { + Optional last = getLastPlayingFrame(); + return last.isEmpty(); + } + + public int getCurrentFrameNumber() { + Optional last = getLastPlayingFrame(); + if (last.isEmpty()) { + return ROUND_COUNT; + } + Frame currentFrame = last.get(); + return frames.indexOf(currentFrame) + 1; + + } + + private Optional getLastPlayingFrame() { + return frames.stream() + .filter(frame -> !frame.isEnd()) + .findFirst(); + } + + public void forEach(Consumer consumer) { + frames.forEach(consumer); + } +} diff --git a/src/main/java/bowling/view/InputView.java b/src/main/java/bowling/view/InputView.java new file mode 100644 index 0000000000..6428a712a6 --- /dev/null +++ b/src/main/java/bowling/view/InputView.java @@ -0,0 +1,23 @@ +package bowling.view; + +import bowling.domain.Name; +import bowling.domain.Hit; + +import java.util.Scanner; + +public class InputView { + private static final Scanner scanner = new Scanner(System.in); + + public static Name inputName() { + System.out.print("플레이어 이름은 (3 english letters)?: "); + + String name = scanner.nextLine(); + return new Name(name); + } + + public static Hit inputPins(int frame) { + System.out.printf("%d프레임 투구: ", frame); + + return new Hit(scanner.nextInt()); + } +} diff --git a/src/main/java/bowling/view/ResultView.java b/src/main/java/bowling/view/ResultView.java new file mode 100644 index 0000000000..56812d8f0a --- /dev/null +++ b/src/main/java/bowling/view/ResultView.java @@ -0,0 +1,72 @@ +package bowling.view; + +import bowling.domain.*; + +public class ResultView { + + public static void printRound(Name name, Round round) { + System.out.println("| NAME | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 |"); + System.out.printf("| %s |", name.getName()); + + round.forEach(ResultView::printFrame); + System.out.println(); + } + + private static void printFrame(Frame frame) { + String frameString = " "; + FrameStatus firstStatus = frame.getFirstStatus(); + frameString += getFrameCharacter(firstStatus, frame.getFirstHit()); + FrameStatus secondStatus = frame.getSecondStatus(); + frameString += getSecondFrameCharacter(secondStatus, frame.getSecondHit()); + FrameStatus thirdStatus = frame.getThirdStatus(); + frameString += getThirdFrameCharacter(thirdStatus, frame.getThirdHit()); + System.out.print(frameString); + } + + private static String getFrameCharacter(FrameStatus frameStatus, Hit hit) { + if (frameStatus == FrameStatus.BEFORE) { + return " "; + } + if (frameStatus == FrameStatus.STRIKE) { + return "X"; + } + if (frameStatus == FrameStatus.GUTTER) { + return "-"; + } + return Integer.toString(hit.getScore()); + } + + private static String getSecondFrameCharacter(FrameStatus frameStatus, Hit hit) { + if (frameStatus == FrameStatus.BEFORE + || frameStatus == FrameStatus.SKIP) { + return " "; + } + if (frameStatus == FrameStatus.GUTTER) { + return "|-"; + } + if (frameStatus == FrameStatus.SPARE) { + return "|/"; + } + if (frameStatus == FrameStatus.STRIKE) { + return "|X"; + } + return String.format("|%d", hit.getScore()); + } + + private static String getThirdFrameCharacter(FrameStatus frameStatus, Hit hit) { + if (frameStatus == FrameStatus.BEFORE + || frameStatus == FrameStatus.SKIP) { + return " |"; + } + if (frameStatus == FrameStatus.GUTTER) { + return "|-|"; + } + if (frameStatus == FrameStatus.SPARE) { + return "|/|"; + } + if (frameStatus == FrameStatus.STRIKE) { + return "|X|"; + } + return String.format("|%d|", hit.getScore()); + } +} diff --git a/src/test/java/bowling/domain/HitTest.java b/src/test/java/bowling/domain/HitTest.java new file mode 100644 index 0000000000..7aaa3465af --- /dev/null +++ b/src/test/java/bowling/domain/HitTest.java @@ -0,0 +1,40 @@ +package bowling.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class HitTest { + + @Test + @DisplayName("최댓값 테스트") + void max() { + Hit hit = new Hit(Hit.MAX); + assertThat(hit.isMax()).isTrue(); + assertThat(hit.isMin()).isFalse(); + } + + @Test + @DisplayName("최솟값 테스트") + void min() { + Hit hit = new Hit(Hit.MIN); + assertThat(hit.isMax()).isFalse(); + assertThat(hit.isMin()).isTrue(); + } + + @Test + @DisplayName("범위 초과 테스트") + void range() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new Hit(11)); + } + + @Test + @DisplayName("add에 의한 범위 초과 테스트") + void range_add() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new Hit(3).plus(new Hit(8))); + } +} diff --git a/src/test/java/bowling/domain/LastFrameTest.java b/src/test/java/bowling/domain/LastFrameTest.java new file mode 100644 index 0000000000..85aea89884 --- /dev/null +++ b/src/test/java/bowling/domain/LastFrameTest.java @@ -0,0 +1,99 @@ +package bowling.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LastFrameTest { + + @Test + @DisplayName("첫번째 투구에 스트라이크를 친 경우 3회째 투구 가능") + void strike_first() { + Frame frame = new LastFrame(); + frame.play(new Hit(10)); + frame.play(new Hit(5)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(10)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.STRIKE); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(5)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.MISS); + assertThat(frame.isEnd()).isFalse(); + } + + @Test + @DisplayName("스페어인 경우 세번째 투구 가능") + void spare() { + Frame frame = new LastFrame(); + frame.play(new Hit(8)); + frame.play(new Hit(2)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(8)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.MISS); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(2)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.SPARE); + assertThat(frame.isEnd()).isFalse(); + } + + @Test + @DisplayName("3회 스트라이크") + void strike() { + Frame frame = new LastFrame(); + frame.play(new Hit(10)); + frame.play(new Hit(10)); + frame.play(new Hit(10)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(10)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.STRIKE); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(10)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.STRIKE); + assertThat(frame.getThirdHit()).isEqualTo(new Hit(10)); + assertThat(frame.getThirdStatus()).isEqualTo(FrameStatus.STRIKE); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("거터") + void gutter() { + Frame frame = new LastFrame(); + frame.play(new Hit(0)); + frame.play(new Hit(5)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(0)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.GUTTER); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(5)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.MISS); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("두번의 투구 모두 거터") + void gutter_2() { + Frame frame = new LastFrame(); + frame.play(new Hit(0)); + frame.play(new Hit(0)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(0)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.GUTTER); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(0)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.GUTTER); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("종료되지 않은 상태 테스트") + void isEnd() { + Frame frame = new LastFrame(); + + assertThat(frame.isEnd()).isFalse(); + } + + @Test + @DisplayName("한번 던져서 스트라이크를 치지 않은 뒤 종료되지 않은 상태 테스트") + void isEnd_afterFirst() { + Frame frame = new LastFrame(); + frame.play(new Hit(5)); + + assertThat(frame.isEnd()).isFalse(); + } +} diff --git a/src/test/java/bowling/domain/NameTest.java b/src/test/java/bowling/domain/NameTest.java new file mode 100644 index 0000000000..c09142160a --- /dev/null +++ b/src/test/java/bowling/domain/NameTest.java @@ -0,0 +1,27 @@ +package bowling.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.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class NameTest { + + @Test + @DisplayName("기본기능 테스트") + void name() { + Name name = new Name("ASD"); + assertThat(name.getName()).isEqualTo("ASD"); + } + + @ParameterizedTest + @ValueSource(strings = {"AA", "DDDD"}) + @DisplayName("범위를 벗어나는 이름 테스트") + void name_range(String value) { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new Name(value)); + } +} diff --git a/src/test/java/bowling/domain/NormalFrameTest.java b/src/test/java/bowling/domain/NormalFrameTest.java new file mode 100644 index 0000000000..7651b50c76 --- /dev/null +++ b/src/test/java/bowling/domain/NormalFrameTest.java @@ -0,0 +1,81 @@ +package bowling.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NormalFrameTest { + + @Test + @DisplayName("스트라이크") + void strike() { + Frame frame = new NormalFrame(); + frame.play(new Hit(10)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(10)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.STRIKE); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(0)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.SKIP); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("스페어") + void spare() { + Frame frame = new NormalFrame(); + frame.play(new Hit(8)); + frame.play(new Hit(2)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(8)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.MISS); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(2)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.SPARE); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("거터") + void gutter() { + Frame frame = new NormalFrame(); + frame.play(new Hit(0)); + frame.play(new Hit(5)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(0)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.GUTTER); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(5)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.MISS); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("두번의 투구 모두 거터") + void gutter_2() { + Frame frame = new NormalFrame(); + frame.play(new Hit(0)); + frame.play(new Hit(0)); + + assertThat(frame.getFirstHit()).isEqualTo(new Hit(0)); + assertThat(frame.getFirstStatus()).isEqualTo(FrameStatus.GUTTER); + assertThat(frame.getSecondHit()).isEqualTo(new Hit(0)); + assertThat(frame.getSecondStatus()).isEqualTo(FrameStatus.GUTTER); + assertThat(frame.isEnd()).isTrue(); + } + + @Test + @DisplayName("종료되지 않은 상태 테스트") + void isEnd() { + Frame frame = new NormalFrame(); + + assertThat(frame.isEnd()).isFalse(); + } + + @Test + @DisplayName("한번 던져서 스트라이크를 치지 않은 뒤 종료되지 않은 상태 테스트") + void isEnd_afterFirst() { + Frame frame = new NormalFrame(); + frame.play(new Hit(5)); + + assertThat(frame.isEnd()).isFalse(); + } +} diff --git a/src/test/java/bowling/domain/RoundTest.java b/src/test/java/bowling/domain/RoundTest.java new file mode 100644 index 0000000000..1646043c76 --- /dev/null +++ b/src/test/java/bowling/domain/RoundTest.java @@ -0,0 +1,49 @@ +package bowling.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RoundTest { + + @Test + @DisplayName("초기 상태 테스트") + void round_init() { + Round round = new Round(); + + assertThat(round.isEnd()).isFalse(); + assertThat(round.getCurrentFrameNumber()).isEqualTo(1); + } + + @Test + @DisplayName("스트라이크 투구 테스트") + void round_hit() { + Round round = new Round(); + round.hit(new Hit(10)); + round.hit(new Hit(5)); + + assertThat(round.isEnd()).isFalse(); + assertThat(round.getCurrentFrameNumber()).isEqualTo(2); + } + + @Test + @DisplayName("라운드 종료 테스트") + void round_end() { + Round round = new Round(); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(10)); + round.hit(new Hit(5)); + round.hit(new Hit(4)); + + assertThat(round.isEnd()).isTrue(); + assertThat(round.getCurrentFrameNumber()).isEqualTo(10); + } +}