From 980b330d0e1d74735d0190c10d01802d3b5b1479 Mon Sep 17 00:00:00 2001 From: Sanghyeok Hyun Date: Thu, 17 Nov 2022 21:48:08 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=80=201=EB=8B=A8=EA=B3=84=20-=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EC=82=AD=EC=A0=9C=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#9?= =?UTF-8?q?88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 요구사항 정리 * refactor: QnAService 의 '게시글 작성자가 본인인지 검증'로직을 Question으로 이동 * refactor: Question 의 List을 일급 콜렉션 Answers로 변환, Answer 검증 로직 이전 * refactor: Answers 의 검증 로직을 Question의 검증 로직에 병합 * refactor: DeleteHistory의 일급 콜렉션 DeleteHistories를 생성 * docs: DeleteHistories를 가변객체로 변경함에 따른 요구사항 변경 * refactor: QnAService 의 Answer을 삭제하는 로직을 Answers로 이동 * refactor: QnAService의 Question을 삭제하는 로직을 Question으로 이동 * refactor: Question의 delete() 로직에서 위 로직을 모두 수행하고, 질문과 답글이 모두 포함된 DeleteHistories를 반환하도록 변경 * test: 댓글 삭제 메소드 테스트 케이스 추가 * test: 테스트 순서에 따라 결과가 달라지는 문제 수정 * refactor: DeleteHistories를 불변객체로 변경 * refactor: DeleteHistory 생성 시 사용되는 LocalDateTime를 서비스 레이어에서 주입하도록 변경 * test: delete() 메서드에서 반환하는 DeleteHistories가 올바른 상태의 DeleteHistory 객체를 포함하고 있는지 확인하도록 수정 --- src/main/java/qna/README.md | 14 +++++ src/main/java/qna/domain/Answer.java | 26 +++++--- src/main/java/qna/domain/Answers.java | 46 ++++++++++++++ src/main/java/qna/domain/DeleteHistories.java | 28 +++++++++ src/main/java/qna/domain/Question.java | 62 +++++++++---------- src/main/java/qna/service/QnAService.java | 24 +------ src/test/java/qna/domain/AnswerTest.java | 41 +++++++++++- src/test/java/qna/domain/AnswersTest.java | 41 ++++++++++++ .../java/qna/domain/DeleteHistoriesTest.java | 27 ++++++++ src/test/java/qna/domain/QuestionTest.java | 58 ++++++++++++++++- 10 files changed, 299 insertions(+), 68 deletions(-) create mode 100644 src/main/java/qna/README.md create mode 100644 src/main/java/qna/domain/Answers.java create mode 100644 src/main/java/qna/domain/DeleteHistories.java create mode 100644 src/test/java/qna/domain/AnswersTest.java create mode 100644 src/test/java/qna/domain/DeleteHistoriesTest.java diff --git a/src/main/java/qna/README.md b/src/main/java/qna/README.md new file mode 100644 index 0000000000..eaae3e4804 --- /dev/null +++ b/src/main/java/qna/README.md @@ -0,0 +1,14 @@ +# QnA 서비스 리팩토링 요구사항 +- [x] `QnAService` 의 `게시글 작성자가 본인인지 검증` 로직을 `Question`으로 이동 + - [x] 검증 로직 테스트 +- [x] `Question` 의 `List`을 일급 콜렉션 `Answers`로 변환 + - [x] `Answers` 로 `QnAService`의 `다른 사람이 작성한 댓글이 있는지 검증` 로직을 이동 + - [x] 검증 로직 테스트 +- [x] `Answers` 의 검증 로직을 `Question`의 검증 로직에 병합 +- [x] `DeleteHistory`의 일급 콜렉션 `DeleteHistories`를 생성 + - [x] 테스트 작성 +- [x] `QnAService` 의 `Answer을 삭제` 하는 로직을 `Answers`로 이동 + - [x] 테스트 작성 +- [x] `QnAService` 의 `Question을 삭제` 하는 로직을 `Question`으로 이동 + - [x] 테스트 작성 +- [x] `Question` 의 `delete()` 로직에서 위 로직을 모두 수행하고, 질문과 답글이 모두 포함된 `DeleteHistories` 를 반환하도록 변경 \ No newline at end of file diff --git a/src/main/java/qna/domain/Answer.java b/src/main/java/qna/domain/Answer.java index 548b71ed71..375afe3fc3 100644 --- a/src/main/java/qna/domain/Answer.java +++ b/src/main/java/qna/domain/Answer.java @@ -1,9 +1,11 @@ package qna.domain; +import qna.CannotDeleteException; import qna.NotFoundException; import qna.UnAuthorizedException; import javax.persistence.*; +import java.time.LocalDateTime; @Entity public class Answer extends AbstractEntity { @@ -43,19 +45,10 @@ public Answer(Long id, User writer, Question question, String contents) { this.contents = contents; } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } - public boolean isDeleted() { return deleted; } - public boolean isOwner(User writer) { - return this.writer.equals(writer); - } - public User getWriter() { return writer; } @@ -68,6 +61,21 @@ public void toQuestion(Question question) { this.question = question; } + public DeleteHistories delete(DeleteHistories deleteHistories, LocalDateTime dateTime) { + deleted = true; + return deleteHistories.add(new DeleteHistory(ContentType.ANSWER, getId(), writer, dateTime)); + } + + void validateOwner(User loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + } + + private boolean isOwner(User writer) { + return this.writer.equals(writer); + } + @Override public String toString() { return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]"; diff --git a/src/main/java/qna/domain/Answers.java b/src/main/java/qna/domain/Answers.java new file mode 100644 index 0000000000..edf6c2ed79 --- /dev/null +++ b/src/main/java/qna/domain/Answers.java @@ -0,0 +1,46 @@ +package qna.domain; + +import org.hibernate.annotations.Where; +import qna.CannotDeleteException; + +import javax.persistence.CascadeType; +import javax.persistence.Embeddable; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Embeddable +public class Answers { + + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL) + @Where(clause = "deleted = false") + @OrderBy("id ASC") + private final List answers = new ArrayList<>(); + + public Answers() { + } + + public void add(Answer answer) { + answers.add(answer); + } + + public DeleteHistories deleteAll(DeleteHistories deleteHistories, LocalDateTime dateTime) { + for (Answer answer : answers) { + deleteHistories = answer.delete(deleteHistories, dateTime); + } + return deleteHistories; + } + + public void validateOwner(User loginUser) throws CannotDeleteException { + for (Answer answer : answers) { + answer.validateOwner(loginUser); + } + } + + public List answers() { + return Collections.unmodifiableList(answers); + } +} diff --git a/src/main/java/qna/domain/DeleteHistories.java b/src/main/java/qna/domain/DeleteHistories.java new file mode 100644 index 0000000000..89c7309adf --- /dev/null +++ b/src/main/java/qna/domain/DeleteHistories.java @@ -0,0 +1,28 @@ +package qna.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DeleteHistories { + private final List deleteHistories; + + public DeleteHistories() { + this.deleteHistories = new ArrayList<>(); + } + + public DeleteHistories(List deleteHistories) { + this.deleteHistories = new ArrayList<>(deleteHistories); + } + + public DeleteHistories add(DeleteHistory deleteHistory) { + List newDeleteHistories = new ArrayList<>(deleteHistories); + newDeleteHistories.add(deleteHistory); + + return new DeleteHistories(newDeleteHistories); + } + + public List histories() { + return Collections.unmodifiableList(deleteHistories); + } +} diff --git a/src/main/java/qna/domain/Question.java b/src/main/java/qna/domain/Question.java index 1e8bb11251..83e1678ace 100644 --- a/src/main/java/qna/domain/Question.java +++ b/src/main/java/qna/domain/Question.java @@ -1,10 +1,9 @@ package qna.domain; -import org.hibernate.annotations.Where; +import qna.CannotDeleteException; import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; +import java.time.LocalDateTime; @Entity public class Question extends AbstractEntity { @@ -18,10 +17,8 @@ public class Question extends AbstractEntity { @JoinColumn(foreignKey = @ForeignKey(name = "fk_question_writer")) private User writer; - @OneToMany(mappedBy = "question", cascade = CascadeType.ALL) - @Where(clause = "deleted = false") - @OrderBy("id ASC") - private List answers = new ArrayList<>(); + @Embedded + private Answers answers = new Answers(); private boolean deleted = false; @@ -39,24 +36,6 @@ public Question(long id, String title, String contents) { this.contents = contents; } - public String getTitle() { - return title; - } - - public Question setTitle(String title) { - this.title = title; - return this; - } - - public String getContents() { - return contents; - } - - public Question setContents(String contents) { - this.contents = contents; - return this; - } - public User getWriter() { return writer; } @@ -71,21 +50,36 @@ public void addAnswer(Answer answer) { answers.add(answer); } - public boolean isOwner(User loginUser) { - return writer.equals(loginUser); + public boolean isDeleted() { + return deleted; } - public Question setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + public Answers getAnswers() { + return answers; } - public boolean isDeleted() { - return deleted; + public DeleteHistories delete(User loginUser, LocalDateTime dateTime) throws CannotDeleteException { + validateOwner(loginUser); + + DeleteHistories deleteHistories = delete(new DeleteHistories(), dateTime); + deleteHistories = answers.deleteAll(deleteHistories, dateTime); + return deleteHistories; } - public List getAnswers() { - return answers; + DeleteHistories delete(DeleteHistories deleteHistories, LocalDateTime dateTime) { + deleted = true; + return deleteHistories.add(new DeleteHistory(ContentType.QUESTION, getId(), writer, dateTime)); + } + + void validateOwner(User loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } + answers.validateOwner(loginUser); + } + + private boolean isOwner(User writer) { + return this.writer.equals(writer); } @Override diff --git a/src/main/java/qna/service/QnAService.java b/src/main/java/qna/service/QnAService.java index 66821cd9c2..7cdf36a8b8 100644 --- a/src/main/java/qna/service/QnAService.java +++ b/src/main/java/qna/service/QnAService.java @@ -10,8 +10,6 @@ import javax.annotation.Resource; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; @Service("qnaService") public class QnAService { @@ -35,24 +33,8 @@ public Question findQuestionById(Long id) { @Transactional public void deleteQuestion(User loginUser, long questionId) throws CannotDeleteException { Question question = findQuestionById(questionId); - if (!question.isOwner(loginUser)) { - throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); - } - - List answers = question.getAnswers(); - for (Answer answer : answers) { - if (!answer.isOwner(loginUser)) { - throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); - } - } - - List deleteHistories = new ArrayList<>(); - question.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.QUESTION, questionId, question.getWriter(), LocalDateTime.now())); - for (Answer answer : answers) { - answer.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); - } - deleteHistoryService.saveAll(deleteHistories); + DeleteHistories deleteHistories = question.delete(loginUser, LocalDateTime.now()); + + deleteHistoryService.saveAll(deleteHistories.histories()); } } diff --git a/src/test/java/qna/domain/AnswerTest.java b/src/test/java/qna/domain/AnswerTest.java index d858181e31..54c53ccd84 100644 --- a/src/test/java/qna/domain/AnswerTest.java +++ b/src/test/java/qna/domain/AnswerTest.java @@ -1,6 +1,43 @@ package qna.domain; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import qna.CannotDeleteException; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; + public class AnswerTest { - public static final Answer A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); - public static final Answer A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + public static Answer A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + public static Answer A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + + @AfterEach + void tearDown() { + A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + } + + @Test + void validation_작성자_본인_여부_정상() { + assertThatCode(() -> A1.validateOwner(UserTest.JAVAJIGI)) + .doesNotThrowAnyException(); + } + + @Test + void validation_작성자_본인_여부_오류() { + assertThatThrownBy(() -> A1.validateOwner(UserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class); + } + + @Test + void 댓글_삭제() { + LocalDateTime localDateTime = LocalDateTime.now(); + DeleteHistories deleteHistories = A1.delete(new DeleteHistories(), localDateTime); + + assertThat(A1.isDeleted()).isTrue(); + assertThat(deleteHistories.histories()).containsExactly( + new DeleteHistory(ContentType.ANSWER, A1.getId(), A1.getWriter(), localDateTime) + ); + } } diff --git a/src/test/java/qna/domain/AnswersTest.java b/src/test/java/qna/domain/AnswersTest.java new file mode 100644 index 0000000000..c89f5db075 --- /dev/null +++ b/src/test/java/qna/domain/AnswersTest.java @@ -0,0 +1,41 @@ +package qna.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static qna.domain.AnswerTest.A1; +import static qna.domain.AnswerTest.A2; + +class AnswersTest { + + private Answers answers; + + @BeforeEach + void setUp() { + answers = new Answers(); + answers.add(A1); + answers.add(A2); + } + + @Test + void add() { + assertThat(answers.answers()).containsExactly(A1, A2); + } + + @Test + void 답글_전쳬_삭제() { + LocalDateTime localDateTime = LocalDateTime.now(); + DeleteHistories deleteHistories = answers.deleteAll(new DeleteHistories(), LocalDateTime.now()); + + assertThat(A1.isDeleted()).isTrue(); + assertThat(A2.isDeleted()).isTrue(); + assertThat(deleteHistories.histories()).containsExactly( + new DeleteHistory(ContentType.ANSWER, A1.getId(), A1.getWriter(), localDateTime), + new DeleteHistory(ContentType.ANSWER, A2.getId(), A2.getWriter(), localDateTime) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/qna/domain/DeleteHistoriesTest.java b/src/test/java/qna/domain/DeleteHistoriesTest.java new file mode 100644 index 0000000000..af4bb35791 --- /dev/null +++ b/src/test/java/qna/domain/DeleteHistoriesTest.java @@ -0,0 +1,27 @@ +package qna.domain; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class DeleteHistoriesTest { + + private static final DeleteHistory DH1 = new DeleteHistory( + ContentType.QUESTION, + QuestionTest.Q1.getId(), + QuestionTest.Q1.getWriter(), + LocalDateTime.now() + ); + + @Test + void 원소추가시_새로운_객체_반환() { + DeleteHistories deleteHistories1 = new DeleteHistories(); + DeleteHistories deleteHistories2 = deleteHistories1.add(DH1); + + assertThat(deleteHistories1.histories()).hasSize(0); + assertThat(deleteHistories2.histories()).hasSize(1); + assertThat(deleteHistories1).isNotSameAs(deleteHistories2); + } +} \ No newline at end of file diff --git a/src/test/java/qna/domain/QuestionTest.java b/src/test/java/qna/domain/QuestionTest.java index b48c9a2209..60f9dfd281 100644 --- a/src/test/java/qna/domain/QuestionTest.java +++ b/src/test/java/qna/domain/QuestionTest.java @@ -1,6 +1,60 @@ package qna.domain; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import qna.CannotDeleteException; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; +import static qna.domain.AnswerTest.A1; +import static qna.domain.AnswerTest.A2; + public class QuestionTest { - public static final Question Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); - public static final Question Q2 = new Question("title2", "contents2").writeBy(UserTest.SANJIGI); + public static Question Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); + + @AfterEach + void tearDown() { + Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); + } + + @Test + void validation_작성자_본인_여부_정상() { + assertThatCode(() -> Q1.validateOwner(UserTest.JAVAJIGI)) + .doesNotThrowAnyException(); + } + + @Test + void validation_작성자_본인_여부_오류() { + assertThatThrownBy(() -> Q1.validateOwner(UserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class); + } + + @Test + void 게시물_삭제() { + DeleteHistories deleteHistories = Q1.delete(new DeleteHistories(), LocalDateTime.now()); + + assertThat(Q1.isDeleted()).isTrue(); + assertThat(deleteHistories.histories()).hasSize(1); + } + + @Test + void 게시물_및_댓글_삭졔_정상() throws CannotDeleteException { + Q1.addAnswer(A1); + LocalDateTime localDateTime = LocalDateTime.now(); + DeleteHistories deleteHistories = Q1.delete(UserTest.JAVAJIGI, localDateTime); + + assertThat(deleteHistories.histories()).containsExactly( + new DeleteHistory(ContentType.QUESTION, Q1.getId(), Q1.getWriter(), localDateTime), + new DeleteHistory(ContentType.ANSWER, A1.getId(), A1.getWriter(), localDateTime) + ); + } + + @Test + void 게시물_및_댓글_삭졔_오류() { + Q1.addAnswer(A2); + + assertThatThrownBy(() -> Q1.delete(UserTest.JAVAJIGI, LocalDateTime.now())) + .isInstanceOf(CannotDeleteException.class); + } } From a210e73f2fc5a88bb1f7d6a9a7ccf32963c57c9a Mon Sep 17 00:00:00 2001 From: Sanghyeok Hyun Date: Mon, 2 Jan 2023 14:23:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?2=EB=8B=A8=EA=B3=84=20-=20=EB=B3=BC?= =?UTF-8?q?=EB=A7=81=20=EC=A0=90=EC=88=98=ED=8C=90(=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0)=20(#1006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 요구사항 문서 작성 * docs: 요구사항 문서 작성 * feat: Pins 구현 * feat: Status 상속 구조 구현 * test: FinishedTest 클래스를 각 구현 클래스 별로 분리 * feat: state 및 Pin 클래스에서 결과 문자열을 가지고 제공하도록 구현 * feat: Frame 인터페이스 정의 * feat: Frame을 구현하는 NormalFrame 구현 * feat: Frame의 일급콜렉션 Frames 구현 * feat: FinalFrame 구현 * feat: UI 클래스, Controller, FrameResultsDto 구현 * feat: Player 클래스 구현 * feat: Player까지 한번에 출력하도록 FrameResultsDto, InputView, Controller 수정 * test: Frames 테스트 케이스 구현 * refactor: NormalFrame init() 정적 팩토리 메소드 추가, 인스턴스 변수가 없는 Status 클래스에 equals(), hashcode() 구현 * refactor: FinalFrame init() 정적 팩토리 메소드 추가 * test: Player 정상 생성 테스트 케이스 추가 * refactor: ResultDto를 ResultLines로 변경, OutputView에서 생성하던 결과 리스트를 ResultLines에서 생성하도록 변경 --- src/main/java/bowling/GameController.java | 24 ++++ src/main/java/bowling/README.md | 47 ++++++++ src/main/java/bowling/domain/FinalFrame.java | 100 ++++++++++++++++ src/main/java/bowling/domain/Frame.java | 13 ++ src/main/java/bowling/domain/Frames.java | 61 ++++++++++ src/main/java/bowling/domain/NormalFrame.java | 76 ++++++++++++ src/main/java/bowling/domain/Pin.java | 55 +++++++++ src/main/java/bowling/domain/Player.java | 24 ++++ src/main/java/bowling/domain/ResultLines.java | 51 ++++++++ .../java/bowling/domain/state/Finished.java | 17 +++ .../java/bowling/domain/state/FirstPin.java | 39 ++++++ src/main/java/bowling/domain/state/Miss.java | 36 ++++++ src/main/java/bowling/domain/state/Ready.java | 35 ++++++ .../java/bowling/domain/state/Running.java | 10 ++ src/main/java/bowling/domain/state/Spare.java | 34 ++++++ .../java/bowling/domain/state/Status.java | 11 ++ .../java/bowling/domain/state/Strike.java | 28 +++++ src/main/java/bowling/view/InputView.java | 22 ++++ src/main/java/bowling/view/OutputView.java | 36 ++++++ .../java/bowling/domain/FinalFrameTest.java | 83 +++++++++++++ .../java/bowling/domain/FrameFactory.java | 12 ++ src/test/java/bowling/domain/FramesTest.java | 45 +++++++ .../java/bowling/domain/NormalFrameTest.java | 113 ++++++++++++++++++ src/test/java/bowling/domain/PinTest.java | 41 +++++++ src/test/java/bowling/domain/PlayerTest.java | 23 ++++ .../java/bowling/domain/ResultLinesTest.java | 48 ++++++++ .../bowling/domain/state/FirstPinTest.java | 36 ++++++ .../java/bowling/domain/state/MissTest.java | 30 +++++ .../java/bowling/domain/state/ReadyTest.java | 34 ++++++ .../java/bowling/domain/state/SpareTest.java | 27 +++++ .../java/bowling/domain/state/StrikeTest.java | 26 ++++ 31 files changed, 1237 insertions(+) create mode 100644 src/main/java/bowling/GameController.java create mode 100644 src/main/java/bowling/README.md create mode 100644 src/main/java/bowling/domain/FinalFrame.java create mode 100644 src/main/java/bowling/domain/Frame.java create mode 100644 src/main/java/bowling/domain/Frames.java create mode 100644 src/main/java/bowling/domain/NormalFrame.java create mode 100644 src/main/java/bowling/domain/Pin.java create mode 100644 src/main/java/bowling/domain/Player.java create mode 100644 src/main/java/bowling/domain/ResultLines.java create mode 100644 src/main/java/bowling/domain/state/Finished.java create mode 100644 src/main/java/bowling/domain/state/FirstPin.java create mode 100644 src/main/java/bowling/domain/state/Miss.java create mode 100644 src/main/java/bowling/domain/state/Ready.java create mode 100644 src/main/java/bowling/domain/state/Running.java create mode 100644 src/main/java/bowling/domain/state/Spare.java create mode 100644 src/main/java/bowling/domain/state/Status.java create mode 100644 src/main/java/bowling/domain/state/Strike.java create mode 100644 src/main/java/bowling/view/InputView.java create mode 100644 src/main/java/bowling/view/OutputView.java create mode 100644 src/test/java/bowling/domain/FinalFrameTest.java create mode 100644 src/test/java/bowling/domain/FrameFactory.java create mode 100644 src/test/java/bowling/domain/FramesTest.java create mode 100644 src/test/java/bowling/domain/NormalFrameTest.java create mode 100644 src/test/java/bowling/domain/PinTest.java create mode 100644 src/test/java/bowling/domain/PlayerTest.java create mode 100644 src/test/java/bowling/domain/ResultLinesTest.java create mode 100644 src/test/java/bowling/domain/state/FirstPinTest.java create mode 100644 src/test/java/bowling/domain/state/MissTest.java create mode 100644 src/test/java/bowling/domain/state/ReadyTest.java create mode 100644 src/test/java/bowling/domain/state/SpareTest.java create mode 100644 src/test/java/bowling/domain/state/StrikeTest.java diff --git a/src/main/java/bowling/GameController.java b/src/main/java/bowling/GameController.java new file mode 100644 index 0000000000..7fc9932c01 --- /dev/null +++ b/src/main/java/bowling/GameController.java @@ -0,0 +1,24 @@ +package bowling; + +import bowling.domain.Frames; +import bowling.domain.Player; +import bowling.domain.ResultLines; +import bowling.view.InputView; +import bowling.view.OutputView; + +public class GameController { + + public static void main(String[] args) { + Frames frames = new Frames(); + Player player = InputView.getPlayer(); + + OutputView.printFrameResult(new ResultLines(player, frames.results())); + + while (!frames.gameFinished()) { + frames.bowl(InputView.getNextPin(frames.currentFrameNumber())); + + OutputView.printFrameResult(new ResultLines(player, frames.results())); + } + } + +} diff --git a/src/main/java/bowling/README.md b/src/main/java/bowling/README.md new file mode 100644 index 0000000000..5f70541530 --- /dev/null +++ b/src/main/java/bowling/README.md @@ -0,0 +1,47 @@ +## 볼링 1단계 기능 요구사항 +### Pin +* 볼링공을 굴렸을 때 쓰러뜨린 핀의 개수(amount)를 가짐 +* 현재 amount가 전체 핀의 개수와 일치하는지 확인하는 isMax() +* 자신이 가진 amount와 인자로 주어진 핀의 amount를 비교해 핀 전체를 쓰려뜨렸는지 확인하는 isClear() + +### Player + +### Frame +* NormalFrame와 FinalFrame에서 구현되는 인터페이스 +* bowl(Pin pin), isFinished(), nextFrame()을 갖는다 + +### NormalFrame +* 상태 값(Status)을 가짐 +* bowl() 메소드로 게임을 진행 -> Status에게 위임 +* isFinished()로 게임이 종료되었는지 확인 -> Status 클래스에게 판단을 위임 +* nextFrame() 으로 다음 frame을 반환 + * 게임이 종료되었을 경우 새로운 프레임, 게임이 진행중이면 기존 프레임 반환 + +### FinalFrame +* 상태 값(Status)의 리스트(statuses)를 가짐 +* 10번째 프레임에서는 Strike/Spare 시 총 3번의 bowl() 기회가 주어짐 + * bowlCount 변수를 가져 예외 로직 처리 +* ifFinished()에서 bowlCount가 3이면, 무조건 true + * bowlCount가 2면, Miss가 있을 경우에만 true + * 나머지(bawlCount <= 1 || Miss가 없는 BowlCount == 2) 는 false +* nextFrame()은 Exception 발생 (마지막 프레임) + +### Frames +* Frame의 일급컬렉션 (볼링 게임이 진행되는 동안 최대 10개의 Frame을 관리) +* bowl() 메소드로 게임을 진행 -> Frame에게 위임 +* 전체 볼링 게임 종료 여부 gameFinished() + +### 상태 클래스 +* 필요에 따라 pins를 가져 게임 진행 상태에 대한 정보를 기록 +* isFinished() 메소드로 종료 여부를 판단 +* bowl() 메소드로 게임을 진행 + * 현재 상태와 인자(Pins)에 따라 다른 상태로 변환됨 + +* Status(interface) + * Running(abstract) + * Ready -> Frame의 최초 상태 + * FirstPin -> 한 번 bowl()을 했으나 아직 한 Frame이 종료되지 않은 상태 + * Finished(abstract) + * Miss + * Strike + * Spare \ No newline at end of file diff --git a/src/main/java/bowling/domain/FinalFrame.java b/src/main/java/bowling/domain/FinalFrame.java new file mode 100644 index 0000000000..dd1474dc35 --- /dev/null +++ b/src/main/java/bowling/domain/FinalFrame.java @@ -0,0 +1,100 @@ +package bowling.domain; + +import bowling.domain.state.Miss; +import bowling.domain.state.Ready; +import bowling.domain.state.Status; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class FinalFrame implements Frame { + + public static final int MAX_BOWLCOUNT = 3; + public static final String FINALFRAME_MESSAGE_DELIMITER = "|"; + + private final List statuses; + private int bowlCount = 0; + + FinalFrame(List statuses) { + this.statuses = statuses; + } + + public static Frame init() { + List statuses = new ArrayList<>(MAX_BOWLCOUNT); + statuses.add(new Ready()); + + return new FinalFrame(statuses); + } + + @Override + public void bowl(Pin pin) { + assertFinished(); + if (currentStatus().isFinished()) { + statuses.add(new Ready()); + } + + Status currentStatus = currentStatus(); + statuses.remove(currentStatusIndex()); + + statuses.add(currentStatus.bowl(pin)); + bowlCount++; + } + + private Status currentStatus() { + return statuses.get(currentStatusIndex()); + } + + private int currentStatusIndex() { + return statuses.size() - 1; + } + + private void assertFinished() { + if (isFinished()) { + throw new IllegalStateException("현재 프레임에서는 더 이상 게임을 진행할 수 없습니다."); + } + } + + @Override + public boolean isFinished() { + if (bowlCount == MAX_BOWLCOUNT) { + return true; + } + if (bowlCount == MAX_BOWLCOUNT - 1) { + return statuses.stream() + .anyMatch(status -> status instanceof Miss); + } + return false; + } + + @Override + public Frame nextFrame() { + throw new IllegalStateException("마지막 프레임입니다. 다음 프레임을 생성할 수 없습니다."); + } + + @Override + public int frameNumber() { + return Frames.MAX_FRAMENUMBER; + } + + @Override + public String toString() { + return statuses.stream() + .map(Object::toString) + .collect(Collectors.joining(FINALFRAME_MESSAGE_DELIMITER)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FinalFrame that = (FinalFrame) o; + return bowlCount == that.bowlCount && Objects.equals(statuses, that.statuses); + } + + @Override + public int hashCode() { + return Objects.hash(statuses, bowlCount); + } +} diff --git a/src/main/java/bowling/domain/Frame.java b/src/main/java/bowling/domain/Frame.java new file mode 100644 index 0000000000..ca7a09e40c --- /dev/null +++ b/src/main/java/bowling/domain/Frame.java @@ -0,0 +1,13 @@ +package bowling.domain; + +public interface Frame { + + void bowl(Pin pin); + + boolean isFinished(); + + Frame nextFrame(); + + int frameNumber(); + +} diff --git a/src/main/java/bowling/domain/Frames.java b/src/main/java/bowling/domain/Frames.java new file mode 100644 index 0000000000..0c96fba4f0 --- /dev/null +++ b/src/main/java/bowling/domain/Frames.java @@ -0,0 +1,61 @@ +package bowling.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Frames { + + public static final int MIN_FRAMENUMBER = 1; + public static final int MAX_FRAMENUMBER = 10; + + private final List frames; + + public Frames() { + this(initFrames()); + } + + public Frames(List frames) { + this.frames = frames; + } + + private static List initFrames() { + List frames = new ArrayList<>(); + frames.add(NormalFrame.init(1)); + return frames; + } + + public int currentFrameNumber() { + return currentFrame().frameNumber(); + } + + public void bowl(Pin pin) { + currentFrame().bowl(pin); + nextFrame(); + } + + private void nextFrame() { + if (frames.size() == MAX_FRAMENUMBER) { + return; + } + if (currentFrame().isFinished()) { + frames.add(currentFrame().nextFrame()); + } + } + + public boolean gameFinished() { + if (frames.size() < MAX_FRAMENUMBER) { + return false; + } + return currentFrame().isFinished(); + } + + private Frame currentFrame() { + return frames.get(frames.size() - 1); + } + + public List results() { + return Collections.unmodifiableList(frames); + } + +} diff --git a/src/main/java/bowling/domain/NormalFrame.java b/src/main/java/bowling/domain/NormalFrame.java new file mode 100644 index 0000000000..98df2b6c79 --- /dev/null +++ b/src/main/java/bowling/domain/NormalFrame.java @@ -0,0 +1,76 @@ +package bowling.domain; + +import bowling.domain.state.Ready; +import bowling.domain.state.Status; + +import java.util.Objects; + +public class NormalFrame implements Frame { + + public static final int NORMALFRAME_MAX_FRAMENUMBER = 9; + + private final int frameNumber; + private Status status; + + NormalFrame(int frameNumber, Status status) { + validate(frameNumber); + this.frameNumber = frameNumber; + this.status = status; + } + + public static Frame init(int frameNumber) { + return new NormalFrame(frameNumber, new Ready()); + } + + private static void validate(int frameNumber) { + if (frameNumber < Frames.MIN_FRAMENUMBER) { + throw new IllegalArgumentException("Frame은 1번부터 시작합니다."); + } + if (frameNumber > NORMALFRAME_MAX_FRAMENUMBER) { + throw new IllegalArgumentException("NormalFrame은 9번까지만 존재합니다."); + } + } + + public int frameNumber() { + return frameNumber; + } + + @Override + public void bowl(Pin pin) { + status = status.bowl(pin); + } + + @Override + public boolean isFinished() { + return status.isFinished(); + } + + @Override + public Frame nextFrame() { + if (!isFinished()) { + return this; + } + if (frameNumber < NORMALFRAME_MAX_FRAMENUMBER) { + return NormalFrame.init(frameNumber + 1); + } + return FinalFrame.init(); + } + + @Override + public String toString() { + return status.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NormalFrame that = (NormalFrame) o; + return frameNumber == that.frameNumber && Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(frameNumber, status); + } +} diff --git a/src/main/java/bowling/domain/Pin.java b/src/main/java/bowling/domain/Pin.java new file mode 100644 index 0000000000..f5c00c1e54 --- /dev/null +++ b/src/main/java/bowling/domain/Pin.java @@ -0,0 +1,55 @@ +package bowling.domain; + +import java.util.Objects; + +public class Pin { + + public static final int MIN_AMOUNT = 0; + public static final int MAX_AMOUNT = 10; + public static final String GUTTER_MESSAGE = "-"; + + private final int amount; + + public Pin(int amount) { + validate(amount); + this.amount = amount; + } + + private void validate(int amount) { + if (amount < MIN_AMOUNT) { + throw new IllegalArgumentException("볼링 핀의 개수는 0개 이상이어야 합니다."); + } + if (amount > MAX_AMOUNT) { + throw new IllegalArgumentException("볼링 핀의 개수는 10개 이하여야 합니다."); + } + } + + public boolean isMax() { + return amount == MAX_AMOUNT; + } + + public boolean isClear(Pin pin) { + return amount + pin.amount == MAX_AMOUNT; + } + + @Override + public String toString() { + if (amount == MIN_AMOUNT) { + return GUTTER_MESSAGE; + } + return String.valueOf(amount); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pin pin = (Pin) o; + return amount == pin.amount; + } + + @Override + public int hashCode() { + return Objects.hash(amount); + } +} diff --git a/src/main/java/bowling/domain/Player.java b/src/main/java/bowling/domain/Player.java new file mode 100644 index 0000000000..9ead22d596 --- /dev/null +++ b/src/main/java/bowling/domain/Player.java @@ -0,0 +1,24 @@ +package bowling.domain; + +public class Player { + + public static final int PLAYER_NAME_LENGTH = 3; + + private final String name; + + public Player(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (name.length() != PLAYER_NAME_LENGTH) { + throw new IllegalArgumentException("플레이어의 이름은 3글자여야 합니다."); + } + } + + public String name() { + return name; + } + +} diff --git a/src/main/java/bowling/domain/ResultLines.java b/src/main/java/bowling/domain/ResultLines.java new file mode 100644 index 0000000000..36f9c3b9bd --- /dev/null +++ b/src/main/java/bowling/domain/ResultLines.java @@ -0,0 +1,51 @@ +package bowling.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ResultLines { + + public static final String NAME_MESSAGE = " NAME "; + public static final String BLANK_MESSAGE = ""; + public static final String NUMBER_FORMAT = "%02d"; + + static final List FIRST_LINE; + private final LinkedList secondLine; + + static { + FIRST_LINE = new ArrayList<>(); + FIRST_LINE.add(NAME_MESSAGE); + FIRST_LINE.addAll(IntStream.rangeClosed(Frames.MIN_FRAMENUMBER, Frames.MAX_FRAMENUMBER) + .mapToObj(s -> String.format(NUMBER_FORMAT, s)) + .map(String::valueOf) + .collect(Collectors.toList())); + } + + public ResultLines(Player player, List frameResults) { + secondLine = frameResults.stream() + .map(Object::toString) + .collect(Collectors.toCollection(LinkedList::new)); + + makeResultsMaxSize(frameResults); + secondLine.addFirst(player.name()); + } + + private void makeResultsMaxSize(List frameResults) { + int frameResultsSize = frameResults.size(); + for (int i = 0; i < (Frames.MAX_FRAMENUMBER - frameResultsSize); i++) { + secondLine.addLast(BLANK_MESSAGE); + } + } + + public List firstLine() { + return Collections.unmodifiableList(FIRST_LINE); + } + + public List secondLine() { + return Collections.unmodifiableList(secondLine); + } +} diff --git a/src/main/java/bowling/domain/state/Finished.java b/src/main/java/bowling/domain/state/Finished.java new file mode 100644 index 0000000000..522bf68862 --- /dev/null +++ b/src/main/java/bowling/domain/state/Finished.java @@ -0,0 +1,17 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +abstract public class Finished implements Status { + + @Override + public boolean isFinished() { + return true; + } + + @Override + public Status bowl(Pin pin) { + throw new IllegalStateException("현재 상태에서는 더 이상 게임을 진행할 수 없습니다."); + } + +} diff --git a/src/main/java/bowling/domain/state/FirstPin.java b/src/main/java/bowling/domain/state/FirstPin.java new file mode 100644 index 0000000000..72f5a4b0fb --- /dev/null +++ b/src/main/java/bowling/domain/state/FirstPin.java @@ -0,0 +1,39 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +import java.util.Objects; + +public class FirstPin extends Running { + + private final Pin firstPin; + + public FirstPin(Pin firstPin) { + this.firstPin = firstPin; + } + + @Override + public Status bowl(Pin secondPin) { + if (firstPin.isClear(secondPin)) { + return new Spare(firstPin); + } + return new Miss(firstPin, secondPin); + } + + @Override + public String toString() { + return firstPin.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + return getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash(); + } +} diff --git a/src/main/java/bowling/domain/state/Miss.java b/src/main/java/bowling/domain/state/Miss.java new file mode 100644 index 0000000000..fe93476e1f --- /dev/null +++ b/src/main/java/bowling/domain/state/Miss.java @@ -0,0 +1,36 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +import java.util.Objects; + +public class Miss extends Finished { + + public static final String MISS_MESSAGE = "|"; + + private final Pin firstPin; + private final Pin secondPin; + + public Miss(Pin firstPin, Pin secondPin) { + this.firstPin = firstPin; + this.secondPin = secondPin; + } + + @Override + public String toString() { + return firstPin + MISS_MESSAGE + secondPin; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Miss miss = (Miss) o; + return Objects.equals(firstPin, miss.firstPin) && Objects.equals(secondPin, miss.secondPin); + } + + @Override + public int hashCode() { + return Objects.hash(firstPin, secondPin); + } +} diff --git a/src/main/java/bowling/domain/state/Ready.java b/src/main/java/bowling/domain/state/Ready.java new file mode 100644 index 0000000000..9d4c2543f3 --- /dev/null +++ b/src/main/java/bowling/domain/state/Ready.java @@ -0,0 +1,35 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +import java.util.Objects; + +public class Ready extends Running { + + public static final String READY_MESSAGE = ""; + + @Override + public Status bowl(Pin pin) { + if (pin.isMax()) { + return new Strike(); + } + return new FirstPin(pin); + } + + @Override + public String toString() { + return READY_MESSAGE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + return getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash(); + } +} diff --git a/src/main/java/bowling/domain/state/Running.java b/src/main/java/bowling/domain/state/Running.java new file mode 100644 index 0000000000..487aa43782 --- /dev/null +++ b/src/main/java/bowling/domain/state/Running.java @@ -0,0 +1,10 @@ +package bowling.domain.state; + +abstract public class Running implements Status { + + @Override + public boolean isFinished() { + return false; + } + +} diff --git a/src/main/java/bowling/domain/state/Spare.java b/src/main/java/bowling/domain/state/Spare.java new file mode 100644 index 0000000000..e17eba75d5 --- /dev/null +++ b/src/main/java/bowling/domain/state/Spare.java @@ -0,0 +1,34 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +import java.util.Objects; + +public class Spare extends Finished { + + public static final String SPARE_MESSAGE = "|/"; + + private final Pin firstPin; + + public Spare(Pin firstPin) { + this.firstPin = firstPin; + } + + @Override + public String toString() { + return firstPin + SPARE_MESSAGE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Spare spare = (Spare) o; + return Objects.equals(firstPin, spare.firstPin); + } + + @Override + public int hashCode() { + return Objects.hash(firstPin); + } +} diff --git a/src/main/java/bowling/domain/state/Status.java b/src/main/java/bowling/domain/state/Status.java new file mode 100644 index 0000000000..0b61c3078e --- /dev/null +++ b/src/main/java/bowling/domain/state/Status.java @@ -0,0 +1,11 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +public interface Status { + + boolean isFinished(); + + Status bowl(Pin pin); + +} diff --git a/src/main/java/bowling/domain/state/Strike.java b/src/main/java/bowling/domain/state/Strike.java new file mode 100644 index 0000000000..85dd0f4f3f --- /dev/null +++ b/src/main/java/bowling/domain/state/Strike.java @@ -0,0 +1,28 @@ +package bowling.domain.state; + +import java.util.Objects; + +public class Strike extends Finished { + + public static final String STRIKE_MESSAGE = "X"; + + public Strike() { + } + + @Override + public String toString() { + return STRIKE_MESSAGE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + return getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash(); + } +} diff --git a/src/main/java/bowling/view/InputView.java b/src/main/java/bowling/view/InputView.java new file mode 100644 index 0000000000..aa485af2ac --- /dev/null +++ b/src/main/java/bowling/view/InputView.java @@ -0,0 +1,22 @@ +package bowling.view; + +import bowling.domain.Pin; +import bowling.domain.Player; + +import java.util.Scanner; + +public class InputView { + + private static final Scanner sc = new Scanner(System.in); + + public static Player getPlayer() { + System.out.print("플레이어 이름은(3 english letters)?: "); + return new Player(sc.nextLine()); + } + + public static Pin getNextPin(int currentFrameNumber) { + System.out.print(currentFrameNumber + "프레임 투구 : "); + return new Pin(sc.nextInt()); + } + +} diff --git a/src/main/java/bowling/view/OutputView.java b/src/main/java/bowling/view/OutputView.java new file mode 100644 index 0000000000..d670f3d671 --- /dev/null +++ b/src/main/java/bowling/view/OutputView.java @@ -0,0 +1,36 @@ +package bowling.view; + +import bowling.domain.ResultLines; + +import java.util.List; + +public class OutputView { + + public static final String FRAME_DELIMETER = "|"; + + public static void printFrameResult(ResultLines results) { + printResult(results.firstLine()); + System.out.println(); + printResult(results.secondLine()); + + System.out.println(); + System.out.println(); + } + + private static void printResult(List results) { + printFrameDelimeter(); + for (String result : results) { + System.out.print(formatMessage(result)); + printFrameDelimeter(); + } + } + + private static void printFrameDelimeter() { + System.out.print(FRAME_DELIMETER); + } + + private static String formatMessage(String message) { + return String.format("%6s", String.format("%-4s", message)); + } + +} diff --git a/src/test/java/bowling/domain/FinalFrameTest.java b/src/test/java/bowling/domain/FinalFrameTest.java new file mode 100644 index 0000000000..ae28eb9e2e --- /dev/null +++ b/src/test/java/bowling/domain/FinalFrameTest.java @@ -0,0 +1,83 @@ +package bowling.domain; + +import bowling.domain.state.Miss; +import bowling.domain.state.Ready; +import bowling.domain.state.Spare; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class FinalFrameTest { + + @Test + void init() { + assertThat(FinalFrame.init()).isEqualTo(new FinalFrame(List.of(new Ready()))); + } + + @Test + void 프레임_번호_얻기() { + assertThat(FinalFrame.init().frameNumber()).isEqualTo(10); + } + + @Test + void 다음_프레임_얻기() { + assertThatThrownBy(() -> FinalFrame.init().nextFrame()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 게임진행_1회() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("5"); + } + + @Test + void 게임진행_2회_strike_spare가_없는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(1)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("5" + Miss.MISS_MESSAGE + "1"); + } + + @Test + void 게임진행_2회_strike() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(10)); + frame.bowl(new Pin(1)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("X" + FinalFrame.FINALFRAME_MESSAGE_DELIMITER + "1"); + } + + @Test + void 게임진행_2회_spare() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(9)); + frame.bowl(new Pin(1)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("9" + Spare.SPARE_MESSAGE); + } + + @Test + void 게임진행_3회() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(9)); + frame.bowl(new Pin(1)); + frame.bowl(new Pin(0)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("9" + + Spare.SPARE_MESSAGE + + FinalFrame.FINALFRAME_MESSAGE_DELIMITER + + Pin.GUTTER_MESSAGE); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/FrameFactory.java b/src/test/java/bowling/domain/FrameFactory.java new file mode 100644 index 0000000000..235b4c45e9 --- /dev/null +++ b/src/test/java/bowling/domain/FrameFactory.java @@ -0,0 +1,12 @@ +package bowling.domain; + +public class FrameFactory { + + public static Frame frameImplProvider(int frameNumber) { + if (frameNumber == 10) { + return FinalFrame.init(); + } + return NormalFrame.init(frameNumber); + } + +} diff --git a/src/test/java/bowling/domain/FramesTest.java b/src/test/java/bowling/domain/FramesTest.java new file mode 100644 index 0000000000..504e4d9564 --- /dev/null +++ b/src/test/java/bowling/domain/FramesTest.java @@ -0,0 +1,45 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FramesTest { + + public static final int GAME_FINISHING_BOWL_TRIES = 12; + + @Test + void 게임종료여부_종료되지않은_상태() { + assertThat(framesProvider(1).gameFinished()).isFalse(); + } + + @Test + void 게임종료여부_종료된_상태() { + assertThat(framesProvider(GAME_FINISHING_BOWL_TRIES).gameFinished()).isTrue(); + } + + @Test + void 게임진행_종료되지않은_상태() { + Frames frames = new Frames(); + assertThat(frames.results()).hasSize(1); + + frames.bowl(new Pin(Pin.MAX_AMOUNT)); + assertThat(frames.results()).hasSize(2); + } + + @Test + void 게임진행_종료된_상태() { + Frames frames = framesProvider(GAME_FINISHING_BOWL_TRIES); + assertThatThrownBy(() -> frames.bowl(new Pin(Pin.MAX_AMOUNT))) + .isInstanceOf(IllegalStateException.class); + } + + private Frames framesProvider(int bowlTries) { + Frames frames = new Frames(); + for (int i = 0; i < bowlTries; i++) { + frames.bowl(new Pin(Pin.MAX_AMOUNT)); + } + return frames; + } +} diff --git a/src/test/java/bowling/domain/NormalFrameTest.java b/src/test/java/bowling/domain/NormalFrameTest.java new file mode 100644 index 0000000000..270d10d874 --- /dev/null +++ b/src/test/java/bowling/domain/NormalFrameTest.java @@ -0,0 +1,113 @@ +package bowling.domain; + +import bowling.domain.state.Miss; +import bowling.domain.state.Ready; +import bowling.domain.state.Spare; +import bowling.domain.state.Strike; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class NormalFrameTest { + + @Test + void init() { + assertThat(NormalFrame.init(1)).isEqualTo(new NormalFrame(1, new Ready())); + } + + @Test + void 생성_framenumber_1번_미만() { + assertThatThrownBy(() -> NormalFrame.init(0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 생성_framenumber_9번_초과() { + assertThatThrownBy(() -> NormalFrame.init(10)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void firstPin() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("5"); + } + + @Test + void strike() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(10)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo(Strike.STRIKE_MESSAGE); + } + + @Test + void spare() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(4)); + frame.bowl(new Pin(6)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("4" + Spare.SPARE_MESSAGE); + } + + @Test + void miss_non_gutter() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(4)); + frame.bowl(new Pin(5)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("4" + Miss.MISS_MESSAGE + "5"); + } + + @Test + void miss_gutter() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(4)); + frame.bowl(new Pin(0)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("4" + Miss.MISS_MESSAGE + Pin.GUTTER_MESSAGE); + } + + @Test + void 다음_프레임_얻기_현재_프레임이_종료되지않은_경우() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + + assertThat(frame.nextFrame().frameNumber()).isEqualTo(1); + assertThat(frame.nextFrame()).isSameAs(frame); + } + + @Test + void 다음_프레임_얻기_현재_프레임이_종료된_경우() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(10)); + + assertThat(frame.nextFrame().frameNumber()).isEqualTo(2); + } + + @Test + void 다음_프레임_얻기_NormalFrame() { + Frame frame = NormalFrame.init(8); + frame.bowl(new Pin(10)); + + assertThat(frame.nextFrame()).isInstanceOf(NormalFrame.class); + assertThat(frame.nextFrame().frameNumber()).isEqualTo(9); + } + + @Test + void 다음_프레임_얻기_FinalFrame() { + Frame frame = NormalFrame.init(9); + frame.bowl(new Pin(10)); + + assertThat(frame.nextFrame()).isInstanceOf(FinalFrame.class); + assertThat(frame.nextFrame().frameNumber()).isEqualTo(10); + } +} diff --git a/src/test/java/bowling/domain/PinTest.java b/src/test/java/bowling/domain/PinTest.java new file mode 100644 index 0000000000..074b1751f2 --- /dev/null +++ b/src/test/java/bowling/domain/PinTest.java @@ -0,0 +1,41 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PinTest { + + @Test + void 생성_0개_미만() { + assertThatThrownBy(() -> new Pin(-1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 생성_10개_초과() { + assertThatThrownBy(() -> new Pin(11)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource(value = {"9,false", "10,true"}) + void 볼링_핀이_최대값을_가지는지_확인(int amount, boolean expected) { + assertThat(new Pin(amount).isMax()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"5,5,true", "0,10,true", "0,0,false", "0,9,false"}) + void 볼링_핀의_합계가_최대값이_되는지_확인(int firstPinAmount, int secondPinAmount, boolean expected) { + assertThat(new Pin(firstPinAmount).isClear(new Pin(secondPinAmount))).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"0, -", "1, 1", "10, 10"}) + void 볼링_핀_개수_출력(int amount, String expected) { + assertThat(new Pin(amount).toString()).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/PlayerTest.java b/src/test/java/bowling/domain/PlayerTest.java new file mode 100644 index 0000000000..55619f4901 --- /dev/null +++ b/src/test/java/bowling/domain/PlayerTest.java @@ -0,0 +1,23 @@ +package bowling.domain; + +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.assertThatThrownBy; + +class PlayerTest { + + @Test + void 생성_valid() { + assertThat(new Player("HSH").name()).isEqualTo("HSH"); + } + + @ParameterizedTest + @ValueSource(strings = {"abcd", "ab"}) + void 생성_invalid(String name) { + assertThatThrownBy(() -> new Player(name)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/bowling/domain/ResultLinesTest.java b/src/test/java/bowling/domain/ResultLinesTest.java new file mode 100644 index 0000000000..f90f688673 --- /dev/null +++ b/src/test/java/bowling/domain/ResultLinesTest.java @@ -0,0 +1,48 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResultLinesTest { + + public static final String PLAYER_NAME_HSH = "HSH"; + + @Test + void firstline_생성() { + assertThat(ResultLines.FIRST_LINE.get(0)).isEqualTo(ResultLines.NAME_MESSAGE); + assertThat(ResultLines.FIRST_LINE).hasSize(11); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 9, 10}) + void frame_최대개수만큼_생성(int frameResultsLength) { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, frameResultsLength).secondLine()) + .hasSize(11); + } + + @Test + void player_이름이_결과리스트의_첫번째원소에_들어가야_한다() { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, 1).secondLine().get(0)) + .isEqualTo(PLAYER_NAME_HSH); + } + + private ResultLines resultsLinesProvider(String playerName, int frameResultsLength) { + if (frameResultsLength > Frames.MAX_FRAMENUMBER) { + throw new IllegalArgumentException("프레임은 최대 10개까지만 생성 가능합니다."); + } + + List frameResults = new ArrayList<>(); + for (int frameNumber = 1; frameNumber <= frameResultsLength; frameNumber++) { + frameResults.add(FrameFactory.frameImplProvider(frameNumber)); + } + + return new ResultLines(new Player(playerName), frameResults); + } + +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/FirstPinTest.java b/src/test/java/bowling/domain/state/FirstPinTest.java new file mode 100644 index 0000000000..35a802f581 --- /dev/null +++ b/src/test/java/bowling/domain/state/FirstPinTest.java @@ -0,0 +1,36 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +class FirstPinTest { + + @Test + void 종료_여부_판단() { + assertThat(new FirstPin(new Pin(0)).isFinished()).isFalse(); + } + + @ParameterizedTest + @CsvSource(value = {"0, 10", "1, 9"}) + void 게임진행_Spare(int firstPinAmount, int secondPinAmount) { + assertThat(new FirstPin(new Pin(firstPinAmount)).bowl(new Pin(secondPinAmount))) + .isInstanceOf(Spare.class); + } + + @ParameterizedTest + @CsvSource(value = {"0, 9", "0, 0"}) + void 게임진행_Miss(int firstPinAmount, int secondPinAmount) { + assertThat(new FirstPin(new Pin(firstPinAmount)).bowl(new Pin(secondPinAmount))) + .isInstanceOf(Miss.class); + } + + @Test + void 메시지_출력() { + assertThat(new FirstPin(new Pin(5)).toString()).isEqualTo("5"); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/MissTest.java b/src/test/java/bowling/domain/state/MissTest.java new file mode 100644 index 0000000000..1e656189ab --- /dev/null +++ b/src/test/java/bowling/domain/state/MissTest.java @@ -0,0 +1,30 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class MissTest { + + @Test + void 종료_여부_판단() { + assertThat(new Miss(new Pin(0), new Pin(0)).isFinished()).isTrue(); + } + + @Test + void 게임진행_예외발생() { + assertThatThrownBy(() -> new Miss(new Pin(0), new Pin(0)).bowl(new Pin(0))) + .isInstanceOf(IllegalStateException.class); + } + + @ParameterizedTest + @CsvSource(value = {"0, 0, -, -", "5, 4, 5, 4", "5, 0, 5, -"}) + void 메시지_출력(int firstPinAmount, int secondPinAmount, String expectedFirst, String expectedSecond) { + assertThat(new Miss(new Pin(firstPinAmount), new Pin(secondPinAmount)).toString()) + .isEqualTo(expectedFirst + Miss.MISS_MESSAGE + expectedSecond); + } +} diff --git a/src/test/java/bowling/domain/state/ReadyTest.java b/src/test/java/bowling/domain/state/ReadyTest.java new file mode 100644 index 0000000000..73f4c667e5 --- /dev/null +++ b/src/test/java/bowling/domain/state/ReadyTest.java @@ -0,0 +1,34 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +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; + +class ReadyTest { + + @Test + void 종료_여부_판단() { + assertThat(new Ready().isFinished()).isFalse(); + } + + @Test + void 게임진행_Strike() { + assertThat(new Ready().bowl(new Pin(10))) + .isInstanceOf(Strike.class); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 9}) + void 게임진행_FirstPin(int amount) { + assertThat(new Ready().bowl(new Pin(amount))) + .isInstanceOf(FirstPin.class); + } + + @Test + void 메시지_출력() { + assertThat(new Ready().toString()).isEqualTo(Ready.READY_MESSAGE); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/SpareTest.java b/src/test/java/bowling/domain/state/SpareTest.java new file mode 100644 index 0000000000..0775fd579a --- /dev/null +++ b/src/test/java/bowling/domain/state/SpareTest.java @@ -0,0 +1,27 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class SpareTest { + + @Test + void 종료_여부_판단() { + assertThat(new Spare(new Pin(0)).isFinished()).isTrue(); + } + + @Test + void 게임진행_예외발생() { + assertThatThrownBy(() -> new Spare(new Pin(0)).bowl(new Pin(0))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 메시지_출력() { + assertThat(new Spare(new Pin(5)).toString()).isEqualTo("5" + Spare.SPARE_MESSAGE); + } + +} diff --git a/src/test/java/bowling/domain/state/StrikeTest.java b/src/test/java/bowling/domain/state/StrikeTest.java new file mode 100644 index 0000000000..696c5f8814 --- /dev/null +++ b/src/test/java/bowling/domain/state/StrikeTest.java @@ -0,0 +1,26 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class StrikeTest { + + @Test + void 종료_여부_판단() { + assertThat(new Strike().isFinished()).isTrue(); + } + + @Test + void 게임진행_예외발생() { + assertThatThrownBy(() -> new Strike().bowl(new Pin(0))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 메시지_출력() { + assertThat(new Strike().toString()).isEqualTo(Strike.STRIKE_MESSAGE); + } +} From 2b43ed5ce18126a3853a7d66319824f58ec9f47a Mon Sep 17 00:00:00 2001 From: Sanghyeok Hyun Date: Mon, 16 Jan 2023 19:14:17 +0900 Subject: [PATCH 3/3] =?UTF-8?q?3=EB=8B=A8=EA=B3=84=20-=20=EB=B3=BC?= =?UTF-8?q?=EB=A7=81=20=EC=A0=90=EC=88=98=ED=8C=90(=EC=A0=90=EC=88=98=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0)=20(#1007)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 점수 표시 요구사항 작성 * refactor: NormalFrame과 FinalFrame의 공통 로직을 AbstractFrame으로 추출 * feat: Score 클래스 구현 * refactor: Score 클래스 불변객체로 변경 * feat: State 클래스들에 score(), calculateScore() 메소드 구현 * fix: 한 Frame에서 10개 이상의 볼링핀을 쓰러트릴 수 있던 문제 수정 * feat: Frame에 현재 점수 계산 메소드, 이전 프레임에서 위임된 점수 계산 메소드 추가 * feat: 점수를 출력하도록 OutputView와 ResultLines 로직 수정 --- src/main/java/bowling/README.md | 55 ++++++++++-- .../java/bowling/domain/AbstractFrame.java | 61 +++++++++++++ src/main/java/bowling/domain/FinalFrame.java | 71 ++++++++------- src/main/java/bowling/domain/Frame.java | 6 ++ src/main/java/bowling/domain/NormalFrame.java | 89 ++++++++++++------- src/main/java/bowling/domain/Pin.java | 10 ++- src/main/java/bowling/domain/ResultLines.java | 62 ++++++++----- src/main/java/bowling/domain/Score.java | 69 ++++++++++++++ .../java/bowling/domain/state/FirstPin.java | 6 ++ src/main/java/bowling/domain/state/Miss.java | 13 +++ src/main/java/bowling/domain/state/Ready.java | 6 ++ .../java/bowling/domain/state/Running.java | 9 ++ src/main/java/bowling/domain/state/Spare.java | 13 +++ .../java/bowling/domain/state/Status.java | 5 ++ .../java/bowling/domain/state/Strike.java | 13 +++ src/main/java/bowling/view/OutputView.java | 6 +- .../java/bowling/domain/FinalFrameTest.java | 85 ++++++++++++++++-- .../java/bowling/domain/NormalFrameTest.java | 60 ++++++++++++- src/test/java/bowling/domain/PinTest.java | 9 +- .../java/bowling/domain/ResultLinesTest.java | 23 +++-- src/test/java/bowling/domain/ScoreTest.java | 61 +++++++++++++ .../bowling/domain/state/FirstPinTest.java | 14 +++ .../java/bowling/domain/state/MissTest.java | 14 +++ .../java/bowling/domain/state/ReadyTest.java | 14 +++ .../java/bowling/domain/state/SpareTest.java | 12 +++ .../java/bowling/domain/state/StrikeTest.java | 12 +++ 26 files changed, 679 insertions(+), 119 deletions(-) create mode 100644 src/main/java/bowling/domain/AbstractFrame.java create mode 100644 src/main/java/bowling/domain/Score.java create mode 100644 src/test/java/bowling/domain/ScoreTest.java diff --git a/src/main/java/bowling/README.md b/src/main/java/bowling/README.md index 5f70541530..51b282f147 100644 --- a/src/main/java/bowling/README.md +++ b/src/main/java/bowling/README.md @@ -1,23 +1,23 @@ -## 볼링 1단계 기능 요구사항 -### Pin +# 볼링 2단계 기능 요구사항 +## Pin * 볼링공을 굴렸을 때 쓰러뜨린 핀의 개수(amount)를 가짐 * 현재 amount가 전체 핀의 개수와 일치하는지 확인하는 isMax() * 자신이 가진 amount와 인자로 주어진 핀의 amount를 비교해 핀 전체를 쓰려뜨렸는지 확인하는 isClear() -### Player +## Player -### Frame +## Frame * NormalFrame와 FinalFrame에서 구현되는 인터페이스 * bowl(Pin pin), isFinished(), nextFrame()을 갖는다 -### NormalFrame +## NormalFrame * 상태 값(Status)을 가짐 * bowl() 메소드로 게임을 진행 -> Status에게 위임 * isFinished()로 게임이 종료되었는지 확인 -> Status 클래스에게 판단을 위임 * nextFrame() 으로 다음 frame을 반환 * 게임이 종료되었을 경우 새로운 프레임, 게임이 진행중이면 기존 프레임 반환 -### FinalFrame +## FinalFrame * 상태 값(Status)의 리스트(statuses)를 가짐 * 10번째 프레임에서는 Strike/Spare 시 총 3번의 bowl() 기회가 주어짐 * bowlCount 변수를 가져 예외 로직 처리 @@ -26,12 +26,12 @@ * 나머지(bawlCount <= 1 || Miss가 없는 BowlCount == 2) 는 false * nextFrame()은 Exception 발생 (마지막 프레임) -### Frames +## Frames * Frame의 일급컬렉션 (볼링 게임이 진행되는 동안 최대 10개의 Frame을 관리) * bowl() 메소드로 게임을 진행 -> Frame에게 위임 * 전체 볼링 게임 종료 여부 gameFinished() -### 상태 클래스 +## 상태 클래스 * 필요에 따라 pins를 가져 게임 진행 상태에 대한 정보를 기록 * isFinished() 메소드로 종료 여부를 판단 * bowl() 메소드로 게임을 진행 @@ -44,4 +44,41 @@ * Finished(abstract) * Miss * Strike - * Spare \ No newline at end of file + * Spare + +--- +# 볼링 3단계 요구사항 +## 리팩토링 요구사항 +* NormalFrame, FinalFrame의 공통 로직을 AbstractFrame 으로 추출 + * Normal, Final에서 Abstract를 호출할 때 값을 다르게 주어 공통 로직을 활용 +## 기능 요구사항 +* 점수 계산 로직 구현하기 + * **스트라이크**면 이후 2번의 투구 점수를 합산해야 하며, + * **스페어**면 이후 1번의 투구 점수를 합산해야 한다. + +### Frame +* 점수 계산은 Frame에서 수행 + * 점수 계산을 하기 위해선 **이후 Frame**에 접근 가능해야 한다 + * `Frame next` -> private로 두어도 같은 클래스 내이므로 계속해서 다음 프레임에 접근 가능 + * Score에서 점수 가능 여부를 판단하고, 다음 Frame까지를 고려해 점수를 계산해야 한다면 다음 Frame에게 점수 계산을 위임한다. + * 현재 프레임의 점수가 계산가능하면, 바로 계산해서 반환 + * 현재 프레임의 점수 계산 `calculateScore()` + * 점수가 계산 가능하지 않다면, 위임 + * 이전 프레임으로부터 위임받은 **이전 프레임의** 점수 계산 `calculateLastFrameScore()` -> 먼저 `state.calculateScore(Score lastScore)` 호출 + * -> next Frame의 상태가 `Finished`면, 1. 점수를 반환하거나 2. 계속해서 next Frame으로 요청 전달 + * -> 상태가 `Finished`가 아니면서 현재 Frame의 `calculateScore(Score lastScore)` 결과가 계산 불가능하다면 `Optional.empty()` 반환 (Finished가 아니어도 우선 현재 상태를 Score에 반영해봐야함에 주의) + +### Score +* `현재까지의 점수 - currentScore`, `점수 계산 가능까지 남은 bowl() 횟수 - leftBowlCount`를 저장 +* `bowl(int pinCount)` -> pinCount만큼 currentScore을 증가시키고, leftBowlCount를 감소시킴 +* `canCalculateScore()` -> 현 상태에서 점수 계산 가능 여부를 반환 +* `score()` -> 점수가 계산 가능할 경우, 현재 점수를 반환 + +### State <-> Score +* Score 생성 시, bowl() 시 State에 저장된 `fallenPin` 값이 필요 + * 현재는 외부에서 접근 불가 (toString()으로 출력에서만 사용) +* State에 `score()` 과 같은 메소드를 만들어 State의 상태를 활용해 Score를 생성할 수 있도록 구현해보기 +* 또, Score의 상태를 업데이트해 새로운 Score를 반환하는 메소드도 제공하기 - `calculateScore(Score lastScore)` + * 예를 들어, Miss, Spare는 Score의 bowl()을 2회 호출해야 하고 + * Strike, FirstPin은 Score의 bowl()을 1회 호출해야 함 + diff --git a/src/main/java/bowling/domain/AbstractFrame.java b/src/main/java/bowling/domain/AbstractFrame.java new file mode 100644 index 0000000000..915d767fa1 --- /dev/null +++ b/src/main/java/bowling/domain/AbstractFrame.java @@ -0,0 +1,61 @@ +package bowling.domain; + +import bowling.domain.state.Ready; +import bowling.domain.state.Status; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public abstract class AbstractFrame implements Frame { + + protected final List statuses; + protected final int frameNumber; + + AbstractFrame(int maxBowlCount, int frameNumber) { + statuses = new ArrayList<>(maxBowlCount); + statuses.add(new Ready()); + + this.frameNumber = frameNumber; + } + + @Override + public void bowl(Pin pin) { + Status currentStatus = currentStatus(); + statuses.remove(currentStatusIndex()); + + statuses.add(currentStatus.bowl(pin)); + } + + @Override + public int frameNumber() { + return frameNumber; + } + + protected void assertFinished() { + if (isFinished()) { + throw new IllegalStateException("현재 프레임에서는 더 이상 게임을 진행할 수 없습니다."); + } + } + + protected Status currentStatus() { + return statuses.get(currentStatusIndex()); + } + + protected int currentStatusIndex() { + return statuses.size() - 1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractFrame that = (AbstractFrame) o; + return frameNumber == that.frameNumber && Objects.equals(statuses, that.statuses); + } + + @Override + public int hashCode() { + return Objects.hash(statuses, frameNumber); + } +} diff --git a/src/main/java/bowling/domain/FinalFrame.java b/src/main/java/bowling/domain/FinalFrame.java index dd1474dc35..bd0f13ad5c 100644 --- a/src/main/java/bowling/domain/FinalFrame.java +++ b/src/main/java/bowling/domain/FinalFrame.java @@ -4,58 +4,38 @@ import bowling.domain.state.Ready; import bowling.domain.state.Status; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; -public class FinalFrame implements Frame { +public class FinalFrame extends AbstractFrame { public static final int MAX_BOWLCOUNT = 3; - public static final String FINALFRAME_MESSAGE_DELIMITER = "|"; + public static final int FRAMENUMBER = 10; + public static final String MESSAGE_DELIMITER = "|"; - private final List statuses; private int bowlCount = 0; - FinalFrame(List statuses) { - this.statuses = statuses; + FinalFrame(int maxBowlCount, int frameNumber) { + super(maxBowlCount, frameNumber); } public static Frame init() { - List statuses = new ArrayList<>(MAX_BOWLCOUNT); - statuses.add(new Ready()); - - return new FinalFrame(statuses); + return new FinalFrame(MAX_BOWLCOUNT, FRAMENUMBER); } @Override public void bowl(Pin pin) { assertFinished(); + if (currentStatus().isFinished()) { statuses.add(new Ready()); } - Status currentStatus = currentStatus(); - statuses.remove(currentStatusIndex()); - - statuses.add(currentStatus.bowl(pin)); + super.bowl(pin); bowlCount++; } - private Status currentStatus() { - return statuses.get(currentStatusIndex()); - } - - private int currentStatusIndex() { - return statuses.size() - 1; - } - - private void assertFinished() { - if (isFinished()) { - throw new IllegalStateException("현재 프레임에서는 더 이상 게임을 진행할 수 없습니다."); - } - } - @Override public boolean isFinished() { if (bowlCount == MAX_BOWLCOUNT) { @@ -74,27 +54,50 @@ public Frame nextFrame() { } @Override - public int frameNumber() { - return Frames.MAX_FRAMENUMBER; + public Optional calculateScore() { + if (!isFinished()) { + return Optional.empty(); + } + + Score score = statuses.get(0).score(); + for (Status state : statuses.subList(1, statuses.size())) { + score = state.calculateScore(score); + } + return Optional.of(score.score()); + } + + @Override + public Optional calculateLastFrameScore(Score lastScore) { + Score newScore = lastScore; + for (Status state : statuses) { + newScore = state.calculateScore(newScore); + } + + if (newScore.canCalculateScore()) { + return Optional.of(newScore.score()); + } + + return Optional.empty(); } @Override public String toString() { return statuses.stream() .map(Object::toString) - .collect(Collectors.joining(FINALFRAME_MESSAGE_DELIMITER)); + .collect(Collectors.joining(MESSAGE_DELIMITER)); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; FinalFrame that = (FinalFrame) o; - return bowlCount == that.bowlCount && Objects.equals(statuses, that.statuses); + return bowlCount == that.bowlCount; } @Override public int hashCode() { - return Objects.hash(statuses, bowlCount); + return Objects.hash(super.hashCode(), bowlCount); } } diff --git a/src/main/java/bowling/domain/Frame.java b/src/main/java/bowling/domain/Frame.java index ca7a09e40c..94476c817b 100644 --- a/src/main/java/bowling/domain/Frame.java +++ b/src/main/java/bowling/domain/Frame.java @@ -1,5 +1,7 @@ package bowling.domain; +import java.util.Optional; + public interface Frame { void bowl(Pin pin); @@ -10,4 +12,8 @@ public interface Frame { int frameNumber(); + Optional calculateScore(); + + Optional calculateLastFrameScore(Score lastScore); + } diff --git a/src/main/java/bowling/domain/NormalFrame.java b/src/main/java/bowling/domain/NormalFrame.java index 98df2b6c79..133286adf2 100644 --- a/src/main/java/bowling/domain/NormalFrame.java +++ b/src/main/java/bowling/domain/NormalFrame.java @@ -1,48 +1,75 @@ package bowling.domain; -import bowling.domain.state.Ready; -import bowling.domain.state.Status; +import java.util.Optional; -import java.util.Objects; +public class NormalFrame extends AbstractFrame { -public class NormalFrame implements Frame { + public static final int MAX_BOWLCOUNT = 1; + public static final int MAX_FRAMENUMBER = 9; - public static final int NORMALFRAME_MAX_FRAMENUMBER = 9; + private Frame next; - private final int frameNumber; - private Status status; - - NormalFrame(int frameNumber, Status status) { - validate(frameNumber); - this.frameNumber = frameNumber; - this.status = status; + NormalFrame(int maxBowlCount, int frameNumber) { + super(maxBowlCount, frameNumber); } public static Frame init(int frameNumber) { - return new NormalFrame(frameNumber, new Ready()); + validate(frameNumber); + return new NormalFrame(MAX_BOWLCOUNT, frameNumber); } private static void validate(int frameNumber) { if (frameNumber < Frames.MIN_FRAMENUMBER) { throw new IllegalArgumentException("Frame은 1번부터 시작합니다."); } - if (frameNumber > NORMALFRAME_MAX_FRAMENUMBER) { + if (frameNumber > MAX_FRAMENUMBER) { throw new IllegalArgumentException("NormalFrame은 9번까지만 존재합니다."); } } - public int frameNumber() { - return frameNumber; + @Override + public void bowl(Pin pin) { + assertFinished(); + + super.bowl(pin); } @Override - public void bowl(Pin pin) { - status = status.bowl(pin); + public Optional calculateScore() { + if (!isFinished()) { + return Optional.empty(); + } + + Score score = currentStatus().score(); + if (score.canCalculateScore()) { + return Optional.of(score.score()); + } + + if (next == null) { + return Optional.empty(); + } + return next.calculateLastFrameScore(score); + } + + public Optional calculateLastFrameScore(Score lastScore) { + Score newScore = currentStatus().calculateScore(lastScore); + if (newScore.canCalculateScore()) { + return Optional.of(newScore.score()); + } + + if (!isFinished()) { + return Optional.empty(); + } + + if (next == null) { + return Optional.empty(); + } + return next.calculateLastFrameScore(newScore); } @Override public boolean isFinished() { - return status.isFinished(); + return currentStatus().isFinished(); } @Override @@ -50,27 +77,21 @@ public Frame nextFrame() { if (!isFinished()) { return this; } - if (frameNumber < NORMALFRAME_MAX_FRAMENUMBER) { - return NormalFrame.init(frameNumber + 1); + if (frameNumber < MAX_FRAMENUMBER) { + Frame next = NormalFrame.init(frameNumber + 1); + this.next = next; + return next; } - return FinalFrame.init(); + Frame next = FinalFrame.init(); + this.next = next; + return next; } @Override public String toString() { - return status.toString(); + return currentStatus().toString(); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - NormalFrame that = (NormalFrame) o; - return frameNumber == that.frameNumber && Objects.equals(status, that.status); - } - @Override - public int hashCode() { - return Objects.hash(frameNumber, status); - } + } diff --git a/src/main/java/bowling/domain/Pin.java b/src/main/java/bowling/domain/Pin.java index f5c00c1e54..3a10ee6be2 100644 --- a/src/main/java/bowling/domain/Pin.java +++ b/src/main/java/bowling/domain/Pin.java @@ -24,12 +24,20 @@ private void validate(int amount) { } } + public int amount() { + return amount; + } + public boolean isMax() { return amount == MAX_AMOUNT; } public boolean isClear(Pin pin) { - return amount + pin.amount == MAX_AMOUNT; + int newAmount = amount + pin.amount; + if (newAmount > MAX_AMOUNT) { + throw new IllegalArgumentException("볼링 핀의 전체 개수는 10개 입니다."); + } + return newAmount == MAX_AMOUNT; } @Override diff --git a/src/main/java/bowling/domain/ResultLines.java b/src/main/java/bowling/domain/ResultLines.java index 36f9c3b9bd..4e54fee0fb 100644 --- a/src/main/java/bowling/domain/ResultLines.java +++ b/src/main/java/bowling/domain/ResultLines.java @@ -1,9 +1,6 @@ package bowling.domain; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -12,40 +9,63 @@ public class ResultLines { public static final String NAME_MESSAGE = " NAME "; public static final String BLANK_MESSAGE = ""; public static final String NUMBER_FORMAT = "%02d"; + public static final int RESULT_MAX_SIZE = Frames.MAX_FRAMENUMBER + 1; - static final List FIRST_LINE; - private final LinkedList secondLine; + static final List FRAME_NUMBERS; + private final LinkedList frameResults; + private final LinkedList frameScores; static { - FIRST_LINE = new ArrayList<>(); - FIRST_LINE.add(NAME_MESSAGE); - FIRST_LINE.addAll(IntStream.rangeClosed(Frames.MIN_FRAMENUMBER, Frames.MAX_FRAMENUMBER) + FRAME_NUMBERS = new ArrayList<>(); + FRAME_NUMBERS.add(NAME_MESSAGE); + FRAME_NUMBERS.addAll(IntStream.rangeClosed(Frames.MIN_FRAMENUMBER, Frames.MAX_FRAMENUMBER) .mapToObj(s -> String.format(NUMBER_FORMAT, s)) .map(String::valueOf) .collect(Collectors.toList())); } - public ResultLines(Player player, List frameResults) { - secondLine = frameResults.stream() + public ResultLines(Player player, List frames) { + frameResults = createFrameResults(player, frames); + frameScores = createFrameScores(frames); + } + + private LinkedList createFrameScores(List frames) { + LinkedList frameScores = frames.stream() + .map(Frame::calculateScore) + .filter(Optional::isPresent) + .map(Optional::get) + .map(String::valueOf) + .collect(Collectors.toCollection(LinkedList::new)); + frameScores.addFirst(BLANK_MESSAGE); + + return makeResultsSizeMax(frameScores, frameScores.size()); + } + + private LinkedList createFrameResults(Player player, List frames) { + LinkedList frameResults = frames.stream() .map(Object::toString) .collect(Collectors.toCollection(LinkedList::new)); + frameResults.addFirst(player.name()); - makeResultsMaxSize(frameResults); - secondLine.addFirst(player.name()); + return makeResultsSizeMax(frameResults, frameResults.size()); } - private void makeResultsMaxSize(List frameResults) { - int frameResultsSize = frameResults.size(); - for (int i = 0; i < (Frames.MAX_FRAMENUMBER - frameResultsSize); i++) { - secondLine.addLast(BLANK_MESSAGE); + private LinkedList makeResultsSizeMax(LinkedList results, int currentResultsSize) { + for (int i = 0; i < (RESULT_MAX_SIZE - currentResultsSize); i++) { + results.addLast(BLANK_MESSAGE); } + return results; + } + + public List frameNumbers() { + return Collections.unmodifiableList(FRAME_NUMBERS); } - public List firstLine() { - return Collections.unmodifiableList(FIRST_LINE); + public List frameResults() { + return Collections.unmodifiableList(frameResults); } - public List secondLine() { - return Collections.unmodifiableList(secondLine); + public List frameScores() { + return Collections.unmodifiableList(frameScores); } } diff --git a/src/main/java/bowling/domain/Score.java b/src/main/java/bowling/domain/Score.java new file mode 100644 index 0000000000..74139303d6 --- /dev/null +++ b/src/main/java/bowling/domain/Score.java @@ -0,0 +1,69 @@ +package bowling.domain; + +import java.util.Objects; + +public class Score { + + public static final int MAX_INITIAL_SCORE = 10; + public static final int STRIKE_LEFT_BOWL_COUNT = 2; + public static final int SPARE_LEFT_BOWL_COUNT = 1; + public static final int MISS_LEFT_BOWL_COUNT = 0; + + private final int currentScore; + private final int leftBowlCount; + + public Score(int initialScore, int leftBowlCount) { + validateInitialScore(initialScore); + this.currentScore = initialScore; + this.leftBowlCount = leftBowlCount; + } + + private static void validateInitialScore(int initialScore) { + if (initialScore < 0) { + throw new IllegalArgumentException("점수는 0점보다 작을 수 없습니다."); + } + } + + public static Score ofStrike() { + return new Score(MAX_INITIAL_SCORE, STRIKE_LEFT_BOWL_COUNT); + } + + public static Score ofSpare() { + return new Score(MAX_INITIAL_SCORE, SPARE_LEFT_BOWL_COUNT); + } + + public static Score ofMiss(int currentScore) { + return new Score(currentScore, MISS_LEFT_BOWL_COUNT); + } + + public Score bowl(int fallenPinCount) { + if (leftBowlCount == 0) { + return this; + } + return new Score(currentScore + fallenPinCount, leftBowlCount - 1); + } + + public boolean canCalculateScore() { + return leftBowlCount == 0; + } + + public int score() { + if (canCalculateScore()) { + return currentScore; + } + throw new IllegalStateException("현재는 점수를 계산할 수 없는 상태입니다!"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Score score = (Score) o; + return currentScore == score.currentScore && leftBowlCount == score.leftBowlCount; + } + + @Override + public int hashCode() { + return Objects.hash(currentScore, leftBowlCount); + } +} diff --git a/src/main/java/bowling/domain/state/FirstPin.java b/src/main/java/bowling/domain/state/FirstPin.java index 72f5a4b0fb..dfea2177d6 100644 --- a/src/main/java/bowling/domain/state/FirstPin.java +++ b/src/main/java/bowling/domain/state/FirstPin.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import java.util.Objects; @@ -20,6 +21,11 @@ public Status bowl(Pin secondPin) { return new Miss(firstPin, secondPin); } + @Override + public Score calculateScore(Score lastScore) { + return lastScore.bowl(firstPin.amount()); + } + @Override public String toString() { return firstPin.toString(); diff --git a/src/main/java/bowling/domain/state/Miss.java b/src/main/java/bowling/domain/state/Miss.java index fe93476e1f..152e0f5298 100644 --- a/src/main/java/bowling/domain/state/Miss.java +++ b/src/main/java/bowling/domain/state/Miss.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import java.util.Objects; @@ -16,6 +17,18 @@ public Miss(Pin firstPin, Pin secondPin) { this.secondPin = secondPin; } + @Override + public Score score() { + return Score.ofMiss(firstPin.amount() + secondPin.amount()); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore + .bowl(firstPin.amount()) + .bowl(secondPin.amount()); + } + @Override public String toString() { return firstPin + MISS_MESSAGE + secondPin; diff --git a/src/main/java/bowling/domain/state/Ready.java b/src/main/java/bowling/domain/state/Ready.java index 9d4c2543f3..9349c0e87b 100644 --- a/src/main/java/bowling/domain/state/Ready.java +++ b/src/main/java/bowling/domain/state/Ready.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import java.util.Objects; @@ -16,6 +17,11 @@ public Status bowl(Pin pin) { return new FirstPin(pin); } + @Override + public Score calculateScore(Score lastScore) { + return lastScore; + } + @Override public String toString() { return READY_MESSAGE; diff --git a/src/main/java/bowling/domain/state/Running.java b/src/main/java/bowling/domain/state/Running.java index 487aa43782..0eaef1e2a0 100644 --- a/src/main/java/bowling/domain/state/Running.java +++ b/src/main/java/bowling/domain/state/Running.java @@ -1,7 +1,16 @@ package bowling.domain.state; +import bowling.domain.Score; + abstract public class Running implements Status { + public static final String CANNOT_CALCULATE_SCORE_ERROR_MESSAGE = "현재 상태에서는 점수를 계산할 수 없습니다."; + + @Override + public Score score() { + throw new IllegalStateException(CANNOT_CALCULATE_SCORE_ERROR_MESSAGE); + } + @Override public boolean isFinished() { return false; diff --git a/src/main/java/bowling/domain/state/Spare.java b/src/main/java/bowling/domain/state/Spare.java index e17eba75d5..982c67202e 100644 --- a/src/main/java/bowling/domain/state/Spare.java +++ b/src/main/java/bowling/domain/state/Spare.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import java.util.Objects; @@ -14,6 +15,18 @@ public Spare(Pin firstPin) { this.firstPin = firstPin; } + @Override + public Score score() { + return Score.ofSpare(); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore + .bowl(firstPin.amount()) + .bowl(Pin.MAX_AMOUNT - firstPin.amount()); + } + @Override public String toString() { return firstPin + SPARE_MESSAGE; diff --git a/src/main/java/bowling/domain/state/Status.java b/src/main/java/bowling/domain/state/Status.java index 0b61c3078e..1f207dc21e 100644 --- a/src/main/java/bowling/domain/state/Status.java +++ b/src/main/java/bowling/domain/state/Status.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; public interface Status { @@ -8,4 +9,8 @@ public interface Status { Status bowl(Pin pin); + Score score(); + + Score calculateScore(Score lastScore); + } diff --git a/src/main/java/bowling/domain/state/Strike.java b/src/main/java/bowling/domain/state/Strike.java index 85dd0f4f3f..4b1fdfd051 100644 --- a/src/main/java/bowling/domain/state/Strike.java +++ b/src/main/java/bowling/domain/state/Strike.java @@ -1,5 +1,8 @@ package bowling.domain.state; +import bowling.domain.Pin; +import bowling.domain.Score; + import java.util.Objects; public class Strike extends Finished { @@ -9,6 +12,16 @@ public class Strike extends Finished { public Strike() { } + @Override + public Score score() { + return Score.ofStrike(); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore.bowl(Pin.MAX_AMOUNT); + } + @Override public String toString() { return STRIKE_MESSAGE; diff --git a/src/main/java/bowling/view/OutputView.java b/src/main/java/bowling/view/OutputView.java index d670f3d671..0e33042830 100644 --- a/src/main/java/bowling/view/OutputView.java +++ b/src/main/java/bowling/view/OutputView.java @@ -9,9 +9,11 @@ public class OutputView { public static final String FRAME_DELIMETER = "|"; public static void printFrameResult(ResultLines results) { - printResult(results.firstLine()); + printResult(results.frameNumbers()); System.out.println(); - printResult(results.secondLine()); + printResult(results.frameResults()); + System.out.println(); + printResult(results.frameScores()); System.out.println(); System.out.println(); diff --git a/src/test/java/bowling/domain/FinalFrameTest.java b/src/test/java/bowling/domain/FinalFrameTest.java index ae28eb9e2e..a83fb2f3a9 100644 --- a/src/test/java/bowling/domain/FinalFrameTest.java +++ b/src/test/java/bowling/domain/FinalFrameTest.java @@ -1,11 +1,10 @@ package bowling.domain; import bowling.domain.state.Miss; -import bowling.domain.state.Ready; import bowling.domain.state.Spare; import org.junit.jupiter.api.Test; -import java.util.List; +import java.util.Optional; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -13,8 +12,8 @@ class FinalFrameTest { @Test - void init() { - assertThat(FinalFrame.init()).isEqualTo(new FinalFrame(List.of(new Ready()))); + void FinalFrame은_bowlCount_3_frameNumber_10으로_init_된다() { + assertThat(FinalFrame.init()).isEqualTo(new FinalFrame(3, 10)); } @Test @@ -54,7 +53,7 @@ void init() { frame.bowl(new Pin(1)); assertThat(frame.isFinished()).isFalse(); - assertThat(frame.toString()).isEqualTo("X" + FinalFrame.FINALFRAME_MESSAGE_DELIMITER + "1"); + assertThat(frame.toString()).isEqualTo("X" + FinalFrame.MESSAGE_DELIMITER + "1"); } @Test @@ -77,7 +76,81 @@ void init() { assertThat(frame.isFinished()).isTrue(); assertThat(frame.toString()).isEqualTo("9" + Spare.SPARE_MESSAGE - + FinalFrame.FINALFRAME_MESSAGE_DELIMITER + + FinalFrame.MESSAGE_DELIMITER + Pin.GUTTER_MESSAGE); } + + @Test + void 점수계산_계산불가_Spare가_있는_경우() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(1)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(9)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산불가_Strike가_있는_경우() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(10)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(0)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산가능_Strike나_Spare가_없는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(9)); + } + + @Test + void 점수계산_계산가능_Strike가_있는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(10)); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(19)); + } + + @Test + void 점수계산_계산가능_Spare가_있는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(14)); + } + + @Test + void NormalFrame에서_위임된_점수계산_Strike() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.of(20)); + } + + @Test + void NormalFrame에서_위임된_점수계산_Spare() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateLastFrameScore(Score.ofSpare())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofSpare())).isEqualTo(Optional.of(15)); + } } \ No newline at end of file diff --git a/src/test/java/bowling/domain/NormalFrameTest.java b/src/test/java/bowling/domain/NormalFrameTest.java index 270d10d874..727398ec88 100644 --- a/src/test/java/bowling/domain/NormalFrameTest.java +++ b/src/test/java/bowling/domain/NormalFrameTest.java @@ -1,19 +1,20 @@ package bowling.domain; import bowling.domain.state.Miss; -import bowling.domain.state.Ready; import bowling.domain.state.Spare; import bowling.domain.state.Strike; import org.junit.jupiter.api.Test; +import java.util.Optional; + import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class NormalFrameTest { @Test - void init() { - assertThat(NormalFrame.init(1)).isEqualTo(new NormalFrame(1, new Ready())); + void NormalFrame은_maxBowlCount_1로_init_된다() { + assertThat(NormalFrame.init(1)).isEqualTo(new NormalFrame(1, 1)); } @Test @@ -110,4 +111,57 @@ void miss_gutter() { assertThat(frame.nextFrame()).isInstanceOf(FinalFrame.class); assertThat(frame.nextFrame().frameNumber()).isEqualTo(10); } + + @Test + void 점수계산_계산불가_Running() { + Frame frame = NormalFrame.init(1); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산불가_Strike() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(10)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산불가_Spare() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(5)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산가능_Miss() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(9)); + } + + @Test + void 점수계산_계산가능_Spare() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + + assertThat(frame.calculateLastFrameScore(Score.ofSpare())).isEqualTo(Optional.of(15)); + } + + @Test + void 점수계산_계산가능_Strike() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(3)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.of(18)); + } } diff --git a/src/test/java/bowling/domain/PinTest.java b/src/test/java/bowling/domain/PinTest.java index 074b1751f2..cd333dbb93 100644 --- a/src/test/java/bowling/domain/PinTest.java +++ b/src/test/java/bowling/domain/PinTest.java @@ -33,9 +33,16 @@ class PinTest { assertThat(new Pin(firstPinAmount).isClear(new Pin(secondPinAmount))).isEqualTo(expected); } + @Test + void 볼링_핀의_합계가_최대값을_초과하는_경우() { + assertThatThrownBy(() -> new Pin(5).isClear(new Pin(6))) + .isInstanceOf(IllegalArgumentException.class); + } + @ParameterizedTest @CsvSource(value = {"0, -", "1, 1", "10, 10"}) - void 볼링_핀_개수_출력(int amount, String expected) { + void 볼링_핀_개수_toString(int amount, String expected) { assertThat(new Pin(amount).toString()).isEqualTo(expected); } + } \ No newline at end of file diff --git a/src/test/java/bowling/domain/ResultLinesTest.java b/src/test/java/bowling/domain/ResultLinesTest.java index f90f688673..caba5982b3 100644 --- a/src/test/java/bowling/domain/ResultLinesTest.java +++ b/src/test/java/bowling/domain/ResultLinesTest.java @@ -15,20 +15,27 @@ class ResultLinesTest { @Test void firstline_생성() { - assertThat(ResultLines.FIRST_LINE.get(0)).isEqualTo(ResultLines.NAME_MESSAGE); - assertThat(ResultLines.FIRST_LINE).hasSize(11); + assertThat(ResultLines.FRAME_NUMBERS.get(0)).isEqualTo(ResultLines.NAME_MESSAGE); + assertThat(ResultLines.FRAME_NUMBERS).hasSize(11); } @ParameterizedTest @ValueSource(ints = {0, 1, 9, 10}) - void frame_최대개수만큼_생성(int frameResultsLength) { - assertThat(resultsLinesProvider(PLAYER_NAME_HSH, frameResultsLength).secondLine()) + void frameResults_최대개수만큼_생성(int frameResultsLength) { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, frameResultsLength).frameResults()) + .hasSize(11); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 9, 10}) + void frameScores_최대개수만큼_생성(int frameResultsLength) { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, frameResultsLength).frameScores()) .hasSize(11); } @Test void player_이름이_결과리스트의_첫번째원소에_들어가야_한다() { - assertThat(resultsLinesProvider(PLAYER_NAME_HSH, 1).secondLine().get(0)) + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, 1).frameResults().get(0)) .isEqualTo(PLAYER_NAME_HSH); } @@ -37,12 +44,12 @@ private ResultLines resultsLinesProvider(String playerName, int frameResultsLeng throw new IllegalArgumentException("프레임은 최대 10개까지만 생성 가능합니다."); } - List frameResults = new ArrayList<>(); + List frames = new ArrayList<>(); for (int frameNumber = 1; frameNumber <= frameResultsLength; frameNumber++) { - frameResults.add(FrameFactory.frameImplProvider(frameNumber)); + frames.add(FrameFactory.frameImplProvider(frameNumber)); } - return new ResultLines(new Player(playerName), frameResults); + return new ResultLines(new Player(playerName), frames); } } \ No newline at end of file diff --git a/src/test/java/bowling/domain/ScoreTest.java b/src/test/java/bowling/domain/ScoreTest.java new file mode 100644 index 0000000000..c7d44add4a --- /dev/null +++ b/src/test/java/bowling/domain/ScoreTest.java @@ -0,0 +1,61 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class ScoreTest { + + @Test + void Strike_초기_Score_생성() { + assertThat(Score.ofStrike()).isEqualTo(new Score(10, 2)); + } + + @Test + void Spare_초기_Score_생성() { + assertThat(Score.ofSpare()).isEqualTo(new Score(10, 1)); + } + + @Test + void Miss_초기_Score_생성() { + assertThat(Score.ofMiss(5)).isEqualTo(new Score(5, 0)); + } + + @Test + void 생성_invalid() { + assertThatThrownBy(() -> new Score(-1, 0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 생성_valid() { + assertThatCode(() -> new Score(0, 0)).doesNotThrowAnyException(); + assertThat(new Score(10, 0).score()).isEqualTo(10); + } + + @Test + void 게임진행_계산불가() { + Score score = new Score(10, 2).bowl(10); + + assertThat(score).isEqualTo(new Score(20, 1)); + assertThat(score.canCalculateScore()).isFalse(); + assertThatThrownBy(score::score) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 게임진행_계산가능() { + Score score = new Score(10, 1).bowl(10); + + assertThat(score).isEqualTo(new Score(20, 0)); + assertThat(score.canCalculateScore()).isTrue(); + assertThat(score.score()).isEqualTo(20); + } + + @Test + void 게임진행_leftBowlCount_가_0인_경우() { + Score score = new Score(10, 0).bowl(10); + + assertThat(score).isEqualTo(new Score(10, 0)); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/FirstPinTest.java b/src/test/java/bowling/domain/state/FirstPinTest.java index 35a802f581..8b4f010f7d 100644 --- a/src/test/java/bowling/domain/state/FirstPinTest.java +++ b/src/test/java/bowling/domain/state/FirstPinTest.java @@ -1,10 +1,12 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -33,4 +35,16 @@ class FirstPinTest { void 메시지_출력() { assertThat(new FirstPin(new Pin(5)).toString()).isEqualTo("5"); } + + @Test + void Score_생성() { + assertThatThrownBy(() -> new FirstPin(new Pin(5)).score()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void Score_계산() { + assertThat(new FirstPin(new Pin(5)).calculateScore(new Score(10, 1))) + .isEqualTo(new Score(15, 0)); + } } \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/MissTest.java b/src/test/java/bowling/domain/state/MissTest.java index 1e656189ab..2d1f0fead0 100644 --- a/src/test/java/bowling/domain/state/MissTest.java +++ b/src/test/java/bowling/domain/state/MissTest.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -27,4 +28,17 @@ public class MissTest { assertThat(new Miss(new Pin(firstPinAmount), new Pin(secondPinAmount)).toString()) .isEqualTo(expectedFirst + Miss.MISS_MESSAGE + expectedSecond); } + + @Test + void Score_생성() { + assertThat(new Miss(new Pin(1), new Pin(2)).score()) + .isEqualTo(new Score(3, 0)); + } + + @Test + void Score_계산() { + assertThat(new Miss(new Pin(1), new Pin(2)).calculateScore(new Score(10, 2))) + .isEqualTo(new Score(13, 0)); + } + } diff --git a/src/test/java/bowling/domain/state/ReadyTest.java b/src/test/java/bowling/domain/state/ReadyTest.java index 73f4c667e5..71af1f205b 100644 --- a/src/test/java/bowling/domain/state/ReadyTest.java +++ b/src/test/java/bowling/domain/state/ReadyTest.java @@ -1,11 +1,13 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; 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.assertThatThrownBy; class ReadyTest { @@ -31,4 +33,16 @@ class ReadyTest { void 메시지_출력() { assertThat(new Ready().toString()).isEqualTo(Ready.READY_MESSAGE); } + + @Test + void Score_생성() { + assertThatThrownBy(() -> new Ready().score()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void Score_계산() { + assertThatThrownBy(() -> new Ready().calculateScore(Score.ofStrike())) + .isInstanceOf(IllegalStateException.class); + } } \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/SpareTest.java b/src/test/java/bowling/domain/state/SpareTest.java index 0775fd579a..307f214902 100644 --- a/src/test/java/bowling/domain/state/SpareTest.java +++ b/src/test/java/bowling/domain/state/SpareTest.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -24,4 +25,15 @@ public class SpareTest { assertThat(new Spare(new Pin(5)).toString()).isEqualTo("5" + Spare.SPARE_MESSAGE); } + @Test + void Score_생성() { + assertThat(new Spare(new Pin(5)).score()).isEqualTo(Score.ofSpare()); + } + + @Test + void Score_계산() { + assertThat(new Spare(new Pin(5)).calculateScore(new Score(10, 2))) + .isEqualTo(new Score(20, 0)); + } + } diff --git a/src/test/java/bowling/domain/state/StrikeTest.java b/src/test/java/bowling/domain/state/StrikeTest.java index 696c5f8814..4d63722594 100644 --- a/src/test/java/bowling/domain/state/StrikeTest.java +++ b/src/test/java/bowling/domain/state/StrikeTest.java @@ -1,6 +1,7 @@ package bowling.domain.state; import bowling.domain.Pin; +import bowling.domain.Score; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -23,4 +24,15 @@ public class StrikeTest { void 메시지_출력() { assertThat(new Strike().toString()).isEqualTo(Strike.STRIKE_MESSAGE); } + + @Test + void Score_생성() { + assertThat(new Strike().score()).isEqualTo(Score.ofStrike()); + } + + @Test + void Score_계산() { + assertThat(new Strike().calculateScore(new Score(10, 1))) + .isEqualTo(new Score(20, 0)); + } }