From ea2fcab2632852371c8453ed99c2b09678b86f71 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 13:48:47 +0900 Subject: [PATCH 01/36] =?UTF-8?q?docs:=202.1=20=EA=B8=B0=EB=8A=A5=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8204a7762..94220f24d 100644 --- a/README.md +++ b/README.md @@ -99,27 +99,52 @@ - [x] 기물 규칙 및 제약에 맞게 이동한다. - [차(車)] - [x] 상하좌우로 장애물을 만날 때까지 칸 수 제한 없이 이동한다. + - [ ] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. - [x] 이동하려는 경로 중간에 다른 기물이 **있**다면 `IllegalArgumentException`을 발생시킨다. - [포(包)] - [x] 상하좌우로 이동하되, 반드시 중간에 다른 기물을 하나 뛰어넘어야 합니다. + - [ ] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. + - 이때에도 경로 상에 반드시 기물이 있어야 한다. - [x] 이동하려는 경로 중간에 다른 기물이 **없**다면 `IllegalArgumentException`을 발생시킨다. - [x] 넘으려는 기물이 포(包) 일 경우 `IllegalArgumentException`을 발생시킨다. - [x] 목적지 좌표의 기물이 포(包)일 경우 `IllegalArgumentException`을 발생시킨다. - [마(馬)] - [x] 직선 1칸 + 대각선 1칸(日) 이동한다. + - 궁성 내부 대각선 이용 불가 - [x] 목적지 좌표(직선 1칸 또는 대각선 1칸)에 다른 기물이 있으면 `IllegalArgumentException`을 발생시킨다. - [상(象)] - [x] 직선 1칸 + 대각선 2칸(用) 이동한다. + - 궁성 내부 대각선 이용 불가 - [x] 목적지 좌표(직선 1칸 또는 대각선 1칸)에 다른 기물이 있으면 `IllegalArgumentException`을 발생시킨다. - - [궁(楚/漢), 사(士)] + - [궁(楚/漢)] - [x] 상하좌우 직선 1칸 이동한다. + - [ ] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - [ ] 상대 기물에게 잡힐 경우 게임이 종료되며 상대방이 게임을 승리한다. + + - [사(士)] + - [x] 상하좌우 직선 1칸 이동한다. + - [ ] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. - [졸/병(卒/兵)] - [x] 상좌우 직선 1칸 이동한다. 후진은 불가 주의 + - [ ] 상대 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + +### 기물 별 점수 + +『초』 총점 : 72.0점 +『한』 총점 : 73.5점 (후수이기 때문에 점수 + 1.5) + +- [차(車)] : 13점 +- [포(包)] : 7점 +- [마(馬)] : 5점 +- [상(象)] : 3점 +- [사(士)] : 3점 +- [졸/병(卒/兵)] : 2점 +- [궁(楚/漢)] : 점수 없음 -> 죽는 순간 상대방 🥇게임 승리🥇 --- From b8963ee0fd34ed4ddd1d8c6a3958e65a73e01fad Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 14:27:49 +0900 Subject: [PATCH 02/36] =?UTF-8?q?feat:=20=EA=B6=81/=EC=82=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=AC=BC=20=EC=95=84=EA=B5=B0=20=EA=B6=81=EC=84=B1=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EC=9D=B4=EB=8F=99=20=EB=B6=88=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 궁성 외부 이동시 예외 테스트 추가 --- src/main/java/janggi/domain/piece/Camp.java | 18 +++++++++ .../java/janggi/domain/piece/PieceType.java | 6 +-- .../domain/piece/strategy/PalaceStrategy.java | 15 +++++++ .../strategy/SingleStepStraightStrategy.java | 2 +- .../java/janggi/domain/piece/PieceTest.java | 10 ++++- ...ategyTest.java => PalaceStrategyTest.java} | 40 ++++++++++++++----- 6 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java rename src/test/java/janggi/domain/piece/strategy/{SingleStepStraightStrategyTest.java => PalaceStrategyTest.java} (51%) diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index 3b3277d0d..dd96ed65c 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -1,5 +1,6 @@ package janggi.domain.piece; +import janggi.domain.Position; import java.util.List; public enum Camp { @@ -27,6 +28,7 @@ public Camp next() { } }; private static final String INVALID_BACKWARD_MOVEMENT = "[ERROR] 해당 기물은 후진할 수 없습니다."; + private static final String INVALID_PALACE_MOVEMENT = "[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."; private final int forwardDirection; private final int startRowPosition; @@ -49,4 +51,20 @@ public void validateForwardDirection(int rowDirection) { public int getStartRowPosition() { return startRowPosition; } + + public void validatePalace(Position destination) { + int absRowDifference = Math.abs(destination.row() - startRowPosition); + + if (isPalaceRowOutOfRange(absRowDifference) || isPalaceColumnOutOfRange(destination)) { + throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); + } + } + + private boolean isPalaceRowOutOfRange(int absRowDifference) { + return absRowDifference > 2; + } + + private boolean isPalaceColumnOutOfRange(Position destination) { + return destination.column() < 3 || destination.column() > 5; + } } diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index 1d2924a0d..866b59b74 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -9,17 +9,17 @@ import janggi.domain.piece.strategy.HorseStrategy; import janggi.domain.piece.strategy.MoveStrategy; import janggi.domain.piece.strategy.MultiStepStraightStrategy; -import janggi.domain.piece.strategy.SingleStepStraightStrategy; +import janggi.domain.piece.strategy.PalaceStrategy; import janggi.domain.piece.strategy.SoldierStrategy; import java.util.List; public enum PieceType { - GENERAL(new SingleStepStraightStrategy(), new EmptyCondition()), + GENERAL(new PalaceStrategy(), new EmptyCondition()), CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition()), HORSE(new HorseStrategy(), new EmptyCondition()), CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition()), - GUARD(new SingleStepStraightStrategy(), new EmptyCondition()), + GUARD(new PalaceStrategy(), new EmptyCondition()), ELEPHANT(new ElephantStrategy(), new EmptyCondition()), SOLDIER(new SoldierStrategy(), new EmptyCondition()); diff --git a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java new file mode 100644 index 000000000..45d64135f --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java @@ -0,0 +1,15 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import java.util.List; + +public class PalaceStrategy extends SingleStepStraightStrategy { + + @Override + public List findPath(Position source, Position destination, Camp camp) { + camp.validatePalace(destination); + + return super.findPath(source, destination, camp); + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index 73010f66f..b00b7bfe6 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -4,7 +4,7 @@ import janggi.domain.piece.Camp; import java.util.List; -public class SingleStepStraightStrategy implements MoveStrategy { +public abstract class SingleStepStraightStrategy implements MoveStrategy { public static final int SINGLE_STEP_DISTANCE = 1; private static final String INVALID_SINGLE_STEP_STRAIGHT_MOVE = String.format( diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index d0f521e73..e5914edea 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -48,6 +48,9 @@ class General { ); } + // TODO: 한나라 예외 추가 + // TODO: 대각선 이동도 가능하므로 예외 메시지 수정 고려 + // TODO: 궁성 외부 이동 예외 테스트 추가 @Test void 궁은_행마법을_따르지_않으면_예외가_발생한다() { // given @@ -55,7 +58,7 @@ class General { BoardChecker board = new Board(Map::of); // when & then Assertions.assertThatThrownBy( - () -> piece.validateMove(new Position(1, 4), new Position(3, 4), board)) + () -> piece.validateMove(new Position(0, 4), new Position(2, 4), board)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); } @@ -75,6 +78,9 @@ class Guard { ); } + // TODO: 한나라 예외 추가 + // TODO: 대각선 이동도 가능하므로 예외 메시지 수정 고려 + // TODO: 궁성 외부 이동 예외 테스트 추가 @Test void 사는_행마법을_따르지_않으면_예외가_발생한다() { // given @@ -318,6 +324,6 @@ class SoldierHan { () -> piece.validateMove(new Position(6, 0), new Position(1, 0), board)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + } } } -} diff --git a/src/test/java/janggi/domain/piece/strategy/SingleStepStraightStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java similarity index 51% rename from src/test/java/janggi/domain/piece/strategy/SingleStepStraightStrategyTest.java rename to src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java index 23968920b..94a902e43 100644 --- a/src/test/java/janggi/domain/piece/strategy/SingleStepStraightStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java @@ -12,24 +12,24 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -class SingleStepStraightStrategyTest { +class PalaceStrategyTest { - private final MoveStrategy strategy = new SingleStepStraightStrategy(); + private final MoveStrategy strategy = new PalaceStrategy(); private static Stream successMovePositions() { return Stream.of( - Arguments.of(new Position(1, 4), new Position(1, 5)), - Arguments.of(new Position(1, 4), new Position(1, 3)), - Arguments.of(new Position(1, 4), new Position(0, 4)), - Arguments.of(new Position(1, 4), new Position(2, 4)) + Arguments.of(new Position(1, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(0, 4), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 4), Camp.CHO) ); } @ParameterizedTest @MethodSource("successMovePositions") - void 궁과_사는_상하좌우_1칸_이동한다(Position source, Position destination) { + void 궁과_사는_상하좌우_1칸_이동한다(Position source, Position destination, Camp camp) { //when - List path = strategy.findPath(source, destination, Camp.HAN); + List path = strategy.findPath(source, destination, camp); //then SoftAssertions.assertSoftly(assertSoftly -> { assertSoftly.assertThat(path).hasSize(1); @@ -39,8 +39,30 @@ private static Stream successMovePositions() { @Test void 궁과_사는_1칸_이동이_아니면_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(3, 0), new Position(5, 0), Camp.HAN)) + assertThatThrownBy(() -> strategy.findPath(new Position(0, 4), new Position(2, 4), Camp.CHO)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); } + + private static Stream exceptionPalaceMovePositions() { + return Stream.of( + Arguments.of(new Position(2, 3), new Position(2, 2), Camp.CHO), + Arguments.of(new Position(2, 3), new Position(3, 3), Camp.CHO), + Arguments.of(new Position(2, 5), new Position(3, 5), Camp.CHO), + Arguments.of(new Position(2, 5), new Position(2, 6), Camp.CHO), + + Arguments.of(new Position(7, 3), new Position(7, 2), Camp.HAN), + Arguments.of(new Position(7, 3), new Position(6, 3), Camp.HAN), + Arguments.of(new Position(7, 5), new Position(6, 5), Camp.HAN), + Arguments.of(new Position(7, 5), new Position(7, 6), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionPalaceMovePositions") + void 궁과_사는_아군_궁성_밖으로_벗어나면_예외가_발생한다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } } From 77c16a18e5dc3cfd1264ea741bf77702a2c76be9 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 15:02:18 +0900 Subject: [PATCH 03/36] =?UTF-8?q?feat:=20=EA=B6=81=EC=84=B1=20=EB=82=B4=20?= =?UTF-8?q?=EB=8C=80=EA=B0=81=EC=84=A0=201=EC=B9=B8=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 궁성 내 1칸 대각선 이동 테스트 추가 --- src/main/java/janggi/domain/piece/Camp.java | 19 +-- .../strategy/SingleStepStraightStrategy.java | 28 +++- .../piece/strategy/PalaceStrategyTest.java | 120 ++++++++++++------ 3 files changed, 114 insertions(+), 53 deletions(-) diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index dd96ed65c..456072fda 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -52,19 +52,22 @@ public int getStartRowPosition() { return startRowPosition; } - public void validatePalace(Position destination) { - int absRowDifference = Math.abs(destination.row() - startRowPosition); - - if (isPalaceRowOutOfRange(absRowDifference) || isPalaceColumnOutOfRange(destination)) { + public void validatePalace(Position position) { + if (!(isPalaceRow(position) && isPalaceColumn(position))) { throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); } } - private boolean isPalaceRowOutOfRange(int absRowDifference) { - return absRowDifference > 2; + public boolean isPalace(Position position) { + return isPalaceRow(position) && isPalaceColumn(position); + } + + private boolean isPalaceRow(Position position) { + int absRowDifference = Math.abs(position.row() - startRowPosition); + return absRowDifference <= 2; } - private boolean isPalaceColumnOutOfRange(Position destination) { - return destination.column() < 3 || destination.column() > 5; + private boolean isPalaceColumn(Position position) { + return position.column() >= 3 && position.column() <= 5; } } diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index b00b7bfe6..38b2d5e5f 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -15,15 +15,37 @@ public abstract class SingleStepStraightStrategy implements MoveStrategy { @Override public List findPath(Position source, Position destination, Camp camp) { DirectionInformation directionInformation = new DirectionInformation(source, destination); - validateSingleStepMovement(directionInformation); + if (camp.isPalace(source) && camp.isPalace(destination)) { + validatePalaceSingleStepMovement(directionInformation); + return List.of(destination); + } + + validateSingleStepMovement(directionInformation); return List.of(destination); } private void validateSingleStepMovement(DirectionInformation directionInformation) { - if (directionInformation.calculateAbsRowDifference() - + directionInformation.calculateAbsColumnDifference() != SINGLE_STEP_DISTANCE) { + if (isNotSingleStep(directionInformation)) { throw new IllegalArgumentException(INVALID_SINGLE_STEP_STRAIGHT_MOVE); } } + + private void validatePalaceSingleStepMovement(DirectionInformation directionInformation) { + if (isNotSingleDiagonalStep(directionInformation) && isNotSingleStep(directionInformation)) { + throw new IllegalArgumentException(INVALID_SINGLE_STEP_STRAIGHT_MOVE); + } + } + + private boolean isNotSingleDiagonalStep(DirectionInformation directionInformation) { + int absRowDifference = directionInformation.calculateAbsRowDifference(); + int absColumnDifference = directionInformation.calculateAbsColumnDifference(); + + return absRowDifference != 1 || absColumnDifference != 1; + } + + private boolean isNotSingleStep(DirectionInformation directionInformation) { + return directionInformation.calculateAbsRowDifference() + + directionInformation.calculateAbsColumnDifference() != SINGLE_STEP_DISTANCE; + } } diff --git a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java index 94a902e43..38af0c205 100644 --- a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -16,53 +18,87 @@ class PalaceStrategyTest { private final MoveStrategy strategy = new PalaceStrategy(); - private static Stream successMovePositions() { - return Stream.of( - Arguments.of(new Position(1, 4), new Position(1, 5), Camp.CHO), - Arguments.of(new Position(1, 4), new Position(1, 3), Camp.CHO), - Arguments.of(new Position(1, 4), new Position(0, 4), Camp.CHO), - Arguments.of(new Position(1, 4), new Position(2, 4), Camp.CHO) - ); - } + @DisplayName("정상 경우") + @Nested + class success { + private static Stream successMovePositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(0, 4), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 4), Camp.CHO) + ); + } - @ParameterizedTest - @MethodSource("successMovePositions") - void 궁과_사는_상하좌우_1칸_이동한다(Position source, Position destination, Camp camp) { - //when - List path = strategy.findPath(source, destination, camp); - //then - SoftAssertions.assertSoftly(assertSoftly -> { - assertSoftly.assertThat(path).hasSize(1); - assertSoftly.assertThat(path).containsExactly(destination); - }); - } + @ParameterizedTest + @MethodSource("successMovePositions") + void 궁과_사는_상하좌우_1칸_이동한다(Position source, Position destination, Camp camp) { + //when + List path = strategy.findPath(source, destination, camp); + //then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } - @Test - void 궁과_사는_1칸_이동이_아니면_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(0, 4), new Position(2, 4), Camp.CHO)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); - } + private static Stream successDiagonalMovePositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(0, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(0, 5), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 5), Camp.CHO), - private static Stream exceptionPalaceMovePositions() { - return Stream.of( - Arguments.of(new Position(2, 3), new Position(2, 2), Camp.CHO), - Arguments.of(new Position(2, 3), new Position(3, 3), Camp.CHO), - Arguments.of(new Position(2, 5), new Position(3, 5), Camp.CHO), - Arguments.of(new Position(2, 5), new Position(2, 6), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(7, 3), Camp.HAN), + Arguments.of(new Position(8, 4), new Position(7, 5), Camp.HAN), + Arguments.of(new Position(8, 4), new Position(9, 3), Camp.HAN), + Arguments.of(new Position(8, 4), new Position(9, 5), Camp.HAN) + ); + } - Arguments.of(new Position(7, 3), new Position(7, 2), Camp.HAN), - Arguments.of(new Position(7, 3), new Position(6, 3), Camp.HAN), - Arguments.of(new Position(7, 5), new Position(6, 5), Camp.HAN), - Arguments.of(new Position(7, 5), new Position(7, 6), Camp.HAN) - ); + @ParameterizedTest + @MethodSource("successDiagonalMovePositions") + void 궁과_사는_궁성_내부에서_대각선으로_1칸_이동한다(Position source, Position destination, Camp camp) { + //when + List path = strategy.findPath(source, destination, camp); + //then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } } - @ParameterizedTest - @MethodSource("exceptionPalaceMovePositions") - void 궁과_사는_아군_궁성_밖으로_벗어나면_예외가_발생한다(Position source, Position destination, Camp camp) { - assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + @DisplayName("예외 경우") + @Nested + class exception { + @Test + void 궁과_사는_1칸_이동이_아니면_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(0, 4), new Position(2, 4), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + } + + private static Stream exceptionPalaceMovePositions() { + return Stream.of( + Arguments.of(new Position(2, 3), new Position(2, 2), Camp.CHO), + Arguments.of(new Position(2, 3), new Position(3, 3), Camp.CHO), + Arguments.of(new Position(2, 5), new Position(3, 5), Camp.CHO), + Arguments.of(new Position(2, 5), new Position(2, 6), Camp.CHO), + + Arguments.of(new Position(7, 3), new Position(7, 2), Camp.HAN), + Arguments.of(new Position(7, 3), new Position(6, 3), Camp.HAN), + Arguments.of(new Position(7, 5), new Position(6, 5), Camp.HAN), + Arguments.of(new Position(7, 5), new Position(7, 6), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionPalaceMovePositions") + void 궁과_사는_아군_궁성_밖으로_벗어나면_예외가_발생한다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } } } From 9b56d348a0e86f9391947b97a86bcaab202dec59 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 15:48:59 +0900 Subject: [PATCH 04/36] =?UTF-8?q?feat:=20=EA=B6=81=EC=84=B1=20=EB=82=B4=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EB=8C=80=EA=B0=81=EC=84=A0=20=EC=9D=B4=EB=8F=99=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 궁성 내 연결되지 않은 대각선 이동 예외 테스트 추가 --- src/main/java/janggi/domain/piece/Camp.java | 9 ++++ .../strategy/SingleStepStraightStrategy.java | 46 +++++++++++++------ .../java/janggi/domain/piece/PieceTest.java | 4 +- .../piece/strategy/PalaceStrategyTest.java | 46 ++++++++++++++++++- 4 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index 456072fda..f7468d266 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -70,4 +70,13 @@ private boolean isPalaceRow(Position position) { private boolean isPalaceColumn(Position position) { return position.column() >= 3 && position.column() <= 5; } + + public boolean isPalaceCenter(Position source, Position destination) { + return checkPalaceCenter(source) || checkPalaceCenter(destination); + } + + private boolean checkPalaceCenter(Position position) { + int absRowDifference = Math.abs(position.row() - startRowPosition); + return position.column() == 4 && absRowDifference == 1; + } } diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index 38b2d5e5f..e07e74a33 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -11,13 +11,19 @@ public abstract class SingleStepStraightStrategy implements MoveStrategy { "[ERROR] 해당 기물은 직선으로 %d칸 이동해야 합니다.", SINGLE_STEP_DISTANCE ); + private static final String INVALID_PALACE_SINGLE_STEP_MOVE = String.format( + "[ERROR] 해당 기물은 궁성 내에서 연결된 %d칸만 이동할 수 있습니다.", + SINGLE_STEP_DISTANCE + ); + private static final String INVALID_PALACE_DIAGONAL_STEP_MOVE = + "[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."; @Override public List findPath(Position source, Position destination, Camp camp) { DirectionInformation directionInformation = new DirectionInformation(source, destination); - if (camp.isPalace(source) && camp.isPalace(destination)) { - validatePalaceSingleStepMovement(directionInformation); + if (isPalace(source, destination, camp)) { + validatePalaceSingleStepMovement(source, destination, camp); return List.of(destination); } @@ -25,27 +31,41 @@ public List findPath(Position source, Position destination, Camp camp) return List.of(destination); } - private void validateSingleStepMovement(DirectionInformation directionInformation) { - if (isNotSingleStep(directionInformation)) { - throw new IllegalArgumentException(INVALID_SINGLE_STEP_STRAIGHT_MOVE); - } + private boolean isPalace(Position source, Position destination, Camp camp) { + return camp.isPalace(source) && camp.isPalace(destination); } - private void validatePalaceSingleStepMovement(DirectionInformation directionInformation) { - if (isNotSingleDiagonalStep(directionInformation) && isNotSingleStep(directionInformation)) { - throw new IllegalArgumentException(INVALID_SINGLE_STEP_STRAIGHT_MOVE); + private void validatePalaceSingleStepMovement(Position source, Position destination, Camp camp) { + DirectionInformation directionInformation = new DirectionInformation(source, destination); + if (isSingleDiagonalStep(directionInformation)) { + validateDiagonalMove(source, destination, camp); + } + if (!isSingleDiagonalStep(directionInformation) && !isSingleStep(directionInformation)) { + throw new IllegalArgumentException(INVALID_PALACE_SINGLE_STEP_MOVE); } } - private boolean isNotSingleDiagonalStep(DirectionInformation directionInformation) { + private boolean isSingleDiagonalStep(DirectionInformation directionInformation) { int absRowDifference = directionInformation.calculateAbsRowDifference(); int absColumnDifference = directionInformation.calculateAbsColumnDifference(); - return absRowDifference != 1 || absColumnDifference != 1; + return absRowDifference == 1 && absColumnDifference == 1; } - private boolean isNotSingleStep(DirectionInformation directionInformation) { + private void validateDiagonalMove(Position source, Position destination, Camp camp) { + if (!camp.isPalaceCenter(source, destination)) { + throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); + } + } + + private boolean isSingleStep(DirectionInformation directionInformation) { return directionInformation.calculateAbsRowDifference() - + directionInformation.calculateAbsColumnDifference() != SINGLE_STEP_DISTANCE; + + directionInformation.calculateAbsColumnDifference() == SINGLE_STEP_DISTANCE; + } + + private void validateSingleStepMovement(DirectionInformation directionInformation) { + if (!isSingleStep(directionInformation)) { + throw new IllegalArgumentException(INVALID_SINGLE_STEP_STRAIGHT_MOVE); + } } } diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index e5914edea..91ae45c83 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -60,7 +60,7 @@ class General { Assertions.assertThatThrownBy( () -> piece.validateMove(new Position(0, 4), new Position(2, 4), board)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + .hasMessage("[ERROR] 해당 기물은 궁성 내에서 연결된 1칸만 이동할 수 있습니다."); } } @@ -90,7 +90,7 @@ class Guard { Assertions.assertThatThrownBy( () -> piece.validateMove(new Position(0, 3), new Position(0, 5), board)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + .hasMessage("[ERROR] 해당 기물은 궁성 내에서 연결된 1칸만 이동할 수 있습니다."); } } diff --git a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java index 38af0c205..1ba6037a8 100644 --- a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java @@ -76,7 +76,7 @@ class exception { void 궁과_사는_1칸_이동이_아니면_예외가_발생한다() { assertThatThrownBy(() -> strategy.findPath(new Position(0, 4), new Position(2, 4), Camp.CHO)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + .hasMessage("[ERROR] 해당 기물은 궁성 내에서 연결된 1칸만 이동할 수 있습니다."); } private static Stream exceptionPalaceMovePositions() { @@ -100,5 +100,49 @@ private static Stream exceptionPalaceMovePositions() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); } + + private static Stream exceptionPalaceCampPositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(0, 3), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(0, 5), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(2, 3), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(2, 5), Camp.HAN), + + Arguments.of(new Position(8, 4), new Position(7, 3), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(7, 5), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 3), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 5), Camp.CHO) + ); + } + + @ParameterizedTest + @MethodSource("exceptionPalaceMovePositions") + void 궁과_사는_상대_궁성_영역에서_이동하면_예외가_발생한다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } + + private static Stream exceptionInvalidDiagonalMovePositions() { + return Stream.of( + Arguments.of(new Position(0, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(0, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 3), new Position(2, 4), Camp.CHO), + Arguments.of(new Position(1, 5), new Position(2, 4), Camp.CHO), + + Arguments.of(new Position(7, 4), new Position(8, 3), Camp.HAN), + Arguments.of(new Position(7, 4), new Position(8, 5), Camp.HAN), + Arguments.of(new Position(8, 3), new Position(9, 4), Camp.HAN), + Arguments.of(new Position(8, 5), new Position(9, 4), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionInvalidDiagonalMovePositions") + void 궁과_사는_궁성_대각선이_연결되지_않은_칸으로는_대각선_이동할_수_없다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } } } From e8c3326449c627c0ec73c74e23c22cab3cafeb91 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 15:49:41 +0900 Subject: [PATCH 05/36] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94220f24d..8bcfe7a1d 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,12 @@ - [궁(楚/漢)] - [x] 상하좌우 직선 1칸 이동한다. - - [ ] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - [x] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. - [ ] 상대 기물에게 잡힐 경우 게임이 종료되며 상대방이 게임을 승리한다. - [사(士)] - [x] 상하좌우 직선 1칸 이동한다. - - [ ] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - [x] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. - [졸/병(卒/兵)] - [x] 상좌우 직선 1칸 이동한다. 후진은 불가 주의 From 1845fcd8a189988aba42935fc1bcb6752f404aa9 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 16:29:33 +0900 Subject: [PATCH 06/36] =?UTF-8?q?feat:=20=EC=A1=B8/=EB=B3=91=EC=9D=98=20?= =?UTF-8?q?=EA=B6=81=EC=84=B1=20=EB=82=B4=20=EB=8C=80=EA=B0=81=EC=84=A0=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 졸/병의 궁성 내 대각선 이동 테스트 추가 --- src/main/java/janggi/domain/piece/Camp.java | 34 +++++++--- .../strategy/SingleStepStraightStrategy.java | 18 ++--- .../piece/strategy/SoldierStrategyTest.java | 68 +++++++++++++++++-- 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index f7468d266..5fd8a1252 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -1,6 +1,7 @@ package janggi.domain.piece; import janggi.domain.Position; +import java.util.Arrays; import java.util.List; public enum Camp { @@ -52,31 +53,42 @@ public int getStartRowPosition() { return startRowPosition; } - public void validatePalace(Position position) { - if (!(isPalaceRow(position) && isPalaceColumn(position))) { + public void validateFriendlyPalace(Position position) { + if (!(isFriendlyPalaceRow(position) && isPalaceColumn(position))) { throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); } } - public boolean isPalace(Position position) { + private boolean isFriendlyPalaceRow(Position position) { + int absRowDifference = Math.abs(position.row() - startRowPosition); + return absRowDifference <= 2; + } + + public static boolean isPalace(Position position) { return isPalaceRow(position) && isPalaceColumn(position); } - private boolean isPalaceRow(Position position) { - int absRowDifference = Math.abs(position.row() - startRowPosition); - return absRowDifference <= 2; + private static boolean isPalaceRow(Position position) { + return Arrays.stream(values()) + .anyMatch(camp -> { + int absRowDifference = Math.abs(position.row() - camp.startRowPosition); + return absRowDifference <= 2; + }); } - private boolean isPalaceColumn(Position position) { + private static boolean isPalaceColumn(Position position) { return position.column() >= 3 && position.column() <= 5; } - public boolean isPalaceCenter(Position source, Position destination) { + public static boolean isPalaceCenter(Position source, Position destination) { return checkPalaceCenter(source) || checkPalaceCenter(destination); } - private boolean checkPalaceCenter(Position position) { - int absRowDifference = Math.abs(position.row() - startRowPosition); - return position.column() == 4 && absRowDifference == 1; + private static boolean checkPalaceCenter(Position position) { + return Arrays.stream(values()) + .anyMatch(camp -> { + int absRowDifference = Math.abs(position.row() - camp.startRowPosition); + return position.column() == 4 && absRowDifference == 1; + }); } } diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index e07e74a33..6cf99c67f 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -22,8 +22,8 @@ public abstract class SingleStepStraightStrategy implements MoveStrategy { public List findPath(Position source, Position destination, Camp camp) { DirectionInformation directionInformation = new DirectionInformation(source, destination); - if (isPalace(source, destination, camp)) { - validatePalaceSingleStepMovement(source, destination, camp); + if (isPalace(source, destination)) { + validatePalaceSingleStepMovement(source, destination, directionInformation); return List.of(destination); } @@ -31,14 +31,14 @@ public List findPath(Position source, Position destination, Camp camp) return List.of(destination); } - private boolean isPalace(Position source, Position destination, Camp camp) { - return camp.isPalace(source) && camp.isPalace(destination); + private boolean isPalace(Position source, Position destination) { + return Camp.isPalace(source) && Camp.isPalace(destination); } - private void validatePalaceSingleStepMovement(Position source, Position destination, Camp camp) { - DirectionInformation directionInformation = new DirectionInformation(source, destination); + private void validatePalaceSingleStepMovement(Position source, Position destination, + DirectionInformation directionInformation) { if (isSingleDiagonalStep(directionInformation)) { - validateDiagonalMove(source, destination, camp); + validateDiagonalMove(source, destination); } if (!isSingleDiagonalStep(directionInformation) && !isSingleStep(directionInformation)) { throw new IllegalArgumentException(INVALID_PALACE_SINGLE_STEP_MOVE); @@ -52,8 +52,8 @@ private boolean isSingleDiagonalStep(DirectionInformation directionInformation) return absRowDifference == 1 && absColumnDifference == 1; } - private void validateDiagonalMove(Position source, Position destination, Camp camp) { - if (!camp.isPalaceCenter(source, destination)) { + private void validateDiagonalMove(Position source, Position destination) { + if (!Camp.isPalaceCenter(source, destination)) { throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); } } diff --git a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java index 6d05937be..58f955f19 100644 --- a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java @@ -30,18 +30,30 @@ private static Stream successMovePositions() { ); } - private static Stream exceptionMovePositions() { + @ParameterizedTest + @MethodSource("successMovePositions") + void 병의_1칸_이동_여부를_확인한다(Position source, Position destination) { + List path = strategy.findPath(source, destination, Camp.HAN); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } + + private static Stream successDiagonalMovePositions() { return Stream.of( - Arguments.of(new Position(6, 0), new Position(5, 1)), - Arguments.of(new Position(6, 0), new Position(6, 2)), - Arguments.of(new Position(3, 2), new Position(2, 3)) + Arguments.of(new Position(2, 3), new Position(1, 4), Camp.HAN), + Arguments.of(new Position(2, 5), new Position(1, 4), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(0, 3), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(0, 5), Camp.HAN) ); } @ParameterizedTest - @MethodSource("successMovePositions") - void 병의_1칸_이동_여부를_확인한다(Position source, Position destination) { - List path = strategy.findPath(source, destination, Camp.HAN); + @MethodSource("successDiagonalMovePositions") + void 병은_상대_궁성_내부에서_대각선_이동한다(Position source, Position destination, Camp camp) { + List path = strategy.findPath(source, destination, camp); SoftAssertions.assertSoftly(assertSoftly -> { assertSoftly.assertThat(path).hasSize(1); @@ -49,6 +61,14 @@ private static Stream exceptionMovePositions() { }); } + private static Stream exceptionMovePositions() { + return Stream.of( + Arguments.of(new Position(6, 0), new Position(5, 1)), + Arguments.of(new Position(6, 0), new Position(6, 2)), + Arguments.of(new Position(3, 2), new Position(2, 3)) + ); + } + @ParameterizedTest @MethodSource("exceptionMovePositions") void 병은_1칸_이동이_아니면_예외가_발생한다(Position source, Position destination) { @@ -63,6 +83,13 @@ private static Stream exceptionMovePositions() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 후진할 수 없습니다."); } + + @Test + void 병은_궁성에서_대각선이_없을때_대각_이동_시_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(1, 3), new Position(0, 4), Camp.HAN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } } @DisplayName("졸 행마법 테스트") @@ -81,6 +108,26 @@ class Zol { }); } + private static Stream successDiagonalMovePositions() { + return Stream.of( + Arguments.of(new Position(7, 3), new Position(8, 4), Camp.CHO), + Arguments.of(new Position(7, 5), new Position(8, 4), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 3), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 5), Camp.CHO) + ); + } + + @ParameterizedTest + @MethodSource("successDiagonalMovePositions") + void 졸은_상대_궁성_내부에서_대각선_이동한다(Position source, Position destination, Camp camp) { + List path = strategy.findPath(source, destination, camp); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } + @Test void 졸은_1칸_이동이_아니면_예외가_발생한다() { assertThatThrownBy(() -> strategy.findPath(new Position(3, 0), new Position(5, 0), Camp.CHO)) @@ -94,5 +141,12 @@ class Zol { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 후진할 수 없습니다."); } + + @Test + void 졸은_궁성에서_대각선이_없을때_대각_이동_시_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(8, 3), new Position(9, 4), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } } } From f6bb47f0e8c0d296b710f0c124c8ac3102555984 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 16:30:05 +0900 Subject: [PATCH 07/36] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용하지 않던 MethodSource를 사용해서 테스트 코드를 수정했습니다. --- src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java | 2 +- .../java/janggi/domain/piece/strategy/PalaceStrategyTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java index 45d64135f..65e783c84 100644 --- a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java @@ -8,7 +8,7 @@ public class PalaceStrategy extends SingleStepStraightStrategy { @Override public List findPath(Position source, Position destination, Camp camp) { - camp.validatePalace(destination); + camp.validateFriendlyPalace(destination); return super.findPath(source, destination, camp); } diff --git a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java index 1ba6037a8..a88fa8b81 100644 --- a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java @@ -116,7 +116,7 @@ private static Stream exceptionPalaceCampPositions() { } @ParameterizedTest - @MethodSource("exceptionPalaceMovePositions") + @MethodSource("exceptionPalaceCampPositions") void 궁과_사는_상대_궁성_영역에서_이동하면_예외가_발생한다(Position source, Position destination, Camp camp) { assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) .isInstanceOf(IllegalArgumentException.class) From 83ed25778df2c43e3a42e4aa9a446b6c550498c8 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 16:31:34 +0900 Subject: [PATCH 08/36] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bcfe7a1d..89af901fa 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,8 @@ - [졸/병(卒/兵)] - [x] 상좌우 직선 1칸 이동한다. 후진은 불가 주의 - - [ ] 상대 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - [x] 상대 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - 졸/병은 규칙에 의해 애초에 아군 궁성으로 이동조차 불가능하다. ### 기물 별 점수 From 92b1b9d25ba44eef7e801e247c374cc0a1d37557 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 22:01:40 +0900 Subject: [PATCH 09/36] =?UTF-8?q?feat:=20=EC=B0=A8/=ED=8F=AC=20=EA=B6=81?= =?UTF-8?q?=EC=84=B1=20=EB=82=B4=20=EB=8C=80=EA=B0=81=EC=84=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 차/포 궁성 내 대각선 이동 테스트 및 예외 테스트 추가 --- src/main/java/janggi/domain/piece/Camp.java | 6 +- .../strategy/MultiStepStraightStrategy.java | 71 ++++- .../MultiStepStraightStrategyTest.java | 242 ++++++++++++------ 3 files changed, 235 insertions(+), 84 deletions(-) diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index 5fd8a1252..f1c910cf8 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -80,11 +80,7 @@ private static boolean isPalaceColumn(Position position) { return position.column() >= 3 && position.column() <= 5; } - public static boolean isPalaceCenter(Position source, Position destination) { - return checkPalaceCenter(source) || checkPalaceCenter(destination); - } - - private static boolean checkPalaceCenter(Position position) { + public static boolean isPalaceCenter(Position position) { return Arrays.stream(values()) .anyMatch(camp -> { int absRowDifference = Math.abs(position.row() - camp.startRowPosition); diff --git a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java index ea69eb7ad..25019a088 100644 --- a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java @@ -9,17 +9,61 @@ public class MultiStepStraightStrategy implements MoveStrategy { private static final String ONLY_STRAIGHT_MOVE_ALLOWED = "[ERROR] 해당 기물은 직선 이동만 가능합니다."; private static final String PIECE_MUST_MOVE = "[ERROR] 기물은 반드시 이동해야 합니다."; + private static final String INVALID_PALACE_DIAGONAL_STEP_MOVE = + "[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."; @Override public List findPath(Position source, Position destination, Camp camp) { DirectionInformation directionInformation = new DirectionInformation(source, destination); + validatePieceMoved(directionInformation); + + if (isPalace(source, destination) && isDiagonalStep(directionInformation)) { + return createPalaceDiagonalPath(source, directionInformation); + } validateStraightMove(directionInformation); + return createStraightPath(source, directionInformation); + } - if (directionInformation.isRowBiggerThanColumn()) { - return createRowPath(source, directionInformation.rowDifference()); + private boolean isPalace(Position source, Position destination) { + return Camp.isPalace(source) && Camp.isPalace(destination); + } + + private boolean isDiagonalStep(DirectionInformation directionInformation) { + int absRowDifference = directionInformation.calculateAbsRowDifference(); + int absColumnDifference = directionInformation.calculateAbsColumnDifference(); + + return absRowDifference == absColumnDifference; + } + + private List createPalaceDiagonalPath(Position source, DirectionInformation directionInformation) { + List path = createDiagonalPath(source, directionInformation); + validatePalaceDiagonalPath(source, path); + return path; + } + + private List createDiagonalPath(Position source, DirectionInformation directionInformation) { + List path = new ArrayList<>(); + + Position current = source; + int rowDirection = directionInformation.calculateRowDirection(); + int columnDirection = directionInformation.calculateColumnDirection(); + int stepCount = directionInformation.calculateAbsRowDifference(); + + for (int i = 0; i < stepCount; i++) { + current = current.moveDiagonal(rowDirection, columnDirection); + path.add(current); + } + return path; + } + + private void validatePalaceDiagonalPath(Position source, List path) { + boolean passesPalaceCenter = + Camp.isPalaceCenter(source) || path.stream().anyMatch(Camp::isPalaceCenter); + + if (!passesPalaceCenter) { + throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); } - return createColumnPath(source, directionInformation.columnDifference()); } private void validateStraightMove(DirectionInformation directionInformation) { @@ -30,19 +74,31 @@ private void validateStraightMove(DirectionInformation directionInformation) { if (sum != rowDifference && sum != columnDifference) { throw new IllegalArgumentException(ONLY_STRAIGHT_MOVE_ALLOWED); } + } + private void validatePieceMoved(DirectionInformation directionInformation) { + int rowDifference = directionInformation.rowDifference(); + int columnDifference = directionInformation.columnDifference(); if (rowDifference == 0 && columnDifference == 0) { throw new IllegalArgumentException(PIECE_MUST_MOVE); } } + private List createStraightPath(Position source, DirectionInformation directionInformation) { + if (directionInformation.rowDifference() != 0) { + return createRowPath(source, directionInformation.rowDifference()); + } + return createColumnPath(source, directionInformation.columnDifference()); + } + private List createRowPath(Position source, int rowDifference) { List path = new ArrayList<>(); + Position current = source; int rowDirection = rowDifference / Math.abs(rowDifference); while (rowDifference != 0) { - source = source.moveRow(rowDirection); - path.add(source); + current = current.moveRow(rowDirection); + path.add(current); rowDifference -= rowDirection; } return path; @@ -51,10 +107,11 @@ private List createRowPath(Position source, int rowDifference) { private List createColumnPath(Position source, int columnDifference) { List path = new ArrayList<>(); + Position current = source; int columnDirection = columnDifference / Math.abs(columnDifference); while (columnDifference != 0) { - source = source.moveColumn(columnDirection); - path.add(source); + current = current.moveColumn(columnDirection); + path.add(current); columnDifference -= columnDirection; } return path; diff --git a/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java index 8d1c203e0..e9b5af42b 100644 --- a/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -16,82 +18,178 @@ public class MultiStepStraightStrategyTest { private final MoveStrategy strategy = new MultiStepStraightStrategy(); - private static Stream createPositionsAndPath() { - return Stream.of( - Arguments.of(new Position(0, 0), new Position(9, 0), 9, - List.of( - new Position(1, 0), - new Position(2, 0), - new Position(3, 0), - new Position(4, 0), - new Position(5, 0), - new Position(6, 0), - new Position(7, 0), - new Position(8, 0), - new Position(9, 0) - )), - Arguments.of(new Position(0, 0), new Position(0, 8), 8, - List.of( - new Position(0, 1), - new Position(0, 2), - new Position(0, 3), - new Position(0, 4), - new Position(0, 5), - new Position(0, 6), - new Position(0, 7), - new Position(0, 8) - ) - ), - Arguments.of(new Position(0, 8), new Position(0, 0), 8, - List.of( - new Position(0, 7), - new Position(0, 6), - new Position(0, 5), - new Position(0, 4), - new Position(0, 3), - new Position(0, 2), - new Position(0, 1), - new Position(0, 0) - ) - ), - Arguments.of(new Position(9, 0), new Position(0, 0), 9, - List.of( - new Position(8, 0), - new Position(7, 0), - new Position(6, 0), - new Position(5, 0), - new Position(4, 0), - new Position(3, 0), - new Position(2, 0), - new Position(1, 0), - new Position(0, 0) - ) - ) - ); - } + @DisplayName("정상 경우") + @Nested + class success { + private static Stream createPositionsAndPath() { + return Stream.of( + Arguments.of(new Position(0, 0), new Position(9, 0), 9, + List.of( + new Position(1, 0), + new Position(2, 0), + new Position(3, 0), + new Position(4, 0), + new Position(5, 0), + new Position(6, 0), + new Position(7, 0), + new Position(8, 0), + new Position(9, 0) + )), + Arguments.of(new Position(0, 0), new Position(0, 8), 8, + List.of( + new Position(0, 1), + new Position(0, 2), + new Position(0, 3), + new Position(0, 4), + new Position(0, 5), + new Position(0, 6), + new Position(0, 7), + new Position(0, 8) + ) + ), + Arguments.of(new Position(0, 8), new Position(0, 0), 8, + List.of( + new Position(0, 7), + new Position(0, 6), + new Position(0, 5), + new Position(0, 4), + new Position(0, 3), + new Position(0, 2), + new Position(0, 1), + new Position(0, 0) + ) + ), + Arguments.of(new Position(9, 0), new Position(0, 0), 9, + List.of( + new Position(8, 0), + new Position(7, 0), + new Position(6, 0), + new Position(5, 0), + new Position(4, 0), + new Position(3, 0), + new Position(2, 0), + new Position(1, 0), + new Position(0, 0) + ) + ), + Arguments.of(new Position(0, 5), new Position(4, 5), 4, + List.of( + new Position(1, 5), + new Position(2, 5), + new Position(3, 5), + new Position(4, 5) + ) + ) + ); + } - @ParameterizedTest - @MethodSource("createPositionsAndPath") - void 차와_포는_한_방향으로만_1칸_이상_이동_할_수_있다(Position source, Position destination, int size, List expectedPath) { - List path = strategy.findPath(source, destination, Camp.HAN); + @ParameterizedTest + @MethodSource("createPositionsAndPath") + void 차와_포는_한_방향으로만_1칸_이상_이동_할_수_있다(Position source, Position destination, int size, + List expectedPath) { + List path = strategy.findPath(source, destination, Camp.HAN); - SoftAssertions.assertSoftly(assertSoftly -> { - assertSoftly.assertThat(path).hasSize(size); - assertSoftly.assertThat(path).containsExactlyElementsOf(expectedPath); - }); - } + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(size); + assertSoftly.assertThat(path).containsExactlyElementsOf(expectedPath); + }); + } + + private static Stream createDiagonalPositionsAndPath() { + return Stream.of( + Arguments.of(new Position(0, 3), new Position(2, 5), 2, + List.of( + new Position(1, 4), + new Position(2, 5) + )), + Arguments.of(new Position(0, 5), new Position(2, 3), 2, + List.of( + new Position(1, 4), + new Position(2, 3) + )), + Arguments.of(new Position(7, 3), new Position(9, 5), 2, + List.of( + new Position(8, 4), + new Position(9, 5) + )), + Arguments.of(new Position(7, 5), new Position(9, 3), 2, + List.of( + new Position(8, 4), + new Position(9, 3) + )), + Arguments.of(new Position(1, 4), new Position(2, 5), 1, + List.of( + new Position(2, 5) + )) + ); + } - @Test - void 차와_포는_한_방향으로_이동하지_않으면_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(5, 5), Camp.CHO)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); + @ParameterizedTest + @MethodSource("createDiagonalPositionsAndPath") + void 차와_포는_궁성_내에서_대각선으로_이동_할_수_있다(Position source, Position destination, int size, + List expectedPath) { + List path = strategy.findPath(source, destination, Camp.HAN); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(size); + assertSoftly.assertThat(path).containsExactlyElementsOf(expectedPath); + }); + } } - @Test - void 차와_포는_제자리_이동_시_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(0, 0), Camp.CHO)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 기물은 반드시 이동해야 합니다."); + @DisplayName("예외 경우") + @Nested + class exception { + private static Stream exceptionInvalidPalaceDiagonalPath() { + return Stream.of( + Arguments.of(new Position(0, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(0, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 3), new Position(2, 4), Camp.CHO), + Arguments.of(new Position(1, 5), new Position(2, 4), Camp.CHO), + Arguments.of(new Position(7, 4), new Position(8, 3), Camp.HAN), + Arguments.of(new Position(7, 4), new Position(8, 5), Camp.HAN), + Arguments.of(new Position(8, 3), new Position(9, 4), Camp.HAN), + Arguments.of(new Position(8, 5), new Position(9, 4), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionInvalidPalaceDiagonalPath") + void 차와_포는_궁성_대각선이_연결되지_않은_경로로_이동할_수_없다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } + + private static Stream exceptionDiagonalPathOutOfPalace() { + return Stream.of( + Arguments.of(new Position(0, 3), new Position(3, 6), Camp.CHO), + Arguments.of(new Position(3, 6), new Position(0, 3), Camp.CHO), + Arguments.of(new Position(7, 3), new Position(6, 2), Camp.HAN), + Arguments.of(new Position(6, 2), new Position(7, 3), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionDiagonalPathOutOfPalace") + void 차와_포는_궁성_영역을_벗어나는_대각선으로_이동할_수_없다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); + } + + @Test + void 차와_포는_한_방향으로_이동하지_않으면_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(5, 5), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); + } + + @Test + void 차와_포는_제자리_이동_시_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(0, 0), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 기물은 반드시 이동해야 합니다."); + } } } From ad5c85e0edd23be96ff7d7307f9c83f68cfdf72f Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 22:01:53 +0900 Subject: [PATCH 10/36] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Camp의 메서드 파라미터 수정으로 인한 변경 --- .../domain/piece/strategy/SingleStepStraightStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index 6cf99c67f..cd645285e 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -53,7 +53,7 @@ private boolean isSingleDiagonalStep(DirectionInformation directionInformation) } private void validateDiagonalMove(Position source, Position destination) { - if (!Camp.isPalaceCenter(source, destination)) { + if (!Camp.isPalaceCenter(source) && !Camp.isPalaceCenter(destination)) { throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); } } From a2ec5e6a91b5531ee8d97cee3e5b72c79c1d0fb8 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 22:02:42 +0900 Subject: [PATCH 11/36] =?UTF-8?q?test:=20=EC=B0=A8/=ED=8F=AC=EC=9D=98=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=20=ED=96=89=EB=A7=88=EB=B2=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B6=81=EC=84=B1=20=EB=82=B4=EB=B6=80=20=EB=8C=80?= =?UTF-8?q?=EA=B0=81=EC=84=A0=20=EC=9D=B4=EB=8F=99=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/janggi/domain/piece/PieceTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index 91ae45c83..366704b9a 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -204,6 +204,19 @@ class Cannon { piece.validateMove(new Position(2, 1), new Position(8, 1), board) ); } + + @Test + void 포는_궁성_내에서_대각선으로_이동할_수_있다() { + // given + Piece piece = new Piece(PieceType.CANNON, Camp.CHO); + BoardChecker board = new Board(() -> Map.of( + new Position(1, 4), new Piece(PieceType.CHARIOT, Camp.HAN) + )); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 3), new Position(2, 5), board) + ); + } } @DisplayName("차 행마법 테스트") @@ -247,6 +260,17 @@ class Chariot { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); } + + @Test + void 차는_궁성_내에서_대각선으로_이동할_수_있다() { + // given + Piece piece = new Piece(PieceType.CHARIOT, Camp.CHO); + BoardChecker board = new Board(Map::of); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 3), new Position(2, 5), board) + ); + } } @DisplayName("졸 행마법 테스트") From 471f823856f9f3f450b5d20df729c7bf366ede0f Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Tue, 31 Mar 2026 22:03:48 +0900 Subject: [PATCH 12/36] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 89af901fa..7aa0b6154 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,12 @@ - [x] 기물 규칙 및 제약에 맞게 이동한다. - [차(車)] - [x] 상하좌우로 장애물을 만날 때까지 칸 수 제한 없이 이동한다. - - [ ] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. + - [x] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. - [x] 이동하려는 경로 중간에 다른 기물이 **있**다면 `IllegalArgumentException`을 발생시킨다. - [포(包)] - [x] 상하좌우로 이동하되, 반드시 중간에 다른 기물을 하나 뛰어넘어야 합니다. - - [ ] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. + - [x] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. - 이때에도 경로 상에 반드시 기물이 있어야 한다. - [x] 이동하려는 경로 중간에 다른 기물이 **없**다면 `IllegalArgumentException`을 발생시킨다. - [x] 넘으려는 기물이 포(包) 일 경우 `IllegalArgumentException`을 발생시킨다. From 15e452af09e816e59316c6dadf0364b80bb12823 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 13:33:46 +0900 Subject: [PATCH 13/36] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/board/Board.java | 4 ++-- src/main/java/janggi/domain/board/BoardChecker.java | 2 +- src/main/java/janggi/domain/piece/Piece.java | 2 +- .../domain/piece/condition/OnePieceExistsCondition.java | 4 ++-- src/test/java/janggi/domain/board/BoardTest.java | 2 +- src/test/java/janggi/domain/piece/PieceTest.java | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 54000940d..87f89dbaa 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -32,10 +32,10 @@ public boolean isSameCampPieceAt(Position position, Camp camp) { } @Override - public boolean hasSamePieceRuleAt(Position position, PieceType pieceType) { + public boolean hasSamePieceTypeAt(Position position, PieceType pieceType) { if (board.containsKey(position)) { Piece foundPiece = board.get(position); - return foundPiece.isSamePieceRule(pieceType); + return foundPiece.isSamePieceType(pieceType); } return false; } diff --git a/src/main/java/janggi/domain/board/BoardChecker.java b/src/main/java/janggi/domain/board/BoardChecker.java index 23cb0dcd6..d8ce969e5 100644 --- a/src/main/java/janggi/domain/board/BoardChecker.java +++ b/src/main/java/janggi/domain/board/BoardChecker.java @@ -10,5 +10,5 @@ public interface BoardChecker { boolean isSameCampPieceAt(Position position, Camp camp); - boolean hasSamePieceRuleAt(Position position, PieceType pieceType); + boolean hasSamePieceTypeAt(Position position, PieceType pieceType); } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 4c8dc9c94..b3eef973f 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -11,7 +11,7 @@ public void validateMove(Position source, Position destination, BoardChecker boa pieceType.checkPath(path, camp, board); } - public boolean isSamePieceRule(PieceType pieceType) { + public boolean isSamePieceType(PieceType pieceType) { return this.pieceType == pieceType; } diff --git a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java index 12b010112..7c249a85b 100644 --- a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java +++ b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java @@ -38,7 +38,7 @@ private int countPieceAt(BoardChecker board, PieceType pieceType, Position posit } private void validateSamePieceRule(BoardChecker board, PieceType pieceType, Position position) { - if (board.hasSamePieceRuleAt(position, pieceType)) { + if (board.hasSamePieceTypeAt(position, pieceType)) { throw new IllegalArgumentException(SAME_PIECE_TYPE_IN_PATH); } } @@ -54,7 +54,7 @@ private void validateDestination(Position destination, Camp camp, BoardChecker b throw new IllegalArgumentException(SAME_CAMP_PIECE_AT_DESTINATION); } - if (board.hasSamePieceRuleAt(destination, pieceType)) { + if (board.hasSamePieceTypeAt(destination, pieceType)) { throw new IllegalArgumentException(SAME_PIECE_TYPE_AT_DESTINATION); } } diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 9d0504c67..741c6d6d1 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -25,7 +25,7 @@ source, new Piece(PieceType.CANNON, Camp.HAN) // when board.movePiece(source, destination, Camp.HAN); // then - boolean destinationExists = board.hasSamePieceRuleAt(destination, PieceType.CANNON); + boolean destinationExists = board.hasSamePieceTypeAt(destination, PieceType.CANNON); boolean sourceExists = board.hasPieceAt(source); SoftAssertions.assertSoftly(assertSoftly -> { diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index 366704b9a..09fef8332 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -19,7 +19,7 @@ class PieceTest { // given Piece piece = new Piece(PieceType.CANNON, Camp.CHO); // when - boolean result = piece.isSamePieceRule(PieceType.CANNON); + boolean result = piece.isSamePieceType(PieceType.CANNON); // then assertThat(result).isTrue(); } From 9d9d5cdec4e9771c0f3339d953e91059a1c5f89a Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 15:00:54 +0900 Subject: [PATCH 14/36] =?UTF-8?q?feat:=20=EA=B6=81=20=EC=9E=A1=ED=9E=90=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B2=8C=EC=9E=84=20=EC=A2=85=EB=A3=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 궁 잡힐 시 게임 종료 테스트 추가 - 입출력 시 나라별 색 추가 --- src/main/java/janggi/JanggiGame.java | 27 +++++++++---------- src/main/java/janggi/domain/board/Board.java | 26 ++++++++++++------ src/main/java/janggi/domain/piece/Piece.java | 4 +++ src/main/java/janggi/view/InputView.java | 7 +++-- src/main/java/janggi/view/OutputView.java | 8 ++++++ .../java/janggi/domain/board/BoardTest.java | 21 +++++++++++++++ 6 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java index 04a12fda8..235c8d393 100644 --- a/src/main/java/janggi/JanggiGame.java +++ b/src/main/java/janggi/JanggiGame.java @@ -47,18 +47,26 @@ private ElephantSetUpDto readElephantSetUp(Camp camp) { private void play(Board board) { Turn turn = new Turn(); - while (true) { - retryOnInvalidInput(() -> playTurn(board, turn)); + boolean continueGame = true; + while (continueGame) { + continueGame = retryOnInvalidInput(() -> playTurn(board, turn)); outputView.printBoard(board.getBoard()); } + outputView.printWinner(turn.currentTurn()); } - private void playTurn(Board board, Turn turn) { + private boolean playTurn(Board board, Turn turn) { Camp camp = turn.currentTurn(); + Position source = retryOnInvalidInput(() -> readSource(board, camp)); Position destination = retryOnInvalidInput(inputView::readDestination); - board.movePiece(source, destination, camp); + + boolean gameEnded = board.movePiece(source, destination, camp); + if (gameEnded) { + return false; + } turn.finishTurn(); + return true; } private Position readSource(Board board, Camp camp) { @@ -76,15 +84,4 @@ private T retryOnInvalidInput(Supplier input) { } } } - - private void retryOnInvalidInput(Runnable input) { - while (true) { - try { - input.run(); - return; - } catch (IllegalArgumentException e) { - outputView.printError(e.getMessage()); - } - } - } } diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 87f89dbaa..8cbc008fb 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -33,19 +33,29 @@ public boolean isSameCampPieceAt(Position position, Camp camp) { @Override public boolean hasSamePieceTypeAt(Position position, PieceType pieceType) { - if (board.containsKey(position)) { - Piece foundPiece = board.get(position); - return foundPiece.isSamePieceType(pieceType); + if (!board.containsKey(position)) { + return false; } - return false; + Piece foundPiece = board.get(position); + return foundPiece.isSamePieceType(pieceType); } - public void movePiece(Position source, Position destination, Camp turn) { + public boolean movePiece(Position source, Position destination, Camp turn) { validateCampTurn(source, turn); - Piece piece = board.get(source); - piece.validateMove(source, destination, this); - board.put(destination, piece); + Piece movingPiece = board.get(source); + movingPiece.validateMove(source, destination, this); + + boolean isDestinationGeneral = isGeneralAt(destination); board.remove(source); + board.put(destination, movingPiece); + return isDestinationGeneral; + } + + private boolean isGeneralAt(Position position) { + if (!board.containsKey(position)) { + return false; + } + return board.get(position).isGeneral(); } public void validateCampTurn(Position source, Camp turn) { diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index b3eef973f..0291d4d7d 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -18,4 +18,8 @@ public boolean isSamePieceType(PieceType pieceType) { public boolean isSameCamp(Camp camp) { return this.camp == camp; } + + public boolean isGeneral() { + return pieceType == PieceType.GENERAL; + } } diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index ac7c7bea9..b68358deb 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -13,6 +13,7 @@ public final class InputView { private static final String INVALID_INPUT_FORMAT = "[ERROR] 잘못된 입력 형식입니다."; private static final String DELIMITER = ","; private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String RESET = "\u001B[0m"; private static final String ELEPHANT_SETTING = """ @@ -30,9 +31,10 @@ public InputView(Scanner scanner) { } public ElephantSetUp readElephantSetting(CampDto campDto) { + String campName = campDto.color() + campDto.name() + RESET; System.out.println(String.format( ELEPHANT_SETTING, - campDto.name(), + campName, ElephantSetUpFormat.outputMessage()) ); return ElephantSetUpFormat.from(readLine()).toElephantSetting(); @@ -51,7 +53,8 @@ private void validateInput(String input) { } public Position readSource(CampDto campDto) { - System.out.println(String.format(TURN, campDto.name())); + String campName = campDto.color() + campDto.name() + RESET; + System.out.println(String.format(TURN, campName)); System.out.println(SOURCE); return toPosition(Parser.parseByDelimiter(DELIMITER, readLine())); } diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index 3b1c28920..8ec028b48 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -3,6 +3,7 @@ import static java.util.stream.Collectors.joining; import janggi.domain.Position; +import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.view.dto.CampDto; import janggi.view.dto.PiecePositionDto; @@ -26,6 +27,8 @@ public final class OutputView { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + private static final String WINNER = "%s나라가 승리하였습니다!! 축하드립니다!!"; + public void printError(String errorMessage) { System.out.println(errorMessage); } @@ -34,6 +37,11 @@ public void printBoard(Map boardState) { System.out.println(renderBoard(toPiecePositions(boardState))); } + public void printWinner(Camp camp) { + String winnerName = CampDto.from(camp).color() + CampDto.from(camp).name() + RESET; + System.out.printf((WINNER) + "%n", winnerName); + } + private List toPiecePositions(Map boardState) { return boardState.entrySet().stream() .map(entry -> PiecePositionDto.from(entry.getKey(), entry.getValue())) diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 741c6d6d1..818ceb4ec 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -1,5 +1,7 @@ package janggi.domain.board; +import static org.assertj.core.api.Assertions.assertThat; + import janggi.domain.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; @@ -63,4 +65,23 @@ source, new Piece(PieceType.CANNON, Camp.CHO) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 출발지에 기물이 존재하지 않습니다."); } + + @Test + void 궁을_잡으면_게임이_끝난다() { + // given + Position source = new Position(7, 1); + Position destination = new Position(0, 1); + + Board board = new Board(() -> Map.of( + new Position(4, 1), new Piece(PieceType.SOLDIER, Camp.HAN), + destination, new Piece(PieceType.GENERAL, Camp.CHO), + source, new Piece(PieceType.CANNON, Camp.HAN) + )); + + // when + boolean gameEnded = board.movePiece(source, destination, Camp.HAN); + + // then + assertThat(gameEnded).isTrue(); + } } From 78c3991b74a4efaeda34afa46616c8d155d69758 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 15:01:27 +0900 Subject: [PATCH 15/36] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/board/Board.java | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 8cbc008fb..8814d201c 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -51,13 +51,6 @@ public boolean movePiece(Position source, Position destination, Camp turn) { return isDestinationGeneral; } - private boolean isGeneralAt(Position position) { - if (!board.containsKey(position)) { - return false; - } - return board.get(position).isGeneral(); - } - public void validateCampTurn(Position source, Camp turn) { validateSource(source); Piece piece = board.get(source); @@ -66,13 +59,20 @@ public void validateCampTurn(Position source, Camp turn) { } } + public Map getBoard() { + return Map.copyOf(board); + } + + private boolean isGeneralAt(Position position) { + if (!board.containsKey(position)) { + return false; + } + return board.get(position).isGeneral(); + } + private void validateSource(Position source) { if (!board.containsKey(source)) { throw new IllegalArgumentException(SOURCE_NOT_EXISTS); } } - - public Map getBoard() { - return Map.copyOf(board); - } } From ceaf0829a7bead2ba7342859ec3b9106356a120d Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 15:37:54 +0900 Subject: [PATCH 16/36] =?UTF-8?q?feat:=20=EC=A0=90=EC=88=98=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 점수 계산 기능 테스트 추가 --- src/main/java/janggi/JanggiGame.java | 1 + src/main/java/janggi/domain/board/Board.java | 16 ++++++++++ src/main/java/janggi/domain/piece/Camp.java | 17 ++++++++-- src/main/java/janggi/domain/piece/Piece.java | 4 +++ .../java/janggi/domain/piece/PieceType.java | 22 ++++++++----- src/main/java/janggi/view/OutputView.java | 17 ++++++++-- .../java/janggi/domain/board/BoardTest.java | 31 ++++++++++++++++--- 7 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java index 235c8d393..65e6cb5d3 100644 --- a/src/main/java/janggi/JanggiGame.java +++ b/src/main/java/janggi/JanggiGame.java @@ -49,6 +49,7 @@ private void play(Board board) { Turn turn = new Turn(); boolean continueGame = true; while (continueGame) { + outputView.printScore(board.calculateScore()); continueGame = retryOnInvalidInput(() -> playTurn(board, turn)); outputView.printBoard(board.getBoard()); } diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 8814d201c..4542d6a24 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -59,6 +59,22 @@ public void validateCampTurn(Position source, Camp turn) { } } + public Map calculateScore() { + Map resultScore = new HashMap<>(); + + Camp.getAllCamp().forEach(camp -> { + double totalScore = board.values().stream() + .filter(piece -> piece.isSameCamp(camp)) + .mapToDouble(Piece::score) + .sum(); + + totalScore += camp.getBonusScoreForSecondPlayer(); + resultScore.put(camp, totalScore); + }); + + return resultScore; + } + public Map getBoard() { return Map.copyOf(board); } diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index f1c910cf8..c1cd1c0b4 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -6,7 +6,7 @@ public enum Camp { - HAN(-1, 9) { + HAN(-1, 9, 1.5) { @Override public List convertElephantColumns(List columns) { return columns; @@ -17,7 +17,7 @@ public Camp next() { return CHO; } }, - CHO(1, 0) { + CHO(1, 0, 0) { @Override public List convertElephantColumns(List columns) { return columns.reversed(); @@ -33,10 +33,12 @@ public Camp next() { private final int forwardDirection; private final int startRowPosition; + private final double bonusScoreForSecondPlayer; - Camp(int forwardDirection, int startRowPosition) { + Camp(int forwardDirection, int startRowPosition, double bonusScoreForSecondPlayer) { this.forwardDirection = forwardDirection; this.startRowPosition = startRowPosition; + this.bonusScoreForSecondPlayer = bonusScoreForSecondPlayer; } public abstract List convertElephantColumns(List columns); @@ -53,6 +55,10 @@ public int getStartRowPosition() { return startRowPosition; } + public double getBonusScoreForSecondPlayer() { + return bonusScoreForSecondPlayer; + } + public void validateFriendlyPalace(Position position) { if (!(isFriendlyPalaceRow(position) && isPalaceColumn(position))) { throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); @@ -87,4 +93,9 @@ public static boolean isPalaceCenter(Position position) { return position.column() == 4 && absRowDifference == 1; }); } + + public static List getAllCamp() { + return Arrays.stream(values()) + .toList(); + } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 0291d4d7d..3899ea6c5 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -22,4 +22,8 @@ public boolean isSameCamp(Camp camp) { public boolean isGeneral() { return pieceType == PieceType.GENERAL; } + + public double score() { + return pieceType().score(); + } } diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index 866b59b74..b7f9360f8 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -15,20 +15,22 @@ public enum PieceType { - GENERAL(new PalaceStrategy(), new EmptyCondition()), - CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition()), - HORSE(new HorseStrategy(), new EmptyCondition()), - CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition()), - GUARD(new PalaceStrategy(), new EmptyCondition()), - ELEPHANT(new ElephantStrategy(), new EmptyCondition()), - SOLDIER(new SoldierStrategy(), new EmptyCondition()); + GENERAL(new PalaceStrategy(), new EmptyCondition(), 0), + CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition(), 13), + CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition(), 7), + HORSE(new HorseStrategy(), new EmptyCondition(), 5), + ELEPHANT(new ElephantStrategy(), new EmptyCondition(), 3), + GUARD(new PalaceStrategy(), new EmptyCondition(), 3), + SOLDIER(new SoldierStrategy(), new EmptyCondition(), 2); private final MoveStrategy moveStrategy; private final MoveCondition moveCondition; + private final double score; - PieceType(MoveStrategy moveStrategy, MoveCondition moveCondition) { + PieceType(MoveStrategy moveStrategy, MoveCondition moveCondition, int score) { this.moveStrategy = moveStrategy; this.moveCondition = moveCondition; + this.score = score; } public List findPath(Position source, Position destination, Camp camp) { @@ -38,4 +40,8 @@ public List findPath(Position source, Position destination, Camp camp) public void checkPath(List path, Camp camp, BoardChecker board) { moveCondition.checkPath(path, camp, board, this); } + + public double score() { + return score; + } } diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index 8ec028b48..3785f8b61 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -27,6 +27,7 @@ public final class OutputView { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + private static final String SCORE = "%s나라 점수: %.1f"; private static final String WINNER = "%s나라가 승리하였습니다!! 축하드립니다!!"; public void printError(String errorMessage) { @@ -34,11 +35,11 @@ public void printError(String errorMessage) { } public void printBoard(Map boardState) { - System.out.println(renderBoard(toPiecePositions(boardState))); + System.out.println(renderBoard(toPiecePositions(boardState)) + LINE_SEPARATOR); } public void printWinner(Camp camp) { - String winnerName = CampDto.from(camp).color() + CampDto.from(camp).name() + RESET; + String winnerName = toCampName(camp); System.out.printf((WINNER) + "%n", winnerName); } @@ -92,4 +93,16 @@ private String colorize(PiecePositionDto piecePosition) { private String fullWidthNumber(int number) { return FULL_WIDTH_NUMBERS[number]; } + + public void printScore(Map eachCampScore) { + for (Camp camp : eachCampScore.keySet()) { + String campName = toCampName(camp); + System.out.printf(SCORE + "%n", campName, eachCampScore.get(camp)); + } + } + + private String toCampName(Camp camp) { + CampDto campDto = CampDto.from(camp); + return campDto.color() + campDto.name() + RESET; + } } diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 818ceb4ec..ebdddb3f6 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -69,13 +69,13 @@ source, new Piece(PieceType.CANNON, Camp.CHO) @Test void 궁을_잡으면_게임이_끝난다() { // given - Position source = new Position(7, 1); - Position destination = new Position(0, 1); + Position source = new Position(7, 4); + Position destination = new Position(1, 4); Board board = new Board(() -> Map.of( - new Position(4, 1), new Piece(PieceType.SOLDIER, Camp.HAN), - destination, new Piece(PieceType.GENERAL, Camp.CHO), - source, new Piece(PieceType.CANNON, Camp.HAN) + new Position(6, 4), new Piece(PieceType.SOLDIER, Camp.HAN), + source, new Piece(PieceType.CANNON, Camp.HAN), + destination, new Piece(PieceType.GENERAL, Camp.CHO) )); // when @@ -84,4 +84,25 @@ source, new Piece(PieceType.CANNON, Camp.HAN) // then assertThat(gameEnded).isTrue(); } + + @Test + void 각_진영의_남아있는_기물로_점수를_계산한다() { + // given + Board board = new Board(() -> Map.of( + new Position(1, 4), new Piece(PieceType.GENERAL, Camp.CHO), + new Position(0, 0), new Piece(PieceType.CHARIOT, Camp.CHO), + new Position(0, 8), new Piece(PieceType.CHARIOT, Camp.CHO), + + new Position(4, 1), new Piece(PieceType.SOLDIER, Camp.HAN), + new Position(7, 1), new Piece(PieceType.CANNON, Camp.HAN), + new Position(7, 7), new Piece(PieceType.CANNON, Camp.HAN) + )); + + // when + Map eachCampScore = board.calculateScore(); + + // then + assertThat(eachCampScore.get(Camp.CHO)).isEqualTo(26); + assertThat(eachCampScore.get(Camp.HAN)).isEqualTo(17.5); + } } From dc95de087f8a1fba9423efa2dc197655b06b0530 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 15:44:48 +0900 Subject: [PATCH 17/36] =?UTF-8?q?refactor:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EA=B0=9D=EC=B2=B4=EC=97=90=20=EC=9E=AC?= =?UTF-8?q?=ED=95=A0=EB=8B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/piece/strategy/HorseStrategy.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java index 291ed56dd..7d52a01a3 100644 --- a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java @@ -34,11 +34,11 @@ private List createRowFirstPath(Position source, DirectionInformation int rowDirection = directionInformation.calculateRowDirection(); int columnDirection = directionInformation.calculateColumnDirection(); - source = source.moveRow(rowDirection); - path.add(source); + Position current = source.moveRow(rowDirection); + path.add(current); - source = source.moveDiagonal(rowDirection, columnDirection); - path.add(source); + current = current.moveDiagonal(rowDirection, columnDirection); + path.add(current); return path; } @@ -47,11 +47,11 @@ private List createColumnFirstPath(Position source, DirectionInformati int rowDirection = directionInformation.calculateRowDirection(); int columnDirection = directionInformation.calculateColumnDirection(); - source = source.moveColumn(columnDirection); - path.add(source); + Position current = source.moveColumn(columnDirection); + path.add(current); - source = source.moveDiagonal(rowDirection, columnDirection); - path.add(source); + current = current.moveDiagonal(rowDirection, columnDirection); + path.add(current); return path; } From 2d3237d5dd07fde942c371ef062b2f3d55b6b3b0 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 15:50:57 +0900 Subject: [PATCH 18/36] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20private?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/Position.java | 6 +++--- .../java/janggi/domain/piece/condition/EmptyCondition.java | 2 +- .../domain/piece/condition/OnePieceExistsCondition.java | 2 +- .../java/janggi/domain/piece/strategy/ElephantStrategy.java | 4 ++-- .../java/janggi/domain/piece/strategy/HorseStrategy.java | 4 ++-- .../domain/piece/strategy/SingleStepStraightStrategy.java | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/janggi/domain/Position.java b/src/main/java/janggi/domain/Position.java index 18a631295..922245312 100644 --- a/src/main/java/janggi/domain/Position.java +++ b/src/main/java/janggi/domain/Position.java @@ -2,9 +2,9 @@ public record Position(int row, int column) { - public static final int MIN_POSITION_INDEX = 0; - public static final int MAX_ROW_INDEX = 9; - public static final int MAX_COLUMN_INDEX = 8; + private static final int MIN_POSITION_INDEX = 0; + private static final int MAX_ROW_INDEX = 9; + private static final int MAX_COLUMN_INDEX = 8; private static final String ROW_OUT_OF_RANGE = String.format( "[ERROR] 행은 %d행 이상 %d행 이하여야 합니다.", MIN_POSITION_INDEX, diff --git a/src/main/java/janggi/domain/piece/condition/EmptyCondition.java b/src/main/java/janggi/domain/piece/condition/EmptyCondition.java index f793e3066..a106335d9 100644 --- a/src/main/java/janggi/domain/piece/condition/EmptyCondition.java +++ b/src/main/java/janggi/domain/piece/condition/EmptyCondition.java @@ -9,7 +9,7 @@ public class EmptyCondition implements MoveCondition { private static final String PATH_NOT_EMPTY = "[ERROR] 경로 상에 기물이 존재합니다."; - public static final String SAME_CAMP_PIECE_AT_DESTINATION = "[ERROR] 목적지에 같은 진영의 기물이 존재합니다."; + private static final String SAME_CAMP_PIECE_AT_DESTINATION = "[ERROR] 목적지에 같은 진영의 기물이 존재합니다."; @Override public void checkPath(List path, Camp camp, BoardChecker board, PieceType pieceType) { diff --git a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java index 7c249a85b..0545bd921 100644 --- a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java +++ b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java @@ -8,7 +8,7 @@ public class OnePieceExistsCondition implements MoveCondition { - public static final int PASS_PIECE_COUNT = 1; + private static final int PASS_PIECE_COUNT = 1; private static final String SAME_PIECE_TYPE_IN_PATH = "[ERROR] 경로상에 같은 종류의 기물이 존재합니다."; private static final String INVALID_JUMPED_PIECE_COUNT = String.format( "[ERROR] 해당 기물은 정확히 %d개의 기물만 뛰어넘을 수 있습니다.", diff --git a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java index 375cb8ab2..7c4d65d60 100644 --- a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java @@ -10,8 +10,8 @@ public class ElephantStrategy implements MoveStrategy { private static final int DIAGONAL_COUNT = 2; private static final int MIN_ABS_DELTA = 2; private static final int MAX_ABS_DELTA = 3; - public static final int ELEPHANT_STRAIGHT_MOVE_DISTANCE = 1; - public static final int ELEPHANT_DIAGONAL_MOVE_DISTANCE = 2; + private static final int ELEPHANT_STRAIGHT_MOVE_DISTANCE = 1; + private static final int ELEPHANT_DIAGONAL_MOVE_DISTANCE = 2; private static final String INVALID_ELEPHANT_MOVE = String.format( "[ERROR] 해당 기물은 직선 %d칸 이동 후 대각선 %d칸 이동만 가능합니다.", ELEPHANT_STRAIGHT_MOVE_DISTANCE, diff --git a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java index 7d52a01a3..a6231667d 100644 --- a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java @@ -9,8 +9,8 @@ public class HorseStrategy implements MoveStrategy { private static final int MIN_ABS_DELTA = 1; private static final int MAX_ABS_DELTA = 2; - public static final int HORSE_STRAIGHT_MOVE_DISTANCE = 1; - public static final int HORSE_DIAGONAL_MOVE_DISTANCE = 1; + private static final int HORSE_STRAIGHT_MOVE_DISTANCE = 1; + private static final int HORSE_DIAGONAL_MOVE_DISTANCE = 1; private static final String INVALID_HORSE_MOVE = String.format( "[ERROR] 해당 기물은 직선 %d칸 이동 후 대각선 %d칸 이동만 가능합니다.", HORSE_STRAIGHT_MOVE_DISTANCE, diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index cd645285e..310a065b4 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -6,7 +6,7 @@ public abstract class SingleStepStraightStrategy implements MoveStrategy { - public static final int SINGLE_STEP_DISTANCE = 1; + private static final int SINGLE_STEP_DISTANCE = 1; private static final String INVALID_SINGLE_STEP_STRAIGHT_MOVE = String.format( "[ERROR] 해당 기물은 직선으로 %d칸 이동해야 합니다.", SINGLE_STEP_DISTANCE From 4927a1914c69d18ba0d7f380447759ada9b6adf9 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 15:51:28 +0900 Subject: [PATCH 19/36] =?UTF-8?q?refactor:=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85=20=EC=A0=84=EC=9A=A9=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/AppConfig.java | 19 ------------------- src/main/java/janggi/JanggiApplication.java | 7 +++++-- 2 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 src/main/java/janggi/AppConfig.java diff --git a/src/main/java/janggi/AppConfig.java b/src/main/java/janggi/AppConfig.java deleted file mode 100644 index 54fa745ed..000000000 --- a/src/main/java/janggi/AppConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package janggi; - -import janggi.view.InputView; -import janggi.view.OutputView; -import java.util.Scanner; - -public class AppConfig { - public JanggiGame janggi() { - return new JanggiGame(inputView(), outputView()); - } - - public InputView inputView() { - return new InputView(new Scanner(System.in)); - } - - public OutputView outputView() { - return new OutputView(); - } -} diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java index 1e3c5416e..66824bce2 100644 --- a/src/main/java/janggi/JanggiApplication.java +++ b/src/main/java/janggi/JanggiApplication.java @@ -1,9 +1,12 @@ package janggi; +import janggi.view.InputView; +import janggi.view.OutputView; +import java.util.Scanner; + public class JanggiApplication { public static void main(String[] args) { - AppConfig app = new AppConfig(); - JanggiGame janggi = app.janggi(); + JanggiGame janggi = new JanggiGame(new InputView(new Scanner(System.in)), new OutputView()); janggi.run(); } } From c9c75791dfd3b3f21bad9d88ee218d192d9591bb Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 17:11:54 +0900 Subject: [PATCH 20/36] =?UTF-8?q?refactor:=20dto=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dto 제거 후 Map으로 인자 수정 --- src/main/java/janggi/JanggiGame.java | 15 +++++----- .../initializer/InitialPiecePlacement.java | 6 +--- .../initializer/StandardBoardInitializer.java | 29 ++++++++++--------- .../initializer/dto/ElephantSetUpDto.java | 14 --------- .../board/StandardBoardInitializerTest.java | 20 ++++++------- 5 files changed, 34 insertions(+), 50 deletions(-) delete mode 100644 src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java index 65e6cb5d3..01d2e0ab6 100644 --- a/src/main/java/janggi/JanggiGame.java +++ b/src/main/java/janggi/JanggiGame.java @@ -6,11 +6,12 @@ import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.board.initializer.ElephantSetUp; import janggi.domain.board.initializer.StandardBoardInitializer; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; import janggi.domain.piece.Camp; import janggi.view.InputView; import janggi.view.OutputView; import janggi.view.dto.CampDto; +import java.util.HashMap; +import java.util.Map; import java.util.function.Supplier; public class JanggiGame { @@ -30,19 +31,19 @@ public void run() { } private Board createBoard() { - ElephantSetUpDto hanElephantSetUp = readElephantSetUp(Camp.HAN); - ElephantSetUpDto choElephantSetUp = readElephantSetUp(Camp.CHO); + Map elephantSetUps = new HashMap<>(); + readElephantSetUp(elephantSetUps, Camp.HAN); + readElephantSetUp(elephantSetUps, Camp.CHO); - BoardInitializer initializer - = new StandardBoardInitializer(hanElephantSetUp, choElephantSetUp); + BoardInitializer initializer = new StandardBoardInitializer(elephantSetUps); return new Board(initializer); } - private ElephantSetUpDto readElephantSetUp(Camp camp) { + private void readElephantSetUp(Map elephantSetUps, Camp camp) { ElephantSetUp elephantSetUp = retryOnInvalidInput( () -> inputView.readElephantSetting(CampDto.from(camp)) ); - return new ElephantSetUpDto(camp, elephantSetUp); + elephantSetUps.put(camp, elephantSetUp); } private void play(Board board) { diff --git a/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java b/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java index 14be6c0ff..a6fd9ec0b 100644 --- a/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java +++ b/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java @@ -1,7 +1,6 @@ package janggi.domain.board.initializer; import janggi.domain.Position; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; @@ -44,15 +43,12 @@ public enum InitialPiecePlacement { this.piece = new Piece(pieceType, camp); } - public static Map init(ElephantSetUpDto firstChoice, ElephantSetUpDto secondChoice) { + public static Map initialize() { Map board = new HashMap<>(); for (InitialPiecePlacement placement : values()) { board.put(placement.position, placement.piece); } - - board.putAll(firstChoice.settingUp()); - board.putAll(secondChoice.settingUp()); return board; } } diff --git a/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java b/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java index 5f1d352ed..65059fea6 100644 --- a/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java +++ b/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java @@ -1,31 +1,32 @@ package janggi.domain.board.initializer; import janggi.domain.Position; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; +import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; +import java.util.HashMap; import java.util.Map; public class StandardBoardInitializer implements BoardInitializer { private static final String INVALID_ELEPHANT_SETUP = "[ERROR] 진영별 상차림은 각각 하나씩만 존재해야 합니다."; - private final ElephantSetUpDto firstElephantSetUp; - private final ElephantSetUpDto secondElephantSetUp; + private final Map elephantSetUps; - public StandardBoardInitializer(ElephantSetUpDto firstElephantSetUp, ElephantSetUpDto secondElephantSetUp) { - validateDifferentCamp(firstElephantSetUp, secondElephantSetUp); - this.firstElephantSetUp = firstElephantSetUp; - this.secondElephantSetUp = secondElephantSetUp; - } - - private void validateDifferentCamp(ElephantSetUpDto firstElephantSetUp, ElephantSetUpDto secondElephantSetUp) { - if (firstElephantSetUp.camp() == secondElephantSetUp.camp()) { - throw new IllegalArgumentException(INVALID_ELEPHANT_SETUP); - } + public StandardBoardInitializer(Map elephantSetUps) { + validate(elephantSetUps); + this.elephantSetUps = Map.copyOf(elephantSetUps); } @Override public Map initialize() { - return InitialPiecePlacement.init(firstElephantSetUp, secondElephantSetUp); + Map board = new HashMap<>(InitialPiecePlacement.initialize()); + elephantSetUps.forEach((camp, elephantSetUp) -> board.putAll(elephantSetUp.settingUp(camp))); + return board; + } + + private void validate(Map elephantSetUps) { + if (elephantSetUps.size() != Camp.getAllCamp().size()) { + throw new IllegalArgumentException(INVALID_ELEPHANT_SETUP); + } } } diff --git a/src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java b/src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java deleted file mode 100644 index 9c0d7153a..000000000 --- a/src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package janggi.domain.board.initializer.dto; - -import janggi.domain.Position; -import janggi.domain.board.initializer.ElephantSetUp; -import janggi.domain.piece.Camp; -import janggi.domain.piece.Piece; -import java.util.Map; - -public record ElephantSetUpDto(Camp camp, ElephantSetUp elephantSetUp) { - - public Map settingUp() { - return elephantSetUp.settingUp(camp); - } -} diff --git a/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java b/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java index c076b4169..9317f12d6 100644 --- a/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java +++ b/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java @@ -6,7 +6,6 @@ import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.board.initializer.ElephantSetUp; import janggi.domain.board.initializer.StandardBoardInitializer; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; @@ -18,10 +17,9 @@ public class StandardBoardInitializerTest { @Test - void 같은_진영의_상차림을_두_번_전달하면_예외가_발생한다() { + void 두_진영의_상차림이_모두_존재하지_않으면_예외가_발생한다() { assertThatThrownBy(() -> new StandardBoardInitializer( - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT), - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.RIGHT_ELEPHANT))) + Map.of(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT))) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 진영별 상차림은 각각 하나씩만 존재해야 합니다."); } @@ -29,9 +27,10 @@ public class StandardBoardInitializerTest { @Test void 한_상마상마_초_마상마상_으로_보드를_초기화한다() { // given - BoardInitializer initializer = new StandardBoardInitializer( - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT), - new ElephantSetUpDto(Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT)); + BoardInitializer initializer = new StandardBoardInitializer(Map.of( + Camp.HAN, ElephantSetUp.LEFT_ELEPHANT, + Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT + )); Map expectedBoard = createExpectedBoard(); expectedBoard.putAll(createChoLeftHanRightBoard()); // when @@ -46,9 +45,10 @@ public class StandardBoardInitializerTest { @Test void 한_상마마상_초_마상상마_으로_보드를_초기화한다() { // given - BoardInitializer initializer = new StandardBoardInitializer( - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.OUTER_ELEPHANT), - new ElephantSetUpDto(Camp.CHO, ElephantSetUp.INNER_ELEPHANT)); + BoardInitializer initializer = new StandardBoardInitializer(Map.of( + Camp.HAN, ElephantSetUp.OUTER_ELEPHANT, + Camp.CHO, ElephantSetUp.INNER_ELEPHANT + )); Map expectedBoard = createExpectedBoard(); expectedBoard.putAll(createChoInnerHanOuterBoard()); // when From d2c74e98e1336b68e1246b544b0eab5f99b2dc37 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 17:12:08 +0900 Subject: [PATCH 21/36] =?UTF-8?q?refactor:=20switch=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/janggi/view/format/PieceFormat.java | 66 +++++++------------ 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/src/main/java/janggi/view/format/PieceFormat.java b/src/main/java/janggi/view/format/PieceFormat.java index 23f331d88..30acbc92a 100644 --- a/src/main/java/janggi/view/format/PieceFormat.java +++ b/src/main/java/janggi/view/format/PieceFormat.java @@ -1,61 +1,41 @@ package janggi.view.format; +import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; +import java.util.Arrays; public enum PieceFormat { - GENERAL_CHO("楚"), - GENERAL_HAN("漢"), - SOLDIER_CHO("卒"), - SOLDIER_HAN("兵"), - CHARIOT_CHO("車"), - CHARIOT_HAN("車"), - HORSE_CHO("馬"), - HORSE_HAN("馬"), - CANNON_CHO("包"), - CANNON_HAN("包"), - GUARD_CHO("士"), - GUARD_HAN("士"), - ELEPHANT_CHO("象"), - ELEPHANT_HAN("象"), + GENERAL_CHO("楚", PieceType.GENERAL, Camp.CHO), + GENERAL_HAN("漢", PieceType.GENERAL, Camp.HAN), + SOLDIER_CHO("卒", PieceType.SOLDIER, Camp.CHO), + SOLDIER_HAN("兵", PieceType.SOLDIER, Camp.HAN), + CHARIOT_CHO("車", PieceType.CHARIOT, Camp.CHO), + CHARIOT_HAN("車", PieceType.CHARIOT, Camp.HAN), + HORSE_CHO("馬", PieceType.HORSE, Camp.CHO), + HORSE_HAN("馬", PieceType.HORSE, Camp.HAN), + CANNON_CHO("包", PieceType.CANNON, Camp.CHO), + CANNON_HAN("包", PieceType.CANNON, Camp.HAN), + GUARD_CHO("士", PieceType.GUARD, Camp.CHO), + GUARD_HAN("士", PieceType.GUARD, Camp.HAN), + ELEPHANT_CHO("象", PieceType.ELEPHANT, Camp.CHO), + ELEPHANT_HAN("象", PieceType.ELEPHANT, Camp.HAN), ; private final String symbol; + private final Piece piece; - PieceFormat(String symbol) { + PieceFormat(String symbol, PieceType pieceType, Camp camp) { this.symbol = symbol; + this.piece = new Piece(pieceType, camp); } public static PieceFormat from(Piece piece) { - return switch (piece.camp()) { - case CHO -> choOf(piece.pieceType()); - case HAN -> hanOf(piece.pieceType()); - }; - } - - private static PieceFormat choOf(PieceType pieceType) { - return switch (pieceType) { - case GENERAL -> GENERAL_CHO; - case CHARIOT -> CHARIOT_CHO; - case HORSE -> HORSE_CHO; - case CANNON -> CANNON_CHO; - case GUARD -> GUARD_CHO; - case ELEPHANT -> ELEPHANT_CHO; - case SOLDIER -> SOLDIER_CHO; - }; - } - - private static PieceFormat hanOf(PieceType pieceType) { - return switch (pieceType) { - case GENERAL -> GENERAL_HAN; - case CHARIOT -> CHARIOT_HAN; - case HORSE -> HORSE_HAN; - case CANNON -> CANNON_HAN; - case GUARD -> GUARD_HAN; - case ELEPHANT -> ELEPHANT_HAN; - case SOLDIER -> SOLDIER_HAN; - }; + return Arrays.stream(values()) + .filter(pieceFormat -> pieceFormat.piece.equals(piece)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 기물입니다.")); } public String symbol() { From 9ff0ad0971a2e7b5a8feb010a089854979886f6d Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 17:19:31 +0900 Subject: [PATCH 22/36] =?UTF-8?q?refactor:=20Camp=EC=9D=98=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=EC=B0=A8=EB=A6=BC=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/initializer/ElephantSetUp.java | 9 ++++++++- src/main/java/janggi/domain/piece/Camp.java | 12 ------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java b/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java index 127ada10b..d8342bf77 100644 --- a/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java +++ b/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java @@ -24,7 +24,7 @@ public enum ElephantSetUp { } public Map settingUp(Camp camp) { - List settingColumns = camp.convertElephantColumns(SETTING_COLUMNS); + List settingColumns = settingColumnsOf(camp); Map map = new HashMap<>(); for (int i = 0; i < settingColumns.size(); i++) { @@ -36,4 +36,11 @@ public Map settingUp(Camp camp) { return map; } + + private List settingColumnsOf(Camp camp) { + if (camp == Camp.CHO) { + return SETTING_COLUMNS.reversed(); + } + return SETTING_COLUMNS; + } } diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index c1cd1c0b4..20641d9bb 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -7,22 +7,12 @@ public enum Camp { HAN(-1, 9, 1.5) { - @Override - public List convertElephantColumns(List columns) { - return columns; - } - @Override public Camp next() { return CHO; } }, CHO(1, 0, 0) { - @Override - public List convertElephantColumns(List columns) { - return columns.reversed(); - } - @Override public Camp next() { return HAN; @@ -41,8 +31,6 @@ public Camp next() { this.bonusScoreForSecondPlayer = bonusScoreForSecondPlayer; } - public abstract List convertElephantColumns(List columns); - public abstract Camp next(); public void validateForwardDirection(int rowDirection) { From 7302c9286e0f27e28b3a960bdf8e524c50a23812 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 17:29:34 +0900 Subject: [PATCH 23/36] =?UTF-8?q?refactor:=20Parser=EC=9D=98=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/view/InputView.java | 10 ++++------ src/main/java/janggi/{util => view}/Parser.java | 2 +- src/test/java/janggi/{util => view}/ParserTest.java | 10 +++++++++- 3 files changed, 14 insertions(+), 8 deletions(-) rename src/main/java/janggi/{util => view}/Parser.java (97%) rename src/test/java/janggi/{util => view}/ParserTest.java (64%) diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index b68358deb..3fc10cc55 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -2,7 +2,6 @@ import janggi.domain.Position; import janggi.domain.board.initializer.ElephantSetUp; -import janggi.util.Parser; import janggi.view.dto.CampDto; import janggi.view.format.ElephantSetUpFormat; import java.util.List; @@ -32,11 +31,10 @@ public InputView(Scanner scanner) { public ElephantSetUp readElephantSetting(CampDto campDto) { String campName = campDto.color() + campDto.name() + RESET; - System.out.println(String.format( - ELEPHANT_SETTING, + System.out.printf( + (ELEPHANT_SETTING) + "%n", campName, - ElephantSetUpFormat.outputMessage()) - ); + ElephantSetUpFormat.outputMessage()); return ElephantSetUpFormat.from(readLine()).toElephantSetting(); } @@ -54,7 +52,7 @@ private void validateInput(String input) { public Position readSource(CampDto campDto) { String campName = campDto.color() + campDto.name() + RESET; - System.out.println(String.format(TURN, campName)); + System.out.printf((TURN) + "%n", campName); System.out.println(SOURCE); return toPosition(Parser.parseByDelimiter(DELIMITER, readLine())); } diff --git a/src/main/java/janggi/util/Parser.java b/src/main/java/janggi/view/Parser.java similarity index 97% rename from src/main/java/janggi/util/Parser.java rename to src/main/java/janggi/view/Parser.java index 21a0d569c..07195da6a 100644 --- a/src/main/java/janggi/util/Parser.java +++ b/src/main/java/janggi/view/Parser.java @@ -1,4 +1,4 @@ -package janggi.util; +package janggi.view; import java.util.Arrays; import java.util.List; diff --git a/src/test/java/janggi/util/ParserTest.java b/src/test/java/janggi/view/ParserTest.java similarity index 64% rename from src/test/java/janggi/util/ParserTest.java rename to src/test/java/janggi/view/ParserTest.java index 3a5769c30..230e59ee7 100644 --- a/src/test/java/janggi/util/ParserTest.java +++ b/src/test/java/janggi/view/ParserTest.java @@ -1,5 +1,6 @@ -package janggi.util; +package janggi.view; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import org.assertj.core.api.SoftAssertions; @@ -21,4 +22,11 @@ class ParserTest { assertSoftly.assertThat(parsedExpression.getLast()).isEqualTo(20); }); } + + @Test + void 숫자가_아닌_문자가_포함되면_예외가_발생한다() { + assertThatThrownBy(() -> Parser.parseByDelimiter(",", "10,a")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 숫자만 입력 가능합니다"); + } } From 87225ef0f6fe5ce644ad9edb330312ca73b6010b Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 17:36:29 +0900 Subject: [PATCH 24/36] =?UTF-8?q?refactor:=20int=ED=98=95=20double?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/piece/PieceType.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index b7f9360f8..a2c4c4686 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -15,19 +15,19 @@ public enum PieceType { - GENERAL(new PalaceStrategy(), new EmptyCondition(), 0), - CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition(), 13), - CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition(), 7), - HORSE(new HorseStrategy(), new EmptyCondition(), 5), - ELEPHANT(new ElephantStrategy(), new EmptyCondition(), 3), - GUARD(new PalaceStrategy(), new EmptyCondition(), 3), - SOLDIER(new SoldierStrategy(), new EmptyCondition(), 2); + GENERAL(new PalaceStrategy(), new EmptyCondition(), 0.0), + CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition(), 13.0), + CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition(), 7.0), + HORSE(new HorseStrategy(), new EmptyCondition(), 5.0), + ELEPHANT(new ElephantStrategy(), new EmptyCondition(), 3.0), + GUARD(new PalaceStrategy(), new EmptyCondition(), 3.0), + SOLDIER(new SoldierStrategy(), new EmptyCondition(), 2.0); private final MoveStrategy moveStrategy; private final MoveCondition moveCondition; private final double score; - PieceType(MoveStrategy moveStrategy, MoveCondition moveCondition, int score) { + PieceType(MoveStrategy moveStrategy, MoveCondition moveCondition, double score) { this.moveStrategy = moveStrategy; this.moveCondition = moveCondition; this.score = score; From aadbbd1de6d5054fe84e370534cc0714e9a1fcba Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 1 Apr 2026 19:13:26 +0900 Subject: [PATCH 25/36] =?UTF-8?q?refactor:=20=EB=A7=A4=EC=A7=81=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/piece/Camp.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index 20641d9bb..4fe008842 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -21,6 +21,12 @@ public Camp next() { private static final String INVALID_BACKWARD_MOVEMENT = "[ERROR] 해당 기물은 후진할 수 없습니다."; private static final String INVALID_PALACE_MOVEMENT = "[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."; + private static final int PALACE_CENTER_ROW_DISTANCE = 1; + private static final int FRIENDLY_PALACE_ROW_RANGE = 2; + private static final int PALACE_START_COLUMN = 3; + private static final int PALACE_CENTER_COLUMN = 4; + private static final int PALACE_END_COLUMN = 5; + private final int forwardDirection; private final int startRowPosition; private final double bonusScoreForSecondPlayer; @@ -55,7 +61,7 @@ public void validateFriendlyPalace(Position position) { private boolean isFriendlyPalaceRow(Position position) { int absRowDifference = Math.abs(position.row() - startRowPosition); - return absRowDifference <= 2; + return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; } public static boolean isPalace(Position position) { @@ -66,19 +72,21 @@ private static boolean isPalaceRow(Position position) { return Arrays.stream(values()) .anyMatch(camp -> { int absRowDifference = Math.abs(position.row() - camp.startRowPosition); - return absRowDifference <= 2; + return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; }); } private static boolean isPalaceColumn(Position position) { - return position.column() >= 3 && position.column() <= 5; + return position.column() >= PALACE_START_COLUMN + && position.column() <= PALACE_END_COLUMN; } public static boolean isPalaceCenter(Position position) { return Arrays.stream(values()) .anyMatch(camp -> { int absRowDifference = Math.abs(position.row() - camp.startRowPosition); - return position.column() == 4 && absRowDifference == 1; + return position.column() == PALACE_CENTER_COLUMN + && absRowDifference == PALACE_CENTER_ROW_DISTANCE; }); } From ad1a8c1c9bcd687aa7bba2c515212d90d4e43bac Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Thu, 2 Apr 2026 13:29:50 +0900 Subject: [PATCH 26/36] =?UTF-8?q?refactor:=20=EA=B6=81=EC=84=B1=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Camp에서 궁성 판단 책임을 분리했습니다 --- src/main/java/janggi/domain/board/Palace.java | 55 ++++++++++++ src/main/java/janggi/domain/piece/Camp.java | 45 ---------- .../strategy/MultiStepStraightStrategy.java | 5 +- .../domain/piece/strategy/PalaceStrategy.java | 3 +- .../strategy/SingleStepStraightStrategy.java | 5 +- .../java/janggi/domain/board/PalaceTest.java | 87 +++++++++++++++++++ 6 files changed, 150 insertions(+), 50 deletions(-) create mode 100644 src/main/java/janggi/domain/board/Palace.java create mode 100644 src/test/java/janggi/domain/board/PalaceTest.java diff --git a/src/main/java/janggi/domain/board/Palace.java b/src/main/java/janggi/domain/board/Palace.java new file mode 100644 index 000000000..05faffe4f --- /dev/null +++ b/src/main/java/janggi/domain/board/Palace.java @@ -0,0 +1,55 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import java.util.Arrays; + +public final class Palace { + + private static final String INVALID_PALACE_MOVEMENT = "[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."; + private static final int PALACE_CENTER_ROW_DISTANCE = 1; + private static final int FRIENDLY_PALACE_ROW_RANGE = 2; + private static final int PALACE_START_COLUMN = 3; + private static final int PALACE_CENTER_COLUMN = 4; + private static final int PALACE_END_COLUMN = 5; + + private Palace() { + } + + public static void validateFriendlyPalace(Camp camp, Position position) { + if (!(isFriendlyPalaceRow(camp, position) && isPalaceColumn(position))) { + throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); + } + } + + private static boolean isFriendlyPalaceRow(Camp camp, Position position) { + int absRowDifference = Math.abs(position.row() - camp.getStartRowPosition()); + return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; + } + + public static boolean isPalace(Position position) { + return isPalaceRow(position) && isPalaceColumn(position); + } + + private static boolean isPalaceRow(Position position) { + return Arrays.stream(Camp.values()) + .anyMatch(camp -> { + int absRowDifference = Math.abs(position.row() - camp.getStartRowPosition()); + return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; + }); + } + + private static boolean isPalaceColumn(Position position) { + return position.column() >= PALACE_START_COLUMN + && position.column() <= PALACE_END_COLUMN; + } + + public static boolean isPalaceCenter(Position position) { + return Arrays.stream(Camp.values()) + .anyMatch(camp -> { + int absRowDifference = Math.abs(position.row() - camp.getStartRowPosition()); + return position.column() == PALACE_CENTER_COLUMN + && absRowDifference == PALACE_CENTER_ROW_DISTANCE; + }); + } +} diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index 4fe008842..4927ea0c2 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -1,6 +1,5 @@ package janggi.domain.piece; -import janggi.domain.Position; import java.util.Arrays; import java.util.List; @@ -19,13 +18,6 @@ public Camp next() { } }; private static final String INVALID_BACKWARD_MOVEMENT = "[ERROR] 해당 기물은 후진할 수 없습니다."; - private static final String INVALID_PALACE_MOVEMENT = "[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."; - - private static final int PALACE_CENTER_ROW_DISTANCE = 1; - private static final int FRIENDLY_PALACE_ROW_RANGE = 2; - private static final int PALACE_START_COLUMN = 3; - private static final int PALACE_CENTER_COLUMN = 4; - private static final int PALACE_END_COLUMN = 5; private final int forwardDirection; private final int startRowPosition; @@ -53,43 +45,6 @@ public double getBonusScoreForSecondPlayer() { return bonusScoreForSecondPlayer; } - public void validateFriendlyPalace(Position position) { - if (!(isFriendlyPalaceRow(position) && isPalaceColumn(position))) { - throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); - } - } - - private boolean isFriendlyPalaceRow(Position position) { - int absRowDifference = Math.abs(position.row() - startRowPosition); - return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; - } - - public static boolean isPalace(Position position) { - return isPalaceRow(position) && isPalaceColumn(position); - } - - private static boolean isPalaceRow(Position position) { - return Arrays.stream(values()) - .anyMatch(camp -> { - int absRowDifference = Math.abs(position.row() - camp.startRowPosition); - return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; - }); - } - - private static boolean isPalaceColumn(Position position) { - return position.column() >= PALACE_START_COLUMN - && position.column() <= PALACE_END_COLUMN; - } - - public static boolean isPalaceCenter(Position position) { - return Arrays.stream(values()) - .anyMatch(camp -> { - int absRowDifference = Math.abs(position.row() - camp.startRowPosition); - return position.column() == PALACE_CENTER_COLUMN - && absRowDifference == PALACE_CENTER_ROW_DISTANCE; - }); - } - public static List getAllCamp() { return Arrays.stream(values()) .toList(); diff --git a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java index 25019a088..51417082f 100644 --- a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java @@ -1,6 +1,7 @@ package janggi.domain.piece.strategy; import janggi.domain.Position; +import janggi.domain.board.Palace; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; @@ -26,7 +27,7 @@ public List findPath(Position source, Position destination, Camp camp) } private boolean isPalace(Position source, Position destination) { - return Camp.isPalace(source) && Camp.isPalace(destination); + return Palace.isPalace(source) && Palace.isPalace(destination); } private boolean isDiagonalStep(DirectionInformation directionInformation) { @@ -59,7 +60,7 @@ private List createDiagonalPath(Position source, DirectionInformation private void validatePalaceDiagonalPath(Position source, List path) { boolean passesPalaceCenter = - Camp.isPalaceCenter(source) || path.stream().anyMatch(Camp::isPalaceCenter); + Palace.isPalaceCenter(source) || path.stream().anyMatch(Palace::isPalaceCenter); if (!passesPalaceCenter) { throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); diff --git a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java index 65e783c84..c08669e0f 100644 --- a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java @@ -1,6 +1,7 @@ package janggi.domain.piece.strategy; import janggi.domain.Position; +import janggi.domain.board.Palace; import janggi.domain.piece.Camp; import java.util.List; @@ -8,7 +9,7 @@ public class PalaceStrategy extends SingleStepStraightStrategy { @Override public List findPath(Position source, Position destination, Camp camp) { - camp.validateFriendlyPalace(destination); + Palace.validateFriendlyPalace(camp, destination); return super.findPath(source, destination, camp); } diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index 310a065b4..648b26e94 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -1,6 +1,7 @@ package janggi.domain.piece.strategy; import janggi.domain.Position; +import janggi.domain.board.Palace; import janggi.domain.piece.Camp; import java.util.List; @@ -32,7 +33,7 @@ public List findPath(Position source, Position destination, Camp camp) } private boolean isPalace(Position source, Position destination) { - return Camp.isPalace(source) && Camp.isPalace(destination); + return Palace.isPalace(source) && Palace.isPalace(destination); } private void validatePalaceSingleStepMovement(Position source, Position destination, @@ -53,7 +54,7 @@ private boolean isSingleDiagonalStep(DirectionInformation directionInformation) } private void validateDiagonalMove(Position source, Position destination) { - if (!Camp.isPalaceCenter(source) && !Camp.isPalaceCenter(destination)) { + if (!Palace.isPalaceCenter(source) && !Palace.isPalaceCenter(destination)) { throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); } } diff --git a/src/test/java/janggi/domain/board/PalaceTest.java b/src/test/java/janggi/domain/board/PalaceTest.java new file mode 100644 index 000000000..226f0217b --- /dev/null +++ b/src/test/java/janggi/domain/board/PalaceTest.java @@ -0,0 +1,87 @@ +package janggi.domain.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PalaceTest { + + private static Stream friendlyPalacePositions() { + return Stream.of( + Arguments.of(Camp.CHO, new Position(0, 3)), + Arguments.of(Camp.CHO, new Position(1, 4)), + Arguments.of(Camp.HAN, new Position(8, 4)), + Arguments.of(Camp.HAN, new Position(9, 5)) + ); + } + + @ParameterizedTest + @MethodSource("friendlyPalacePositions") + void 아군_궁성_내부를_판별한다(Camp camp, Position position) { + assertDoesNotThrow(() -> + Palace.validateFriendlyPalace(camp, position) + ); + } + + private static Stream palacePositions() { + return Stream.of( + Arguments.of(new Position(0, 3), true), + Arguments.of(new Position(1, 4), true), + Arguments.of(new Position(2, 5), true), + Arguments.of(new Position(7, 3), true), + Arguments.of(new Position(8, 4), true), + Arguments.of(new Position(9, 5), true), + Arguments.of(new Position(0, 2), false), + Arguments.of(new Position(3, 3), false), + Arguments.of(new Position(6, 4), false), + Arguments.of(new Position(9, 6), false) + ); + } + + @ParameterizedTest + @MethodSource("palacePositions") + void 궁성을_판별한다(Position position, boolean expected) { + assertThat(Palace.isPalace(position)).isEqualTo(expected); + } + + private static Stream palaceCenterPositions() { + return Stream.of( + Arguments.of(new Position(1, 4), true), + Arguments.of(new Position(8, 4), true), + Arguments.of(new Position(0, 4), false), + Arguments.of(new Position(1, 3), false), + Arguments.of(new Position(9, 4), false), + Arguments.of(new Position(8, 5), false) + ); + } + + @ParameterizedTest + @MethodSource("palaceCenterPositions") + void 궁성_중앙을_판별한다(Position position, boolean expected) { + assertThat(Palace.isPalaceCenter(position)).isEqualTo(expected); + } + + private static Stream invalidFriendlyPalacePositions() { + return Stream.of( + Arguments.of(Camp.CHO, new Position(8, 4)), + Arguments.of(Camp.CHO, new Position(3, 3)), + Arguments.of(Camp.HAN, new Position(1, 4)), + Arguments.of(Camp.HAN, new Position(6, 4)) + ); + } + + @ParameterizedTest + @MethodSource("invalidFriendlyPalacePositions") + void 상대_궁성이나_궁성_밖은_예외가_발생한다(Camp camp, Position position) { + assertThatThrownBy(() -> Palace.validateFriendlyPalace(camp, position)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } +} From 77fa7be06b64048f43deb48cc50f8f4362b4494f Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 09:09:42 +0900 Subject: [PATCH 27/36] =?UTF-8?q?feat:=20DB=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B4=88=EA=B8=B0=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++ build.gradle | 1 + .../java/janggi/db/DatabaseInitializer.java | 43 +++++++++++++++++++ .../java/janggi/db/H2ConnectionManager.java | 25 +++++++++++ src/main/resources/schema.sql | 14 ++++++ 5 files changed, 87 insertions(+) create mode 100644 src/main/java/janggi/db/DatabaseInitializer.java create mode 100644 src/main/java/janggi/db/H2ConnectionManager.java create mode 100644 src/main/resources/schema.sql diff --git a/.gitignore b/.gitignore index 6c0187813..eee940060 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ HELP.md .gradle +.gradle-local/ build/ +data/ +*.mv.db +*.trace.db !gradle/wrapper/gradle-wrapper.jar !**/src/main/** !**/src/test/** diff --git a/build.gradle b/build.gradle index ce846f70c..672be9de7 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ repositories { } dependencies { + runtimeOnly('com.h2database:h2:2.4.240') testImplementation platform('org.junit:junit-bom:5.11.4') testImplementation platform('org.assertj:assertj-bom:3.27.3') testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/src/main/java/janggi/db/DatabaseInitializer.java b/src/main/java/janggi/db/DatabaseInitializer.java new file mode 100644 index 000000000..f34f1292e --- /dev/null +++ b/src/main/java/janggi/db/DatabaseInitializer.java @@ -0,0 +1,43 @@ +package janggi.db; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public final class DatabaseInitializer { + + private static final String SCHEMA_PATH = "/schema.sql"; + private static final String SCHEMA_LOAD_FAILED = "[ERROR] DB 스키마를 읽을 수 없습니다."; + private static final String SCHEMA_INIT_FAILED = "[ERROR] DB 스키마를 초기화할 수 없습니다."; + + public void initialize() { + try (Connection connection = H2ConnectionManager.getConnection()) { + String schema = loadSchema(); + for (String statement : schema.split(";")) { + String sql = statement.trim(); + if (sql.isEmpty()) { + continue; + } + try (Statement jdbcStatement = connection.createStatement()) { + jdbcStatement.execute(sql); + } + } + } catch (SQLException e) { + throw new IllegalStateException(SCHEMA_INIT_FAILED, e); + } + } + + private String loadSchema() { + try (InputStream inputStream = DatabaseInitializer.class.getResourceAsStream(SCHEMA_PATH)) { + if (inputStream == null) { + throw new IllegalStateException(SCHEMA_LOAD_FAILED); + } + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException(SCHEMA_LOAD_FAILED, e); + } + } +} diff --git a/src/main/java/janggi/db/H2ConnectionManager.java b/src/main/java/janggi/db/H2ConnectionManager.java new file mode 100644 index 000000000..f22bea62c --- /dev/null +++ b/src/main/java/janggi/db/H2ConnectionManager.java @@ -0,0 +1,25 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public final class H2ConnectionManager { + + private static final String URL = "jdbc:h2:file:./data/janggi"; + private static final String USER = "stark"; + private static final String PASSWORD = "stark123!"; + + private static final String UNABLE_TO_ACCESS_DATABASE = "[ERROR] H2 데이터베이스에 연결할 수 없습니다."; + + private H2ConnectionManager() { + } + + public static Connection getConnection() { + try { + return DriverManager.getConnection(URL, USER, PASSWORD); + } catch (SQLException e) { + throw new IllegalStateException(UNABLE_TO_ACCESS_DATABASE); + } + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..e88a0e48d --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,14 @@ +create table if not exists game_state ( + game_id bigint primary key, + current_turn varchar(20) not null +); + +create table if not exists game_piece ( + game_id bigint not null, + row_position int not null, + column_position int not null, + piece_type varchar(30) not null, + camp varchar(20) not null, + primary key (game_id, row_position, column_position), + foreign key (game_id) references game_state(game_id) +); From ef41c47e071ccaac7217e7806d5228e7779c5adc Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 09:13:26 +0900 Subject: [PATCH 28/36] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Turn 객체 삭제 후 Game 객체의 내부 필드로 Camp 자체를 추가했습니다. - Game 객체를 추가해서 장기판과 현재 플레이턴을 내부 상태로 가지고 있게했습니다. --- src/main/java/janggi/JanggiApplication.java | 12 ++- src/main/java/janggi/JanggiGame.java | 55 ++++++++---- src/main/java/janggi/domain/Game.java | 55 ++++++++++++ src/main/java/janggi/domain/Turn.java | 15 ---- .../initializer/SnapshotBoardInitializer.java | 20 +++++ .../repository/GamePieceRepository.java | 89 +++++++++++++++++++ .../repository/GameStateRepository.java | 76 ++++++++++++++++ src/main/java/janggi/service/GameService.java | 81 +++++++++++++++++ src/test/java/janggi/domain/TurnTest.java | 37 -------- 9 files changed, 368 insertions(+), 72 deletions(-) create mode 100644 src/main/java/janggi/domain/Game.java delete mode 100644 src/main/java/janggi/domain/Turn.java create mode 100644 src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java create mode 100644 src/main/java/janggi/repository/GamePieceRepository.java create mode 100644 src/main/java/janggi/repository/GameStateRepository.java create mode 100644 src/main/java/janggi/service/GameService.java delete mode 100644 src/test/java/janggi/domain/TurnTest.java diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java index 66824bce2..2bbedd513 100644 --- a/src/main/java/janggi/JanggiApplication.java +++ b/src/main/java/janggi/JanggiApplication.java @@ -1,12 +1,22 @@ package janggi; +import janggi.db.DatabaseInitializer; +import janggi.repository.GamePieceRepository; +import janggi.repository.GameStateRepository; +import janggi.service.GameService; import janggi.view.InputView; import janggi.view.OutputView; import java.util.Scanner; public class JanggiApplication { public static void main(String[] args) { - JanggiGame janggi = new JanggiGame(new InputView(new Scanner(System.in)), new OutputView()); + new DatabaseInitializer().initialize(); + + JanggiGame janggi = new JanggiGame( + new InputView(new Scanner(System.in)), + new OutputView(), + new GameService(new GameStateRepository(), new GamePieceRepository()) + ); janggi.run(); } } diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java index 01d2e0ab6..453c5f955 100644 --- a/src/main/java/janggi/JanggiGame.java +++ b/src/main/java/janggi/JanggiGame.java @@ -1,12 +1,13 @@ package janggi; -import janggi.domain.Position; -import janggi.domain.Turn; +import janggi.domain.Game; import janggi.domain.board.Board; +import janggi.domain.board.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.board.initializer.ElephantSetUp; import janggi.domain.board.initializer.StandardBoardInitializer; import janggi.domain.piece.Camp; +import janggi.service.GameService; import janggi.view.InputView; import janggi.view.OutputView; import janggi.view.dto.CampDto; @@ -16,18 +17,34 @@ public class JanggiGame { + private static final long SINGLE_GAME_ID = 1L; + private final InputView inputView; private final OutputView outputView; + private final GameService gameService; - public JanggiGame(InputView inputView, OutputView outputView) { + public JanggiGame(InputView inputView, OutputView outputView, GameService gameService) { this.inputView = inputView; this.outputView = outputView; + this.gameService = gameService; } public void run() { + Game game = loadOrCreateGame(); + outputView.printBoard(game.boardSnapshot()); + play(game); + } + + private Game loadOrCreateGame() { + return gameService.findById(SINGLE_GAME_ID) + .orElseGet(this::createNewGame); + } + + private Game createNewGame() { Board board = createBoard(); - outputView.printBoard(board.getBoard()); - play(board); + Game game = Game.start(SINGLE_GAME_ID, board); + gameService.save(game); + return game; } private Board createBoard() { @@ -46,34 +63,34 @@ private void readElephantSetUp(Map elephantSetUps, Camp cam elephantSetUps.put(camp, elephantSetUp); } - private void play(Board board) { - Turn turn = new Turn(); + private void play(Game game) { boolean continueGame = true; while (continueGame) { - outputView.printScore(board.calculateScore()); - continueGame = retryOnInvalidInput(() -> playTurn(board, turn)); - outputView.printBoard(board.getBoard()); + outputView.printScore(game.calculateScore()); + continueGame = retryOnInvalidInput(() -> playTurn(game)); + outputView.printBoard(game.boardSnapshot()); } - outputView.printWinner(turn.currentTurn()); + outputView.printWinner(game.currentTurn()); } - private boolean playTurn(Board board, Turn turn) { - Camp camp = turn.currentTurn(); + private boolean playTurn(Game game) { + Camp currentTurn = game.currentTurn(); - Position source = retryOnInvalidInput(() -> readSource(board, camp)); + Position source = retryOnInvalidInput(() -> readSource(game, currentTurn)); Position destination = retryOnInvalidInput(inputView::readDestination); - boolean gameEnded = board.movePiece(source, destination, camp); + boolean gameEnded = game.play(source, destination); if (gameEnded) { + gameService.deleteById(game.id()); return false; } - turn.finishTurn(); + gameService.save(game); return true; } - private Position readSource(Board board, Camp camp) { - Position source = inputView.readSource(CampDto.from(camp)); - board.validateCampTurn(source, camp); + private Position readSource(Game game, Camp currentTurn) { + Position source = inputView.readSource(CampDto.from(currentTurn)); + game.validateSourceForCurrentTurn(source); return source; } diff --git a/src/main/java/janggi/domain/Game.java b/src/main/java/janggi/domain/Game.java new file mode 100644 index 000000000..d6993a6ea --- /dev/null +++ b/src/main/java/janggi/domain/Game.java @@ -0,0 +1,55 @@ +package janggi.domain; + +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import java.util.Map; + +public final class Game { + private final long id; + private final Board board; + private Camp currentTurn; + + private Game(long id, Board board, Camp currentTurn) { + this.id = id; + this.board = board; + this.currentTurn = currentTurn; + } + + public long id() { + return id; + } + + public static Game start(long id, Board board) { + return new Game(id, board, Camp.CHO); + } + + public static Game restore(long id, Board board, Camp currentTurn) { + return new Game(id, board, currentTurn); + } + + public Camp currentTurn() { + return currentTurn; + } + + public Map boardSnapshot() { + return board.getBoard(); + } + + public Map calculateScore() { + return board.calculateScore(); + } + + public void validateSourceForCurrentTurn(Position source) { + board.validateCampTurn(source, currentTurn()); + } + + public boolean play(Position source, Position destination) { + boolean gameEnded = board.movePiece(source, destination, currentTurn()); + if (!gameEnded) { + currentTurn = currentTurn.next(); + } + return gameEnded; + } +} diff --git a/src/main/java/janggi/domain/Turn.java b/src/main/java/janggi/domain/Turn.java deleted file mode 100644 index 2b6c85c1a..000000000 --- a/src/main/java/janggi/domain/Turn.java +++ /dev/null @@ -1,15 +0,0 @@ -package janggi.domain; - -import janggi.domain.piece.Camp; - -public class Turn { - private Camp currentCamp = Camp.CHO; - - public Camp currentTurn() { - return currentCamp; - } - - public void finishTurn() { - currentCamp = currentCamp.next(); - } -} diff --git a/src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java b/src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java new file mode 100644 index 000000000..0ef0e8da4 --- /dev/null +++ b/src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java @@ -0,0 +1,20 @@ +package janggi.domain.board.initializer; + +import janggi.domain.board.Position; +import janggi.domain.piece.Piece; +import java.util.HashMap; +import java.util.Map; + +public final class SnapshotBoardInitializer implements BoardInitializer { + + private final Map snapshot; + + public SnapshotBoardInitializer(Map snapshot) { + this.snapshot = Map.copyOf(snapshot); + } + + @Override + public Map initialize() { + return new HashMap<>(snapshot); + } +} diff --git a/src/main/java/janggi/repository/GamePieceRepository.java b/src/main/java/janggi/repository/GamePieceRepository.java new file mode 100644 index 000000000..a1d8e8e9a --- /dev/null +++ b/src/main/java/janggi/repository/GamePieceRepository.java @@ -0,0 +1,89 @@ +package janggi.repository; + +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +public final class GamePieceRepository { + + private static final String SELECT_GAME_PIECES = """ + select row_position, column_position, piece_type, camp + from game_piece + where game_id = ? + """; + private static final String DELETE_GAME_PIECES = """ + delete from game_piece + where game_id = ? + """; + private static final String INSERT_GAME_PIECE = """ + insert into game_piece (game_id, row_position, column_position, piece_type, camp) + values (?, ?, ?, ?, ?) + """; + + public Map findByGameId(Connection connection, long gameId) throws SQLException { + Map boardSnapshot = new HashMap<>(); + PreparedStatement statement = connection.prepareStatement(SELECT_GAME_PIECES); + statement.setLong(1, gameId); + + try (statement; ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + boardSnapshot.put(toPosition(resultSet), toPiece(resultSet)); + } + } + return boardSnapshot; + } + + public void replaceByGameId(Connection connection, long gameId, Map boardSnapshot) + throws SQLException { + deleteByGameId(connection, gameId); + + if (boardSnapshot.isEmpty()) { + return; + } + + try (PreparedStatement statement = connection.prepareStatement(INSERT_GAME_PIECE)) { + for (Map.Entry entry : boardSnapshot.entrySet()) { + setPieceStatement(statement, gameId, entry.getKey(), entry.getValue()); + } + statement.executeBatch(); + } + } + + public void deleteByGameId(Connection connection, long gameId) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(DELETE_GAME_PIECES)) { + statement.setLong(1, gameId); + statement.executeUpdate(); + } + } + + private Position toPosition(ResultSet resultSet) throws SQLException { + return new Position( + resultSet.getInt("row_position"), + resultSet.getInt("column_position") + ); + } + + private Piece toPiece(ResultSet resultSet) throws SQLException { + return new Piece( + PieceType.valueOf(resultSet.getString("piece_type")), + Camp.valueOf(resultSet.getString("camp")) + ); + } + + private void setPieceStatement(PreparedStatement statement, long gameId, Position position, Piece piece) + throws SQLException { + statement.setLong(1, gameId); + statement.setInt(2, position.row()); + statement.setInt(3, position.column()); + statement.setString(4, piece.pieceType().name()); + statement.setString(5, piece.camp().name()); + statement.addBatch(); + } +} diff --git a/src/main/java/janggi/repository/GameStateRepository.java b/src/main/java/janggi/repository/GameStateRepository.java new file mode 100644 index 000000000..ebade15c2 --- /dev/null +++ b/src/main/java/janggi/repository/GameStateRepository.java @@ -0,0 +1,76 @@ +package janggi.repository; + +import janggi.domain.piece.Camp; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +public final class GameStateRepository { + + private static final String SELECT_GAME_STATE = """ + select current_turn + from game_state + where game_id = ? + """; + private static final String UPDATE_GAME_STATE = """ + update game_state + set current_turn = ? + where game_id = ? + """; + private static final String INSERT_GAME_STATE = """ + insert into game_state (game_id, current_turn) + values (?, ?) + """; + private static final String DELETE_GAME_STATE = """ + delete from game_state + where game_id = ? + """; + + public Optional findById(Connection connection, long gameId) throws SQLException { + PreparedStatement statement = connection.prepareStatement(SELECT_GAME_STATE); + statement.setLong(1, gameId); + + try (statement; ResultSet resultSet = statement.executeQuery()) { + if (!resultSet.next()) { + return Optional.empty(); + } + return Optional.of(Camp.valueOf(resultSet.getString("current_turn"))); + } + } + + public void save(Connection connection, long gameId, Camp currentTurn) throws SQLException { + if (update(connection, gameId, currentTurn) > 0) { + return; + } + insert(connection, gameId, currentTurn); + } + + public void deleteById(Connection connection, long gameId) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(DELETE_GAME_STATE)) { + statement.setLong(1, gameId); + statement.executeUpdate(); + } + } + + private int update(Connection connection, long gameId, Camp currentTurn) throws SQLException { + PreparedStatement statement = connection.prepareStatement(UPDATE_GAME_STATE); + statement.setString(1, currentTurn.name()); + statement.setLong(2, gameId); + + try (statement) { + return statement.executeUpdate(); + } + } + + private void insert(Connection connection, long gameId, Camp currentTurn) throws SQLException { + PreparedStatement statement = connection.prepareStatement(INSERT_GAME_STATE); + statement.setLong(1, gameId); + statement.setString(2, currentTurn.name()); + + try (statement) { + statement.executeUpdate(); + } + } +} diff --git a/src/main/java/janggi/service/GameService.java b/src/main/java/janggi/service/GameService.java new file mode 100644 index 000000000..97c921cc6 --- /dev/null +++ b/src/main/java/janggi/service/GameService.java @@ -0,0 +1,81 @@ +package janggi.service; + +import janggi.db.H2ConnectionManager; +import janggi.domain.Game; +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.board.initializer.SnapshotBoardInitializer; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.repository.GamePieceRepository; +import janggi.repository.GameStateRepository; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; +import java.util.Optional; + +public final class GameService { + + private static final String GAME_ACCESS_FAILED = "[ERROR] 게임 상태를 DB에서 처리하는 중 문제가 발생했습니다."; + + private final GameStateRepository gameStateRepository; + private final GamePieceRepository gamePieceRepository; + + public GameService(GameStateRepository gameStateRepository, GamePieceRepository gamePieceRepository) { + this.gameStateRepository = gameStateRepository; + this.gamePieceRepository = gamePieceRepository; + } + + public Optional findById(long gameId) { + try (Connection connection = H2ConnectionManager.getConnection()) { + Optional currentTurn = gameStateRepository.findById(connection, gameId); + if (currentTurn.isEmpty()) { + return Optional.empty(); + } + + Map boardSnapshot = gamePieceRepository.findByGameId(connection, gameId); + Board board = new Board(new SnapshotBoardInitializer(boardSnapshot)); + return Optional.of(Game.restore(gameId, board, currentTurn.orElseThrow())); + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } + + public void save(Game game) { + try (Connection connection = H2ConnectionManager.getConnection()) { + connection.setAutoCommit(false); + + try { + gameStateRepository.save(connection, game.id(), game.currentTurn()); + gamePieceRepository.replaceByGameId( + connection, + game.id(), + game.boardSnapshot() + ); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw e; + } + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } + + public void deleteById(long gameId) { + try (Connection connection = H2ConnectionManager.getConnection()) { + connection.setAutoCommit(false); + + try { + gamePieceRepository.deleteByGameId(connection, gameId); + gameStateRepository.deleteById(connection, gameId); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw e; + } + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } +} diff --git a/src/test/java/janggi/domain/TurnTest.java b/src/test/java/janggi/domain/TurnTest.java deleted file mode 100644 index 3032d0c6f..000000000 --- a/src/test/java/janggi/domain/TurnTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package janggi.domain; - -import janggi.domain.piece.Camp; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.Test; - -class TurnTest { - - @Test - void 첫번쨰_턴은_초나라가_시작한다() { - //given - Turn turn = new Turn(); - //when - //then - Assertions.assertThat(turn.currentTurn()).isEqualTo(Camp.CHO); - } - - @Test - void 자신의_턴_종료_후_다음_턴은_반드시_상대_진영이다() { - //given - Turn turn = new Turn(); - //when - Camp firstTurn = turn.currentTurn(); - turn.finishTurn(); - Camp secondTurn = turn.currentTurn(); - turn.finishTurn(); - Camp thirdTurn = turn.currentTurn(); - //then - SoftAssertions.assertSoftly(assertSoftly -> { - assertSoftly.assertThat(firstTurn).isEqualTo(Camp.CHO); - assertSoftly.assertThat(secondTurn).isEqualTo(Camp.HAN); - assertSoftly.assertThat(thirdTurn).isEqualTo(Camp.CHO); - }); - } - -} From 027c84817a87b087fb5d25e79dd0fa4cf140b31a Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 09:14:05 +0900 Subject: [PATCH 29/36] =?UTF-8?q?refactor:=20Position=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/board/Board.java | 1 - src/main/java/janggi/domain/board/BoardChecker.java | 1 - src/main/java/janggi/domain/board/Palace.java | 1 - src/main/java/janggi/domain/{ => board}/Position.java | 2 +- .../java/janggi/domain/board/initializer/BoardInitializer.java | 2 +- .../java/janggi/domain/board/initializer/ElephantSetUp.java | 2 +- .../janggi/domain/board/initializer/InitialPiecePlacement.java | 2 +- .../domain/board/initializer/StandardBoardInitializer.java | 2 +- src/main/java/janggi/domain/piece/Piece.java | 2 +- src/main/java/janggi/domain/piece/PieceType.java | 2 +- src/main/java/janggi/domain/piece/condition/EmptyCondition.java | 2 +- src/main/java/janggi/domain/piece/condition/MoveCondition.java | 2 +- .../janggi/domain/piece/condition/OnePieceExistsCondition.java | 2 +- .../java/janggi/domain/piece/strategy/DirectionInformation.java | 2 +- .../java/janggi/domain/piece/strategy/ElephantStrategy.java | 2 +- src/main/java/janggi/domain/piece/strategy/HorseStrategy.java | 2 +- src/main/java/janggi/domain/piece/strategy/MoveStrategy.java | 2 +- .../janggi/domain/piece/strategy/MultiStepStraightStrategy.java | 2 +- src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java | 2 +- .../domain/piece/strategy/SingleStepStraightStrategy.java | 2 +- src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java | 2 +- src/main/java/janggi/view/InputView.java | 2 +- src/main/java/janggi/view/OutputView.java | 2 +- src/main/java/janggi/view/dto/PiecePositionDto.java | 2 +- src/test/java/janggi/domain/board/BoardTest.java | 1 - .../janggi/domain/board/EmptyConditionTestBoardInitializer.java | 1 - src/test/java/janggi/domain/board/PalaceTest.java | 1 - src/test/java/janggi/domain/{ => board}/PositionTest.java | 2 +- .../java/janggi/domain/board/StandardBoardInitializerTest.java | 1 - src/test/java/janggi/domain/piece/PieceTest.java | 2 +- .../java/janggi/domain/piece/condition/EmptyConditionTest.java | 2 +- .../domain/piece/condition/OnePieceExistsConditionTest.java | 2 +- .../java/janggi/domain/piece/strategy/ElephantStrategyTest.java | 2 +- .../java/janggi/domain/piece/strategy/HorseStrategyTest.java | 2 +- .../domain/piece/strategy/MultiStepStraightStrategyTest.java | 2 +- .../java/janggi/domain/piece/strategy/PalaceStrategyTest.java | 2 +- .../java/janggi/domain/piece/strategy/SoldierStrategyTest.java | 2 +- 37 files changed, 30 insertions(+), 37 deletions(-) rename src/main/java/janggi/domain/{ => board}/Position.java (98%) rename src/test/java/janggi/domain/{ => board}/PositionTest.java (98%) diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 4542d6a24..337739dee 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; diff --git a/src/main/java/janggi/domain/board/BoardChecker.java b/src/main/java/janggi/domain/board/BoardChecker.java index d8ce969e5..b8631979f 100644 --- a/src/main/java/janggi/domain/board/BoardChecker.java +++ b/src/main/java/janggi/domain/board/BoardChecker.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; diff --git a/src/main/java/janggi/domain/board/Palace.java b/src/main/java/janggi/domain/board/Palace.java index 05faffe4f..c8cb39a29 100644 --- a/src/main/java/janggi/domain/board/Palace.java +++ b/src/main/java/janggi/domain/board/Palace.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.piece.Camp; import java.util.Arrays; diff --git a/src/main/java/janggi/domain/Position.java b/src/main/java/janggi/domain/board/Position.java similarity index 98% rename from src/main/java/janggi/domain/Position.java rename to src/main/java/janggi/domain/board/Position.java index 922245312..0cc94eb74 100644 --- a/src/main/java/janggi/domain/Position.java +++ b/src/main/java/janggi/domain/board/Position.java @@ -1,4 +1,4 @@ -package janggi.domain; +package janggi.domain.board; public record Position(int row, int column) { diff --git a/src/main/java/janggi/domain/board/initializer/BoardInitializer.java b/src/main/java/janggi/domain/board/initializer/BoardInitializer.java index 3cb0e323d..3d36b5896 100644 --- a/src/main/java/janggi/domain/board/initializer/BoardInitializer.java +++ b/src/main/java/janggi/domain/board/initializer/BoardInitializer.java @@ -1,6 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Piece; import java.util.Map; diff --git a/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java b/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java index d8342bf77..41321fa78 100644 --- a/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java +++ b/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java @@ -1,6 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; diff --git a/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java b/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java index a6fd9ec0b..7426f6c59 100644 --- a/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java +++ b/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java @@ -1,6 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; diff --git a/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java b/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java index 65059fea6..6aaa9ea68 100644 --- a/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java +++ b/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java @@ -1,6 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import java.util.HashMap; diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 3899ea6c5..bbbe5738a 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -1,7 +1,7 @@ package janggi.domain.piece; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import java.util.List; public record Piece(PieceType pieceType, Camp camp) { diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index a2c4c4686..ae4032667 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -1,7 +1,7 @@ package janggi.domain.piece; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.condition.EmptyCondition; import janggi.domain.piece.condition.MoveCondition; import janggi.domain.piece.condition.OnePieceExistsCondition; diff --git a/src/main/java/janggi/domain/piece/condition/EmptyCondition.java b/src/main/java/janggi/domain/piece/condition/EmptyCondition.java index a106335d9..0ab07e21b 100644 --- a/src/main/java/janggi/domain/piece/condition/EmptyCondition.java +++ b/src/main/java/janggi/domain/piece/condition/EmptyCondition.java @@ -1,7 +1,7 @@ package janggi.domain.piece.condition; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/condition/MoveCondition.java b/src/main/java/janggi/domain/piece/condition/MoveCondition.java index e73fc05e5..7992e0897 100644 --- a/src/main/java/janggi/domain/piece/condition/MoveCondition.java +++ b/src/main/java/janggi/domain/piece/condition/MoveCondition.java @@ -1,7 +1,7 @@ package janggi.domain.piece.condition; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java index 0545bd921..3176c6767 100644 --- a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java +++ b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java @@ -1,7 +1,7 @@ package janggi.domain.piece.condition; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java b/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java index d6678151f..9d65cd2fc 100644 --- a/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java +++ b/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; public record DirectionInformation(int rowDifference, int columnDifference) { diff --git a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java index 7c4d65d60..4bd934afd 100644 --- a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java index a6231667d..bdfb867f9 100644 --- a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java b/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java index 17e88a8b6..95d81443d 100644 --- a/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java index 51417082f..d9ac5fcf7 100644 --- a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java @@ -1,7 +1,7 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; import janggi.domain.board.Palace; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java index c08669e0f..c0a49b8ed 100644 --- a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java @@ -1,7 +1,7 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; import janggi.domain.board.Palace; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index 648b26e94..43b0bf8d8 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -1,7 +1,7 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; import janggi.domain.board.Palace; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java b/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java index f8f901eb7..6e3a414f4 100644 --- a/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index 3fc10cc55..4b915d0e5 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -1,6 +1,6 @@ package janggi.view; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.board.initializer.ElephantSetUp; import janggi.view.dto.CampDto; import janggi.view.format.ElephantSetUpFormat; diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index 3785f8b61..959da8025 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -2,7 +2,7 @@ import static java.util.stream.Collectors.joining; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.view.dto.CampDto; diff --git a/src/main/java/janggi/view/dto/PiecePositionDto.java b/src/main/java/janggi/view/dto/PiecePositionDto.java index 22c351d12..bd7a7a9e3 100644 --- a/src/main/java/janggi/view/dto/PiecePositionDto.java +++ b/src/main/java/janggi/view/dto/PiecePositionDto.java @@ -1,6 +1,6 @@ package janggi.view.dto; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Piece; import janggi.view.format.PieceFormat; diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index ebdddb3f6..3e413b9a7 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import janggi.domain.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; diff --git a/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java b/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java index ea0d4d8e8..b3b533770 100644 --- a/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java +++ b/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; diff --git a/src/test/java/janggi/domain/board/PalaceTest.java b/src/test/java/janggi/domain/board/PalaceTest.java index 226f0217b..703b8fecd 100644 --- a/src/test/java/janggi/domain/board/PalaceTest.java +++ b/src/test/java/janggi/domain/board/PalaceTest.java @@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import janggi.domain.Position; import janggi.domain.piece.Camp; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; diff --git a/src/test/java/janggi/domain/PositionTest.java b/src/test/java/janggi/domain/board/PositionTest.java similarity index 98% rename from src/test/java/janggi/domain/PositionTest.java rename to src/test/java/janggi/domain/board/PositionTest.java index 93099e5a6..cc536a47b 100644 --- a/src/test/java/janggi/domain/PositionTest.java +++ b/src/test/java/janggi/domain/board/PositionTest.java @@ -1,4 +1,4 @@ -package janggi.domain; +package janggi.domain.board; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java b/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java index 9317f12d6..42e35885d 100644 --- a/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java +++ b/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.board.initializer.ElephantSetUp; import janggi.domain.board.initializer.StandardBoardInitializer; diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index 09fef8332..5a06823e1 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -3,9 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import janggi.domain.Position; import janggi.domain.board.Board; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java b/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java index cb6dde24e..b4492a855 100644 --- a/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java +++ b/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java @@ -2,9 +2,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; import janggi.domain.board.Board; import janggi.domain.board.EmptyConditionTestBoardInitializer; +import janggi.domain.board.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; diff --git a/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java b/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java index 05f00266e..dc0bd9f15 100644 --- a/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java +++ b/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; import janggi.domain.board.Board; +import janggi.domain.board.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; diff --git a/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java index 26c8ccde8..02aea5115 100644 --- a/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java index 9cc299553..bd0c9fd0a 100644 --- a/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java index e9b5af42b..d2fd7f660 100644 --- a/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java index a88fa8b81..05c7341ab 100644 --- a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java index 58f955f19..1d73163be 100644 --- a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; From 6b91d0f5b477ed07ae2ae3800fcd376462c9fbb5 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 17:01:38 +0900 Subject: [PATCH 30/36] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EA=B4=80=EB=A6=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/JanggiApplication.java | 8 +- .../java/janggi/db/DatabaseInitializer.java | 8 +- .../java/janggi/db/H2ConnectionManager.java | 5 +- .../java/janggi/db/TransactionManager.java | 58 +++++++++++++++ .../repository/GamePieceRepository.java | 2 +- .../repository/GameStateRepository.java | 2 +- src/main/java/janggi/service/GameService.java | 73 +++++++++++-------- src/main/resources/schema.sql | 2 +- 8 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 src/main/java/janggi/db/TransactionManager.java diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java index 2bbedd513..881315fbc 100644 --- a/src/main/java/janggi/JanggiApplication.java +++ b/src/main/java/janggi/JanggiApplication.java @@ -1,6 +1,8 @@ package janggi; import janggi.db.DatabaseInitializer; +import janggi.db.H2ConnectionManager; +import janggi.db.TransactionManager; import janggi.repository.GamePieceRepository; import janggi.repository.GameStateRepository; import janggi.service.GameService; @@ -10,12 +12,14 @@ public class JanggiApplication { public static void main(String[] args) { - new DatabaseInitializer().initialize(); + H2ConnectionManager connectionManager = new H2ConnectionManager(); + new DatabaseInitializer(connectionManager).initialize(); + TransactionManager transactionManager = new TransactionManager(connectionManager); JanggiGame janggi = new JanggiGame( new InputView(new Scanner(System.in)), new OutputView(), - new GameService(new GameStateRepository(), new GamePieceRepository()) + new GameService(transactionManager, new GameStateRepository(), new GamePieceRepository()) ); janggi.run(); } diff --git a/src/main/java/janggi/db/DatabaseInitializer.java b/src/main/java/janggi/db/DatabaseInitializer.java index f34f1292e..1374b1770 100644 --- a/src/main/java/janggi/db/DatabaseInitializer.java +++ b/src/main/java/janggi/db/DatabaseInitializer.java @@ -13,8 +13,14 @@ public final class DatabaseInitializer { private static final String SCHEMA_LOAD_FAILED = "[ERROR] DB 스키마를 읽을 수 없습니다."; private static final String SCHEMA_INIT_FAILED = "[ERROR] DB 스키마를 초기화할 수 없습니다."; + private final H2ConnectionManager connectionManager; + + public DatabaseInitializer(H2ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + public void initialize() { - try (Connection connection = H2ConnectionManager.getConnection()) { + try (Connection connection = connectionManager.createConnection()) { String schema = loadSchema(); for (String statement : schema.split(";")) { String sql = statement.trim(); diff --git a/src/main/java/janggi/db/H2ConnectionManager.java b/src/main/java/janggi/db/H2ConnectionManager.java index f22bea62c..8833fb86b 100644 --- a/src/main/java/janggi/db/H2ConnectionManager.java +++ b/src/main/java/janggi/db/H2ConnectionManager.java @@ -12,10 +12,7 @@ public final class H2ConnectionManager { private static final String UNABLE_TO_ACCESS_DATABASE = "[ERROR] H2 데이터베이스에 연결할 수 없습니다."; - private H2ConnectionManager() { - } - - public static Connection getConnection() { + public Connection createConnection() { try { return DriverManager.getConnection(URL, USER, PASSWORD); } catch (SQLException e) { diff --git a/src/main/java/janggi/db/TransactionManager.java b/src/main/java/janggi/db/TransactionManager.java new file mode 100644 index 000000000..4573e6445 --- /dev/null +++ b/src/main/java/janggi/db/TransactionManager.java @@ -0,0 +1,58 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.SQLException; + +public final class TransactionManager { + + private final H2ConnectionManager connectionManager; + + public TransactionManager(H2ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + public T readOnly(SqlFunction action) throws SQLException { + try (Connection connection = connectionManager.createConnection()) { + return action.apply(connection); + } + } + + public T inTransaction(SqlFunction action) throws SQLException { + try (Connection connection = connectionManager.createConnection()) { + connection.setAutoCommit(false); + + try { + T result = action.apply(connection); + connection.commit(); + return result; + } catch (SQLException e) { + connection.rollback(); + throw e; + } + } + } + + public void inTransaction(SqlConsumer action) throws SQLException { + try (Connection connection = connectionManager.createConnection()) { + connection.setAutoCommit(false); + + try { + action.accept(connection); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw e; + } + } + } + + @FunctionalInterface + public interface SqlFunction { + T apply(Connection connection) throws SQLException; + } + + @FunctionalInterface + public interface SqlConsumer { + void accept(Connection connection) throws SQLException; + } +} diff --git a/src/main/java/janggi/repository/GamePieceRepository.java b/src/main/java/janggi/repository/GamePieceRepository.java index a1d8e8e9a..a96a8cea9 100644 --- a/src/main/java/janggi/repository/GamePieceRepository.java +++ b/src/main/java/janggi/repository/GamePieceRepository.java @@ -40,7 +40,7 @@ public Map findByGameId(Connection connection, long gameId) thr return boardSnapshot; } - public void replaceByGameId(Connection connection, long gameId, Map boardSnapshot) + public void saveGameByBoard(Connection connection, long gameId, Map boardSnapshot) throws SQLException { deleteByGameId(connection, gameId); diff --git a/src/main/java/janggi/repository/GameStateRepository.java b/src/main/java/janggi/repository/GameStateRepository.java index ebade15c2..fe6d934a9 100644 --- a/src/main/java/janggi/repository/GameStateRepository.java +++ b/src/main/java/janggi/repository/GameStateRepository.java @@ -9,7 +9,7 @@ public final class GameStateRepository { - private static final String SELECT_GAME_STATE = """ + private static final String SELECT_CURRENT_TURN = """ select current_turn from game_state where game_id = ? diff --git a/src/main/java/janggi/service/GameService.java b/src/main/java/janggi/service/GameService.java index 97c921cc6..21d21d81d 100644 --- a/src/main/java/janggi/service/GameService.java +++ b/src/main/java/janggi/service/GameService.java @@ -1,6 +1,8 @@ package janggi.service; -import janggi.db.H2ConnectionManager; +import janggi.db.TransactionManager; +import janggi.db.TransactionManager.SqlConsumer; +import janggi.db.TransactionManager.SqlFunction; import janggi.domain.Game; import janggi.domain.board.Board; import janggi.domain.board.Position; @@ -9,8 +11,8 @@ import janggi.domain.piece.Piece; import janggi.repository.GamePieceRepository; import janggi.repository.GameStateRepository; -import java.sql.Connection; import java.sql.SQLException; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -18,23 +20,30 @@ public final class GameService { private static final String GAME_ACCESS_FAILED = "[ERROR] 게임 상태를 DB에서 처리하는 중 문제가 발생했습니다."; + private final TransactionManager transactionManager; private final GameStateRepository gameStateRepository; private final GamePieceRepository gamePieceRepository; - public GameService(GameStateRepository gameStateRepository, GamePieceRepository gamePieceRepository) { + public GameService( + TransactionManager transactionManager, + GameStateRepository gameStateRepository, + GamePieceRepository gamePieceRepository + ) { + this.transactionManager = transactionManager; this.gameStateRepository = gameStateRepository; this.gamePieceRepository = gamePieceRepository; } public Optional findById(long gameId) { - try (Connection connection = H2ConnectionManager.getConnection()) { - Optional currentTurn = gameStateRepository.findById(connection, gameId); + return readOnly(connection -> { + Optional currentTurn = gameStateRepository.findCurrentTurnByGameId(connection, gameId); if (currentTurn.isEmpty()) { return Optional.empty(); } Map boardSnapshot = gamePieceRepository.findByGameId(connection, gameId); Board board = new Board(new SnapshotBoardInitializer(boardSnapshot)); + return Optional.of(Game.restore(gameId, board, currentTurn.orElseThrow())); } catch (SQLException e) { throw new IllegalStateException(GAME_ACCESS_FAILED); @@ -42,38 +51,42 @@ public Optional findById(long gameId) { } public void save(Game game) { - try (Connection connection = H2ConnectionManager.getConnection()) { - connection.setAutoCommit(false); + inTransaction(connection -> { + gameStateRepository.save(connection, game.id(), game.currentTurn()); + gamePieceRepository.saveGameByBoard( + connection, + game.id(), + game.boardSnapshot() + ); + }); + } - try { - gameStateRepository.save(connection, game.id(), game.currentTurn()); - gamePieceRepository.replaceByGameId( - connection, - game.id(), - game.boardSnapshot() - ); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - throw e; - } + public void deleteById(long gameId) { + inTransaction(connection -> { + gamePieceRepository.deleteByGameId(connection, gameId); + gameStateRepository.deleteById(connection, gameId); + }); + } + + private T readOnly(SqlFunction action) { + try { + return transactionManager.readOnly(action); } catch (SQLException e) { throw new IllegalStateException(GAME_ACCESS_FAILED); } } - public void deleteById(long gameId) { - try (Connection connection = H2ConnectionManager.getConnection()) { - connection.setAutoCommit(false); + private T inTransaction(SqlFunction action) { + try { + return transactionManager.inTransaction(action); + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } - try { - gamePieceRepository.deleteByGameId(connection, gameId); - gameStateRepository.deleteById(connection, gameId); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - throw e; - } + private void inTransaction(SqlConsumer action) { + try { + transactionManager.inTransaction(action); } catch (SQLException e) { throw new IllegalStateException(GAME_ACCESS_FAILED); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index e88a0e48d..86d21bcc0 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,5 +1,5 @@ create table if not exists game_state ( - game_id bigint primary key, + game_id bigint generated by default as identity primary key, current_turn varchar(20) not null ); From 13fd788badcc64d775acce123480b1b8603cd3f6 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 19:52:45 +0900 Subject: [PATCH 31/36] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/Game.java | 2 +- src/main/java/janggi/domain/board/Board.java | 2 +- src/test/java/janggi/domain/board/BoardTest.java | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/janggi/domain/Game.java b/src/main/java/janggi/domain/Game.java index d6993a6ea..40e9d549e 100644 --- a/src/main/java/janggi/domain/Game.java +++ b/src/main/java/janggi/domain/Game.java @@ -46,7 +46,7 @@ public void validateSourceForCurrentTurn(Position source) { } public boolean play(Position source, Position destination) { - boolean gameEnded = board.movePiece(source, destination, currentTurn()); + boolean gameEnded = board.movePieceAndCheckGameEnd(source, destination, currentTurn()); if (!gameEnded) { currentTurn = currentTurn.next(); } diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 337739dee..82d71fb0d 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -39,7 +39,7 @@ public boolean hasSamePieceTypeAt(Position position, PieceType pieceType) { return foundPiece.isSamePieceType(pieceType); } - public boolean movePiece(Position source, Position destination, Camp turn) { + public boolean movePieceAndCheckGameEnd(Position source, Position destination, Camp turn) { validateCampTurn(source, turn); Piece movingPiece = board.get(source); movingPiece.validateMove(source, destination, this); diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 3e413b9a7..5e9768a2c 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -24,7 +24,7 @@ destination, new Piece(PieceType.HORSE, Camp.CHO), source, new Piece(PieceType.CANNON, Camp.HAN) )); // when - board.movePiece(source, destination, Camp.HAN); + board.movePieceAndCheckGameEnd(source, destination, Camp.HAN); // then boolean destinationExists = board.hasSamePieceTypeAt(destination, PieceType.CANNON); boolean sourceExists = board.hasPieceAt(source); @@ -47,7 +47,7 @@ destination, new Piece(PieceType.HORSE, Camp.CHO), source, new Piece(PieceType.CANNON, Camp.CHO) )); // then - Assertions.assertThatThrownBy(() -> board.movePiece(source, destination, Camp.HAN)) + Assertions.assertThatThrownBy(() -> board.movePieceAndCheckGameEnd(source, destination, Camp.HAN)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 상대 진영의 기물은 이동할 수 없습니다."); } @@ -60,7 +60,7 @@ source, new Piece(PieceType.CANNON, Camp.CHO) // when Board board = new Board(Map::of); // then - Assertions.assertThatThrownBy(() -> board.movePiece(source, destination, Camp.HAN)) + Assertions.assertThatThrownBy(() -> board.movePieceAndCheckGameEnd(source, destination, Camp.HAN)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 출발지에 기물이 존재하지 않습니다."); } @@ -78,7 +78,7 @@ destination, new Piece(PieceType.GENERAL, Camp.CHO) )); // when - boolean gameEnded = board.movePiece(source, destination, Camp.HAN); + boolean gameEnded = board.movePieceAndCheckGameEnd(source, destination, Camp.HAN); // then assertThat(gameEnded).isTrue(); From f26981f1693245c9965db69f7c3a2b7305d28fe7 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 23:57:16 +0900 Subject: [PATCH 32/36] =?UTF-8?q?refactor:=20=EC=A0=84=EB=9E=B5=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/piece/PieceType.java | 6 +++--- ...eStrategy.java => FriendlyPalaceSingleStepStrategy.java} | 2 +- ...yTest.java => FriendlyPalaceSingleStepStrategyTest.java} | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/janggi/domain/piece/strategy/{PalaceStrategy.java => FriendlyPalaceSingleStepStrategy.java} (82%) rename src/test/java/janggi/domain/piece/strategy/{PalaceStrategyTest.java => FriendlyPalaceSingleStepStrategyTest.java} (98%) diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index ae4032667..8841244a0 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -6,21 +6,21 @@ import janggi.domain.piece.condition.MoveCondition; import janggi.domain.piece.condition.OnePieceExistsCondition; import janggi.domain.piece.strategy.ElephantStrategy; +import janggi.domain.piece.strategy.FriendlyPalaceSingleStepStrategy; import janggi.domain.piece.strategy.HorseStrategy; import janggi.domain.piece.strategy.MoveStrategy; import janggi.domain.piece.strategy.MultiStepStraightStrategy; -import janggi.domain.piece.strategy.PalaceStrategy; import janggi.domain.piece.strategy.SoldierStrategy; import java.util.List; public enum PieceType { - GENERAL(new PalaceStrategy(), new EmptyCondition(), 0.0), + GENERAL(new FriendlyPalaceSingleStepStrategy(), new EmptyCondition(), 0.0), CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition(), 13.0), CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition(), 7.0), HORSE(new HorseStrategy(), new EmptyCondition(), 5.0), ELEPHANT(new ElephantStrategy(), new EmptyCondition(), 3.0), - GUARD(new PalaceStrategy(), new EmptyCondition(), 3.0), + GUARD(new FriendlyPalaceSingleStepStrategy(), new EmptyCondition(), 3.0), SOLDIER(new SoldierStrategy(), new EmptyCondition(), 2.0); private final MoveStrategy moveStrategy; diff --git a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategy.java similarity index 82% rename from src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java rename to src/main/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategy.java index c0a49b8ed..6a0a3b93d 100644 --- a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategy.java @@ -5,7 +5,7 @@ import janggi.domain.piece.Camp; import java.util.List; -public class PalaceStrategy extends SingleStepStraightStrategy { +public class FriendlyPalaceSingleStepStrategy extends SingleStepStraightStrategy { @Override public List findPath(Position source, Position destination, Camp camp) { diff --git a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategyTest.java similarity index 98% rename from src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java rename to src/test/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategyTest.java index 05c7341ab..b18113462 100644 --- a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategyTest.java @@ -14,9 +14,9 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -class PalaceStrategyTest { +class FriendlyPalaceSingleStepStrategyTest { - private final MoveStrategy strategy = new PalaceStrategy(); + private final MoveStrategy strategy = new FriendlyPalaceSingleStepStrategy(); @DisplayName("정상 경우") @Nested From af0d541a05a22d5d1924d5af61c998642ce7c9d9 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 3 Apr 2026 23:57:37 +0900 Subject: [PATCH 33/36] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게임방 생성 기능 테스트 추가 --- src/main/java/janggi/JanggiGame.java | 20 ++-- .../java/janggi/db/ConnectionManager.java | 8 ++ .../java/janggi/db/DatabaseInitializer.java | 4 +- .../java/janggi/db/H2ConnectionManager.java | 3 +- .../java/janggi/db/TransactionManager.java | 4 +- .../repository/GameStateRepository.java | 59 ++++++++++- src/main/java/janggi/service/GameService.java | 22 ++++- src/main/java/janggi/view/InputView.java | 6 ++ src/main/java/janggi/view/OutputView.java | 10 ++ src/main/java/janggi/view/Parser.java | 8 ++ .../java/janggi/db/TestConnectionManager.java | 32 ++++++ .../java/janggi/service/GameServiceTest.java | 98 +++++++++++++++++++ 12 files changed, 256 insertions(+), 18 deletions(-) create mode 100644 src/main/java/janggi/db/ConnectionManager.java create mode 100644 src/test/java/janggi/db/TestConnectionManager.java create mode 100644 src/test/java/janggi/service/GameServiceTest.java diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java index 453c5f955..a6187f45d 100644 --- a/src/main/java/janggi/JanggiGame.java +++ b/src/main/java/janggi/JanggiGame.java @@ -16,8 +16,7 @@ import java.util.function.Supplier; public class JanggiGame { - - private static final long SINGLE_GAME_ID = 1L; + private static final String INVALID_GAME_ROOM = "[ERROR] 존재하지 않는 게임방 번호입니다."; private final InputView inputView; private final OutputView outputView; @@ -30,21 +29,26 @@ public JanggiGame(InputView inputView, OutputView outputView, GameService gameSe } public void run() { - Game game = loadOrCreateGame(); + Game game = retryOnInvalidInput(this::loadOrCreateGame); outputView.printBoard(game.boardSnapshot()); play(game); } private Game loadOrCreateGame() { - return gameService.findById(SINGLE_GAME_ID) - .orElseGet(this::createNewGame); + outputView.printExistGameRoom(gameService.findAllIds()); + long gameId = inputView.readSelectedGameRoom(); + + if (gameId == 0L) { + return createNewGame(); + } + + return gameService.findById(gameId) + .orElseThrow(() -> new IllegalArgumentException(INVALID_GAME_ROOM)); } private Game createNewGame() { Board board = createBoard(); - Game game = Game.start(SINGLE_GAME_ID, board); - gameService.save(game); - return game; + return gameService.create(board); } private Board createBoard() { diff --git a/src/main/java/janggi/db/ConnectionManager.java b/src/main/java/janggi/db/ConnectionManager.java new file mode 100644 index 000000000..334ecda10 --- /dev/null +++ b/src/main/java/janggi/db/ConnectionManager.java @@ -0,0 +1,8 @@ +package janggi.db; + +import java.sql.Connection; + +public interface ConnectionManager { + + Connection createConnection(); +} diff --git a/src/main/java/janggi/db/DatabaseInitializer.java b/src/main/java/janggi/db/DatabaseInitializer.java index 1374b1770..1a7507255 100644 --- a/src/main/java/janggi/db/DatabaseInitializer.java +++ b/src/main/java/janggi/db/DatabaseInitializer.java @@ -13,9 +13,9 @@ public final class DatabaseInitializer { private static final String SCHEMA_LOAD_FAILED = "[ERROR] DB 스키마를 읽을 수 없습니다."; private static final String SCHEMA_INIT_FAILED = "[ERROR] DB 스키마를 초기화할 수 없습니다."; - private final H2ConnectionManager connectionManager; + private final ConnectionManager connectionManager; - public DatabaseInitializer(H2ConnectionManager connectionManager) { + public DatabaseInitializer(ConnectionManager connectionManager) { this.connectionManager = connectionManager; } diff --git a/src/main/java/janggi/db/H2ConnectionManager.java b/src/main/java/janggi/db/H2ConnectionManager.java index 8833fb86b..27576116a 100644 --- a/src/main/java/janggi/db/H2ConnectionManager.java +++ b/src/main/java/janggi/db/H2ConnectionManager.java @@ -4,7 +4,7 @@ import java.sql.DriverManager; import java.sql.SQLException; -public final class H2ConnectionManager { +public final class H2ConnectionManager implements ConnectionManager { private static final String URL = "jdbc:h2:file:./data/janggi"; private static final String USER = "stark"; @@ -12,6 +12,7 @@ public final class H2ConnectionManager { private static final String UNABLE_TO_ACCESS_DATABASE = "[ERROR] H2 데이터베이스에 연결할 수 없습니다."; + @Override public Connection createConnection() { try { return DriverManager.getConnection(URL, USER, PASSWORD); diff --git a/src/main/java/janggi/db/TransactionManager.java b/src/main/java/janggi/db/TransactionManager.java index 4573e6445..7fc56d128 100644 --- a/src/main/java/janggi/db/TransactionManager.java +++ b/src/main/java/janggi/db/TransactionManager.java @@ -5,9 +5,9 @@ public final class TransactionManager { - private final H2ConnectionManager connectionManager; + private final ConnectionManager connectionManager; - public TransactionManager(H2ConnectionManager connectionManager) { + public TransactionManager(ConnectionManager connectionManager) { this.connectionManager = connectionManager; } diff --git a/src/main/java/janggi/repository/GameStateRepository.java b/src/main/java/janggi/repository/GameStateRepository.java index fe6d934a9..0328db378 100644 --- a/src/main/java/janggi/repository/GameStateRepository.java +++ b/src/main/java/janggi/repository/GameStateRepository.java @@ -5,6 +5,9 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; public final class GameStateRepository { @@ -14,6 +17,11 @@ public final class GameStateRepository { from game_state where game_id = ? """; + private static final String SELECT_ALL_GAME_ID = """ + select game_id + from game_state + order by game_id asc + """; private static final String UPDATE_GAME_STATE = """ update game_state set current_turn = ? @@ -23,19 +31,42 @@ public final class GameStateRepository { insert into game_state (game_id, current_turn) values (?, ?) """; + private static final String CREATE_NEW_GAME_STATE = """ + insert into game_state (current_turn) + values (?) + """; private static final String DELETE_GAME_STATE = """ delete from game_state where game_id = ? """; + private static final String CANNOT_FIND_GAME = "[ERROR] 생성된 게임방 번호를 가져올 수 없습니다."; + + public List findAllIds(Connection connection) throws SQLException { + PreparedStatement statement = connection.prepareStatement(SELECT_ALL_GAME_ID); - public Optional findById(Connection connection, long gameId) throws SQLException { - PreparedStatement statement = connection.prepareStatement(SELECT_GAME_STATE); + try (statement; ResultSet resultSet = statement.executeQuery()) { + List gameIds = new ArrayList<>(); + findAllGameIds(resultSet, gameIds); + + return gameIds; + } + } + + private void findAllGameIds(ResultSet resultSet, List gameIds) throws SQLException { + while (resultSet.next()) { + gameIds.add(resultSet.getLong("game_id")); + } + } + + public Optional findCurrentTurnByGameId(Connection connection, long gameId) throws SQLException { + PreparedStatement statement = connection.prepareStatement(SELECT_CURRENT_TURN); statement.setLong(1, gameId); try (statement; ResultSet resultSet = statement.executeQuery()) { if (!resultSet.next()) { return Optional.empty(); } + return Optional.of(Camp.valueOf(resultSet.getString("current_turn"))); } } @@ -47,6 +78,30 @@ public void save(Connection connection, long gameId, Camp currentTurn) throws SQ insert(connection, gameId, currentTurn); } + public long createGame(Connection connection, Camp currentTurn) throws SQLException { + PreparedStatement statement = connection.prepareStatement(CREATE_NEW_GAME_STATE, + Statement.RETURN_GENERATED_KEYS); + statement.setString(1, currentTurn.name()); + + try (statement) { + statement.executeUpdate(); + return getGeneratedKey(statement); + } + } + + private long getGeneratedKey(PreparedStatement statement) throws SQLException { + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + validateKeys(generatedKeys); + return generatedKeys.getLong(1); + } + } + + private void validateKeys(ResultSet generatedKeys) throws SQLException { + if (!generatedKeys.next()) { + throw new IllegalStateException(CANNOT_FIND_GAME); + } + } + public void deleteById(Connection connection, long gameId) throws SQLException { try (PreparedStatement statement = connection.prepareStatement(DELETE_GAME_STATE)) { statement.setLong(1, gameId); diff --git a/src/main/java/janggi/service/GameService.java b/src/main/java/janggi/service/GameService.java index 21d21d81d..4ca43b706 100644 --- a/src/main/java/janggi/service/GameService.java +++ b/src/main/java/janggi/service/GameService.java @@ -34,6 +34,10 @@ public GameService( this.gamePieceRepository = gamePieceRepository; } + public List findAllIds() { + return readOnly(gameStateRepository::findAllIds); + } + public Optional findById(long gameId) { return readOnly(connection -> { Optional currentTurn = gameStateRepository.findCurrentTurnByGameId(connection, gameId); @@ -45,9 +49,21 @@ public Optional findById(long gameId) { Board board = new Board(new SnapshotBoardInitializer(boardSnapshot)); return Optional.of(Game.restore(gameId, board, currentTurn.orElseThrow())); - } catch (SQLException e) { - throw new IllegalStateException(GAME_ACCESS_FAILED); - } + }); + } + + public Game create(Board board) { + return inTransaction(connection -> { + long gameId = gameStateRepository.createGame(connection, Camp.CHO); + Game game = Game.start(gameId, board); + + gamePieceRepository.saveGameByBoard( + connection, + gameId, + game.boardSnapshot() + ); + return game; + }); } public void save(Game game) { diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index 4b915d0e5..5b06633f7 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -22,6 +22,7 @@ public final class InputView { private static final String TURN = LINE_SEPARATOR + "%s나라 차례 입니다."; private static final String SOURCE = "공격할 기물의 좌표를 행,열 순으로 입력해 주세요. (예: 9,8)"; private static final String DESTINATION = LINE_SEPARATOR + "이동 시킬 목적지의 좌표를 행,열 순으로 입력해 주세요. (예: 2,0)"; + private static final String SELECT_GAME = "입장할 게임방 번호를 입력하세요. (새 게임 생성: 0)"; private final Scanner scanner; @@ -72,4 +73,9 @@ private void validatePositionSize(List rawPosition) { throw new IllegalArgumentException(INVALID_INPUT_FORMAT); } } + + public long readSelectedGameRoom() { + System.out.println(SELECT_GAME); + return Parser.parseToLong(readLine()); + } } diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index 959da8025..59c6ad7a6 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -29,6 +29,8 @@ public final class OutputView { private static final String SCORE = "%s나라 점수: %.1f"; private static final String WINNER = "%s나라가 승리하였습니다!! 축하드립니다!!"; + private static final String GAME_ROOM = "현재 게임방: "; + private static final String EMPTY_GAME = "현재 게임방이 존재하지 않습니다."; public void printError(String errorMessage) { System.out.println(errorMessage); @@ -105,4 +107,12 @@ private String toCampName(Camp camp) { CampDto campDto = CampDto.from(camp); return campDto.color() + campDto.name() + RESET; } + + public void printExistGameRoom(List gameIds) { + if (gameIds.isEmpty()) { + System.out.println(LINE_SEPARATOR + EMPTY_GAME); + return; + } + System.out.println(LINE_SEPARATOR + GAME_ROOM + gameIds); + } } diff --git a/src/main/java/janggi/view/Parser.java b/src/main/java/janggi/view/Parser.java index 07195da6a..481d9dfbb 100644 --- a/src/main/java/janggi/view/Parser.java +++ b/src/main/java/janggi/view/Parser.java @@ -24,4 +24,12 @@ private static int parseToInt(String number) { throw new IllegalArgumentException(ONLY_NUMBERS_ALLOWED); } } + + public static long parseToLong(String number) { + try { + return Long.parseLong(number); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(ONLY_NUMBERS_ALLOWED); + } + } } diff --git a/src/test/java/janggi/db/TestConnectionManager.java b/src/test/java/janggi/db/TestConnectionManager.java new file mode 100644 index 000000000..ab7193f8e --- /dev/null +++ b/src/test/java/janggi/db/TestConnectionManager.java @@ -0,0 +1,32 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +public final class TestConnectionManager implements ConnectionManager { + + private static final String UNABLE_TO_ACCESS_DATABASE = "[ERROR] H2 데이터베이스에 연결할 수 없습니다."; + private static final String URL = "jdbc:h2:mem:test-db;DB_CLOSE_DELAY=-1"; + + @Override + public Connection createConnection() { + try { + return DriverManager.getConnection(URL, "sa", ""); + } catch (SQLException e) { + throw new IllegalStateException(UNABLE_TO_ACCESS_DATABASE); + } + } + + public void clear() { + try ( + Connection connection = createConnection(); + Statement statement = connection.createStatement() + ) { + statement.execute("DROP ALL OBJECTS"); + } catch (SQLException e) { + throw new IllegalStateException(UNABLE_TO_ACCESS_DATABASE); + } + } +} diff --git a/src/test/java/janggi/service/GameServiceTest.java b/src/test/java/janggi/service/GameServiceTest.java new file mode 100644 index 000000000..5839a714f --- /dev/null +++ b/src/test/java/janggi/service/GameServiceTest.java @@ -0,0 +1,98 @@ +package janggi.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.db.DatabaseInitializer; +import janggi.db.TestConnectionManager; +import janggi.db.TransactionManager; +import janggi.domain.Game; +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.board.initializer.ElephantSetUp; +import janggi.domain.board.initializer.StandardBoardInitializer; +import janggi.domain.piece.Camp; +import janggi.repository.GamePieceRepository; +import janggi.repository.GameStateRepository; +import java.util.Map; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GameServiceTest { + + private GameService gameService; + private Board board; + private TestConnectionManager connectionManager; + + @BeforeEach + void setUp() { + connectionManager = new TestConnectionManager(); + new DatabaseInitializer(connectionManager).initialize(); + + gameService = new GameService( + new TransactionManager(connectionManager), + new GameStateRepository(), + new GamePieceRepository() + ); + board = createBoard(); + } + + @AfterEach + void tearDown() { + connectionManager.clear(); + } + + @Test + void 새_게임을_생성하고_다시_조회할_수_있다() { + // when + Game createdGame = gameService.create(board); + Game loadedGame = gameService.findById(createdGame.id()).orElseThrow(); + + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(createdGame.id()).isPositive(); + assertSoftly.assertThat(gameService.findAllIds()).containsExactly(createdGame.id()); + assertSoftly.assertThat(loadedGame.currentTurn()).isEqualTo(Camp.CHO); + assertSoftly.assertThat(loadedGame.boardSnapshot()).isEqualTo(createdGame.boardSnapshot()); + }); + } + + @Test + void 게임을_두_개_생성하면_전체_게임방_번호를_조회할_수_있다() { + // when + Game firstGame = gameService.create(createBoard()); + Game secondGame = gameService.create(createBoard()); + + // then + assertThat(gameService.findAllIds()).containsExactly(firstGame.id(), secondGame.id()); + } + + @Test + void 게임을_저장하면_변경된_턴과_보드_상태가_반영된다() { + // given + Game createdGame = gameService.create(board); + Position source = new Position(3, 0); + Position destination = new Position(4, 0); + createdGame.play(source, destination); + + // when + gameService.save(createdGame); + Game loadedGame = gameService.findById(createdGame.id()).orElseThrow(); + + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(loadedGame.currentTurn()).isEqualTo(Camp.HAN); + assertSoftly.assertThat(loadedGame.boardSnapshot()).isEqualTo(createdGame.boardSnapshot()); + assertSoftly.assertThat(loadedGame.boardSnapshot()).doesNotContainKey(source); + assertSoftly.assertThat(loadedGame.boardSnapshot()).containsKey(destination); + }); + } + + private Board createBoard() { + return new Board(new StandardBoardInitializer(Map.of( + Camp.HAN, ElephantSetUp.LEFT_ELEPHANT, + Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT + ))); + } +} From 2e6c3383601bb838b5a784074cbf489d05ef2222 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Sat, 4 Apr 2026 01:25:57 +0900 Subject: [PATCH 34/36] =?UTF-8?q?refactor:=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/{JanggiGame.java => GameRunner.java} | 4 ++-- src/main/java/janggi/JanggiApplication.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/janggi/{JanggiGame.java => GameRunner.java} (97%) diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/GameRunner.java similarity index 97% rename from src/main/java/janggi/JanggiGame.java rename to src/main/java/janggi/GameRunner.java index a6187f45d..b2cff0b6b 100644 --- a/src/main/java/janggi/JanggiGame.java +++ b/src/main/java/janggi/GameRunner.java @@ -15,14 +15,14 @@ import java.util.Map; import java.util.function.Supplier; -public class JanggiGame { +public class GameRunner { private static final String INVALID_GAME_ROOM = "[ERROR] 존재하지 않는 게임방 번호입니다."; private final InputView inputView; private final OutputView outputView; private final GameService gameService; - public JanggiGame(InputView inputView, OutputView outputView, GameService gameService) { + public GameRunner(InputView inputView, OutputView outputView, GameService gameService) { this.inputView = inputView; this.outputView = outputView; this.gameService = gameService; diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java index 881315fbc..6fce6b67c 100644 --- a/src/main/java/janggi/JanggiApplication.java +++ b/src/main/java/janggi/JanggiApplication.java @@ -16,7 +16,7 @@ public static void main(String[] args) { new DatabaseInitializer(connectionManager).initialize(); TransactionManager transactionManager = new TransactionManager(connectionManager); - JanggiGame janggi = new JanggiGame( + GameRunner janggi = new GameRunner( new InputView(new Scanner(System.in)), new OutputView(), new GameService(transactionManager, new GameStateRepository(), new GamePieceRepository()) From 8f87e7c9918bd26fb888cd6a78eb69754aa8cc05 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Sat, 4 Apr 2026 01:32:06 +0900 Subject: [PATCH 35/36] =?UTF-8?q?refactor:=20=EC=99=84=EB=A3=8C=ED=95=9C?= =?UTF-8?q?=20TODO=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/janggi/domain/piece/PieceTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index 5a06823e1..e571cc718 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -48,9 +48,6 @@ class General { ); } - // TODO: 한나라 예외 추가 - // TODO: 대각선 이동도 가능하므로 예외 메시지 수정 고려 - // TODO: 궁성 외부 이동 예외 테스트 추가 @Test void 궁은_행마법을_따르지_않으면_예외가_발생한다() { // given @@ -78,9 +75,6 @@ class Guard { ); } - // TODO: 한나라 예외 추가 - // TODO: 대각선 이동도 가능하므로 예외 메시지 수정 고려 - // TODO: 궁성 외부 이동 예외 테스트 추가 @Test void 사는_행마법을_따르지_않으면_예외가_발생한다() { // given From 4c081957e9615654d1f972e0159bef6aed0b35e8 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Sat, 4 Apr 2026 01:32:20 +0900 Subject: [PATCH 36/36] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7aa0b6154..bf6209334 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ - [궁(楚/漢)] - [x] 상하좌우 직선 1칸 이동한다. - [x] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. - - [ ] 상대 기물에게 잡힐 경우 게임이 종료되며 상대방이 게임을 승리한다. + - [x] 상대 기물에게 잡힐 경우 게임이 종료되며 상대방이 게임을 승리한다. - [사(士)] - [x] 상하좌우 직선 1칸 이동한다.