From 61f7968c15023f43a71ad9d83409e041d0bd4c7f Mon Sep 17 00:00:00 2001 From: softmoca Date: Wed, 1 Apr 2026 09:27:49 +0900 Subject: [PATCH 01/20] =?UTF-8?q?docs=20:=20=EC=B6=94=EA=B0=80=EB=90=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=9A=94=EA=B5=AC=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 77d78d3e88..60f198aefb 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,53 @@ - [X] 타 진영 기물 선택 시 예외 - [X] 빈 칸 선택시 예외 - [X] 도착 지점에 아군이 있는 경우 예외 + +## 2.1단계 - 기물의 확장 + +### 궁성 영역 정의 + +- [ ] 궁성(宮城) 영역 정의 + - 궁성 좌표 범위 정의 +- [ ] 궁성 내 대각선 경로 정의 + - 궁성에는 대각선 선이 존재하며, 대각선 이동은 이 선 위의 좌표에서만 가능 + - 대각선 허용 좌표쌍 정의 +- [ ] 특정 좌표가 궁성 내부인지 판별하는 기능 +- [ ] 두 좌표 간 궁성 대각선 이동 가능 여부 판별 + +### 궁성 관련 기물 이동 규칙 변경 및 추가 + +- [ ] 장(King) + - 궁성 내부에서만 이동 가능 + - 상하좌우 1칸 이동 (기존 유지) + - 궁성 대각선 경로에서 대각선 1칸 이동 가능 +- [ ] 사(Advisor) + - 궁성 내부에서만 이동 가능 + - 상하좌우 1칸 이동 (기존 유지) + - 궁성 대각선 경로에서 대각선 1칸 이동 가능 +- [ ] 차(Tank) + - 기존 직선 이동 규칙 유지 + - 궁성 내부에서 대각선 경로 이동 가능 +- [ ] 포(Cannon) + - 기존 직선 + 1개 기물 넘기 규칙 유지 + - 궁성 내부에서 대각선 경로 이동 가능 +- [ ] 졸(Soldier) + - 기존 전진 + 좌우 이동 유지 + - 궁성 내부에서 대각선 "전진" 이동 가능 + - 후진 대각선 불가 + +### 왕 잡힘 시 게임 종료 + +- [ ] 이동 결과 도착 칸의 기물이 왕(King)인지 판별 +- [ ] 왕이 잡혔을 때 승자 진영 결정 +- [ ] JanggiGame에 게임 종료 상태 반영 + +### 점수 계산 + +- [ ] Piece에 `score()` 추상 메서드 추가 +- [ ] 기물별 점수 정의 +- [ ] Board에서 특정 진영 점수 합산 기능 +- [ ] 양 진영 점수 조회 기능 + + + + From ec9c5be96205198934534e252b070e7dd06a9eed Mon Sep 17 00:00:00 2001 From: softmoca Date: Wed, 1 Apr 2026 09:37:58 +0900 Subject: [PATCH 02/20] =?UTF-8?q?test(Palace)=20:=20=EA=B6=81=EC=84=B1=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=20=EB=B0=8F=20=EB=8C=80=EA=B0=81=EC=84=A0=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- src/test/java/janggi/domain/PalaceTest.java | 68 +++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/test/java/janggi/domain/PalaceTest.java diff --git a/src/test/java/janggi/domain/PalaceTest.java b/src/test/java/janggi/domain/PalaceTest.java new file mode 100644 index 0000000000..4e2373da34 --- /dev/null +++ b/src/test/java/janggi/domain/PalaceTest.java @@ -0,0 +1,68 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.vo.Position; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class PalaceTest { + + @ParameterizedTest + @CsvSource({ + "0, 3", "0, 4", "0, 5", + "1, 3", "1, 4", "1, 5", + "2, 3", "2, 4", "2, 5", + "7, 3", "7, 4", "7, 5", + "8, 3", "8, 4", "8, 5", + "9, 3", "9, 4", "9, 5" + }) + void 궁성_내부_좌표를_판별한다(int row, int col) { + assertThat(Palace.isInsidePalace(new Position(row, col))).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "0, 0", "3, 4", "5, 5", "6, 4" + }) + void 궁성_외부_좌표를_판별한다(int row, int col) { + assertThat(Palace.isInsidePalace(new Position(row, col))).isFalse(); + } + + @Test + void 한_궁성_중앙에서_꼭짓점으로_대각선_이동_가능() { + assertThat(Palace.canMoveDiagonally( + new Position(1, 4), new Position(0, 3))).isTrue(); + } + + @Test + void 한_궁성_꼭짓점에서_중앙으로_대각선_이동_가능() { + assertThat(Palace.canMoveDiagonally( + new Position(2, 5), new Position(1, 4))).isTrue(); + } + + @Test + void 초_궁성_중앙에서_꼭짓점으로_대각선_이동_가능() { + assertThat(Palace.canMoveDiagonally( + new Position(8, 4), new Position(7, 3))).isTrue(); + } + + @Test + void 궁성_내부지만_대각선_선이_아닌_곳은_불가() { + assertThat(Palace.canMoveDiagonally( + new Position(0, 3), new Position(1, 3))).isFalse(); + } + + @Test + void 대각선_2칸_이동_불가() { + assertThat(Palace.canMoveDiagonally( + new Position(0, 3), new Position(2, 5))).isFalse(); + } + + @Test + void 궁성_외부에서는_대각선_이동_불가() { + assertThat(Palace.canMoveDiagonally( + new Position(4, 4), new Position(5, 5))).isFalse(); + } +} From ceb1b804aecc34d31a86de7c8ee38b495582388e Mon Sep 17 00:00:00 2001 From: softmoca Date: Wed, 1 Apr 2026 09:38:26 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat(Palace)=20:=20=EA=B6=81=EC=84=B1=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20=EB=8C=80?= =?UTF-8?q?=EA=B0=81=EC=84=B1=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=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/Palace.java | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/main/java/janggi/domain/Palace.java diff --git a/src/main/java/janggi/domain/Palace.java b/src/main/java/janggi/domain/Palace.java new file mode 100644 index 0000000000..eb032f0e80 --- /dev/null +++ b/src/main/java/janggi/domain/Palace.java @@ -0,0 +1,55 @@ +package janggi.domain; + +import janggi.domain.vo.Position; + +public class Palace { + + private static final int HAN_MIN_ROW = 0; + private static final int HAN_MAX_ROW = 2; + private static final int CHO_MIN_ROW = 7; + private static final int CHO_MAX_ROW = 9; + private static final int MIN_COL = 3; + private static final int MAX_COL = 5; + + private static final Position HAN_CENTER = new Position(1, 4); + private static final Position CHO_CENTER = new Position(8, 4); + + public static boolean isInsidePalace(Position position) { + int row = position.getRow(); + int col = position.getCol(); + if (col < MIN_COL || col > MAX_COL) { + return false; + } + return isHanPalace(row) || isChoPalace(row); + } + + public static boolean canMoveDiagonally(Position from, Position to) { + if (!isInsidePalace(from) || !isInsidePalace(to)) { + return false; + } + if (!isDiagonalOneStep(from, to)) { + return false; + } + return involvesCenter(from, to); + } + + private static boolean isHanPalace(int row) { + return row >= HAN_MIN_ROW && row <= HAN_MAX_ROW; + } + + private static boolean isChoPalace(int row) { + return row >= CHO_MIN_ROW && row <= CHO_MAX_ROW; + } + + private static boolean isDiagonalOneStep(Position from, Position to) { + int rowDiff = Math.abs(to.getRow() - from.getRow()); + int colDiff = Math.abs(to.getCol() - from.getCol()); + return rowDiff == 1 && colDiff == 1; + } + + // 대각서 이동은 중앙을 무조건 지나야만 한다. + private static boolean involvesCenter(Position from, Position to) { + return from.equals(HAN_CENTER) || to.equals(HAN_CENTER) + || from.equals(CHO_CENTER) || to.equals(CHO_CENTER); + } +} From 1fa07f0972eedc979761ec7c49212f768a3bc0ca Mon Sep 17 00:00:00 2001 From: softmoca Date: Wed, 1 Apr 2026 17:24:04 +0900 Subject: [PATCH 04/20] =?UTF-8?q?test=20:=20=EA=B6=81=EC=84=B1=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EA=B7=9C=EC=B9=99=20=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mouveRule/PalaceMoveRuleTest.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java diff --git a/src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java new file mode 100644 index 0000000000..774499dff9 --- /dev/null +++ b/src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java @@ -0,0 +1,187 @@ +package janggi.domain.mouveRule; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.Board; +import janggi.domain.vo.Position; +import org.junit.jupiter.api.Test; + +class PalaceMoveRuleTest { + private final Board board = Board.empty(); + private final MoveRule moveRule = new PalaceMoveRule(); + + //한나라 + @Test + void 한_궁성_중앙은_상하좌우_모두_이동_가능() { + Position center = new Position(1, 4); + + assertThat(moveRule.canMove(center, new Position(0, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(2, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(1, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(1, 5), board)).isTrue(); + } + + @Test + void 한_궁성_위_변은_위로는_이동_불가() { + Position pos = new Position(0, 4); + + assertThat(moveRule.canMove(pos, new Position(1, 4), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(0, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(0, 5), board)).isTrue(); + } + + @Test + void 한_궁성_왼쪽_변은_왼쪽으로는_이동_불가() { + Position pos = new Position(1, 3); + + assertThat(moveRule.canMove(pos, new Position(1, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(0, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(2, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(1, 4), board)).isTrue(); + } + + @Test + void 한_궁성_꼭짓점은_두_방향만_이동_가능() { + Position pos = new Position(0, 3); + + assertThat(moveRule.canMove(pos, new Position(0, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(1, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(0, 4), board)).isTrue(); + } + + + //초나라 + @Test + void 초_궁성_중앙은_상하좌우_모두_이동_가능() { + Position center = new Position(8, 4); + + assertThat(moveRule.canMove(center, new Position(7, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(9, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(8, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(8, 5), board)).isTrue(); + } + + @Test + void 초_궁성_아래_변은_아래로는_이동_불가() { + Position pos = new Position(9, 4); + + assertThat(moveRule.canMove(pos, new Position(8, 4), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 5), board)).isTrue(); + } + + @Test + void 초_궁성_왼쪽_변은_왼쪽으로는_이동_불가() { + Position pos = new Position(8, 3); + + assertThat(moveRule.canMove(pos, new Position(8, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(7, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(8, 4), board)).isTrue(); + } + + @Test + void 초_궁성_꼭짓점은_두_방향만_이동_가능() { + Position pos = new Position(9, 3); + + assertThat(moveRule.canMove(pos, new Position(9, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(8, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 4), board)).isTrue(); + } + + // === 궁성 내 대각선 이동 === + @Test + void 한_궁성_중앙에서_각_꼭짓점으로_대각선_이동_가능() { + Position center = new Position(1, 4); + + assertThat(moveRule.canMove(center, new Position(0, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(0, 5), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(2, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(2, 5), board)).isTrue(); + } + + @Test + void 초_궁성_중앙에서_각_꼭짓점으로_대각선_이동_가능() { + Position center = new Position(8, 4); + + assertThat(moveRule.canMove(center, new Position(7, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(7, 5), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(9, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(9, 5), board)).isTrue(); + } + + @Test + void 한_궁성_꼭짓점에서_중앙으로_대각선_이동_가능() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(1, 4), board)).isTrue(); + assertThat(moveRule.canMove( + new Position(2, 5), new Position(1, 4), board)).isTrue(); + } + + @Test + void 초_궁성_꼭짓점에서_중앙으로_대각선_이동_가능() { + assertThat(moveRule.canMove( + new Position(7, 3), new Position(8, 4), board)).isTrue(); + assertThat(moveRule.canMove( + new Position(9, 5), new Position(8, 4), board)).isTrue(); + } + + // === 궁성 밖으로 나가는 이동 불가 === + + @Test + void 한_궁성에서_궁성_밖으로_나가는_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(0, 2), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(2, 4), new Position(3, 4), board)).isFalse(); + } + + @Test + void 초_궁성에서_궁성_밖으로_나가는_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(9, 3), new Position(9, 2), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(7, 4), new Position(6, 4), board)).isFalse(); + } + + // === 대각선 선이 아닌 곳에서 대각선 이동 불가 === + + @Test + void 한_궁성_내부라도_대각선_선이_아닌_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(0, 4), new Position(1, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(1, 3), new Position(2, 4), board)).isFalse(); + } + + @Test + void 초_궁성_내부라도_대각선_선이_아닌_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(7, 4), new Position(8, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(8, 3), new Position(9, 4), board)).isFalse(); + } + + // === 2칸 이상 이동 불가 === + @Test + void 한_궁성에서는_대각선_두_칸_이상_이동할_수_없다() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(2, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(0, 5), new Position(2, 3), board)).isFalse(); + } + + @Test + void 초_궁성에서는_대각선_두_칸_이상_이동할_수_없다() { + assertThat(moveRule.canMove( + new Position(7, 3), new Position(9, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(7, 5), new Position(9, 3), board)).isFalse(); + } +} From 148e7795be30f9b4c242dd426bf8a739e246b711 Mon Sep 17 00:00:00 2001 From: softmoca Date: Wed, 1 Apr 2026 17:24:20 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EC=9E=A5,=EC=82=AC=20=EA=B6=81?= =?UTF-8?q?=EC=84=B1=20=EC=9D=B4=EB=8F=99=20=EA=B7=9C=EC=B9=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mouveRule/PalaceMoveRule.java | 22 +++++++++++++++++++ .../java/janggi/domain/piece/Advisor.java | 4 ++-- src/main/java/janggi/domain/piece/King.java | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java diff --git a/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java b/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java new file mode 100644 index 0000000000..1ed7884814 --- /dev/null +++ b/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java @@ -0,0 +1,22 @@ +package janggi.domain.mouveRule; + +import janggi.domain.BoardView; +import janggi.domain.Palace; +import janggi.domain.vo.Position; + +public class PalaceMoveRule implements MoveRule { + + @Override + public boolean canMove(Position from, Position to, BoardView board) { + if (!Palace.isInsidePalace(from) || !Palace.isInsidePalace(to)) { + return false; + } + return isStraightOneStep(from, to) || Palace.canMoveDiagonally(from, to); + } + + private boolean isStraightOneStep(Position from, Position to) { + int rowDis = Math.abs(to.getRow() - from.getRow()); + int colDis = Math.abs(to.getCol() - from.getCol()); + return (rowDis + colDis) == 1; + } +} diff --git a/src/main/java/janggi/domain/piece/Advisor.java b/src/main/java/janggi/domain/piece/Advisor.java index d341ec5427..1c741b710e 100644 --- a/src/main/java/janggi/domain/piece/Advisor.java +++ b/src/main/java/janggi/domain/piece/Advisor.java @@ -1,7 +1,7 @@ package janggi.domain.piece; import janggi.domain.mouveRule.MoveRule; -import janggi.domain.mouveRule.OneStepMoveRule; +import janggi.domain.mouveRule.PalaceMoveRule; public class Advisor extends Piece { @@ -16,6 +16,6 @@ public String toString() { @Override public MoveRule moveRule() { - return new OneStepMoveRule(); + return new PalaceMoveRule(); } } diff --git a/src/main/java/janggi/domain/piece/King.java b/src/main/java/janggi/domain/piece/King.java index 4e9350290d..bd6fae713b 100644 --- a/src/main/java/janggi/domain/piece/King.java +++ b/src/main/java/janggi/domain/piece/King.java @@ -1,7 +1,7 @@ package janggi.domain.piece; import janggi.domain.mouveRule.MoveRule; -import janggi.domain.mouveRule.OneStepMoveRule; +import janggi.domain.mouveRule.PalaceMoveRule; public class King extends Piece { @@ -16,6 +16,6 @@ public String toString() { @Override public MoveRule moveRule() { - return new OneStepMoveRule(); + return new PalaceMoveRule(); } } From 8a9bd7b6d824bcce4c991beda13b51cb960c2dda Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 09:42:55 +0900 Subject: [PATCH 06/20] =?UTF-8?q?refactor:=20MoveRule=EC=97=90=EC=84=9C=20?= =?UTF-8?q?Palace=20=EC=A7=81=EC=A0=91=20=EC=9D=98=EC=A1=B4=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 - BoardView에 궁성 관련 메서드 추가 - Board가 Palace에 위임하여 BoardView 메서드 구현 - 모든 MoveRule이 BoardView 인터페이스에만 의존하도록 변경 - Palace 변경 시 영향 범위가 Board 한 곳으로 제한됨 ! --- src/main/java/janggi/domain/Board.java | 20 +++++++++++++++++++ src/main/java/janggi/domain/BoardView.java | 8 ++++++++ .../domain/mouveRule/PalaceMoveRule.java | 6 +----- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java index 2f2d72ba0d..33665d3bf1 100644 --- a/src/main/java/janggi/domain/Board.java +++ b/src/main/java/janggi/domain/Board.java @@ -43,6 +43,26 @@ public boolean isEmptyPosition(Position position) { return findByPosition(position).isEmpty(); } + @Override + public boolean isInsidePalace(Position position) { + return Palace.isInsidePalace(position); + } + + @Override + public boolean canMoveDiagonallyInPalace(Position from, Position to) { + return Palace.canMoveDiagonally(from, to); + } + + @Override + public boolean isDiagonalInPalace(Position from, Position to) { + return Palace.isDiagonalInPalace(from, to); + } + + @Override + public Position getDiagonalMidpointInPalace(Position from, Position to) { + return Palace.getDiagonalMidpoint(from, to); + } + public void move(Position from, Position to, Team currentTeam) { Piece fromPiece = findByPosition(from); Piece toPiece = findByPosition(to); diff --git a/src/main/java/janggi/domain/BoardView.java b/src/main/java/janggi/domain/BoardView.java index e4bbad9255..743136c155 100644 --- a/src/main/java/janggi/domain/BoardView.java +++ b/src/main/java/janggi/domain/BoardView.java @@ -9,4 +9,12 @@ public interface BoardView { boolean isEmptyPosition(Position position); + boolean isInsidePalace(Position position); + + boolean canMoveDiagonallyInPalace(Position from, Position to); + + boolean isDiagonalInPalace(Position from, Position to); + + Position getDiagonalMidpointInPalace(Position from, Position to); + } diff --git a/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java b/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java index 1ed7884814..92ffb848a1 100644 --- a/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java +++ b/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java @@ -1,17 +1,13 @@ package janggi.domain.mouveRule; import janggi.domain.BoardView; -import janggi.domain.Palace; import janggi.domain.vo.Position; public class PalaceMoveRule implements MoveRule { @Override public boolean canMove(Position from, Position to, BoardView board) { - if (!Palace.isInsidePalace(from) || !Palace.isInsidePalace(to)) { - return false; - } - return isStraightOneStep(from, to) || Palace.canMoveDiagonally(from, to); + return isStraightOneStep(from, to) || board.canMoveDiagonallyInPalace(from, to); } private boolean isStraightOneStep(Position from, Position to) { From 24773e84a1c6de89740fec8e135b2e0e1150ae17 Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 09:59:02 +0900 Subject: [PATCH 07/20] =?UTF-8?q?refactor:=20PalaceMoveRule=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=9E=A5/=EC=82=AC=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EA=B7=9C=EC=B9=99=EC=9D=84=20=EA=B0=81=20?= =?UTF-8?q?=EA=B8=B0=EB=AC=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 장과 사가 동일한 PalaceMoveRule을 공유하던 구조를 분리 - 이후 사의 이동 규칙 변경이 장에게 전파되지 않도록 개선 - 기물별 변경 이유에 따라 이동 규칙 책임을 분리 - 기존에는 장과 사가 PalaceMoveRule을 공통으로 사용하고 있었지만, 궁성 이동 규칙이 기물마다 달라질 가능성을 고려했을 때 공통 규칙을 사용하는 구조는 변경에 취약한 설계라고 판단. - 실제로 사의 이동 규칙이 변경될 경우, PalaceMoveRule을 수정하면 장의 이동 규칙까지 함께 영향을 받는 문제가 발생함. - 이를 해결하기 위해 PalaceMoveRule을 제거하고, 장과 사가 각각 자신의 이동 규칙을 가지도록 분리. - 이로 인해 각 기물의 책임이 명확해지고, 변경이 발생하더라도 영향 범위를 해당 기물로 한정할 수 있도록 개선됨 --- .../domain/mouveRule/AdvisorMoveRule.java | 21 ++ ...{PalaceMoveRule.java => KingMoveRule.java} | 5 +- .../domain/mouveRule/OneStepMoveRule.java | 14 -- .../java/janggi/domain/piece/Advisor.java | 4 +- src/main/java/janggi/domain/piece/King.java | 4 +- .../domain/mouveRule/AdvisorMoveRuleTest.java | 188 ++++++++++++++++++ ...oveRuleTest.java => KingMoveRuleTest.java} | 5 +- .../domain/mouveRule/OneStepMoveRuleTest.java | 28 --- 8 files changed, 220 insertions(+), 49 deletions(-) create mode 100644 src/main/java/janggi/domain/mouveRule/AdvisorMoveRule.java rename src/main/java/janggi/domain/mouveRule/{PalaceMoveRule.java => KingMoveRule.java} (76%) delete mode 100644 src/main/java/janggi/domain/mouveRule/OneStepMoveRule.java create mode 100644 src/test/java/janggi/domain/mouveRule/AdvisorMoveRuleTest.java rename src/test/java/janggi/domain/mouveRule/{PalaceMoveRuleTest.java => KingMoveRuleTest.java} (98%) delete mode 100644 src/test/java/janggi/domain/mouveRule/OneStepMoveRuleTest.java diff --git a/src/main/java/janggi/domain/mouveRule/AdvisorMoveRule.java b/src/main/java/janggi/domain/mouveRule/AdvisorMoveRule.java new file mode 100644 index 0000000000..b8e5681e25 --- /dev/null +++ b/src/main/java/janggi/domain/mouveRule/AdvisorMoveRule.java @@ -0,0 +1,21 @@ +package janggi.domain.mouveRule; + +import janggi.domain.BoardView; +import janggi.domain.vo.Position; + +public class AdvisorMoveRule implements MoveRule { + + @Override + public boolean canMove(Position from, Position to, BoardView board) { + if (!board.isInsidePalace(from) || !board.isInsidePalace(to)) { + return false; + } + return isStraightOneStep(from, to) || board.canMoveDiagonallyInPalace(from, to); + } + + private boolean isStraightOneStep(Position from, Position to) { + int rowDis = Math.abs(to.getRow() - from.getRow()); + int colDis = Math.abs(to.getCol() - from.getCol()); + return (rowDis + colDis) == 1; + } +} diff --git a/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java b/src/main/java/janggi/domain/mouveRule/KingMoveRule.java similarity index 76% rename from src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java rename to src/main/java/janggi/domain/mouveRule/KingMoveRule.java index 92ffb848a1..bbd6f2434a 100644 --- a/src/main/java/janggi/domain/mouveRule/PalaceMoveRule.java +++ b/src/main/java/janggi/domain/mouveRule/KingMoveRule.java @@ -3,10 +3,13 @@ import janggi.domain.BoardView; import janggi.domain.vo.Position; -public class PalaceMoveRule implements MoveRule { +public class KingMoveRule implements MoveRule { @Override public boolean canMove(Position from, Position to, BoardView board) { + if (!board.isInsidePalace(from) || !board.isInsidePalace(to)) { + return false; + } return isStraightOneStep(from, to) || board.canMoveDiagonallyInPalace(from, to); } diff --git a/src/main/java/janggi/domain/mouveRule/OneStepMoveRule.java b/src/main/java/janggi/domain/mouveRule/OneStepMoveRule.java deleted file mode 100644 index 338b2b1fe2..0000000000 --- a/src/main/java/janggi/domain/mouveRule/OneStepMoveRule.java +++ /dev/null @@ -1,14 +0,0 @@ -package janggi.domain.mouveRule; - - -import janggi.domain.BoardView; -import janggi.domain.vo.Position; - -public class OneStepMoveRule implements MoveRule { - @Override - public boolean canMove(Position from, Position to, BoardView board) { - int rowDis = Math.abs(to.getRow() - from.getRow()); - int colDis = Math.abs(to.getCol() - from.getCol()); - return rowDis <= 1 && colDis <= 1; - } -} diff --git a/src/main/java/janggi/domain/piece/Advisor.java b/src/main/java/janggi/domain/piece/Advisor.java index 1c741b710e..55482a094d 100644 --- a/src/main/java/janggi/domain/piece/Advisor.java +++ b/src/main/java/janggi/domain/piece/Advisor.java @@ -1,7 +1,7 @@ package janggi.domain.piece; +import janggi.domain.mouveRule.KingMoveRule; import janggi.domain.mouveRule.MoveRule; -import janggi.domain.mouveRule.PalaceMoveRule; public class Advisor extends Piece { @@ -16,6 +16,6 @@ public String toString() { @Override public MoveRule moveRule() { - return new PalaceMoveRule(); + return new KingMoveRule(); } } diff --git a/src/main/java/janggi/domain/piece/King.java b/src/main/java/janggi/domain/piece/King.java index bd6fae713b..234d17a83f 100644 --- a/src/main/java/janggi/domain/piece/King.java +++ b/src/main/java/janggi/domain/piece/King.java @@ -1,7 +1,7 @@ package janggi.domain.piece; +import janggi.domain.mouveRule.KingMoveRule; import janggi.domain.mouveRule.MoveRule; -import janggi.domain.mouveRule.PalaceMoveRule; public class King extends Piece { @@ -16,6 +16,6 @@ public String toString() { @Override public MoveRule moveRule() { - return new PalaceMoveRule(); + return new KingMoveRule(); } } diff --git a/src/test/java/janggi/domain/mouveRule/AdvisorMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/AdvisorMoveRuleTest.java new file mode 100644 index 0000000000..42b0fca95a --- /dev/null +++ b/src/test/java/janggi/domain/mouveRule/AdvisorMoveRuleTest.java @@ -0,0 +1,188 @@ +package janggi.domain.mouveRule; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.Board; +import janggi.domain.vo.Position; +import org.junit.jupiter.api.Test; + +class AdvisorMoveRuleTest { + private final Board board = Board.empty(); + private final MoveRule moveRule = new AdvisorMoveRule(); + + //한나라 + @Test + void 한_궁성_중앙은_상하좌우_모두_이동_가능() { + Position center = new Position(1, 4); + + assertThat(moveRule.canMove(center, new Position(0, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(2, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(1, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(1, 5), board)).isTrue(); + } + + @Test + void 한_궁성_위_변은_위로는_이동_불가() { + Position pos = new Position(0, 4); + + assertThat(moveRule.canMove(pos, new Position(1, 4), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(0, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(0, 5), board)).isTrue(); + } + + @Test + void 한_궁성_왼쪽_변은_왼쪽으로는_이동_불가() { + Position pos = new Position(1, 3); + + assertThat(moveRule.canMove(pos, new Position(1, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(0, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(2, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(1, 4), board)).isTrue(); + } + + @Test + void 한_궁성_꼭짓점은_두_방향만_이동_가능() { + Position pos = new Position(0, 3); + + assertThat(moveRule.canMove(pos, new Position(0, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(1, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(0, 4), board)).isTrue(); + } + + + //초나라 + @Test + void 초_궁성_중앙은_상하좌우_모두_이동_가능() { + Position center = new Position(8, 4); + + assertThat(moveRule.canMove(center, new Position(7, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(9, 4), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(8, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(8, 5), board)).isTrue(); + } + + @Test + void 초_궁성_아래_변은_아래로는_이동_불가() { + Position pos = new Position(9, 4); + + assertThat(moveRule.canMove(pos, new Position(8, 4), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 5), board)).isTrue(); + } + + @Test + void 초_궁성_왼쪽_변은_왼쪽으로는_이동_불가() { + Position pos = new Position(8, 3); + + assertThat(moveRule.canMove(pos, new Position(8, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(7, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(8, 4), board)).isTrue(); + } + + @Test + void 초_궁성_꼭짓점은_두_방향만_이동_가능() { + Position pos = new Position(9, 3); + + assertThat(moveRule.canMove(pos, new Position(9, 2), board)).isFalse(); + assertThat(moveRule.canMove(pos, new Position(8, 3), board)).isTrue(); + assertThat(moveRule.canMove(pos, new Position(9, 4), board)).isTrue(); + } + + // === 궁성 내 대각선 이동 === + @Test + void 한_궁성_중앙에서_각_꼭짓점으로_대각선_이동_가능() { + Position center = new Position(1, 4); + + assertThat(moveRule.canMove(center, new Position(0, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(0, 5), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(2, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(2, 5), board)).isTrue(); + } + + @Test + void 초_궁성_중앙에서_각_꼭짓점으로_대각선_이동_가능() { + Position center = new Position(8, 4); + + assertThat(moveRule.canMove(center, new Position(7, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(7, 5), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(9, 3), board)).isTrue(); + assertThat(moveRule.canMove(center, new Position(9, 5), board)).isTrue(); + } + + @Test + void 한_궁성_꼭짓점에서_중앙으로_대각선_이동_가능() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(1, 4), board)).isTrue(); + assertThat(moveRule.canMove( + new Position(2, 5), new Position(1, 4), board)).isTrue(); + } + + @Test + void 초_궁성_꼭짓점에서_중앙으로_대각선_이동_가능() { + assertThat(moveRule.canMove( + new Position(7, 3), new Position(8, 4), board)).isTrue(); + assertThat(moveRule.canMove( + new Position(9, 5), new Position(8, 4), board)).isTrue(); + } + + // === 궁성 밖으로 나가는 이동 불가 === + + @Test + void 한_궁성에서_궁성_밖으로_나가는_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(0, 2), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(2, 4), new Position(3, 4), board)).isFalse(); + } + + @Test + void 초_궁성에서_궁성_밖으로_나가는_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(9, 3), new Position(9, 2), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(7, 4), new Position(6, 4), board)).isFalse(); + } + + // === 대각선 선이 아닌 곳에서 대각선 이동 불가 === + + @Test + void 한_궁성_내부라도_대각선_선이_아닌_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(0, 4), new Position(1, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(1, 3), new Position(2, 4), board)).isFalse(); + } + + @Test + void 초_궁성_내부라도_대각선_선이_아닌_이동은_불가하다() { + assertThat(moveRule.canMove( + new Position(7, 4), new Position(8, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(8, 3), new Position(9, 4), board)).isFalse(); + } + + // === 2칸 이상 이동 불가 === + @Test + void 한_궁성에서는_대각선_두_칸_이상_이동할_수_없다() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(2, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(0, 5), new Position(2, 3), board)).isFalse(); + } + + @Test + void 초_궁성에서는_대각선_두_칸_이상_이동할_수_없다() { + assertThat(moveRule.canMove( + new Position(7, 3), new Position(9, 5), board)).isFalse(); + + assertThat(moveRule.canMove( + new Position(7, 5), new Position(9, 3), board)).isFalse(); + } +} + diff --git a/src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/KingMoveRuleTest.java similarity index 98% rename from src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java rename to src/test/java/janggi/domain/mouveRule/KingMoveRuleTest.java index 774499dff9..25f11cbd94 100644 --- a/src/test/java/janggi/domain/mouveRule/PalaceMoveRuleTest.java +++ b/src/test/java/janggi/domain/mouveRule/KingMoveRuleTest.java @@ -6,9 +6,9 @@ import janggi.domain.vo.Position; import org.junit.jupiter.api.Test; -class PalaceMoveRuleTest { +class KingMoveRuleTest { private final Board board = Board.empty(); - private final MoveRule moveRule = new PalaceMoveRule(); + private final MoveRule moveRule = new KingMoveRule(); //한나라 @Test @@ -185,3 +185,4 @@ class PalaceMoveRuleTest { new Position(7, 5), new Position(9, 3), board)).isFalse(); } } + diff --git a/src/test/java/janggi/domain/mouveRule/OneStepMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/OneStepMoveRuleTest.java deleted file mode 100644 index f538598f99..0000000000 --- a/src/test/java/janggi/domain/mouveRule/OneStepMoveRuleTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package janggi.domain.mouveRule; - -import static org.assertj.core.api.Assertions.assertThat; - -import janggi.domain.Board; -import janggi.domain.vo.Position; -import org.junit.jupiter.api.Test; - -class OneStepMoveRuleTest { - - private Board board = Board.empty(); - private final MoveRule moveRule = new OneStepMoveRule(); - - @Test - void 상하좌우_1칸_이동가능() { - assertThat(moveRule.canMove(new Position(4, 4), new Position(5, 4), board)).isTrue(); - assertThat(moveRule.canMove(new Position(4, 4), new Position(3, 4), board)).isTrue(); - assertThat(moveRule.canMove(new Position(4, 4), new Position(4, 5), board)).isTrue(); - assertThat(moveRule.canMove(new Position(4, 4), new Position(4, 3), board)).isTrue(); - } - - @Test - void _2칸이상_이동못함() { - assertThat(moveRule.canMove(new Position(4, 4), new Position(6, 4), board)).isFalse(); - assertThat(moveRule.canMove(new Position(4, 4), new Position(4, 6), board)).isFalse(); - assertThat(moveRule.canMove(new Position(4, 4), new Position(6, 6), board)).isFalse(); - } -} From ec987d6497c536b44a35a93df86f3cca083dfb86 Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 13:40:05 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EC=B0=A8=20=EA=B6=81=EC=84=B1=20?= =?UTF-8?q?=EB=8C=80=EA=B0=81=EC=84=A0=20=EC=9D=B4=EB=8F=99=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Palace에 2칸 대각선 판별, 경유지 조회 기능 추가 - TankMoveRule에 궁성 대각선 이동 로직 추가 --- src/main/java/janggi/domain/Palace.java | 46 +++++++++++++++++++ .../janggi/domain/mouveRule/TankMoveRule.java | 35 +++++++++----- src/test/java/janggi/domain/PalaceTest.java | 34 ++++++++++++++ .../domain/mouveRule/TankMoveRuleTest.java | 36 +++++++++++++++ 4 files changed, 139 insertions(+), 12 deletions(-) diff --git a/src/main/java/janggi/domain/Palace.java b/src/main/java/janggi/domain/Palace.java index eb032f0e80..da30bfbca1 100644 --- a/src/main/java/janggi/domain/Palace.java +++ b/src/main/java/janggi/domain/Palace.java @@ -52,4 +52,50 @@ private static boolean involvesCenter(Position from, Position to) { return from.equals(HAN_CENTER) || to.equals(HAN_CENTER) || from.equals(CHO_CENTER) || to.equals(CHO_CENTER); } + + public static boolean isDiagonalInPalace(Position from, Position to) { + if (!isInsidePalace(from) || !isInsidePalace(to)) { + return false; + } + if (!isSamePalace(from, to)) { + return false; + } + return canMoveDiagonally(from, to) || isTwoStepDiagonal(from, to); + } + + public static Position getDiagonalMidpoint(Position from, Position to) { + if (!isTwoStepDiagonal(from, to)) { + return null; + } + int midRow = (from.getRow() + to.getRow()) / 2; + int midCol = (from.getCol() + to.getCol()) / 2; + return new Position(midRow, midCol); + } + + private static boolean isSamePalace(Position a, Position b) { + return (isHanPalace(a.getRow()) && isHanPalace(b.getRow())) + || (isChoPalace(a.getRow()) && isChoPalace(b.getRow())); + } + + private static boolean isTwoStepDiagonal(Position from, Position to) { + if (!isInsidePalace(from) || !isInsidePalace(to)) { + return false; + } + if (!isSamePalace(from, to)) { + return false; + } + int rowDiff = Math.abs(to.getRow() - from.getRow()); + int colDiff = Math.abs(to.getCol() - from.getCol()); + if (rowDiff != 2 || colDiff != 2) { + return false; + } + + // 2칸 대각선이면 중간 경유지가 궁성 중앙이어야 함 + int midRow = (from.getRow() + to.getRow()) / 2; + int midCol = (from.getCol() + to.getCol()) / 2; + Position midpoint = new Position(midRow, midCol); + return midpoint.equals(HAN_CENTER) || midpoint.equals(CHO_CENTER); + } + + } diff --git a/src/main/java/janggi/domain/mouveRule/TankMoveRule.java b/src/main/java/janggi/domain/mouveRule/TankMoveRule.java index 775f5c44d7..930be8d691 100644 --- a/src/main/java/janggi/domain/mouveRule/TankMoveRule.java +++ b/src/main/java/janggi/domain/mouveRule/TankMoveRule.java @@ -6,30 +6,41 @@ public class TankMoveRule implements MoveRule { @Override public boolean canMove(Position from, Position to, BoardView board) { - int fromRow = from.getRow(); - int fromCol = from.getCol(); - int toRow = to.getRow(); - int toCol = to.getCol(); - - if (!isStraightLine(fromRow, fromCol, toRow, toCol)) { - return false; + if (isStraightLine(from, to)) { + return isStraightPathClear(board, from, to); + } + if (board.isDiagonalInPalace(from, to)) { + return isDiagonalPathClear(board, from, to); } + return false; + } - return isPathClear(board, fromRow, fromCol, toRow, toCol); + private boolean isDiagonalPathClear(BoardView board, Position from, Position to) { + Position midpoint = board.getDiagonalMidpointInPalace(from, to); + if (midpoint == null) { + return true; + } + return board.isEmptyPosition(midpoint); } - private boolean isStraightLine(int fromRow, int fromCol, int toRow, int toCol) { - return fromRow == toRow || fromCol == toCol; + private boolean isStraightLine(Position from, Position to) { + return from.getRow() == to.getRow() || from.getCol() == to.getCol(); } - private boolean isPathClear(BoardView board, int fromRow, int fromCol, int toRow, int toCol) { + + private boolean isStraightPathClear(BoardView board, Position from, Position to) { + int fromRow = from.getRow(); + int fromCol = from.getCol(); + int toRow = to.getRow(); + int toCol = to.getCol(); + if (fromRow == toRow) { return isHorizontalPathClear(board, fromRow, fromCol, toCol); } - return isVerticalPathClear(board, fromCol, fromRow, toRow); } + private boolean isHorizontalPathClear(BoardView board, int row, int fromCol, int toCol) { int start = Math.min(fromCol, toCol); int end = Math.max(fromCol, toCol); diff --git a/src/test/java/janggi/domain/PalaceTest.java b/src/test/java/janggi/domain/PalaceTest.java index 4e2373da34..15174f0f7d 100644 --- a/src/test/java/janggi/domain/PalaceTest.java +++ b/src/test/java/janggi/domain/PalaceTest.java @@ -65,4 +65,38 @@ class PalaceTest { assertThat(Palace.canMoveDiagonally( new Position(4, 4), new Position(5, 5))).isFalse(); } + + // 2칸 대각선 판별 차, 포 전용 - 한나라, 초나라 구분 없이 시작 + @Test + void 궁성_꼭짓점에서_반대_꼭짓점으로_2칸_대각선_이동_판별() { + assertThat(Palace.isDiagonalInPalace( + new Position(0, 3), new Position(2, 5))).isTrue(); + assertThat(Palace.isDiagonalInPalace( + new Position(0, 5), new Position(2, 3))).isTrue(); + assertThat(Palace.isDiagonalInPalace( + new Position(7, 3), new Position(9, 5))).isTrue(); + } + + @Test + void 궁성_1칸_대각선도_판별_가능() { + assertThat(Palace.isDiagonalInPalace( + new Position(1, 4), new Position(0, 3))).isTrue(); + } + + + @Test + void 궁성_2칸_대각선의_중간_경유지는_궁성_중앙() { + Position midpoint = Palace.getDiagonalMidpoint( + new Position(0, 3), new Position(2, 5)); + assertThat(midpoint).isEqualTo(new Position(1, 4)); + } + + + @Test + void 궁성_1칸_대각선은_중간_경유지_없음() { + Position midpoint = Palace.getDiagonalMidpoint( + new Position(1, 4), new Position(0, 3)); + assertThat(midpoint).isNull(); + } + } diff --git a/src/test/java/janggi/domain/mouveRule/TankMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/TankMoveRuleTest.java index a2ddc173be..1e05de3c6e 100644 --- a/src/test/java/janggi/domain/mouveRule/TankMoveRuleTest.java +++ b/src/test/java/janggi/domain/mouveRule/TankMoveRuleTest.java @@ -52,4 +52,40 @@ class TankMoveRuleTest { assertThat(moveRule.canMove(from, to, board)).isFalse(); } + + // 궁성 대각선 + + @Test + void 궁성_중앙에서_꼭짓점으로_대각선_1칸_이동_가능() { + from = new Position(1, 4); + to = new Position(0, 3); + assertThat(moveRule.canMove(from, to, board)).isTrue(); + } + + @Test + void 궁성_꼭짓점에서_반대_꼭짓점으로_대각선_2칸_이동_가능() { + // (0,3) → (2,5), 중간 (1,4) 비어있음 + from = new Position(0, 3); + to = new Position(2, 5); + assertThat(moveRule.canMove(from, to, board)).isTrue(); + } + + @Test + void 궁성_2칸_대각선_경로에_기물_있으면_이동_불가() { + Board board = Board.of(Map.of( + new Position(1, 4), new Soldier(Team.HAN) + )); + from = new Position(0, 3); + to = new Position(2, 5); + assertThat(moveRule.canMove(from, to, board)).isFalse(); + } + + @Test + void 궁성_밖에서는_대각선_이동_불가() { + from = new Position(4, 4); + to = new Position(5, 5); + assertThat(moveRule.canMove(from, to, board)).isFalse(); + } + + } From ebbfc8f0cc476e411f54ecaf21be6df71f3c1d2c Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 13:53:40 +0900 Subject: [PATCH 09/20] =?UTF-8?q?test=20:=20=EA=B6=81=EC=84=B1=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EA=B7=9C=EC=B9=99=20=EC=8B=A4=ED=8C=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=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 --- .../domain/mouveRule/CannonMoveRuleTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/test/java/janggi/domain/mouveRule/CannonMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/CannonMoveRuleTest.java index 3b0fcd5f19..ee8d84664c 100644 --- a/src/test/java/janggi/domain/mouveRule/CannonMoveRuleTest.java +++ b/src/test/java/janggi/domain/mouveRule/CannonMoveRuleTest.java @@ -57,4 +57,57 @@ class CannonMoveRuleTest { assertThat(moveRule.canMove(new Position(0, 0), new Position(0, 6), board)).isFalse(); } + + // 궁성 대각선 + @Test + void 궁성_대각선_2칸_이동시_중간에_기물_1개_넘으면_이동_가능() { + Board board = Board.of(Map.of( + new Position(1, 4), new Soldier(Team.HAN) + )); + assertThat(moveRule.canMove( + new Position(0, 3), new Position(2, 5), board)).isTrue(); + } + + @Test + void 초_궁성에서도_대각선_2칸_이동_가능() { + Board board = Board.of(Map.of( + new Position(8, 4), new Soldier(Team.CHO) + )); + assertThat(moveRule.canMove( + new Position(7, 3), new Position(9, 5), board)).isTrue(); + } + + @Test + // 아래 테스트들 PR + void 궁성_대각선_2칸_이동시_중간에_기물_없으면_이동_불가() { + assertThat(moveRule.canMove( + new Position(0, 3), new Position(2, 5), board)).isFalse(); + } + + @Test + void 궁성_대각선_2칸_이동시_다리가_포이면_이동_불가() { + Board board = Board.of(Map.of( + new Position(1, 4), new Cannon(Team.CHO) + )); + assertThat(moveRule.canMove( + new Position(0, 3), new Position(2, 5), board)).isFalse(); + } + + @Test + void 궁성_대각선_2칸_이동시_도착칸에_포가_있으면_이동_불가() { + Board board = Board.of(Map.of( + new Position(1, 4), new Soldier(Team.HAN), + new Position(2, 5), new Cannon(Team.CHO) + )); + assertThat(moveRule.canMove( + new Position(0, 3), new Position(2, 5), board)).isFalse(); + } + + @Test + void 궁성_대각선_1칸은_넘을_기물이_없으므로_이동_불가() { + assertThat(moveRule.canMove( + new Position(1, 4), new Position(0, 3), board)).isFalse(); + } + + } From 2dff7db867e988038d5dff8b802bc07f3f380479 Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 14:01:45 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20=ED=8F=AC=20=EA=B6=81=EC=84=B1=20?= =?UTF-8?q?=EB=8C=80=EA=B0=81=EC=84=A0=20=EC=9D=B4=EB=8F=99=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mouveRule/CannonMoveRule.java | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/main/java/janggi/domain/mouveRule/CannonMoveRule.java b/src/main/java/janggi/domain/mouveRule/CannonMoveRule.java index 90eb9eefc7..282a0f8ba7 100644 --- a/src/main/java/janggi/domain/mouveRule/CannonMoveRule.java +++ b/src/main/java/janggi/domain/mouveRule/CannonMoveRule.java @@ -9,15 +9,38 @@ public class CannonMoveRule implements MoveRule { @Override public boolean canMove(Position from, Position to, BoardView board) { + if (isStraightLine(from, to)) { + return canMoveStraight(from, to, board); + } + if (board.isDiagonalInPalace(from, to)) { + return canMovePalaceDiagonal(from, to, board); + } + + return true; + } + + private boolean canMovePalaceDiagonal(Position from, Position to, BoardView board) { + Position midpoint = board.getDiagonalMidpointInPalace(from, to); + if (midpoint == null) { + return false; + } + + if (board.isEmptyPosition(midpoint)) { + return false; + } + + Piece bridgePiece = board.findByPosition(midpoint); + Piece targetPiece = board.findByPosition(to); + return !isCannon(bridgePiece) && !isCannon(targetPiece); + } + + + private boolean canMoveStraight(Position from, Position to, BoardView board) { int fromRow = from.getRow(); int fromCol = from.getCol(); int toRow = to.getRow(); int toCol = to.getCol(); - if (!isStraightLine(fromRow, fromCol, toRow, toCol)) { - return false; - } - int jumpedPieceCount = countPiecesBetween(board, fromRow, fromCol, toRow, toCol); if (jumpedPieceCount != 1) { return false; @@ -25,14 +48,12 @@ public boolean canMove(Position from, Position to, BoardView board) { Piece bridgePiece = findBridgePiece(board, fromRow, fromCol, toRow, toCol); Piece targetPiece = board.findByPosition(to); - if (isCannon(bridgePiece) || isCannon(targetPiece)) { - return false; - } - return true; + return !isCannon(bridgePiece) && !isCannon(targetPiece); } - private boolean isStraightLine(int fromRow, int fromCol, int toRow, int toCol) { - return fromRow == toRow || fromCol == toCol; + + private boolean isStraightLine(Position from, Position to) { + return from.getRow() == to.getRow() || from.getCol() == to.getCol(); } private int countPiecesBetween(BoardView board, int fromRow, int fromCol, int toRow, int toCol) { From 6082531b180aff27b764254b671668ef5cee0813 Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 15:03:48 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat=20:=20=EC=A1=B8=20=EA=B6=81=EC=84=B1?= =?UTF-8?q?=20=EB=8C=80=EA=B0=81=EC=84=B1=20=EC=9D=B4=EB=8F=99=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mouveRule/SoldierMoveRule.java | 15 ++++- .../domain/mouveRule/SoldierMoveRuleTest.java | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main/java/janggi/domain/mouveRule/SoldierMoveRule.java b/src/main/java/janggi/domain/mouveRule/SoldierMoveRule.java index 7d934f51b4..372bb49d96 100644 --- a/src/main/java/janggi/domain/mouveRule/SoldierMoveRule.java +++ b/src/main/java/janggi/domain/mouveRule/SoldierMoveRule.java @@ -16,7 +16,12 @@ public SoldierMoveRule(Team team) { public boolean canMove(Position from, Position to, BoardView board) { int rowDis = to.getRow() - from.getRow(); int colDis = to.getCol() - from.getCol(); - return isForward(rowDis, colDis) || isSideStep(rowDis, colDis); + + if (isForward(rowDis, colDis) || isSideStep(rowDis, colDis)) { + return true; + } + + return isForwardDiagonalInPalace(from, to, rowDis, board); } // 한이 위쪽배치임 -> 행증가가 전진 @@ -30,4 +35,12 @@ private boolean isSideStep(int rowDis, int colDis) { return rowDis == 0 && Math.abs(colDis) == 1; } + private boolean isForwardDiagonalInPalace(Position from, Position to, int rowDis, BoardView board) { + if (!board.canMoveDiagonallyInPalace(from, to)) { + return false; + } + int forwardDirection = (team == Team.CHO) ? -1 : 1; + return rowDis == forwardDirection; + } + } diff --git a/src/test/java/janggi/domain/mouveRule/SoldierMoveRuleTest.java b/src/test/java/janggi/domain/mouveRule/SoldierMoveRuleTest.java index e0569350af..c6f28ed99f 100644 --- a/src/test/java/janggi/domain/mouveRule/SoldierMoveRuleTest.java +++ b/src/test/java/janggi/domain/mouveRule/SoldierMoveRuleTest.java @@ -50,5 +50,62 @@ class SoldierMoveRuleTest { } + @Test + void 초나라_졸은_한_궁성에서_전진_대각선_이동_가능() { + // 초 전진 = 행 감소 + MoveRule rule = new SoldierMoveRule(Team.CHO); + assertThat(rule.canMove( + new Position(2, 5), new Position(1, 4), board)).isTrue(); + } + + @Test + void 한나라_졸은_초_궁성에서_전진_대각선_이동_가능() { + // 한 전진 = 행 증가 + MoveRule rule = new SoldierMoveRule(Team.HAN); + assertThat(rule.canMove( + new Position(7, 3), new Position(8, 4), board)).isTrue(); + } + + @Test + void 궁성_중앙에서_꼭짓점으로_전진_대각선_가능() { + MoveRule rule = new SoldierMoveRule(Team.CHO); + // 행 감소 = 전진 + assertThat(rule.canMove( + new Position(1, 4), new Position(0, 3), board)).isTrue(); + } + + // PR + @Test + void 초나라_졸은_한_궁성에서_후진_대각선_이동_불가() { + MoveRule rule = new SoldierMoveRule(Team.CHO); + // 행 증가 대각선 = 후진 + assertThat(rule.canMove( + new Position(0, 3), new Position(1, 4), board)).isFalse(); + } + + + @Test + void 한나라_졸은_초_궁성에서_후진_대각선_이동_불가() { + MoveRule rule = new SoldierMoveRule(Team.HAN); + // 행 감소 대각선 = 후진 + assertThat(rule.canMove( + new Position(9, 5), new Position(8, 4), board)).isFalse(); + } + + @Test + void 궁성_밖에서는_대각선_이동_불가() { + MoveRule rule = new SoldierMoveRule(Team.CHO); + // 궁성 밖 대각선 + assertThat(rule.canMove( + new Position(5, 4), new Position(4, 5), board)).isFalse(); + } + + @Test + void 궁성_대각선_선이_아닌_곳에서는_대각선_불가() { + MoveRule rule = new SoldierMoveRule(Team.HAN); + // 궁성 내부지만 중앙 미경유 대각선 + assertThat(rule.canMove( + new Position(7, 4), new Position(8, 5), board)).isFalse(); + } } From 38ace5515aa46c1820bb8cbe4ef9bca170c274f4 Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 15:09:21 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20=EC=99=95=20=EC=9E=A1=ED=9E=98=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=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Board.move()가 잡힌 기물을 반환하도록 변경 - 왕 잡힘 판별 및 승자 기록기능구현 --- .../janggi/controller/JanggiController.java | 4 +- src/main/java/janggi/domain/Board.java | 4 +- src/main/java/janggi/domain/JanggiGame.java | 23 ++++++++ src/test/java/janggi/domain/BoardTest.java | 30 ++++++++++ .../java/janggi/domain/JanggiGameTest.java | 57 +++++++++++++++++++ 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/test/java/janggi/domain/JanggiGameTest.java diff --git a/src/main/java/janggi/controller/JanggiController.java b/src/main/java/janggi/controller/JanggiController.java index 406a6e6c19..c7c3d1b4fd 100644 --- a/src/main/java/janggi/controller/JanggiController.java +++ b/src/main/java/janggi/controller/JanggiController.java @@ -19,7 +19,9 @@ public void run() { while (!janggiGame.isFinished()) { Team currentTeam = janggiGame.findCurrentTeam(); attemptMove(board, currentTeam); - janggiGame.changeTurn(); + if (!janggiGame.isFinished()) { + janggiGame.changeTurn(); + } } } diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java index 33665d3bf1..0f8b99180d 100644 --- a/src/main/java/janggi/domain/Board.java +++ b/src/main/java/janggi/domain/Board.java @@ -63,7 +63,7 @@ public Position getDiagonalMidpointInPalace(Position from, Position to) { return Palace.getDiagonalMidpoint(from, to); } - public void move(Position from, Position to, Team currentTeam) { + public Piece move(Position from, Position to, Team currentTeam) { Piece fromPiece = findByPosition(from); Piece toPiece = findByPosition(to); @@ -75,6 +75,8 @@ public void move(Position from, Position to, Team currentTeam) { place(from, new EmptyPosition(Team.OTHER)); place(to, fromPiece); + + return toPiece; } private void place(Position position, Piece piece) { diff --git a/src/main/java/janggi/domain/JanggiGame.java b/src/main/java/janggi/domain/JanggiGame.java index d9d7f87c68..6919c34ab1 100644 --- a/src/main/java/janggi/domain/JanggiGame.java +++ b/src/main/java/janggi/domain/JanggiGame.java @@ -1,10 +1,13 @@ package janggi.domain; +import janggi.domain.piece.King; +import janggi.domain.piece.Piece; import janggi.domain.piece.Team; public class JanggiGame { private Team currentTurn = Team.CHO; private boolean isFinished = false; + private Team winner = null; public boolean isFinished() { return isFinished; @@ -21,4 +24,24 @@ public void changeTurn() { } currentTurn = Team.CHO; } + + public Team findWinner() { + return winner; + } + + public void processCaptured(Piece capturedPiece) { + if (!(capturedPiece instanceof King)) { + return; + } + isFinished = true; + winner = findOpponent(capturedPiece.findTeam()); + } + + private Team findOpponent(Team team) { + if (team == Team.CHO) { + return Team.HAN; + } + return Team.CHO; + } + } diff --git a/src/test/java/janggi/domain/BoardTest.java b/src/test/java/janggi/domain/BoardTest.java index bb074562b5..486e262bcc 100644 --- a/src/test/java/janggi/domain/BoardTest.java +++ b/src/test/java/janggi/domain/BoardTest.java @@ -5,8 +5,11 @@ import janggi.domain.piece.King; import janggi.domain.piece.Piece; +import janggi.domain.piece.Soldier; +import janggi.domain.piece.Tank; import janggi.domain.piece.Team; import janggi.domain.vo.Position; +import java.util.Map; import org.junit.jupiter.api.Test; public class BoardTest { @@ -30,4 +33,31 @@ public class BoardTest { assertTrue(board.isEmptyPosition(position)); } + @Test + void 이동시_잡힌_기물을_반환한다() { + Board board = Board.of(Map.of( + new Position(0, 0), new Tank(Team.HAN), + new Position(0, 3), new Soldier(Team.CHO) + )); + + Piece captured = board.move( + new Position(0, 0), new Position(0, 3), Team.HAN); + + assertThat(captured).isInstanceOf(Soldier.class); + assertThat(captured.findTeam()).isEqualTo(Team.CHO); + } + + @Test + void 빈칸으로_이동시_빈칸을_반환한다() { + Board board = Board.of(Map.of( + new Position(0, 0), new Tank(Team.HAN) + )); + + Piece captured = board.move( + new Position(0, 0), new Position(0, 3), Team.HAN); + + assertThat(captured.isEmpty()).isTrue(); + } + + } diff --git a/src/test/java/janggi/domain/JanggiGameTest.java b/src/test/java/janggi/domain/JanggiGameTest.java new file mode 100644 index 0000000000..899fd63c3e --- /dev/null +++ b/src/test/java/janggi/domain/JanggiGameTest.java @@ -0,0 +1,57 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.piece.King; +import janggi.domain.piece.Soldier; +import janggi.domain.piece.Team; +import org.junit.jupiter.api.Test; + +class JanggiGameTest { + + @Test + void 게임_시작시_초나라_턴이다() { + JanggiGame game = new JanggiGame(); + assertThat(game.findCurrentTeam()).isEqualTo(Team.CHO); + } + + @Test + void 턴_변경시_한나라로_바뀐다() { + JanggiGame game = new JanggiGame(); + game.changeTurn(); + assertThat(game.findCurrentTeam()).isEqualTo(Team.HAN); + } + + @Test + void 왕이_잡히면_게임이_종료된다() { + JanggiGame game = new JanggiGame(); + King capturedKing = new King(Team.HAN); + + game.processCaptured(capturedKing); + + assertThat(game.isFinished()).isTrue(); + } + + @Test + void 왕이_잡히면_상대_진영이_승자다() { + JanggiGame game = new JanggiGame(); + King capturedKing = new King(Team.HAN); + + game.processCaptured(capturedKing); + + assertThat(game.findWinner()).isEqualTo(Team.CHO); + } + + // PR + @Test + void 일반_기물이_잡혀도_게임은_계속된다() { + JanggiGame game = new JanggiGame(); + Soldier capturedSoldier = new Soldier(Team.HAN); + + game.processCaptured(capturedSoldier); + + assertThat(game.isFinished()).isFalse(); + } + + +} From d766f80dffe20316570b3093368186207f0d83e6 Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 15:20:04 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=EA=B8=B0=EB=AC=BC=20=EC=A0=90?= =?UTF-8?q?=EC=88=98=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/Board.java | 14 +++++ .../java/janggi/domain/piece/Advisor.java | 6 ++ src/main/java/janggi/domain/piece/Cannon.java | 5 ++ .../java/janggi/domain/piece/Elephant.java | 5 ++ .../janggi/domain/piece/EmptyPosition.java | 5 ++ src/main/java/janggi/domain/piece/Horse.java | 5 ++ src/main/java/janggi/domain/piece/King.java | 5 ++ src/main/java/janggi/domain/piece/Piece.java | 2 + .../java/janggi/domain/piece/Soldier.java | 5 ++ src/main/java/janggi/domain/piece/Tank.java | 5 ++ src/test/java/janggi/domain/BoardTest.java | 14 +++++ .../java/janggi/domain/piece/PieceTest.java | 56 +++++++++++++++++++ 12 files changed, 127 insertions(+) create mode 100644 src/test/java/janggi/domain/piece/PieceTest.java diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java index 0f8b99180d..f8ec3eea9e 100644 --- a/src/main/java/janggi/domain/Board.java +++ b/src/main/java/janggi/domain/Board.java @@ -97,4 +97,18 @@ private void validateCommonMove(Team currentTeam, Piece fromPiece, Piece toPiece throw new IllegalArgumentException("이미 도착지점에 플레이어님의 진영 기물이 있습니다."); } } + + public int calculateScore(Team team) { + int totalScore = 0; + for (List row : board) { + for (Piece piece : row) { + if (piece.isSameTeam(team)) { + totalScore += piece.score(); + } + } + } + return totalScore; + } + + } diff --git a/src/main/java/janggi/domain/piece/Advisor.java b/src/main/java/janggi/domain/piece/Advisor.java index 55482a094d..86a36dd859 100644 --- a/src/main/java/janggi/domain/piece/Advisor.java +++ b/src/main/java/janggi/domain/piece/Advisor.java @@ -18,4 +18,10 @@ public String toString() { public MoveRule moveRule() { return new KingMoveRule(); } + + @Override + public int score() { + return 3; + } + } diff --git a/src/main/java/janggi/domain/piece/Cannon.java b/src/main/java/janggi/domain/piece/Cannon.java index 165b786055..88a68df497 100644 --- a/src/main/java/janggi/domain/piece/Cannon.java +++ b/src/main/java/janggi/domain/piece/Cannon.java @@ -18,4 +18,9 @@ public String toString() { public MoveRule moveRule() { return new CannonMoveRule(); } + + @Override + public int score() { + return 7; + } } diff --git a/src/main/java/janggi/domain/piece/Elephant.java b/src/main/java/janggi/domain/piece/Elephant.java index 9b96c64cae..1f93ae80ec 100644 --- a/src/main/java/janggi/domain/piece/Elephant.java +++ b/src/main/java/janggi/domain/piece/Elephant.java @@ -18,4 +18,9 @@ public String toString() { public MoveRule moveRule() { return new ElephantMoveRule(); } + + @Override + public int score() { + return 3; + } } diff --git a/src/main/java/janggi/domain/piece/EmptyPosition.java b/src/main/java/janggi/domain/piece/EmptyPosition.java index f3451cfac5..14e24e833e 100644 --- a/src/main/java/janggi/domain/piece/EmptyPosition.java +++ b/src/main/java/janggi/domain/piece/EmptyPosition.java @@ -22,4 +22,9 @@ public String toString() { public MoveRule moveRule() { return null; } + + @Override + public int score() { + return 0; + } } diff --git a/src/main/java/janggi/domain/piece/Horse.java b/src/main/java/janggi/domain/piece/Horse.java index 950462be01..4fbc30e561 100644 --- a/src/main/java/janggi/domain/piece/Horse.java +++ b/src/main/java/janggi/domain/piece/Horse.java @@ -19,4 +19,9 @@ public MoveRule moveRule() { return new HorseMoveRule(); } + @Override + public int score() { + return 5; + } + } diff --git a/src/main/java/janggi/domain/piece/King.java b/src/main/java/janggi/domain/piece/King.java index 234d17a83f..ae5c11a71a 100644 --- a/src/main/java/janggi/domain/piece/King.java +++ b/src/main/java/janggi/domain/piece/King.java @@ -18,4 +18,9 @@ public String toString() { public MoveRule moveRule() { return new KingMoveRule(); } + + @Override + public int score() { + return 0; + } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 1400428381..83f9ab816a 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -32,4 +32,6 @@ public boolean canMove(Position from, Position to, BoardView board) { @Override public abstract String toString(); + public abstract int score(); + } diff --git a/src/main/java/janggi/domain/piece/Soldier.java b/src/main/java/janggi/domain/piece/Soldier.java index fc8d384d87..9d9428cd30 100644 --- a/src/main/java/janggi/domain/piece/Soldier.java +++ b/src/main/java/janggi/domain/piece/Soldier.java @@ -18,4 +18,9 @@ public String toString() { public MoveRule moveRule() { return new SoldierMoveRule(findTeam()); } + + @Override + public int score() { + return 2; + } } diff --git a/src/main/java/janggi/domain/piece/Tank.java b/src/main/java/janggi/domain/piece/Tank.java index 66248de249..7186cfe68d 100644 --- a/src/main/java/janggi/domain/piece/Tank.java +++ b/src/main/java/janggi/domain/piece/Tank.java @@ -18,4 +18,9 @@ public String toString() { public MoveRule moveRule() { return new TankMoveRule(); } + + @Override + public int score() { + return 13; + } } diff --git a/src/test/java/janggi/domain/BoardTest.java b/src/test/java/janggi/domain/BoardTest.java index 486e262bcc..f632696339 100644 --- a/src/test/java/janggi/domain/BoardTest.java +++ b/src/test/java/janggi/domain/BoardTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import janggi.domain.piece.Cannon; import janggi.domain.piece.King; import janggi.domain.piece.Piece; import janggi.domain.piece.Soldier; @@ -59,5 +60,18 @@ public class BoardTest { assertThat(captured.isEmpty()).isTrue(); } + //점수 계산 + @Test + void 특정_진영의_점수를_계산한다() { + // 한나라: 차(13) + 졸(2) = 15 + Board board = Board.of(Map.of( + new Position(0, 0), new Tank(Team.HAN), + new Position(3, 4), new Soldier(Team.HAN), + new Position(9, 0), new Cannon(Team.CHO) + )); + assertThat(board.calculateScore(Team.HAN)).isEqualTo(15); + assertThat(board.calculateScore(Team.CHO)).isEqualTo(7); + } + } diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java new file mode 100644 index 0000000000..6ba890eca8 --- /dev/null +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -0,0 +1,56 @@ +package janggi.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class PieceTest { + + @Test + void 차의_점수는_13이다() { + Piece piece = new Tank(Team.HAN); + assertThat(piece.score()).isEqualTo(13); + } + + @Test + void 포의_점수는_7이다() { + Piece piece = new Cannon(Team.HAN); + assertThat(piece.score()).isEqualTo(7); + } + + @Test + void 마의_점수는_5이다() { + Piece piece = new Horse(Team.HAN); + assertThat(piece.score()).isEqualTo(5); + } + + @Test + void 상의_점수는_3이다() { + Piece piece = new Elephant(Team.HAN); + assertThat(piece.score()).isEqualTo(3); + } + + @Test + void 사의_점수는_3이다() { + Piece piece = new Advisor(Team.HAN); + assertThat(piece.score()).isEqualTo(3); + } + + @Test + void 졸의_점수는_2이다() { + Piece piece = new Soldier(Team.HAN); + assertThat(piece.score()).isEqualTo(2); + } + + @Test + void 장의_점수는_0이다() { + Piece piece = new King(Team.HAN); + assertThat(piece.score()).isEqualTo(0); + } + + @Test + void 빈칸의_점수는_0이다() { + Piece piece = new EmptyPosition(Team.OTHER); + assertThat(piece.score()).isEqualTo(0); + } +} From 732fc7f101ab2dc053c7fd2419653e0be7dcaaeb Mon Sep 17 00:00:00 2001 From: softmoca Date: Thu, 2 Apr 2026 15:32:01 +0900 Subject: [PATCH 14/20] =?UTF-8?q?docs=20:=202-1=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B0=8F=202-2=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 60f198aefb..7f73110ce5 100644 --- a/README.md +++ b/README.md @@ -64,48 +64,95 @@ ### 궁성 영역 정의 -- [ ] 궁성(宮城) 영역 정의 +- [x] 궁성(宮城) 영역 정의 - 궁성 좌표 범위 정의 -- [ ] 궁성 내 대각선 경로 정의 +- [x] 궁성 내 대각선 경로 정의 - 궁성에는 대각선 선이 존재하며, 대각선 이동은 이 선 위의 좌표에서만 가능 - 대각선 허용 좌표쌍 정의 -- [ ] 특정 좌표가 궁성 내부인지 판별하는 기능 -- [ ] 두 좌표 간 궁성 대각선 이동 가능 여부 판별 +- [x] 특정 좌표가 궁성 내부인지 판별하는 기능 +- [x] 두 좌표 간 궁성 대각선 이동 가능 여부 판별 ### 궁성 관련 기물 이동 규칙 변경 및 추가 -- [ ] 장(King) +- [x] 장(King) - 궁성 내부에서만 이동 가능 - 상하좌우 1칸 이동 (기존 유지) - 궁성 대각선 경로에서 대각선 1칸 이동 가능 -- [ ] 사(Advisor) +- [x] 사(Advisor) - 궁성 내부에서만 이동 가능 - 상하좌우 1칸 이동 (기존 유지) - 궁성 대각선 경로에서 대각선 1칸 이동 가능 -- [ ] 차(Tank) +- [x] 차(Tank) - 기존 직선 이동 규칙 유지 - 궁성 내부에서 대각선 경로 이동 가능 -- [ ] 포(Cannon) +- [x] 포(Cannon) - 기존 직선 + 1개 기물 넘기 규칙 유지 - 궁성 내부에서 대각선 경로 이동 가능 -- [ ] 졸(Soldier) +- [x] 졸(Soldier) - 기존 전진 + 좌우 이동 유지 - 궁성 내부에서 대각선 "전진" 이동 가능 - 후진 대각선 불가 ### 왕 잡힘 시 게임 종료 -- [ ] 이동 결과 도착 칸의 기물이 왕(King)인지 판별 -- [ ] 왕이 잡혔을 때 승자 진영 결정 -- [ ] JanggiGame에 게임 종료 상태 반영 +- [x] 이동 결과 도착 칸의 기물이 왕(King)인지 판별 +- [x] 왕이 잡혔을 때 승자 진영 결정 +- [x] JanggiGame에 게임 종료 상태 반영 ### 점수 계산 -- [ ] Piece에 `score()` 추상 메서드 추가 -- [ ] 기물별 점수 정의 -- [ ] Board에서 특정 진영 점수 합산 기능 -- [ ] 양 진영 점수 조회 기능 +- [x] Piece에 `score()` 추상 메서드 추가 +- [x] 기물별 점수 정의 +- [x] Board에서 특정 진영 점수 합산 기능 +- [x] 양 진영 점수 조회 기능 +## 2.2단계 - DB 적용 +### 1. DB 연결 설정 +- [ ] SQLite JDBC 의존성 추가 +- [ ] DB 연결 관리 클래스 생성 +### 2. 테이블 설계 및 적용 + +- [ ] game 테이블 — 게임 메타 정보 +- [ ] piece 테이블 — 기물 배치 정보 + +### 3. GameRepository 구현 + +- [ ] save + - 새 게임 생성 + - 생성된 gameID 반환 +- [ ] findById + - 특정 게임 조회 +- [ ] findPlayingGames + - 진행 중인 게임 목록 조회 +- [ ] updateTurn + - 턴 변경 시 갱신 +- [ ] updateFinished + - 게임 종료 시 갱신 + +### 4. PieceRepository 구현 + +- [ ] saveAll + - 게임의 모든 기물을 DB에 일괄 저장 + +- [ ] findByGameId + - 특정 게임의 모든 기물 조회 + +- [ ] movePiece + - 기물 이동 반영 + - 도착 칸 기물 삭제 + - 출발 칸 기물 좌표 갱신 + +### 5. Controller 흐름 변경 (새 게임 / 이어하기) + +- [ ] 프로그램 시작 시 + - DB에서 진행 중인 게임 목록 조회 + - 존재하면 → 이어하기 / 새 게임 선택 + - 없으면 → 새 게임 시작 +- [ ] 매 턴 이동 후 + - DB에 기물 이동 반영 + - DB에 턴 변경 반영 +- [ ] 게임 종료 시 + - DB에 게임 종료 상태 반영 From 0bc0f2ec4a3a707263d1dafe10a686a23fa87012 Mon Sep 17 00:00:00 2001 From: softmoca Date: Fri, 3 Apr 2026 15:26:23 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20DB=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ build.gradle | 3 +++ .../infrastructure/DBConnectionManager.java | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/main/java/janggi/infrastructure/DBConnectionManager.java diff --git a/.gitignore b/.gitignore index 6c01878138..ce976069bc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ out/ ### VS Code ### .vscode/ + +# SQLite +*.db diff --git a/build.gradle b/build.gradle index ce846f70cc..4930ffa71b 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,9 @@ repositories { } dependencies { + implementation 'org.xerial:sqlite-jdbc:3.45.1.0' + implementation 'org.slf4j:slf4j-simple:2.0.12' + 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/infrastructure/DBConnectionManager.java b/src/main/java/janggi/infrastructure/DBConnectionManager.java new file mode 100644 index 0000000000..c5173f4964 --- /dev/null +++ b/src/main/java/janggi/infrastructure/DBConnectionManager.java @@ -0,0 +1,18 @@ +package janggi.infrastructure; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DBConnectionManager { + + private static final String URL = "jdbc:sqlite:janggi.db"; + + public static Connection getConnection() { + try { + return DriverManager.getConnection(URL); + } catch (SQLException e) { + throw new RuntimeException("DB 연결에 실패했습니다.", e); + } + } +} From 14673023002ae9ff1889792d1bead12a70cc433b Mon Sep 17 00:00:00 2001 From: softmoca Date: Fri, 3 Apr 2026 15:28:11 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20DB=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/Application.java | 2 + .../janggi/infrastructure/DBInitializer.java | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/main/java/janggi/infrastructure/DBInitializer.java diff --git a/src/main/java/janggi/Application.java b/src/main/java/janggi/Application.java index 59fa0394a5..a56089f6e9 100644 --- a/src/main/java/janggi/Application.java +++ b/src/main/java/janggi/Application.java @@ -1,9 +1,11 @@ package janggi; import janggi.controller.JanggiController; +import janggi.infrastructure.DBInitializer; public class Application { public static void main(String[] args) { + DBInitializer.initialize(); JanggiController janggiController = new JanggiController(); janggiController.run(); } diff --git a/src/main/java/janggi/infrastructure/DBInitializer.java b/src/main/java/janggi/infrastructure/DBInitializer.java new file mode 100644 index 0000000000..d7d6a35d8d --- /dev/null +++ b/src/main/java/janggi/infrastructure/DBInitializer.java @@ -0,0 +1,43 @@ +package janggi.infrastructure; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public class DBInitializer { + + public static void initialize() { + try (Connection connection = DBConnectionManager.getConnection(); + Statement statement = connection.createStatement()) { + + statement.execute(createGameTable()); + statement.execute(createPieceTable()); + + } catch (SQLException e) { + throw new RuntimeException("테이블 초기화에 실패했습니다.", e); + } + } + + private static String createGameTable() { + return "CREATE TABLE IF NOT EXISTS game (" + + "game_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "current_turn TEXT NOT NULL, " + + "game_status TEXT NOT NULL, " + + "winner TEXT, " + + "created_at TEXT DEFAULT (datetime('now', 'localtime')), " + + "updated_at TEXT DEFAULT (datetime('now', 'localtime'))" + + ")"; + } + + private static String createPieceTable() { + return "CREATE TABLE IF NOT EXISTS piece (" + + "piece_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "game_id INTEGER NOT NULL, " + + "row_pos INTEGER NOT NULL, " + + "col_pos INTEGER NOT NULL, " + + "piece_type TEXT NOT NULL, " + + "team TEXT NOT NULL, " + + "FOREIGN KEY (game_id) REFERENCES game(game_id)" + + ")"; + } +} From bb8dedc1a7f3774b953424e01d229f54f6016a9a Mon Sep 17 00:00:00 2001 From: softmoca Date: Fri, 3 Apr 2026 15:30:55 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=9E=84=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/janggi/domain/JanggiGame.java | 22 ++++ .../janggi/repository/GameRepository.java | 112 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/main/java/janggi/repository/GameRepository.java diff --git a/src/main/java/janggi/domain/JanggiGame.java b/src/main/java/janggi/domain/JanggiGame.java index 6919c34ab1..10bdff41ab 100644 --- a/src/main/java/janggi/domain/JanggiGame.java +++ b/src/main/java/janggi/domain/JanggiGame.java @@ -5,10 +5,23 @@ import janggi.domain.piece.Team; public class JanggiGame { + private Long gameId; private Team currentTurn = Team.CHO; private boolean isFinished = false; private Team winner = null; + // 기본 생성자 - 새게임 + public JanggiGame() { + } + + // DB 복원용 + public JanggiGame(Long gameId, Team currentTurn, boolean isFinished, Team winner) { + this.gameId = gameId; + this.currentTurn = currentTurn; + this.isFinished = isFinished; + this.winner = winner; + } + public boolean isFinished() { return isFinished; } @@ -25,6 +38,15 @@ public void changeTurn() { currentTurn = Team.CHO; } + public Long findGameId() { + return gameId; + } + + public void assignId(Long gameId) { + this.gameId = gameId; + } + + public Team findWinner() { return winner; } diff --git a/src/main/java/janggi/repository/GameRepository.java b/src/main/java/janggi/repository/GameRepository.java new file mode 100644 index 0000000000..b63285e807 --- /dev/null +++ b/src/main/java/janggi/repository/GameRepository.java @@ -0,0 +1,112 @@ +package janggi.repository; + +import janggi.domain.JanggiGame; +import janggi.domain.piece.Team; +import janggi.infrastructure.DBConnectionManager; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +public class GameRepository { + + public Long save(JanggiGame game) { + String sql = "INSERT INTO game (current_turn, game_status, winner) VALUES (?, ?, ?)"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + + pstmt.setString(1, game.findCurrentTeam().name()); + pstmt.setString(2, toGameStatus(game)); + pstmt.setString(3, toWinnerString(game)); + pstmt.executeUpdate(); + + ResultSet rs = pstmt.getGeneratedKeys(); + if (rs.next()) { + return rs.getLong(1); + } + throw new SQLException("게임 ID 생성에 실패했습니다."); + + } catch (SQLException e) { + throw new RuntimeException("게임 저장에 실패했습니다.", e); + } + } + + public List findPlayingGames() { + String sql = "SELECT game_id, current_turn, game_status, winner FROM game WHERE game_status = 'PLAYING'"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + ResultSet rs = pstmt.executeQuery(); + List games = new ArrayList<>(); + + while (rs.next()) { + games.add(toJanggiGame(rs)); + } + return games; + + } catch (SQLException e) { + throw new RuntimeException("진행 중인 게임 조회에 실패했습니다.", e); + } + } + + public void updateTurn(JanggiGame game) { + String sql = "UPDATE game SET current_turn = ?, updated_at = datetime('now', 'localtime') WHERE game_id = ?"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setString(1, game.findCurrentTeam().name()); + pstmt.setLong(2, game.findGameId()); + pstmt.executeUpdate(); + + } catch (SQLException e) { + throw new RuntimeException("턴 갱신에 실패했습니다.", e); + } + } + + public void updateFinished(JanggiGame game) { + String sql = "UPDATE game SET game_status = 'FINISHED', winner = ?, updated_at = datetime('now', 'localtime') WHERE game_id = ?"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setString(1, toWinnerString(game)); + pstmt.setLong(2, game.findGameId()); + pstmt.executeUpdate(); + + } catch (SQLException e) { + throw new RuntimeException("게임 종료 갱신에 실패했습니다.", e); + } + } + + private JanggiGame toJanggiGame(ResultSet rs) throws SQLException { + Long gameId = rs.getLong("game_id"); + Team currentTurn = Team.valueOf(rs.getString("current_turn")); + String status = rs.getString("game_status"); + String winnerStr = rs.getString("winner"); + + boolean isFinished = "FINISHED".equals(status); + Team winner = (winnerStr != null) ? Team.valueOf(winnerStr) : null; + + return new JanggiGame(gameId, currentTurn, isFinished, winner); + } + + private String toGameStatus(JanggiGame game) { + if (game.isFinished()) { + return "FINISHED"; + } + return "PLAYING"; + } + + private String toWinnerString(JanggiGame game) { + if (game.findWinner() == null) { + return null; + } + return game.findWinner().name(); + } +} From cc36c630547e47961e8ee22cf7017f733a92e7dd Mon Sep 17 00:00:00 2001 From: softmoca Date: Fri, 3 Apr 2026 15:34:31 +0900 Subject: [PATCH 18/20] =?UTF-8?q?feat=20:=20=EA=B8=B0=EB=AC=BC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Board에서 기물이 있는 좌표와 기물 맵 반환 - PiceType을사용해 DB문자열과 기물 변환 --- src/main/java/janggi/domain/Board.java | 18 +++ .../java/janggi/domain/piece/PieceType.java | 61 +++++++++++ .../janggi/repository/PieceRepository.java | 103 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/main/java/janggi/domain/piece/PieceType.java create mode 100644 src/main/java/janggi/repository/PieceRepository.java diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java index f8ec3eea9e..c85a5f9467 100644 --- a/src/main/java/janggi/domain/Board.java +++ b/src/main/java/janggi/domain/Board.java @@ -4,6 +4,7 @@ import janggi.domain.piece.Piece; import janggi.domain.piece.Team; import janggi.domain.vo.Position; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -111,4 +112,21 @@ public int calculateScore(Team team) { } + // 빈곳은 db에 따로 저장안하려는데.... + // board 자료구조 자체가 map이었으면..... + public Map findAllPieces() { + Map pieces = new HashMap<>(); + for (int row = 0; row < board.size(); row++) { + for (int col = 0; col < board.get(row).size(); col++) { + Position position = new Position(row, col); + Piece piece = findByPosition(position); + if (!piece.isEmpty()) { + pieces.put(position, piece); + } + } + } + return pieces; + } + + } diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java new file mode 100644 index 0000000000..290f59c48b --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -0,0 +1,61 @@ +package janggi.domain.piece; + +public enum PieceType { + TANK, + HORSE, + ELEPHANT, + ADVISOR, + KING, + CANNON, + SOLDIER; + + public static PieceType from(Piece piece) { + if (piece instanceof Tank) { + return TANK; + } + if (piece instanceof Horse) { + return HORSE; + } + if (piece instanceof Elephant) { + return ELEPHANT; + } + if (piece instanceof Advisor) { + return ADVISOR; + } + if (piece instanceof King) { + return KING; + } + if (piece instanceof Cannon) { + return CANNON; + } + if (piece instanceof Soldier) { + return SOLDIER; + } + throw new IllegalArgumentException("알 수 없는 기물 타입입니다."); + } + + public Piece createPiece(Team team) { + if (this == TANK) { + return new Tank(team); + } + if (this == HORSE) { + return new Horse(team); + } + if (this == ELEPHANT) { + return new Elephant(team); + } + if (this == ADVISOR) { + return new Advisor(team); + } + if (this == KING) { + return new King(team); + } + if (this == CANNON) { + return new Cannon(team); + } + if (this == SOLDIER) { + return new Soldier(team); + } + throw new IllegalArgumentException("알 수 없는 기물 타입입니다."); + } +} diff --git a/src/main/java/janggi/repository/PieceRepository.java b/src/main/java/janggi/repository/PieceRepository.java new file mode 100644 index 0000000000..567b4066c3 --- /dev/null +++ b/src/main/java/janggi/repository/PieceRepository.java @@ -0,0 +1,103 @@ +package janggi.repository; + +import janggi.domain.Board; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.domain.piece.Team; +import janggi.domain.vo.Position; +import janggi.infrastructure.DBConnectionManager; +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 class PieceRepository { + + public void saveAll(Long gameId, Board board) { + String sql = "INSERT INTO piece (game_id, row_pos, col_pos, piece_type, team) VALUES (?, ?, ?, ?, ?)"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + Map pieces = board.findAllPieces(); + for (Map.Entry entry : pieces.entrySet()) { + Position position = entry.getKey(); + Piece piece = entry.getValue(); + + pstmt.setLong(1, gameId); + pstmt.setInt(2, position.getRow()); + pstmt.setInt(3, position.getCol()); + pstmt.setString(4, PieceType.from(piece).name()); + pstmt.setString(5, piece.findTeam().name()); + pstmt.executeUpdate(); + } + } catch (SQLException e) { + throw new RuntimeException("기물 저장에 실패했습니다.", e); + } + } + + public Board findByGameId(Long gameId) { + String sql = "SELECT row_pos, col_pos, piece_type, team FROM piece WHERE game_id = ?"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setLong(1, gameId); + ResultSet rs = pstmt.executeQuery(); + + Map pieces = new HashMap<>(); + while (rs.next()) { + Position position = new Position(rs.getInt("row_pos"), rs.getInt("col_pos")); + PieceType pieceType = PieceType.valueOf(rs.getString("piece_type")); + Team team = Team.valueOf(rs.getString("team")); + Piece piece = pieceType.createPiece(team); + pieces.put(position, piece); + } + return Board.of(pieces); + + } catch (SQLException e) { + throw new RuntimeException("기물 조회에 실패했습니다.", e); + } + } + + public void movePiece(Long gameId, Position from, Position to) { + deleteByPosition(gameId, to); + updatePosition(gameId, from, to); + } + + private void deleteByPosition(Long gameId, Position position) { + String sql = "DELETE FROM piece WHERE game_id = ? AND row_pos = ? AND col_pos = ?"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setLong(1, gameId); + pstmt.setInt(2, position.getRow()); + pstmt.setInt(3, position.getCol()); + pstmt.executeUpdate(); + + } catch (SQLException e) { + throw new RuntimeException("기물 삭제에 실패했습니다.", e); + } + } + + private void updatePosition(Long gameId, Position from, Position to) { + String sql = "UPDATE piece SET row_pos = ?, col_pos = ? WHERE game_id = ? AND row_pos = ? AND col_pos = ?"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setInt(1, to.getRow()); + pstmt.setInt(2, to.getCol()); + pstmt.setLong(3, gameId); + pstmt.setInt(4, from.getRow()); + pstmt.setInt(5, from.getCol()); + pstmt.executeUpdate(); + + } catch (SQLException e) { + throw new RuntimeException("기물 이동 갱신에 실패했습니다.", e); + } + } +} From f3b7634a1c193a22db72090f27fd655b66257ade Mon Sep 17 00:00:00 2001 From: softmoca Date: Fri, 3 Apr 2026 15:36:43 +0900 Subject: [PATCH 19/20] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=9E=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=EC=9D=B4=EC=96=B4=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20=EC=9E=85?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../janggi/controller/JanggiController.java | 80 +++++++++++++++++-- .../janggi/domain/piece/EmptyPosition.java | 5 ++ src/main/java/janggi/domain/piece/Piece.java | 5 ++ src/main/java/janggi/domain/piece/Team.java | 15 +++- src/main/java/janggi/view/InputView.java | 15 ++++ src/main/java/janggi/view/OutputView.java | 58 ++++++++++++++ 6 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 src/main/java/janggi/view/OutputView.java diff --git a/src/main/java/janggi/controller/JanggiController.java b/src/main/java/janggi/controller/JanggiController.java index c7c3d1b4fd..94ea8d3137 100644 --- a/src/main/java/janggi/controller/JanggiController.java +++ b/src/main/java/janggi/controller/JanggiController.java @@ -3,45 +3,111 @@ import janggi.domain.Board; import janggi.domain.JanggiGame; import janggi.domain.dto.MoveCommand; +import janggi.domain.piece.Piece; import janggi.domain.piece.Team; import janggi.domain.vo.Position; +import janggi.repository.GameRepository; +import janggi.repository.PieceRepository; import janggi.view.InputView; +import janggi.view.OutputView; +import java.util.List; public class JanggiController { private final InputView inputView = new InputView(); + private final OutputView outputView = new OutputView(); + private final GameRepository gameRepository = new GameRepository(); + private final PieceRepository pieceRepository = new PieceRepository(); + + private JanggiGame janggiGame; + private Board board; public void run() { - Board board = new Board(); + initialize(); + playGame(); + printResult(); + } + + private void initialize() { + List playingGames = gameRepository.findPlayingGames(); + + if (playingGames.isEmpty()) { + startNewGame(); + return; + } + + int choice = inputView.readGameChoice(playingGames); + if (choice == playingGames.size() + 1) { + startNewGame(); + return; + } + + resumeGame(playingGames.get(choice - 1)); + } + + private void startNewGame() { + janggiGame = new JanggiGame(); + Long gameId = gameRepository.save(janggiGame); + janggiGame.assignId(gameId); + + board = new Board(); + pieceRepository.saveAll(gameId, board); - JanggiGame janggiGame = new JanggiGame(); + outputView.printGameStart(gameId); + } + + private void resumeGame(JanggiGame game) { + janggiGame = game; + board = pieceRepository.findByGameId(game.findGameId()); + + outputView.printResume(game.findGameId(), game.findCurrentTeam()); + } + private void playGame() { while (!janggiGame.isFinished()) { Team currentTeam = janggiGame.findCurrentTeam(); - attemptMove(board, currentTeam); + outputView.printCurrentTurn(currentTeam); + outputView.printBoard(board); + attemptMove(currentTeam); + if (!janggiGame.isFinished()) { janggiGame.changeTurn(); + gameRepository.updateTurn(janggiGame); } } } - private void attemptMove(Board board, Team currentTeam) { + private void attemptMove(Team currentTeam) { boolean moved = false; while (!moved) { - moved = tryMove(board, currentTeam); + moved = tryMove(currentTeam); } } - private boolean tryMove(Board board, Team currentTeam) { + private boolean tryMove(Team currentTeam) { try { MoveCommand moveCommand = inputView.readMovePositions(); Position from = moveCommand.getFrom(); Position to = moveCommand.getTo(); - board.move(from, to, currentTeam); + + Piece captured = board.move(from, to, currentTeam); + pieceRepository.movePiece(janggiGame.findGameId(), from, to); + + janggiGame.processCaptured(captured); + if (janggiGame.isFinished()) { + gameRepository.updateFinished(janggiGame); + } + return true; } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); return false; } } + + private void printResult() { + outputView.printGameEnd(janggiGame.findWinner()); + outputView.printScore(Team.CHO, board.calculateScore(Team.CHO)); + outputView.printScore(Team.HAN, board.calculateScore(Team.HAN)); + } } diff --git a/src/main/java/janggi/domain/piece/EmptyPosition.java b/src/main/java/janggi/domain/piece/EmptyPosition.java index 14e24e833e..a5406b3c98 100644 --- a/src/main/java/janggi/domain/piece/EmptyPosition.java +++ b/src/main/java/janggi/domain/piece/EmptyPosition.java @@ -27,4 +27,9 @@ public MoveRule moveRule() { public int score() { return 0; } + + @Override + public String display() { + return " "; + } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 83f9ab816a..98772c2ad3 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -29,6 +29,11 @@ public boolean canMove(Position from, Position to, BoardView board) { protected abstract MoveRule moveRule(); + public String display() { + return team.findPrefix() + toString(); + } + + @Override public abstract String toString(); diff --git a/src/main/java/janggi/domain/piece/Team.java b/src/main/java/janggi/domain/piece/Team.java index 3e51bf138b..08240a9417 100644 --- a/src/main/java/janggi/domain/piece/Team.java +++ b/src/main/java/janggi/domain/piece/Team.java @@ -1,13 +1,20 @@ package janggi.domain.piece; public enum Team { - CHO("초"), - HAN("한"), - OTHER("빈"); + CHO("초", "C"), + HAN("한", "H"), + OTHER("빈", " "); private final String name; + private final String prefix; - Team(String name) { + Team(String name, String prefix) { this.name = name; + this.prefix = prefix; + } + + public String findPrefix() { + return prefix; } } + diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index 7a47eccf40..fc90fed0ae 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -1,7 +1,9 @@ package janggi.view; +import janggi.domain.JanggiGame; import janggi.domain.dto.MoveCommand; import java.util.Arrays; +import java.util.List; import java.util.Scanner; public class InputView { @@ -20,4 +22,17 @@ public MoveCommand readMovePositions() { return MoveCommand.from(numbers); } + public int readGameChoice(List playingGames) { + System.out.println("진행 중인 게임이 있습니다."); + for (int i = 0; i < playingGames.size(); i++) { + JanggiGame game = playingGames.get(i); + System.out.println((i + 1) + ". 게임 " + game.findGameId() + + " (현재 턴: " + game.findCurrentTeam() + ")"); + } + System.out.println((playingGames.size() + 1) + ". 새 게임 시작"); + System.out.println("번호를 입력해 주세요."); + + return Integer.parseInt(scanner.nextLine()); + } + } diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java new file mode 100644 index 0000000000..85939cf4f5 --- /dev/null +++ b/src/main/java/janggi/view/OutputView.java @@ -0,0 +1,58 @@ +package janggi.view; + +import janggi.domain.BoardView; +import janggi.domain.piece.Team; +import janggi.domain.vo.Position; + +public class OutputView { + private static final int ROW_SIZE = 10; + private static final int COL_SIZE = 9; + + public void printGameStart(Long gameId) { + System.out.println("게임 " + gameId + "을(를) 시작합니다."); + } + + public void printResume(Long gameId, Team currentTurn) { + System.out.println("게임 " + gameId + "을(를) 이어서 진행합니다. 현재 턴: " + currentTurn); + } + + public void printCurrentTurn(Team team) { + System.out.println(team + "의 차례입니다."); + } + + public void printGameEnd(Team winner) { + System.out.println("게임이 종료되었습니다. 승자: " + winner); + } + + public void printScore(Team team, int score) { + System.out.println(team + " 점수: " + score); + } + + public void printBoard(BoardView board) { + System.out.println(); + printColumnHeader(); + for (int row = 0; row < ROW_SIZE; row++) { + printRow(board, row); + } + System.out.println(); + } + + private void printColumnHeader() { + StringBuilder sb = new StringBuilder(" "); + for (int col = 0; col < COL_SIZE; col++) { + sb.append(" ").append(col).append(" "); + } + System.out.println(sb); + } + + private void printRow(BoardView board, int row) { + StringBuilder sb = new StringBuilder(); + sb.append(row).append(" "); + for (int col = 0; col < COL_SIZE; col++) { + String display = board.findByPosition(new Position(row, col)).display(); + sb.append("[").append(display).append("]"); + } + System.out.println(sb); + } + +} From 2b6246616db2874212c585115e72e9e79c425f3a Mon Sep 17 00:00:00 2001 From: softmoca Date: Fri, 3 Apr 2026 17:21:06 +0900 Subject: [PATCH 20/20] =?UTF-8?q?test=20:=20DB=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EB=93=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../janggi/repository/GameRepository.java | 22 +++++ .../java/janggi/domain/JanggiGameTest.java | 10 +++ .../DBConnectionManagerTest.java | 15 ++++ .../infrastructure/DBInitializerTest.java | 35 ++++++++ .../janggi/repository/GameRepositoryTest.java | 83 +++++++++++++++++++ .../repository/PieceRepositoryTest.java | 74 +++++++++++++++++ 6 files changed, 239 insertions(+) create mode 100644 src/test/java/janggi/infrastructure/DBConnectionManagerTest.java create mode 100644 src/test/java/janggi/infrastructure/DBInitializerTest.java create mode 100644 src/test/java/janggi/repository/GameRepositoryTest.java create mode 100644 src/test/java/janggi/repository/PieceRepositoryTest.java diff --git a/src/main/java/janggi/repository/GameRepository.java b/src/main/java/janggi/repository/GameRepository.java index b63285e807..06d470a168 100644 --- a/src/main/java/janggi/repository/GameRepository.java +++ b/src/main/java/janggi/repository/GameRepository.java @@ -109,4 +109,26 @@ private String toWinnerString(JanggiGame game) { } return game.findWinner().name(); } + + public JanggiGame findById(Long gameId) { // PR + String sql = "SELECT game_id, current_turn, game_status, winner FROM game WHERE game_id = ?"; + + try (Connection conn = DBConnectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setLong(1, gameId); + ResultSet rs = pstmt.executeQuery(); + + if (!rs.next()) { + throw new IllegalArgumentException("존재하지 않는 게임입니다. id=" + gameId); + } + + return toJanggiGame(rs); + + } catch (SQLException e) { + throw new RuntimeException("게임 조회에 실패했습니다.", e); + } + } + + } diff --git a/src/test/java/janggi/domain/JanggiGameTest.java b/src/test/java/janggi/domain/JanggiGameTest.java index 899fd63c3e..4af1621fc5 100644 --- a/src/test/java/janggi/domain/JanggiGameTest.java +++ b/src/test/java/janggi/domain/JanggiGameTest.java @@ -54,4 +54,14 @@ class JanggiGameTest { } + @Test + void DB에서_복원한_게임_상태가_유지된다() { + JanggiGame game = new JanggiGame(1L, Team.HAN, false, null); + + assertThat(game.findGameId()).isEqualTo(1L); + assertThat(game.findCurrentTeam()).isEqualTo(Team.HAN); + assertThat(game.isFinished()).isFalse(); + assertThat(game.findWinner()).isNull(); + } + } diff --git a/src/test/java/janggi/infrastructure/DBConnectionManagerTest.java b/src/test/java/janggi/infrastructure/DBConnectionManagerTest.java new file mode 100644 index 0000000000..8a39f045cb --- /dev/null +++ b/src/test/java/janggi/infrastructure/DBConnectionManagerTest.java @@ -0,0 +1,15 @@ +package janggi.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import org.junit.jupiter.api.Test; + +class DBConnectionManagerTest { + + @Test + void DB_연결이_정상적으로_생성된다() { + Connection connection = DBConnectionManager.getConnection(); + assertThat(connection).isNotNull(); + } +} diff --git a/src/test/java/janggi/infrastructure/DBInitializerTest.java b/src/test/java/janggi/infrastructure/DBInitializerTest.java new file mode 100644 index 0000000000..45b8996b04 --- /dev/null +++ b/src/test/java/janggi/infrastructure/DBInitializerTest.java @@ -0,0 +1,35 @@ +package janggi.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.Test; + +class DBInitializerTest { + + @Test + void 테이블이_정상적으로_생성된다() throws SQLException { + DBInitializer.initialize(); + + try (Connection connection = DBConnectionManager.getConnection(); + Statement statement = connection.createStatement()) { + + ResultSet rs = statement.executeQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='game'"); + assertThat(rs.next()).isTrue(); + + rs = statement.executeQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='piece'"); + assertThat(rs.next()).isTrue(); + } + } + + @Test + void 테이블_초기화를_여러번_호출해도_에러가_발생하지_않는다() { + DBInitializer.initialize(); + DBInitializer.initialize(); + } +} diff --git a/src/test/java/janggi/repository/GameRepositoryTest.java b/src/test/java/janggi/repository/GameRepositoryTest.java new file mode 100644 index 0000000000..a4249241cf --- /dev/null +++ b/src/test/java/janggi/repository/GameRepositoryTest.java @@ -0,0 +1,83 @@ +package janggi.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.JanggiGame; +import janggi.domain.piece.King; +import janggi.domain.piece.Team; +import janggi.infrastructure.DBInitializer; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GameRepositoryTest { + + private final GameRepository gameRepository = new GameRepository(); + + @BeforeEach + void setUp() { + DBInitializer.initialize(); + } + + @Test + void 새게임을_저장하고_ID로_조회한다() { + JanggiGame game = new JanggiGame(); + Long gameId = gameRepository.save(game); + + JanggiGame found = gameRepository.findById(gameId); + + assertThat(found.findGameId()).isEqualTo(gameId); + assertThat(found.findCurrentTeam()).isEqualTo(Team.CHO); + assertThat(found.isFinished()).isFalse(); + assertThat(found.findWinner()).isNull(); + } + + @Test + void 턴_변경_후_디비에반영이된다() { + JanggiGame game = new JanggiGame(); + Long gameId = gameRepository.save(game); + game.assignId(gameId); + game.changeTurn(); + + gameRepository.updateTurn(game); + + JanggiGame found = gameRepository.findById(gameId); + assertThat(found.findCurrentTeam()).isEqualTo(Team.HAN); + } + + @Test + void 게임_종료_후_갱신한다() { + JanggiGame game = new JanggiGame(); + Long gameId = gameRepository.save(game); + game.assignId(gameId); + game.processCaptured(new King(Team.HAN)); + + gameRepository.updateFinished(game); + + JanggiGame found = gameRepository.findById(gameId); + assertThat(found.isFinished()).isTrue(); + assertThat(found.findWinner()).isEqualTo(Team.CHO); + } + + @Test + void 진행_중인_게임_목록을_조회한다() { + JanggiGame game1 = new JanggiGame(); + Long gameId1 = gameRepository.save(game1); + + JanggiGame game2 = new JanggiGame(); + Long gameId2 = gameRepository.save(game2); + game2.assignId(gameId2); + game2.processCaptured(new King(Team.HAN)); + gameRepository.updateFinished(game2); + + List playingGames = gameRepository.findPlayingGames(); + + boolean containsGame1 = playingGames.stream() + .anyMatch(g -> g.findGameId().equals(gameId1)); + assertThat(containsGame1).isTrue(); + + boolean containsGame2 = playingGames.stream() + .anyMatch(g -> g.findGameId().equals(gameId2)); + assertThat(containsGame2).isFalse(); + } +} diff --git a/src/test/java/janggi/repository/PieceRepositoryTest.java b/src/test/java/janggi/repository/PieceRepositoryTest.java new file mode 100644 index 0000000000..fe1eac9a29 --- /dev/null +++ b/src/test/java/janggi/repository/PieceRepositoryTest.java @@ -0,0 +1,74 @@ +package janggi.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.Board; +import janggi.domain.JanggiGame; +import janggi.domain.piece.Piece; +import janggi.domain.piece.Soldier; +import janggi.domain.piece.Tank; +import janggi.domain.piece.Team; +import janggi.domain.vo.Position; +import janggi.infrastructure.DBInitializer; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PieceRepositoryTest { + + private final GameRepository gameRepository = new GameRepository(); + private final PieceRepository pieceRepository = new PieceRepository(); + + @BeforeEach + void setUp() { + DBInitializer.initialize(); + } + + @Test + void 보드기물들을_저장하고_복원한다() { + Board board = new Board(); + Long gameId = gameRepository.save(new JanggiGame()); + pieceRepository.saveAll(gameId, board); + + Board restored = pieceRepository.findByGameId(gameId); + + Map pieces = restored.findAllPieces(); + assertThat(pieces).hasSize(32); + } + + @Test + void 기물이동을_DB에_반영한다() { + Board board = Board.of(Map.of( + new Position(0, 0), new Tank(Team.HAN) + )); + Long gameId = gameRepository.save(new JanggiGame()); + pieceRepository.saveAll(gameId, board); + + pieceRepository.movePiece(gameId, new Position(0, 0), new Position(0, 3)); + + Board restored = pieceRepository.findByGameId(gameId); + Map pieces = restored.findAllPieces(); + assertThat(pieces).hasSize(1); + assertThat(pieces.containsKey(new Position(0, 3))).isTrue(); + assertThat(pieces.containsKey(new Position(0, 0))).isFalse(); + } + + @Test +//PR + void 상대_기물을_잡으며_이동하면_잡힌_기물이_삭제된다() { + Board board = Board.of(Map.of( + new Position(0, 0), new Tank(Team.HAN), + new Position(0, 3), new Soldier(Team.CHO) + )); + Long gameId = gameRepository.save(new JanggiGame()); + pieceRepository.saveAll(gameId, board); + + pieceRepository.movePiece(gameId, new Position(0, 0), new Position(0, 3)); + + Board restored = pieceRepository.findByGameId(gameId); + Map pieces = restored.findAllPieces(); + assertThat(pieces).hasSize(1); + assertThat(pieces.get(new Position(0, 3))).isInstanceOf(Tank.class); + } + +}