From b66f27c47db914f55d29e4604638497dc8578173 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Sun, 5 Apr 2026 15:46:07 +0900 Subject: [PATCH 01/32] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EB=B3=80=EC=9C=84=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/board/Position.java | 2 +- .../java/model/movement/LinearStrategy.java | 2 +- .../java/model/movement/OneStepStrategy.java | 2 +- .../java/model/movement/SteppingStrategy.java | 2 +- src/main/java/model/piece/Cannon.java | 2 +- src/main/java/model/piece/Chariot.java | 2 +- src/main/java/model/piece/Elephant.java | 2 +- src/main/java/model/piece/Horse.java | 2 +- src/main/java/model/piece/Soldier.java | 24 +++++++++---------- src/test/java/model/board/PositionTest.java | 2 +- 10 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/java/model/board/Position.java b/src/main/java/model/board/Position.java index b2f1da040f..070989068a 100644 --- a/src/main/java/model/board/Position.java +++ b/src/main/java/model/board/Position.java @@ -17,7 +17,7 @@ public record Position(int row, int col) { } } - public Displacement minus(Position other) { + public Displacement toDisplacement(Position other) { return new Displacement(calculateRowDiff(other), calculateColDiff(other)); } diff --git a/src/main/java/model/movement/LinearStrategy.java b/src/main/java/model/movement/LinearStrategy.java index 26668460e3..22ff0aecf5 100644 --- a/src/main/java/model/movement/LinearStrategy.java +++ b/src/main/java/model/movement/LinearStrategy.java @@ -7,7 +7,7 @@ public class LinearStrategy implements MoveStrategy { private static Direction resolveCardinal(Position start, Position end) { - Displacement displacement = end.minus(start); + Displacement displacement = end.toDisplacement(start); return displacement.extractCardinal(); } diff --git a/src/main/java/model/movement/OneStepStrategy.java b/src/main/java/model/movement/OneStepStrategy.java index 5e2648cac3..e5a103cdec 100644 --- a/src/main/java/model/movement/OneStepStrategy.java +++ b/src/main/java/model/movement/OneStepStrategy.java @@ -7,7 +7,7 @@ public class OneStepStrategy implements MoveStrategy { @Override public List extractPath(Position start, Position end) { - Displacement displacement = end.minus(start); + Displacement displacement = end.toDisplacement(start); Direction cardinal = displacement.extractCardinal(); Position step = cardinal.move(start); diff --git a/src/main/java/model/movement/SteppingStrategy.java b/src/main/java/model/movement/SteppingStrategy.java index 6448585dff..a9dae5a9e6 100644 --- a/src/main/java/model/movement/SteppingStrategy.java +++ b/src/main/java/model/movement/SteppingStrategy.java @@ -7,7 +7,7 @@ public class SteppingStrategy implements MoveStrategy { @Override public List extractPath(Position start, Position end) { - Displacement displacement = end.minus(start); + Displacement displacement = end.toDisplacement(start); Direction cardinal = displacement.extractCardinal(); Direction diagonal = displacement.extractDiagonal(); diff --git a/src/main/java/model/piece/Cannon.java b/src/main/java/model/piece/Cannon.java index 47f2b26260..5bc83be98f 100644 --- a/src/main/java/model/piece/Cannon.java +++ b/src/main/java/model/piece/Cannon.java @@ -35,7 +35,7 @@ public void validateTarget(Piece otherPiece) { @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); + Displacement displacement = next.toDisplacement(current); if (displacement.isNotStraight()) { throw new IllegalArgumentException("포가 이동할 수 없는 위치입니다."); } diff --git a/src/main/java/model/piece/Chariot.java b/src/main/java/model/piece/Chariot.java index 619f83ae32..406d9ed2eb 100644 --- a/src/main/java/model/piece/Chariot.java +++ b/src/main/java/model/piece/Chariot.java @@ -12,7 +12,7 @@ public Chariot(Team team) { @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); + Displacement displacement = next.toDisplacement(current); if (displacement.isNotStraight()) { throw new IllegalArgumentException("차가 이동할 수 없는 위치입니다."); } diff --git a/src/main/java/model/piece/Elephant.java b/src/main/java/model/piece/Elephant.java index 15da27c1e3..03432d2cdc 100644 --- a/src/main/java/model/piece/Elephant.java +++ b/src/main/java/model/piece/Elephant.java @@ -15,7 +15,7 @@ public Elephant(Team team) { @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); + Displacement displacement = next.toDisplacement(current); if (displacement.isNotStepCombination(ELEPHANT_LONG_STEP, ELEPHANT_SHORT_STEP)) { throw new IllegalArgumentException("상이 이동할 수 없는 위치입니다."); } diff --git a/src/main/java/model/piece/Horse.java b/src/main/java/model/piece/Horse.java index cd34d14434..02970e993e 100644 --- a/src/main/java/model/piece/Horse.java +++ b/src/main/java/model/piece/Horse.java @@ -15,7 +15,7 @@ public Horse(Team team) { @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); + Displacement displacement = next.toDisplacement(current); if (displacement.isNotStepCombination(HORSE_LONG_STEP, HORSE_SHORT_STEP)) { throw new IllegalArgumentException("마가 이동할 수 없는 위치입니다."); } diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java index f5e21fc7c7..28476577f8 100644 --- a/src/main/java/model/piece/Soldier.java +++ b/src/main/java/model/piece/Soldier.java @@ -6,27 +6,25 @@ public class Soldier extends Piece { - private static final int SOLDIER_FORWARD_STEP = 1; + private final int forwardDirection; public Soldier(Team team) { super(team, PieceType.SOLDIER); + this.forwardDirection = resolveForwardDirection(team); } - @Override - protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); - int forwardCount = resolveForwardCount(); - - if (!(displacement.isForwardBy(forwardCount) || displacement.isSideOneStep())) { - throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); + private static int resolveForwardDirection(Team team) { + if (team.isHan()) { + return 1; } + return -1; } - private int resolveForwardCount() { - if (isCho()) { - return -SOLDIER_FORWARD_STEP; + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.toDisplacement(current); + if (!(displacement.isForwardBy(forwardDirection) || displacement.isSideOneStep())) { + throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); } - return SOLDIER_FORWARD_STEP; } - } diff --git a/src/test/java/model/board/PositionTest.java b/src/test/java/model/board/PositionTest.java index 9148ae7a52..911dc1e44c 100644 --- a/src/test/java/model/board/PositionTest.java +++ b/src/test/java/model/board/PositionTest.java @@ -43,7 +43,7 @@ class PositionTest { Position end = new Position(3, 7); // when - Displacement result = end.minus(start); + Displacement result = end.toDisplacement(start); // then assertThat(result.rowDiff()).isEqualTo(-2); From bf54f5f2454ec545a0de53f76c7125bf0ba772d7 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Mon, 6 Apr 2026 01:56:33 +0900 Subject: [PATCH 02/32] =?UTF-8?q?refactor:=20formation=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=ED=8C=80=20=EC=A0=95=EB=B3=B4=EA=B0=80?= =?UTF-8?q?=20=EB=82=A8=EC=9A=A9=EB=90=98=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/controller/JanggiController.java | 10 +++---- src/main/java/model/Team.java | 2 +- ...anggiFormation.java => FormationType.java} | 4 +-- src/main/java/model/board/TeamFormation.java | 12 +++++++++ src/main/java/model/piece/Piece.java | 8 ++---- src/main/java/view/InputView.java | 26 ++++++++++++------- src/main/java/view/mapper/ViewMapper.java | 14 +++++----- ...mationTest.java => FormationTypeTest.java} | 24 ++++++++--------- 8 files changed, 57 insertions(+), 43 deletions(-) rename src/main/java/model/board/{JanggiFormation.java => FormationType.java} (96%) create mode 100644 src/main/java/model/board/TeamFormation.java rename src/test/java/model/board/{JanggiFormationTest.java => FormationTypeTest.java} (71%) diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java index 12f5c8d293..3326b3af22 100644 --- a/src/main/java/controller/JanggiController.java +++ b/src/main/java/controller/JanggiController.java @@ -9,8 +9,8 @@ import model.Team; import model.board.Board; import model.board.BoardFactory; -import model.board.JanggiFormation; import model.board.Position; +import model.board.TeamFormation; import model.piece.Piece; import view.InputView; import view.OutputView; @@ -36,12 +36,12 @@ public void run() { } private Board createBoardByFormation() { - JanggiFormation hanFormation = retry(() -> inputView.readFormationByTeam(HAN), processError()); - JanggiFormation choFormation = retry(() -> inputView.readFormationByTeam(CHO), processError()); + TeamFormation hanFormation = retry(() -> inputView.readFormationByTeam(HAN), processError()); + TeamFormation choFormation = retry(() -> inputView.readFormationByTeam(CHO), processError()); Board board = BoardFactory.generateDefaultPieces(); - board.arrangePieces(hanFormation.generateByTeam(HAN)); - board.arrangePieces(choFormation.generateByTeam(CHO)); + board.arrangePieces(hanFormation.generate()); + board.arrangePieces(choFormation.generate()); return board; } diff --git a/src/main/java/model/Team.java b/src/main/java/model/Team.java index 68f566eade..9c1c16653d 100644 --- a/src/main/java/model/Team.java +++ b/src/main/java/model/Team.java @@ -12,7 +12,7 @@ public enum Team { } public void validateAlly(Piece piece) { - if (piece.isOtherTeam(this)) { + if (!piece.isSameTeam(this)) { throw new IllegalArgumentException(this.name + "의 기물이 아닙니다."); } } diff --git a/src/main/java/model/board/JanggiFormation.java b/src/main/java/model/board/FormationType.java similarity index 96% rename from src/main/java/model/board/JanggiFormation.java rename to src/main/java/model/board/FormationType.java index b3bf475b70..4340294962 100644 --- a/src/main/java/model/board/JanggiFormation.java +++ b/src/main/java/model/board/FormationType.java @@ -8,7 +8,7 @@ import model.piece.Horse; import model.piece.Piece; -public enum JanggiFormation { +public enum FormationType { SANG_MA_SANG_MA(team -> List.of(new Elephant(team), new Horse(team), new Elephant(team), new Horse(team))), MA_SANG_MA_SANG(team -> List.of(new Horse(team), new Elephant(team), new Horse(team), new Elephant(team))), MA_SANG_SANG_MA(team -> List.of(new Horse(team), new Elephant(team), new Elephant(team), new Horse(team))), @@ -26,7 +26,7 @@ public enum JanggiFormation { private final FormationStrategy strategy; - JanggiFormation(FormationStrategy strategy) { + FormationType(FormationStrategy strategy) { this.strategy = strategy; } diff --git a/src/main/java/model/board/TeamFormation.java b/src/main/java/model/board/TeamFormation.java new file mode 100644 index 0000000000..6e0e536fb4 --- /dev/null +++ b/src/main/java/model/board/TeamFormation.java @@ -0,0 +1,12 @@ +package model.board; + +import java.util.Map; +import model.Team; +import model.piece.Piece; + +public record TeamFormation(Team team, FormationType type) { + + public Map generate() { + return type.generateByTeam(team); + } +} \ No newline at end of file diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index f6cea5ff83..6d353ecc58 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -19,8 +19,8 @@ public List extractPath(Position current, Position next) { return type.extractPath(current, next); } - public boolean isOtherTeam(Team team) { - return this.team != team; + public boolean isSameTeam(Team team) { + return this.team == team; } public void validatePathCondition(List pieces) { @@ -37,10 +37,6 @@ public void validateTarget(Piece otherPiece) { protected abstract void validateMove(Position current, Position next); - protected boolean isCho() { - return !team.isHan(); - } - protected boolean isCannon() { return getType() == PieceType.CANNON; } diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 4a746f5c24..0a344cc7b8 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -8,8 +8,9 @@ import java.util.Optional; import java.util.Scanner; import model.Team; -import model.board.JanggiFormation; +import model.board.FormationType; import model.board.Position; +import model.board.TeamFormation; import model.piece.Piece; import view.parser.InputParser; @@ -17,16 +18,12 @@ public class InputView { private static final Scanner SCANNER = new Scanner(System.in); private static final InputParser PARSER = new InputParser(); - public JanggiFormation readFormationByTeam(Team team) { - System.out.printf("%n%s의 상차림을 선택해주세요.%n", team.getName()); - FORMATION_ORDER_MAPPER.forEach((order, formation) -> - System.out.printf("%d. %s%n", order, FORMATION_DISPLAY_MAPPER.get(formation))); - - String input = SCANNER.nextLine(); - int order = PARSER.parseNumber(input); - - return Optional.ofNullable(FORMATION_ORDER_MAPPER.get(order)) + public TeamFormation readFormationByTeam(Team team) { + int order = readFormationOrder(team); + FormationType formationType = Optional.ofNullable(FORMATION_ORDER_MAPPER.get(order)) .orElseThrow(() -> new IllegalArgumentException("올바른 상차림을 선택해주세요.")); + + return new TeamFormation(team, formationType); } public Position readPiecePositionForMove(Team turn) { @@ -49,4 +46,13 @@ public Position readPiecePositionForArrange(Team turn, Piece piece) { System.out.print("기물: "); return extractPosition(); } + + private int readFormationOrder(Team team) { + System.out.printf("%n%s의 상차림을 선택해주세요.%n", team.getName()); + FORMATION_ORDER_MAPPER.forEach((order, formation) -> + System.out.printf("%d. %s%n", order, FORMATION_DISPLAY_MAPPER.get(formation))); + + String input = SCANNER.nextLine(); + return PARSER.parseNumber(input); + } } diff --git a/src/main/java/view/mapper/ViewMapper.java b/src/main/java/view/mapper/ViewMapper.java index 04b5ebc991..7db0206b93 100644 --- a/src/main/java/view/mapper/ViewMapper.java +++ b/src/main/java/view/mapper/ViewMapper.java @@ -1,23 +1,23 @@ package view.mapper; -import static model.board.JanggiFormation.MA_SANG_MA_SANG; -import static model.board.JanggiFormation.MA_SANG_SANG_MA; -import static model.board.JanggiFormation.SANG_MA_MA_SANG; -import static model.board.JanggiFormation.SANG_MA_SANG_MA; +import static model.board.FormationType.MA_SANG_MA_SANG; +import static model.board.FormationType.MA_SANG_SANG_MA; +import static model.board.FormationType.SANG_MA_MA_SANG; +import static model.board.FormationType.SANG_MA_SANG_MA; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; import model.Team; -import model.board.JanggiFormation; +import model.board.FormationType; import model.piece.PieceType; public class ViewMapper { public static final Map> SYMBOL_MAP = new EnumMap<>(PieceType.class); - public static Map FORMATION_ORDER_MAPPER = new LinkedHashMap<>(); + public static Map FORMATION_ORDER_MAPPER = new LinkedHashMap<>(); - public static Map FORMATION_DISPLAY_MAPPER = Map.of( + public static Map FORMATION_DISPLAY_MAPPER = Map.of( SANG_MA_SANG_MA, "상마상마", MA_SANG_MA_SANG, "마상마상", MA_SANG_SANG_MA, "마상상마", diff --git a/src/test/java/model/board/JanggiFormationTest.java b/src/test/java/model/board/FormationTypeTest.java similarity index 71% rename from src/test/java/model/board/JanggiFormationTest.java rename to src/test/java/model/board/FormationTypeTest.java index 41888b4a09..a91cfa72e2 100644 --- a/src/test/java/model/board/JanggiFormationTest.java +++ b/src/test/java/model/board/FormationTypeTest.java @@ -13,24 +13,24 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -class JanggiFormationTest { +class FormationTypeTest { static Stream formationTestProvider() { return Stream.of( - Arguments.of(JanggiFormation.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), - Arguments.of(JanggiFormation.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) + Arguments.of(FormationType.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), + Arguments.of(FormationType.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), + Arguments.of(FormationType.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), + Arguments.of(FormationType.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) ); } static Stream 포메이션_별_기물_위치() { return Stream.of( // formation, 좌외(1), 좌내(2), 우내(6), 우외(7) 순서 - Arguments.of(JanggiFormation.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), - Arguments.of(JanggiFormation.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) + Arguments.of(FormationType.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), + Arguments.of(FormationType.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), + Arguments.of(FormationType.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), + Arguments.of(FormationType.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) ); } @@ -41,7 +41,7 @@ static Stream choFormationProvider() { @ParameterizedTest(name = "{0} 차림 일 때") @MethodSource("포메이션_별_기물_위치") void 한나라_상차림_기물_순서_테스트( - JanggiFormation formation, + FormationType formation, Class leftOuter, Class leftInner, Class rightInner, @@ -58,7 +58,7 @@ static Stream choFormationProvider() { @ParameterizedTest(name = "{0} 차림일 때") @MethodSource("choFormationProvider") void 초나라_상차림_기물_순서_테스트( - JanggiFormation formation, + FormationType formation, Class leftOuter, Class leftInner, Class rightInner, @@ -77,7 +77,7 @@ static Stream choFormationProvider() { "SANG_MA_SANG_MA, HAN", "MA_SANG_MA_SANG, HAN", "MA_SANG_SANG_MA, HAN", "SANG_MA_MA_SANG, HAN", "SANG_MA_SANG_MA, CHO", "MA_SANG_MA_SANG, CHO", "MA_SANG_SANG_MA, CHO", "SANG_MA_MA_SANG, CHO" }) - void 각_팀별_상차림의_기물_수는_4개여야_한다(JanggiFormation formation, Team team) { + void 각_팀별_상차림의_기물_수는_4개여야_한다(FormationType formation, Team team) { // when assertThat(formation.generateByTeam(team)).hasSize(4); } From d1fd27e34676259529a55a01226f6699a29edcc4 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Mon, 6 Apr 2026 01:59:42 +0900 Subject: [PATCH 03/32] =?UTF-8?q?refactor:=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=ED=84=B4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EA=B8=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EA=B2=80=EC=A6=9D=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=8B=AC=ED=94=8C=ED=95=98=EA=B2=8C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/JanggiGame.java | 8 +++++++- src/main/java/model/Team.java | 8 -------- src/test/java/model/TeamTest.java | 14 -------------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index f68211fdb6..7002bbde31 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -29,10 +29,16 @@ public void movePiece(Position current, Position next) { public Piece selectPiece(Position position) { Piece piece = board.pickPiece(position); - turn.validateAlly(piece); + validateAlly(piece); return piece; } + private void validateAlly(Piece piece) { + if (!piece.isSameTeam(turn)) { + throw new IllegalArgumentException(turn.getName() + "의 기물이 아닙니다."); + } + } + public Team getTurn() { return turn; } diff --git a/src/main/java/model/Team.java b/src/main/java/model/Team.java index 9c1c16653d..7ba69b1f45 100644 --- a/src/main/java/model/Team.java +++ b/src/main/java/model/Team.java @@ -1,7 +1,5 @@ package model; -import model.piece.Piece; - public enum Team { HAN("한나라"), CHO("초나라"); @@ -11,12 +9,6 @@ public enum Team { this.name = name; } - public void validateAlly(Piece piece) { - if (!piece.isSameTeam(this)) { - throw new IllegalArgumentException(this.name + "의 기물이 아닙니다."); - } - } - public boolean isHan() { return this == HAN; } diff --git a/src/test/java/model/TeamTest.java b/src/test/java/model/TeamTest.java index 5f925e4cc0..bdfb889af7 100644 --- a/src/test/java/model/TeamTest.java +++ b/src/test/java/model/TeamTest.java @@ -1,10 +1,7 @@ package model; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import model.piece.Piece; -import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; class TeamTest { @@ -32,15 +29,4 @@ class TeamTest { // then assertThat(next).isEqualTo(Team.HAN); } - - @Test - void 자신의_팀이_아닌_기물을_검증하면_예외가_발생한다() { - // given - Team cho = Team.CHO; - Piece hanPiece = FakePiece.createFake(Team.HAN); - - // when & then - assertThatThrownBy(() -> cho.validateAlly(hanPiece)) - .isInstanceOf(IllegalArgumentException.class); - } } \ No newline at end of file From 7ea6c256a4d9449a920f0ed9b10fcc903b9a9053 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Mon, 6 Apr 2026 02:04:18 +0900 Subject: [PATCH 04/32] =?UTF-8?q?refactor:=20=EC=99=B8=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=80=EB=8B=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/movement/Displacement.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/model/movement/Displacement.java b/src/main/java/model/movement/Displacement.java index cf8d3ae4f1..9bb3a16f66 100644 --- a/src/main/java/model/movement/Displacement.java +++ b/src/main/java/model/movement/Displacement.java @@ -13,14 +13,6 @@ public Direction extractDiagonal() { return Direction.of(rowDiff, colDiff); } - public int absRowDiff() { - return Math.abs(rowDiff); - } - - public int absColDiff() { - return Math.abs(colDiff); - } - public boolean isNotStraight() { return colDiff != 0 && rowDiff != 0; } @@ -37,4 +29,12 @@ public boolean isForwardBy(int forwardCount) { public boolean isSideOneStep() { return rowDiff == 0 && absColDiff() == 1; } + + private int absRowDiff() { + return Math.abs(rowDiff); + } + + private int absColDiff() { + return Math.abs(colDiff); + } } From 680d31c856d3e19bfcf5c00a1f7de056360e0eb8 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Mon, 6 Apr 2026 02:29:24 +0900 Subject: [PATCH 05/32] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EA=B2=BD=EB=A1=9C=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EC=9D=91=EC=A7=91=EB=8F=84=20=ED=96=A5?= =?UTF-8?q?=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/controller/JanggiController.java | 2 +- src/main/java/model/JanggiGame.java | 4 +- src/main/java/model/board/Board.java | 1 + src/main/java/model/board/BoardFactory.java | 1 + src/main/java/model/board/FormationType.java | 1 + src/main/java/model/board/TeamFormation.java | 1 + .../{movement => coordinate}/Direction.java | 3 +- .../Displacement.java | 2 +- .../model/{board => coordinate}/Position.java | 4 +- .../java/model/movement/LinearStrategy.java | 26 ------------- .../java/model/movement/MoveStrategy.java | 9 ----- .../java/model/movement/OneStepStrategy.java | 16 -------- .../java/model/movement/SteppingStrategy.java | 18 --------- src/main/java/model/piece/Cannon.java | 28 ++++++++++++-- src/main/java/model/piece/Chariot.java | 25 ++++++++++++- src/main/java/model/piece/Elephant.java | 18 ++++++++- src/main/java/model/piece/General.java | 8 +++- src/main/java/model/piece/Guard.java | 8 +++- src/main/java/model/piece/Horse.java | 15 +++++++- src/main/java/model/piece/Piece.java | 12 +++--- src/main/java/model/piece/PieceType.java | 37 ++++--------------- src/main/java/model/piece/Soldier.java | 10 ++++- src/main/java/view/InputView.java | 2 +- src/main/java/view/OutputView.java | 2 +- src/test/java/model/JanggiGameTest.java | 2 +- src/test/java/model/board/BoardTest.java | 1 + src/test/java/model/board/DirectionTest.java | 3 +- .../java/model/board/FormationTypeTest.java | 1 + src/test/java/model/board/PositionTest.java | 3 +- .../model/fixture/PieceMovePathFixture.java | 2 +- .../fixture/PieceMovePositionFixture.java | 2 +- src/test/java/model/piece/CannonTest.java | 4 +- src/test/java/model/piece/ChariotTest.java | 4 +- src/test/java/model/piece/ElephantTest.java | 4 +- src/test/java/model/piece/HorseTest.java | 4 +- src/test/java/model/piece/SoldierTest.java | 4 +- src/test/java/model/testdouble/FakePiece.java | 9 ++++- src/test/java/model/testdouble/SpyBoard.java | 2 +- 38 files changed, 151 insertions(+), 147 deletions(-) rename src/main/java/model/{movement => coordinate}/Direction.java (95%) rename src/main/java/model/{movement => coordinate}/Displacement.java (97%) rename src/main/java/model/{board => coordinate}/Position.java (94%) delete mode 100644 src/main/java/model/movement/LinearStrategy.java delete mode 100644 src/main/java/model/movement/MoveStrategy.java delete mode 100644 src/main/java/model/movement/OneStepStrategy.java delete mode 100644 src/main/java/model/movement/SteppingStrategy.java diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java index 3326b3af22..2f126ec386 100644 --- a/src/main/java/controller/JanggiController.java +++ b/src/main/java/controller/JanggiController.java @@ -9,8 +9,8 @@ import model.Team; import model.board.Board; import model.board.BoardFactory; -import model.board.Position; import model.board.TeamFormation; +import model.coordinate.Position; import model.piece.Piece; import view.InputView; import view.OutputView; diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index 7002bbde31..28af47d51d 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -2,7 +2,7 @@ import java.util.List; import model.board.Board; -import model.board.Position; +import model.coordinate.Position; import model.piece.Piece; public class JanggiGame { @@ -18,7 +18,7 @@ public JanggiGame(Board board) { public void movePiece(Position current, Position next) { Piece piece = selectPiece(current); - List path = piece.extractPath(current, next); + List path = piece.pathTo(current, next); List pieces = board.extractPiecesByPath(path); piece.validatePathCondition(pieces); diff --git a/src/main/java/model/board/Board.java b/src/main/java/model/board/Board.java index 8abf94644a..991f82befd 100644 --- a/src/main/java/model/board/Board.java +++ b/src/main/java/model/board/Board.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import model.coordinate.Position; import model.piece.Piece; public class Board { diff --git a/src/main/java/model/board/BoardFactory.java b/src/main/java/model/board/BoardFactory.java index b89731725c..e43cb40af9 100644 --- a/src/main/java/model/board/BoardFactory.java +++ b/src/main/java/model/board/BoardFactory.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.Map; +import model.coordinate.Position; import model.piece.Cannon; import model.piece.Chariot; import model.piece.General; diff --git a/src/main/java/model/board/FormationType.java b/src/main/java/model/board/FormationType.java index 4340294962..1949ab26b6 100644 --- a/src/main/java/model/board/FormationType.java +++ b/src/main/java/model/board/FormationType.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; import model.Team; +import model.coordinate.Position; import model.piece.Elephant; import model.piece.Horse; import model.piece.Piece; diff --git a/src/main/java/model/board/TeamFormation.java b/src/main/java/model/board/TeamFormation.java index 6e0e536fb4..6650ab3c36 100644 --- a/src/main/java/model/board/TeamFormation.java +++ b/src/main/java/model/board/TeamFormation.java @@ -2,6 +2,7 @@ import java.util.Map; import model.Team; +import model.coordinate.Position; import model.piece.Piece; public record TeamFormation(Team team, FormationType type) { diff --git a/src/main/java/model/movement/Direction.java b/src/main/java/model/coordinate/Direction.java similarity index 95% rename from src/main/java/model/movement/Direction.java rename to src/main/java/model/coordinate/Direction.java index 524c07c3f6..5b392affa6 100644 --- a/src/main/java/model/movement/Direction.java +++ b/src/main/java/model/coordinate/Direction.java @@ -1,7 +1,6 @@ -package model.movement; +package model.coordinate; import java.util.stream.Stream; -import model.board.Position; public enum Direction { EAST(0, 1), diff --git a/src/main/java/model/movement/Displacement.java b/src/main/java/model/coordinate/Displacement.java similarity index 97% rename from src/main/java/model/movement/Displacement.java rename to src/main/java/model/coordinate/Displacement.java index 9bb3a16f66..9176ee0810 100644 --- a/src/main/java/model/movement/Displacement.java +++ b/src/main/java/model/coordinate/Displacement.java @@ -1,4 +1,4 @@ -package model.movement; +package model.coordinate; public record Displacement(int rowDiff, int colDiff) { diff --git a/src/main/java/model/board/Position.java b/src/main/java/model/coordinate/Position.java similarity index 94% rename from src/main/java/model/board/Position.java rename to src/main/java/model/coordinate/Position.java index 070989068a..6fc4b74705 100644 --- a/src/main/java/model/board/Position.java +++ b/src/main/java/model/coordinate/Position.java @@ -1,10 +1,8 @@ -package model.board; +package model.coordinate; import static model.board.Board.BOARD_COL; import static model.board.Board.BOARD_ROW; -import model.movement.Displacement; - public record Position(int row, int col) { public Position { diff --git a/src/main/java/model/movement/LinearStrategy.java b/src/main/java/model/movement/LinearStrategy.java deleted file mode 100644 index 22ff0aecf5..0000000000 --- a/src/main/java/model/movement/LinearStrategy.java +++ /dev/null @@ -1,26 +0,0 @@ -package model.movement; - -import java.util.ArrayList; -import java.util.List; -import model.board.Position; - -public class LinearStrategy implements MoveStrategy { - - private static Direction resolveCardinal(Position start, Position end) { - Displacement displacement = end.toDisplacement(start); - return displacement.extractCardinal(); - } - - @Override - public List extractPath(Position start, Position end) { - Direction direction = resolveCardinal(start, end); - - List path = new ArrayList<>(); - Position step = direction.move(start); - while (!step.equals(end)) { - path.add(step); - step = direction.move(step); - } - return path; - } -} \ No newline at end of file diff --git a/src/main/java/model/movement/MoveStrategy.java b/src/main/java/model/movement/MoveStrategy.java deleted file mode 100644 index ea10f61c19..0000000000 --- a/src/main/java/model/movement/MoveStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ -package model.movement; - -import java.util.List; -import model.board.Position; - -public interface MoveStrategy { - - List extractPath(Position start, Position end); -} diff --git a/src/main/java/model/movement/OneStepStrategy.java b/src/main/java/model/movement/OneStepStrategy.java deleted file mode 100644 index e5a103cdec..0000000000 --- a/src/main/java/model/movement/OneStepStrategy.java +++ /dev/null @@ -1,16 +0,0 @@ -package model.movement; - -import java.util.List; -import model.board.Position; - -public class OneStepStrategy implements MoveStrategy { - - @Override - public List extractPath(Position start, Position end) { - Displacement displacement = end.toDisplacement(start); - Direction cardinal = displacement.extractCardinal(); - - Position step = cardinal.move(start); - return List.of(step); - } -} \ No newline at end of file diff --git a/src/main/java/model/movement/SteppingStrategy.java b/src/main/java/model/movement/SteppingStrategy.java deleted file mode 100644 index a9dae5a9e6..0000000000 --- a/src/main/java/model/movement/SteppingStrategy.java +++ /dev/null @@ -1,18 +0,0 @@ -package model.movement; - -import java.util.List; -import model.board.Position; - -public class SteppingStrategy implements MoveStrategy { - - @Override - public List extractPath(Position start, Position end) { - Displacement displacement = end.toDisplacement(start); - Direction cardinal = displacement.extractCardinal(); - Direction diagonal = displacement.extractDiagonal(); - - Position oneStep = cardinal.move(start); - Position stepping = diagonal.move(oneStep); - return List.of(oneStep, stepping); - } -} \ No newline at end of file diff --git a/src/main/java/model/piece/Cannon.java b/src/main/java/model/piece/Cannon.java index 5bc83be98f..d835ebf4f1 100644 --- a/src/main/java/model/piece/Cannon.java +++ b/src/main/java/model/piece/Cannon.java @@ -1,9 +1,11 @@ package model.piece; +import java.util.ArrayList; import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Cannon extends Piece { @@ -19,7 +21,7 @@ public void validatePathCondition(List pieces) { throw new IllegalArgumentException("포는 정확히 하나의 기물을 뛰어넘어야 합니다."); } - boolean hasCannonAsHurdle = pieces.stream().anyMatch(Piece::isCannon); + boolean hasCannonAsHurdle = pieces.stream().anyMatch(piece -> piece.isSameType(this)); if (hasCannonAsHurdle) { throw new IllegalArgumentException("포는 포를 다리로 쓸 수 없습니다."); } @@ -28,7 +30,7 @@ public void validatePathCondition(List pieces) { @Override public void validateTarget(Piece otherPiece) { super.validateTarget(otherPiece); - if (otherPiece.isCannon()) { + if (otherPiece.isSameType(this)) { throw new IllegalArgumentException("포는 포를 잡을 수 없습니다."); } } @@ -40,4 +42,22 @@ protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("포가 이동할 수 없는 위치입니다."); } } + + @Override + protected List extractPath(Position start, Position end) { + Direction direction = resolveCardinal(start, end); + + List path = new ArrayList<>(); + Position step = direction.move(start); + while (!step.equals(end)) { + path.add(step); + step = direction.move(step); + } + return path; + } + + private Direction resolveCardinal(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + return displacement.extractCardinal(); + } } diff --git a/src/main/java/model/piece/Chariot.java b/src/main/java/model/piece/Chariot.java index 406d9ed2eb..7ec66255ea 100644 --- a/src/main/java/model/piece/Chariot.java +++ b/src/main/java/model/piece/Chariot.java @@ -1,8 +1,11 @@ package model.piece; +import java.util.ArrayList; +import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Chariot extends Piece { @@ -17,4 +20,22 @@ protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("차가 이동할 수 없는 위치입니다."); } } + + @Override + protected List extractPath(Position start, Position end) { + Direction direction = resolveCardinal(start, end); + + List path = new ArrayList<>(); + Position step = direction.move(start); + while (!step.equals(end)) { + path.add(step); + step = direction.move(step); + } + return path; + } + + private Direction resolveCardinal(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + return displacement.extractCardinal(); + } } diff --git a/src/main/java/model/piece/Elephant.java b/src/main/java/model/piece/Elephant.java index 03432d2cdc..aa1da422d5 100644 --- a/src/main/java/model/piece/Elephant.java +++ b/src/main/java/model/piece/Elephant.java @@ -1,8 +1,10 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Elephant extends Piece { @@ -20,4 +22,16 @@ protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("상이 이동할 수 없는 위치입니다."); } } + + + @Override + protected List extractPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction cardinal = displacement.extractCardinal(); + Direction diagonal = displacement.extractDiagonal(); + + Position oneStep = cardinal.move(start); + Position stepping = diagonal.move(oneStep); + return List.of(oneStep, stepping); + } } diff --git a/src/main/java/model/piece/General.java b/src/main/java/model/piece/General.java index c0db5579e8..3ef59915ca 100644 --- a/src/main/java/model/piece/General.java +++ b/src/main/java/model/piece/General.java @@ -1,7 +1,8 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; public class General extends Piece { @@ -13,4 +14,9 @@ public General(Team team) { protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("1단계 궁성 영역 미구현"); } + + @Override + protected List extractPath(Position current, Position next) { + throw new IllegalArgumentException("1단계 궁성 영역 미구현"); + } } diff --git a/src/main/java/model/piece/Guard.java b/src/main/java/model/piece/Guard.java index 9c635738f1..432fee6e35 100644 --- a/src/main/java/model/piece/Guard.java +++ b/src/main/java/model/piece/Guard.java @@ -1,7 +1,8 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; public class Guard extends Piece { @@ -13,4 +14,9 @@ public Guard(Team team) { protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("1단계 궁성 영역 미구현"); } + + @Override + protected List extractPath(Position current, Position next) { + throw new IllegalArgumentException("1단계 궁성 영역 미구현"); + } } diff --git a/src/main/java/model/piece/Horse.java b/src/main/java/model/piece/Horse.java index 02970e993e..0137dbd9f1 100644 --- a/src/main/java/model/piece/Horse.java +++ b/src/main/java/model/piece/Horse.java @@ -1,8 +1,10 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Horse extends Piece { @@ -20,4 +22,13 @@ protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("마가 이동할 수 없는 위치입니다."); } } + + @Override + protected List extractPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction cardinal = displacement.extractCardinal(); + + Position step = cardinal.move(start); + return List.of(step); + } } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index 6d353ecc58..380d9fd94e 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -2,7 +2,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; public abstract class Piece { @@ -14,9 +14,9 @@ protected Piece(Team team, PieceType type) { this.type = type; } - public List extractPath(Position current, Position next) { + public List pathTo(Position current, Position next) { validateMove(current, next); - return type.extractPath(current, next); + return extractPath(current, next); } public boolean isSameTeam(Team team) { @@ -37,8 +37,10 @@ public void validateTarget(Piece otherPiece) { protected abstract void validateMove(Position current, Position next); - protected boolean isCannon() { - return getType() == PieceType.CANNON; + protected abstract List extractPath(Position current, Position next); + + protected boolean isSameType(Piece piece) { + return type == piece.type; } public Team getTeam() { diff --git a/src/main/java/model/piece/PieceType.java b/src/main/java/model/piece/PieceType.java index c2afe70b14..d6ae24449e 100644 --- a/src/main/java/model/piece/PieceType.java +++ b/src/main/java/model/piece/PieceType.java @@ -1,34 +1,11 @@ package model.piece; -import java.util.List; -import model.board.Position; -import model.movement.LinearStrategy; -import model.movement.MoveStrategy; -import model.movement.OneStepStrategy; -import model.movement.SteppingStrategy; - public enum PieceType { - - CANNON(new LinearStrategy()), - CHARIOT(new LinearStrategy()), - ELEPHANT(new SteppingStrategy()), - GENERAL((start, end) -> { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - }), - GUARD((start, end) -> { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - }), - HORSE(new OneStepStrategy()), - SOLDIER((start, end) -> List.of()), - ; - - private final MoveStrategy moveStrategy; - - PieceType(MoveStrategy moveStrategy) { - this.moveStrategy = moveStrategy; - } - - public List extractPath(Position current, Position next) { - return moveStrategy.extractPath(current, next); - } + CANNON, + CHARIOT, + ELEPHANT, + GENERAL, + GUARD, + HORSE, + SOLDIER; } diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java index 28476577f8..fa385ac9c7 100644 --- a/src/main/java/model/piece/Soldier.java +++ b/src/main/java/model/piece/Soldier.java @@ -1,8 +1,9 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Soldier extends Piece { @@ -27,4 +28,9 @@ protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); } } + + @Override + protected List extractPath(Position current, Position next) { + return List.of(); + } } diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 0a344cc7b8..494c04cf0b 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -9,8 +9,8 @@ import java.util.Scanner; import model.Team; import model.board.FormationType; -import model.board.Position; import model.board.TeamFormation; +import model.coordinate.Position; import model.piece.Piece; import view.parser.InputParser; diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index dbbe2be93a..4edb0391f8 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -11,7 +11,7 @@ import java.util.Map; import model.board.Board; -import model.board.Position; +import model.coordinate.Position; import model.piece.Piece; public class OutputView { diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java index 3dab3216e1..01e281e097 100644 --- a/src/test/java/model/JanggiGameTest.java +++ b/src/test/java/model/JanggiGameTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import model.board.Position; +import model.coordinate.Position; import model.piece.Horse; import model.piece.Piece; import model.testdouble.FakePiece; diff --git a/src/test/java/model/board/BoardTest.java b/src/test/java/model/board/BoardTest.java index f3dc059f55..66cd5c0c2b 100644 --- a/src/test/java/model/board/BoardTest.java +++ b/src/test/java/model/board/BoardTest.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import model.Team; +import model.coordinate.Position; import model.piece.Piece; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; diff --git a/src/test/java/model/board/DirectionTest.java b/src/test/java/model/board/DirectionTest.java index 2d229e5bee..73382ae432 100644 --- a/src/test/java/model/board/DirectionTest.java +++ b/src/test/java/model/board/DirectionTest.java @@ -4,7 +4,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.stream.Stream; -import model.movement.Direction; +import model.coordinate.Direction; +import model.coordinate.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/src/test/java/model/board/FormationTypeTest.java b/src/test/java/model/board/FormationTypeTest.java index a91cfa72e2..6c7bbf28ba 100644 --- a/src/test/java/model/board/FormationTypeTest.java +++ b/src/test/java/model/board/FormationTypeTest.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.stream.Stream; import model.Team; +import model.coordinate.Position; import model.piece.Elephant; import model.piece.Horse; import model.piece.Piece; diff --git a/src/test/java/model/board/PositionTest.java b/src/test/java/model/board/PositionTest.java index 911dc1e44c..f0d5d342bc 100644 --- a/src/test/java/model/board/PositionTest.java +++ b/src/test/java/model/board/PositionTest.java @@ -3,7 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import model.movement.Displacement; +import model.coordinate.Displacement; +import model.coordinate.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; diff --git a/src/test/java/model/fixture/PieceMovePathFixture.java b/src/test/java/model/fixture/PieceMovePathFixture.java index 6548acb4cb..ab953b76a8 100644 --- a/src/test/java/model/fixture/PieceMovePathFixture.java +++ b/src/test/java/model/fixture/PieceMovePathFixture.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.stream.Stream; import model.Team; -import model.board.Position; +import model.coordinate.Position; import org.junit.jupiter.params.provider.Arguments; public class PieceMovePathFixture { diff --git a/src/test/java/model/fixture/PieceMovePositionFixture.java b/src/test/java/model/fixture/PieceMovePositionFixture.java index 75c0c05cca..2bb9f48605 100644 --- a/src/test/java/model/fixture/PieceMovePositionFixture.java +++ b/src/test/java/model/fixture/PieceMovePositionFixture.java @@ -2,7 +2,7 @@ import java.util.stream.Stream; import model.Team; -import model.board.Position; +import model.coordinate.Position; import org.junit.jupiter.params.provider.Arguments; public class PieceMovePositionFixture { diff --git a/src/test/java/model/piece/CannonTest.java b/src/test/java/model/piece/CannonTest.java index 796291c9f3..14aeb6d14f 100644 --- a/src/test/java/model/piece/CannonTest.java +++ b/src/test/java/model/piece/CannonTest.java @@ -6,7 +6,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -42,7 +42,7 @@ public class CannonTest { Piece chariot = new Cannon(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); diff --git a/src/test/java/model/piece/ChariotTest.java b/src/test/java/model/piece/ChariotTest.java index c0c228b0e3..e9d1ca632b 100644 --- a/src/test/java/model/piece/ChariotTest.java +++ b/src/test/java/model/piece/ChariotTest.java @@ -6,7 +6,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -43,7 +43,7 @@ public class ChariotTest { Piece chariot = new Chariot(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); diff --git a/src/test/java/model/piece/ElephantTest.java b/src/test/java/model/piece/ElephantTest.java index 7d14631c14..d58859230d 100644 --- a/src/test/java/model/piece/ElephantTest.java +++ b/src/test/java/model/piece/ElephantTest.java @@ -6,7 +6,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -42,7 +42,7 @@ public class ElephantTest { Piece chariot = new Elephant(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); diff --git a/src/test/java/model/piece/HorseTest.java b/src/test/java/model/piece/HorseTest.java index 8dbe1c958f..9e98ee90e8 100644 --- a/src/test/java/model/piece/HorseTest.java +++ b/src/test/java/model/piece/HorseTest.java @@ -6,7 +6,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -43,7 +43,7 @@ public class HorseTest { Piece chariot = new Horse(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); diff --git a/src/test/java/model/piece/SoldierTest.java b/src/test/java/model/piece/SoldierTest.java index f229aff1e3..2a65426e0d 100644 --- a/src/test/java/model/piece/SoldierTest.java +++ b/src/test/java/model/piece/SoldierTest.java @@ -6,7 +6,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -43,7 +43,7 @@ public class SoldierTest { Piece soldier = new Soldier(team); // when - List path = soldier.extractPath(current, next); + List path = soldier.pathTo(current, next); // then assertThat(path).isEmpty(); diff --git a/src/test/java/model/testdouble/FakePiece.java b/src/test/java/model/testdouble/FakePiece.java index c8b05d087e..863f406756 100644 --- a/src/test/java/model/testdouble/FakePiece.java +++ b/src/test/java/model/testdouble/FakePiece.java @@ -2,7 +2,7 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.piece.Piece; import model.piece.PieceType; @@ -22,7 +22,7 @@ public static FakePiece createFake(Team team) { } @Override - public List extractPath(Position current, Position next) { + public List pathTo(Position current, Position next) { return path; } @@ -30,4 +30,9 @@ public List extractPath(Position current, Position next) { protected void validateMove(Position current, Position next) { } + + @Override + protected List extractPath(Position current, Position next) { + return List.of(); + } } diff --git a/src/test/java/model/testdouble/SpyBoard.java b/src/test/java/model/testdouble/SpyBoard.java index de1816869a..9749cdd165 100644 --- a/src/test/java/model/testdouble/SpyBoard.java +++ b/src/test/java/model/testdouble/SpyBoard.java @@ -3,7 +3,7 @@ import java.util.Map; import model.Team; import model.board.Board; -import model.board.Position; +import model.coordinate.Position; import model.piece.Horse; import model.piece.Piece; From c505276aecb372b13bf6987119e2bac20261f5cb Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Mon, 6 Apr 2026 14:50:26 +0900 Subject: [PATCH 06/32] =?UTF-8?q?docs:=20cycle=202=20-=201=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=20=EA=B8=B0=EB=8A=A5=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=AC=B8=EC=84=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 152 ++++++++++++++++++++++-- src/main/java/model/piece/Elephant.java | 1 - 2 files changed, 140 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9003d49411..538a2a5706 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# 1단계 - 보드 초기화 +# Cycle 1 + +## 1단계 - 보드 초기화 ``` 한나라의 상차림을 선택해주세요. @@ -35,7 +37,7 @@  +-------------------+ ``` -## 기능 정리 +### 기능 정리 - 상차림 순서 관리 - 한나라부터 상차림 입력을 받는다. @@ -56,7 +58,7 @@ - 나라 정보 - 한, 초 -## 구현 순서 +### 구현 순서 * 초나라/한나라 상차림 입력 * 나라 별 유효한 상차림을 입력받는다. @@ -70,11 +72,11 @@ * 장기판 위치 범위를 초과하면 예외가 발생해야한다. * 장기판이 필요하다. -# 2단계 - 기물 이동 +## 2단계 - 기물 이동 - 2단계에서는 기물 이동 구현이 목적이니까 종료는 `Ctrl+C`로 강제 종료한다. -## 잘못된 기물 선택 시나리오 +### 잘못된 기물 선택 시나리오 1. 다른 나라의 기물을 선택했을 때 1. 예외: 다른나라의 기물 선택 @@ -83,7 +85,7 @@ 1. 예외: 기물이 없는 곳을 선택 2. `[ERROR] 현재 위치에 존재하는 기물이 없습니다.` -## 기물 이동 규칙 +### 기물 이동 규칙 - 궁성 영역 - 고려하지 않는다. (사이클 2) - 기물 이동 규칙 공통 @@ -104,7 +106,7 @@ - 한 칸이라서 경로에 기물이 있을 리가 없다. - `사, 장`: 궁성 영역이므로 구현하지 않는다. -## 입출력 예시 +### 입출력 예시 ``` 1단계 ... @@ -129,7 +131,6 @@ [초나라] 기물 卒의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) 위치: 6,7 -    0 1 2 3 4 5 6 7 8  +-------------------+ 0| 車 馬 象 士 * 士 象 馬 車 | @@ -149,11 +150,9 @@ [ERROR] 현재 위치에 존재하는 기물이 없습니다. - [한나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) 기물: 3,0 - [한나라] 기물 兵의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) 위치: 3,1 @@ -172,7 +171,7 @@  +-------------------+ ``` -## 구현 순서 +### 구현 순서 1. 기물 이동 기능 추가 * 나라 별 입력 @@ -183,4 +182,133 @@ * 기물의 이동할 위치를 입력 받는다. * 잘못된 범위라면 예외를 발생한다. * 해당 위치에 아군의 기물이 존재한다면 예외가 발생한다. - * 기물 별로 이동할 수 없는 목적지라면 예외가 발생한다. \ No newline at end of file + * 기물 별로 이동할 수 없는 목적지라면 예외가 발생한다. + +--- + +# Cycle2 + +## 1단계 - 궁성 영역 구현 + +1. 궁성 영역을 정의한다. + * 한나라 궁성: (0,3) ~ (2,5) 내 9개 위치 + * 초나라 궁성: (7,3) ~ (9,5) 내 9개 위치 + * 궁성 내 대각선 이동 가능 교차점을 정의한다. + * 한나라: (0,3), (0,5), (1,4), (2,3), (2,5) + * 초나라: (7,3), (7,5), (8,4), (9,3), (9,5) +2. 장(將/楚), 사(士)의 이동을 구현한다. + * 출발 지점이 궁성 영역이 아니라면 예외가 발생한다. + * 도착 지점이 궁성 영역이 아니라면 예외가 발생한다. + * 직선(상하좌우) 한 칸 이동이 가능하다. + * 대각선 한 칸 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. +3. 차(車)의 궁성 내 이동을 추가 구현한다. + * 궁성 내에서 대각선 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. + * 대각선 이동 경로 상에 기물이 있으면 예외가 발생한다. +4. 포(包)의 궁성 내 이동을 추가 구현한다. + * 궁성 내에서 대각선 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. + * 대각선 이동 경로 상에 정확히 하나의 기물을 뛰어넘어야 한다. + * 포는 뛰어넘을 수 없다. +5. 졸(卒)/병(兵)의 궁성 내 이동을 추가 구현한다. + * 궁성 내에서 대각선 한 칸 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. + * 전진 방향 대각선만 이동 가능하다. +6. 게임 종료 + * 왕(장)이 잡히면 게임이 종료된다. + * 한나라 왕이 잡히면 초나라가 승리한다. + * 초나라 왕이 잡히면 한나라가 승리한다. + * 게임 종료 시 승리한 팀을 출력한다. +7. 점수 계산 + * 왕이 잡히지 않은 경우 점수로 승패를 결정한다. (빅장) + * 점수가 높은 팀이 승리한다. + * 1.5점 가산으로 인해 무승부는 발생하지 않는다. + * 초나라는 후수이므로 1.5점의 추가 점수를 받는다. + +### 예시 출력 1) 빅장 + +``` +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 卒 * * 卒 卒 * | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ + +[초나라] 빅장입니다! 종료하시겠습니까? (Y, N) +N + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 6,3 + +[초나라] 기물 卒의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) +위치: 5,3 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * 卒 * * * * * | +6| 卒 * 卒 * * * 卒 卒 * | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ + +[한나라] 빅장입니다! 종료하시겠습니까? (Y, N) +Y + +한나라 승 +초나라: 72점 +한나라: 73.5점 +``` + +### 예시 출력 2) 왕 죽음 + +``` +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * 包 * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 7,4 + +[초나라] 기물 包의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) +위치: 1,4 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 包 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * * * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +초나라 승 diff --git a/src/main/java/model/piece/Elephant.java b/src/main/java/model/piece/Elephant.java index aa1da422d5..173d321e29 100644 --- a/src/main/java/model/piece/Elephant.java +++ b/src/main/java/model/piece/Elephant.java @@ -23,7 +23,6 @@ protected void validateMove(Position current, Position next) { } } - @Override protected List extractPath(Position start, Position end) { Displacement displacement = end.toDisplacement(start); From fff2d8c75f422f856ef28eb9b43ba3037a558ff7 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 12:53:01 +0900 Subject: [PATCH 07/32] =?UTF-8?q?feat:=20=EC=9E=A5/=EC=82=AC=20=EA=B6=81?= =?UTF-8?q?=EC=84=B1=20=EC=98=81=EC=97=AD=20=EC=9D=B4=EB=8F=99=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/main/java/model/coordinate/Direction.java | 8 --- .../java/model/coordinate/Displacement.java | 6 ++ src/main/java/model/coordinate/Palace.java | 45 ++++++++++++ src/main/java/model/piece/General.java | 14 +--- src/main/java/model/piece/Guard.java | 14 +--- src/main/java/model/piece/PalacePiece.java | 37 ++++++++++ src/main/java/model/piece/Piece.java | 16 ++++- src/main/java/model/piece/Soldier.java | 6 -- .../fixture/PalaceMovePositionFixture.java | 71 +++++++++++++++++++ .../java/model/piece/PalacePieceTest.java | 41 +++++++++++ 10 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 src/main/java/model/coordinate/Palace.java create mode 100644 src/main/java/model/piece/PalacePiece.java create mode 100644 src/test/java/model/fixture/PalaceMovePositionFixture.java create mode 100644 src/test/java/model/piece/PalacePieceTest.java diff --git a/src/main/java/model/coordinate/Direction.java b/src/main/java/model/coordinate/Direction.java index 5b392affa6..d70de25b91 100644 --- a/src/main/java/model/coordinate/Direction.java +++ b/src/main/java/model/coordinate/Direction.java @@ -34,12 +34,4 @@ private boolean isSameDirection(int rowDiff, int colDiff) { public Position move(Position target) { return target.resolveNext(row, col); } - - public int row() { - return row; - } - - public int col() { - return col; - } } diff --git a/src/main/java/model/coordinate/Displacement.java b/src/main/java/model/coordinate/Displacement.java index 9176ee0810..4d38c50624 100644 --- a/src/main/java/model/coordinate/Displacement.java +++ b/src/main/java/model/coordinate/Displacement.java @@ -30,6 +30,12 @@ public boolean isSideOneStep() { return rowDiff == 0 && absColDiff() == 1; } + public boolean isOneStepInRange() { + int row = absRowDiff(); + int col = absColDiff(); + return (row <= 1 && col <= 1) && (row + col > 0); + } + private int absRowDiff() { return Math.abs(rowDiff); } diff --git a/src/main/java/model/coordinate/Palace.java b/src/main/java/model/coordinate/Palace.java new file mode 100644 index 0000000000..13dd5fa86b --- /dev/null +++ b/src/main/java/model/coordinate/Palace.java @@ -0,0 +1,45 @@ +package model.coordinate; + +import java.util.Set; +import model.Team; + +public class Palace { + + private static final Set HAN_PALACE = Set.of( + new Position(0, 3), new Position(0, 4), new Position(0, 5), + new Position(1, 3), new Position(1, 4), new Position(1, 5), + new Position(2, 3), new Position(2, 4), new Position(2, 5) + ); + private static final Set CHO_PALACE = Set.of( + new Position(7, 3), new Position(7, 4), new Position(7, 5), + new Position(8, 3), new Position(8, 4), new Position(8, 5), + new Position(9, 3), new Position(9, 4), new Position(9, 5) + ); + private static final Set HAN_DIAGONAL_POINTS = Set.of( + new Position(0, 3), new Position(0, 5), + new Position(1, 4), + new Position(2, 3), new Position(2, 5) + ); + private static final Set CHO_DIAGONAL_POINTS = Set.of( + new Position(7, 3), new Position(7, 5), + new Position(8, 4), + new Position(9, 3), new Position(9, 5) + ); + + private Palace() { + } + + public static boolean contains(Team team, Position position) { + if (team.isHan()) { + return HAN_PALACE.contains(position); + } + return CHO_PALACE.contains(position); + } + + public static boolean isDiagonalPoint(Team team, Position position) { + if (team.isHan()) { + return HAN_DIAGONAL_POINTS.contains(position); + } + return CHO_DIAGONAL_POINTS.contains(position); + } +} \ No newline at end of file diff --git a/src/main/java/model/piece/General.java b/src/main/java/model/piece/General.java index 3ef59915ca..23c64ce504 100644 --- a/src/main/java/model/piece/General.java +++ b/src/main/java/model/piece/General.java @@ -1,22 +1,10 @@ package model.piece; -import java.util.List; import model.Team; -import model.coordinate.Position; -public class General extends Piece { +public class General extends PalacePiece { public General(Team team) { super(team, PieceType.GENERAL); } - - @Override - protected void validateMove(Position current, Position next) { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - } - - @Override - protected List extractPath(Position current, Position next) { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - } } diff --git a/src/main/java/model/piece/Guard.java b/src/main/java/model/piece/Guard.java index 432fee6e35..ec01f36d01 100644 --- a/src/main/java/model/piece/Guard.java +++ b/src/main/java/model/piece/Guard.java @@ -1,22 +1,10 @@ package model.piece; -import java.util.List; import model.Team; -import model.coordinate.Position; -public class Guard extends Piece { +public class Guard extends PalacePiece { public Guard(Team team) { super(team, PieceType.GUARD); } - - @Override - protected void validateMove(Position current, Position next) { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - } - - @Override - protected List extractPath(Position current, Position next) { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - } } diff --git a/src/main/java/model/piece/PalacePiece.java b/src/main/java/model/piece/PalacePiece.java new file mode 100644 index 0000000000..b392c73611 --- /dev/null +++ b/src/main/java/model/piece/PalacePiece.java @@ -0,0 +1,37 @@ +package model.piece; + +import model.Team; +import model.coordinate.Displacement; +import model.coordinate.Position; + +public abstract class PalacePiece extends Piece { + protected PalacePiece(Team team, PieceType type) { + super(team, type); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.toDisplacement(current); + validatePalacePosition(current, next); + validateOneStep(displacement); + validateDiagonal(current, displacement); + } + + private void validatePalacePosition(Position current, Position next) { + if (isNotPalace(current) || isNotPalace(next)) { + throw new IllegalArgumentException("궁성 영역 내부에서만 이동할 수 있습니다."); + } + } + + private void validateOneStep(Displacement displacement) { + if (!displacement.isOneStepInRange()) { + throw new IllegalArgumentException("궁성 내부에서 이동할 수 없는 위치입니다."); + } + } + + private void validateDiagonal(Position current, Displacement displacement) { + if (displacement.isNotStraight() && isNotPalaceDiagonal(current)) { + throw new IllegalArgumentException("궁성 대각선 이동은 특정 지점에서만 가능합니다."); + } + } +} \ No newline at end of file diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index 380d9fd94e..1761430f03 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -2,10 +2,10 @@ import java.util.List; import model.Team; +import model.coordinate.Palace; import model.coordinate.Position; public abstract class Piece { - private final Team team; private final PieceType type; @@ -37,12 +37,22 @@ public void validateTarget(Piece otherPiece) { protected abstract void validateMove(Position current, Position next); - protected abstract List extractPath(Position current, Position next); - protected boolean isSameType(Piece piece) { return type == piece.type; } + protected List extractPath(Position current, Position next) { + return List.of(); + } + + protected boolean isNotPalace(Position position) { + return !Palace.contains(team, position); + } + + protected boolean isNotPalaceDiagonal(Position position) { + return !Palace.isDiagonalPoint(team, position); + } + public Team getTeam() { return team; } diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java index fa385ac9c7..034c0ba3b4 100644 --- a/src/main/java/model/piece/Soldier.java +++ b/src/main/java/model/piece/Soldier.java @@ -1,6 +1,5 @@ package model.piece; -import java.util.List; import model.Team; import model.coordinate.Displacement; import model.coordinate.Position; @@ -28,9 +27,4 @@ protected void validateMove(Position current, Position next) { throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); } } - - @Override - protected List extractPath(Position current, Position next) { - return List.of(); - } } diff --git a/src/test/java/model/fixture/PalaceMovePositionFixture.java b/src/test/java/model/fixture/PalaceMovePositionFixture.java new file mode 100644 index 0000000000..0364b70fae --- /dev/null +++ b/src/test/java/model/fixture/PalaceMovePositionFixture.java @@ -0,0 +1,71 @@ +package model.fixture; + +import java.util.stream.Stream; +import model.Team; +import model.coordinate.Position; +import model.piece.PieceType; +import org.junit.jupiter.params.provider.Arguments; + +public class PalaceMovePositionFixture { + + // ============================ + // 將 & 士 (PalacePiece) 성공 케이스 + // ============================ + + public static Stream 장_사_이동_가능한_위치() { + return Stream.of( + // 1. 한나라 (HAN): 상단 궁성 (0,3~2,5) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 4), PieceType.GENERAL), // 사방위 (중앙 -> 하) + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 4), PieceType.GUARD), // 사방위 (중앙 -> 상) + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 3), PieceType.GENERAL), // 사방위 (중앙 -> 좌) + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 5), PieceType.GUARD), // 사방위 (중앙 -> 우) + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 5), PieceType.GUARD), // 대각선 (중앙->우하) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 5), PieceType.GUARD), // 대각선 (중앙->우상) + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 3), PieceType.GENERAL), // 대각선 (중앙->좌하) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 3), PieceType.GENERAL), // 대각선 (중앙->좌상) + + // 2. 초나라 (CHO): 하단 궁성 (7,3~9,5) + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 4), PieceType.GENERAL), // 사방위 (중앙 -> 하) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 4), PieceType.GUARD), // 사방위 (중앙 -> 상) + Arguments.of(Team.CHO, new Position(8, 4), new Position(8, 3), PieceType.GENERAL), // 사방위 (중앙 -> 좌) + Arguments.of(Team.CHO, new Position(8, 4), new Position(8, 5), PieceType.GUARD), // 사방위 (중앙 -> 우) + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 5), PieceType.GENERAL), // 대각선 (중앙->좌상) + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 3), PieceType.GUARD), // 대각선 (중앙->좌하) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 5), PieceType.GUARD), // 대각선 (중앙->우상) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 3), PieceType.GUARD) // 대각선 (중앙->우하) + ); + } + + // ============================ + // 將 & 士 (PalacePiece) 실패 케이스 + // ============================ + + public static Stream 장_사_이동_불가능한_위치() { + return Stream.of( + // 1. 궁성 사방위 이탈 (한나라 기준) - 상단은 장기판 밖 + Arguments.of(Team.HAN, new Position(0, 3), new Position(0, 2), PieceType.GENERAL), // 좌측 이탈 + Arguments.of(Team.HAN, new Position(0, 5), new Position(0, 6), PieceType.GUARD), // 우측 이탈 + Arguments.of(Team.HAN, new Position(2, 4), new Position(3, 4), PieceType.GUARD), // 하단 이탈 + + // 2. 궁성 사방위 이탈 (초나라 기준) - 하단은 장기판 밖 + Arguments.of(Team.CHO, new Position(9, 3), new Position(9, 2), PieceType.GENERAL), // 좌측 이탈 + Arguments.of(Team.CHO, new Position(9, 5), new Position(9, 6), PieceType.GUARD), // 우측 이탈 + Arguments.of(Team.CHO, new Position(7, 4), new Position(6, 4), PieceType.GENERAL), // 상단 이탈 + + // 3. 잘못된 대각선 시도 (대각선 선이 없는 지점) + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 3), PieceType.GENERAL), // 상단 변 -> 좌측 중앙 + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 5), PieceType.GENERAL), // 상단 변 -> 우측 중앙 + Arguments.of(Team.HAN, new Position(1, 3), new Position(0, 4), PieceType.GUARD), // 좌측 변 -> 상단 중앙 + Arguments.of(Team.HAN, new Position(1, 3), new Position(0, 2), PieceType.GUARD), // 좌측 변 -> 하단 중앙 + Arguments.of(Team.CHO, new Position(9, 4), new Position(8, 5), PieceType.GENERAL), // 하단 변 -> 우측 중앙 + Arguments.of(Team.CHO, new Position(9, 4), new Position(8, 3), PieceType.GENERAL), // 하단 변 -> 좌측 중앙 + Arguments.of(Team.CHO, new Position(8, 5), new Position(9, 4), PieceType.GUARD), // 우측 변 -> 하단 중앙 + Arguments.of(Team.CHO, new Position(8, 5), new Position(7, 4), PieceType.GUARD), // 우측 변 -> 상단 중앙 + + // 4. 거리 초과 및 특수 실패 + Arguments.of(Team.HAN, new Position(0, 4), new Position(2, 4), PieceType.GENERAL), // 직선 2칸 + Arguments.of(Team.CHO, new Position(7, 3), new Position(9, 5), PieceType.GUARD), // 대각선 2칸 + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 4), PieceType.GENERAL) // 제자리 이동 + ); + } +} \ No newline at end of file diff --git a/src/test/java/model/piece/PalacePieceTest.java b/src/test/java/model/piece/PalacePieceTest.java new file mode 100644 index 0000000000..460bf345be --- /dev/null +++ b/src/test/java/model/piece/PalacePieceTest.java @@ -0,0 +1,41 @@ +package model.piece; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import model.Team; +import model.coordinate.Position; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class PalacePieceTest { + + @ParameterizedTest(name = "{0}팀 {3}이 {1}에서 {2}로 이동 성공") + @MethodSource("model.fixture.PalaceMovePositionFixture#장_사_이동_가능한_위치") + void 장_사_이동_성공_테스트(Team team, Position current, Position next, PieceType type) { + // given + Piece piece = createPiece(team, type); + + // when & then + assertThatCode(() -> piece.validateMove(current, next)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "{0}팀 {3}이 {1}에서 {2}로 이동 실패") + @MethodSource("model.fixture.PalaceMovePositionFixture#장_사_이동_불가능한_위치") + void 장_사_이동_실패_테스트(Team team, Position current, Position next, PieceType type) { + // given + Piece piece = createPiece(team, type); + + // when & then + assertThatThrownBy(() -> piece.validateMove(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } + + private Piece createPiece(Team team, PieceType type) { + if (type == PieceType.GENERAL) { + return new General(team); + } + return new Guard(team); + } +} \ No newline at end of file From 6335b6bccd1c09d0e5221f52e4509005e07ecacc Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 14:03:08 +0900 Subject: [PATCH 08/32] =?UTF-8?q?feat:=20=EC=B0=A8/=ED=8F=AC=20=EA=B6=81?= =?UTF-8?q?=EC=84=B1=20=EC=98=81=EC=97=AD=20=EA=B8=B0=EB=AC=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/coordinate/Direction.java | 12 ++++ src/main/java/model/piece/Cannon.java | 32 +--------- src/main/java/model/piece/Chariot.java | 33 +--------- .../java/model/piece/LinearMovePiece.java | 52 ++++++++++++++++ src/main/java/model/piece/PalacePiece.java | 2 +- src/main/java/model/piece/Piece.java | 4 +- .../fixture/PalaceMovePositionFixture.java | 62 +++++++++++++++++++ src/test/java/model/piece/CannonTest.java | 26 +++++++- src/test/java/model/piece/ChariotTest.java | 28 ++++++++- 9 files changed, 182 insertions(+), 69 deletions(-) create mode 100644 src/main/java/model/piece/LinearMovePiece.java diff --git a/src/main/java/model/coordinate/Direction.java b/src/main/java/model/coordinate/Direction.java index d70de25b91..10eb9419d9 100644 --- a/src/main/java/model/coordinate/Direction.java +++ b/src/main/java/model/coordinate/Direction.java @@ -1,5 +1,7 @@ package model.coordinate; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; public enum Direction { @@ -31,6 +33,16 @@ private boolean isSameDirection(int rowDiff, int colDiff) { return row == Integer.signum(rowDiff) && col == Integer.signum(colDiff); } + public List pathTo(Position start, Position end) { + List path = new ArrayList<>(); + Position step = move(start); + while (!step.equals(end)) { + path.add(step); + step = move(step); + } + return path; + } + public Position move(Position target) { return target.resolveNext(row, col); } diff --git a/src/main/java/model/piece/Cannon.java b/src/main/java/model/piece/Cannon.java index d835ebf4f1..fe934c18b8 100644 --- a/src/main/java/model/piece/Cannon.java +++ b/src/main/java/model/piece/Cannon.java @@ -1,13 +1,9 @@ package model.piece; -import java.util.ArrayList; import java.util.List; import model.Team; -import model.coordinate.Direction; -import model.coordinate.Displacement; -import model.coordinate.Position; -public class Cannon extends Piece { +public class Cannon extends LinearMovePiece { private static final int CANNON_HURDLE_COUNT = 1; @@ -34,30 +30,4 @@ public void validateTarget(Piece otherPiece) { throw new IllegalArgumentException("포는 포를 잡을 수 없습니다."); } } - - @Override - protected void validateMove(Position current, Position next) { - Displacement displacement = next.toDisplacement(current); - if (displacement.isNotStraight()) { - throw new IllegalArgumentException("포가 이동할 수 없는 위치입니다."); - } - } - - @Override - protected List extractPath(Position start, Position end) { - Direction direction = resolveCardinal(start, end); - - List path = new ArrayList<>(); - Position step = direction.move(start); - while (!step.equals(end)) { - path.add(step); - step = direction.move(step); - } - return path; - } - - private Direction resolveCardinal(Position start, Position end) { - Displacement displacement = end.toDisplacement(start); - return displacement.extractCardinal(); - } } diff --git a/src/main/java/model/piece/Chariot.java b/src/main/java/model/piece/Chariot.java index 7ec66255ea..a8ee1b910b 100644 --- a/src/main/java/model/piece/Chariot.java +++ b/src/main/java/model/piece/Chariot.java @@ -1,41 +1,10 @@ package model.piece; -import java.util.ArrayList; -import java.util.List; import model.Team; -import model.coordinate.Direction; -import model.coordinate.Displacement; -import model.coordinate.Position; -public class Chariot extends Piece { +public class Chariot extends LinearMovePiece { public Chariot(Team team) { super(team, PieceType.CHARIOT); } - - @Override - protected void validateMove(Position current, Position next) { - Displacement displacement = next.toDisplacement(current); - if (displacement.isNotStraight()) { - throw new IllegalArgumentException("차가 이동할 수 없는 위치입니다."); - } - } - - @Override - protected List extractPath(Position start, Position end) { - Direction direction = resolveCardinal(start, end); - - List path = new ArrayList<>(); - Position step = direction.move(start); - while (!step.equals(end)) { - path.add(step); - step = direction.move(step); - } - return path; - } - - private Direction resolveCardinal(Position start, Position end) { - Displacement displacement = end.toDisplacement(start); - return displacement.extractCardinal(); - } } diff --git a/src/main/java/model/piece/LinearMovePiece.java b/src/main/java/model/piece/LinearMovePiece.java new file mode 100644 index 0000000000..ac94eabd49 --- /dev/null +++ b/src/main/java/model/piece/LinearMovePiece.java @@ -0,0 +1,52 @@ +package model.piece; + +import java.util.List; +import model.Team; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; + +public abstract class LinearMovePiece extends Piece { + protected LinearMovePiece(Team team, PieceType type) { + super(team, type); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.toDisplacement(current); + validatePalaceDiagonal(current, next); + validateStraight(current, displacement); + } + + @Override + protected List extractPath(Position start, Position end) { + if (isPalaceDiagonal(start)) { + return extractDiagonalPath(start, end); + } + return extractLinearPath(start, end); + } + + private void validateStraight(Position current, Displacement displacement) { + if (!isPalaceDiagonal(current) && displacement.isNotStraight()) { + throw new IllegalArgumentException("차가 이동할 수 없는 위치입니다."); + } + } + + private void validatePalaceDiagonal(Position current, Position next) { + if (isPalaceDiagonal(current) && !isPalaceDiagonal(next)) { + throw new IllegalArgumentException("궁성 영역 대각선에서 이동 할 수 없는 위치입니다."); + } + } + + private List extractLinearPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction direction = displacement.extractCardinal(); + return direction.pathTo(start, end); + } + + private List extractDiagonalPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction direction = displacement.extractDiagonal(); + return direction.pathTo(start, end); + } +} diff --git a/src/main/java/model/piece/PalacePiece.java b/src/main/java/model/piece/PalacePiece.java index b392c73611..5698a126f8 100644 --- a/src/main/java/model/piece/PalacePiece.java +++ b/src/main/java/model/piece/PalacePiece.java @@ -30,7 +30,7 @@ private void validateOneStep(Displacement displacement) { } private void validateDiagonal(Position current, Displacement displacement) { - if (displacement.isNotStraight() && isNotPalaceDiagonal(current)) { + if (displacement.isNotStraight() && !isPalaceDiagonal(current)) { throw new IllegalArgumentException("궁성 대각선 이동은 특정 지점에서만 가능합니다."); } } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index 1761430f03..c3f46e3cdf 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -49,8 +49,8 @@ protected boolean isNotPalace(Position position) { return !Palace.contains(team, position); } - protected boolean isNotPalaceDiagonal(Position position) { - return !Palace.isDiagonalPoint(team, position); + protected boolean isPalaceDiagonal(Position position) { + return Palace.isDiagonalPoint(team, position); } public Team getTeam() { diff --git a/src/test/java/model/fixture/PalaceMovePositionFixture.java b/src/test/java/model/fixture/PalaceMovePositionFixture.java index 0364b70fae..45dbbdf7e5 100644 --- a/src/test/java/model/fixture/PalaceMovePositionFixture.java +++ b/src/test/java/model/fixture/PalaceMovePositionFixture.java @@ -1,5 +1,6 @@ package model.fixture; +import java.util.List; import java.util.stream.Stream; import model.Team; import model.coordinate.Position; @@ -68,4 +69,65 @@ public class PalaceMovePositionFixture { Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 4), PieceType.GENERAL) // 제자리 이동 ); } + + // ============================ + // 車 & 包 궁성 대각선 케이스 + // ============================ + public static Stream 차_포_궁성_대각선_이동_경로() { + return Stream.of( + // 한나라 - 교차점 → 중앙 한 칸 (경로 없음) + Arguments.of(Team.HAN, new Position(0, 3), new Position(1, 4), List.of()), + Arguments.of(Team.HAN, new Position(0, 5), new Position(1, 4), List.of()), + Arguments.of(Team.HAN, new Position(2, 3), new Position(1, 4), List.of()), + Arguments.of(Team.HAN, new Position(2, 5), new Position(1, 4), List.of()), + // 한나라 - 중앙 → 교차점 한 칸 (경로 없음) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 3), List.of()), + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 5), List.of()), + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 3), List.of()), + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 5), List.of()), + // 한나라 - 교차점 → 교차점 두 칸 (중앙 경유) + Arguments.of(Team.HAN, new Position(0, 3), new Position(2, 5), List.of(new Position(1, 4))), + Arguments.of(Team.HAN, new Position(0, 5), new Position(2, 3), List.of(new Position(1, 4))), + Arguments.of(Team.HAN, new Position(2, 3), new Position(0, 5), List.of(new Position(1, 4))), + Arguments.of(Team.HAN, new Position(2, 5), new Position(0, 3), List.of(new Position(1, 4))), + + // 초나라 - 교차점 → 중앙 한 칸 (경로 없음) + Arguments.of(Team.CHO, new Position(7, 3), new Position(8, 4), List.of()), + Arguments.of(Team.CHO, new Position(7, 5), new Position(8, 4), List.of()), + Arguments.of(Team.CHO, new Position(9, 3), new Position(8, 4), List.of()), + Arguments.of(Team.CHO, new Position(9, 5), new Position(8, 4), List.of()), + // 초나라 - 중앙 → 교차점 한 칸 (경로 없음) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 3), List.of()), + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 5), List.of()), + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 3), List.of()), + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 5), List.of()), + // 초나라 - 교차점 → 교차점 두 칸 (중앙 경유) + Arguments.of(Team.CHO, new Position(7, 3), new Position(9, 5), List.of(new Position(8, 4))), + Arguments.of(Team.CHO, new Position(7, 5), new Position(9, 3), List.of(new Position(8, 4))), + Arguments.of(Team.CHO, new Position(9, 3), new Position(7, 5), List.of(new Position(8, 4))), + Arguments.of(Team.CHO, new Position(9, 5), new Position(7, 3), List.of(new Position(8, 4))) + ); + } + + public static Stream 차_포_궁성_대각선_이동_불가능한_위치() { + return Stream.of( + // 한나라 - 교차점이 아닌 곳에서 대각선 시도 + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 5)), + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 3)), + Arguments.of(Team.HAN, new Position(1, 3), new Position(2, 4)), + Arguments.of(Team.HAN, new Position(1, 5), new Position(2, 4)), + // 한나라 - 궁성 밖으로 대각선 이동 + Arguments.of(Team.HAN, new Position(0, 3), new Position(3, 6)), + Arguments.of(Team.HAN, new Position(2, 5), new Position(4, 7)), + + // 초나라 - 교차점이 아닌 곳에서 대각선 시도 + Arguments.of(Team.CHO, new Position(7, 4), new Position(8, 5)), + Arguments.of(Team.CHO, new Position(7, 4), new Position(8, 3)), + Arguments.of(Team.CHO, new Position(8, 3), new Position(9, 4)), + Arguments.of(Team.CHO, new Position(8, 5), new Position(9, 4)), + // 초나라 - 궁성 밖으로 대각선 이동 + Arguments.of(Team.CHO, new Position(7, 3), new Position(4, 0)), + Arguments.of(Team.CHO, new Position(9, 5), new Position(6, 2)) + ); + } } \ No newline at end of file diff --git a/src/test/java/model/piece/CannonTest.java b/src/test/java/model/piece/CannonTest.java index 14aeb6d14f..92d92c7fe6 100644 --- a/src/test/java/model/piece/CannonTest.java +++ b/src/test/java/model/piece/CannonTest.java @@ -20,7 +20,7 @@ public class CannonTest { Piece cannon = new Cannon(Team.HAN); // when & then - assertThatCode(() -> cannon.validateMove(current, next)) + assertThatCode(() -> cannon.pathTo(current, next)) .doesNotThrowAnyException(); } @@ -95,4 +95,28 @@ public class CannonTest { assertThatThrownBy(() -> cannon.validateTarget(targetCannon)) .isInstanceOf(IllegalArgumentException.class); } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_경로") + void 포는_궁성_교차점에서_대각선으로_이동_경로를_구할_수_있다(Team team, Position current, Position next, List expectedPath) { + // given + Piece cannon = new Cannon(team); + + // when & then + List path = cannon.pathTo(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_불가능한_위치") + void 포는_궁성_교차점이_아닌_곳에서_대각선으로_이동할_수_없다(Team team, Position current, Position next) { + // given + Piece cannon = new Cannon(team); + + // when & then + assertThatThrownBy(() -> cannon.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file diff --git a/src/test/java/model/piece/ChariotTest.java b/src/test/java/model/piece/ChariotTest.java index e9d1ca632b..638d8947da 100644 --- a/src/test/java/model/piece/ChariotTest.java +++ b/src/test/java/model/piece/ChariotTest.java @@ -21,7 +21,7 @@ public class ChariotTest { Piece chariot = new Chariot(Team.HAN); // when & then - assertThatCode(() -> chariot.validateMove(current, next)) + assertThatCode(() -> chariot.pathTo(current, next)) .doesNotThrowAnyException(); } @@ -32,7 +32,7 @@ public class ChariotTest { Piece chariot = new Chariot(Team.HAN); // when & then - assertThatThrownBy(() -> chariot.validateMove(current, next)) + assertThatThrownBy(() -> chariot.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } @@ -59,4 +59,28 @@ public class ChariotTest { assertThatThrownBy(() -> chariot.validatePathCondition(obstacles)) .isInstanceOf(IllegalArgumentException.class); } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_경로") + void 차는_궁성_교차점에서_대각선으로_이동_경로를_구할_수_있다(Team team, Position current, Position next, List expectedPath) { + // given + Piece cannon = new Chariot(team); + + // when & then + List path = cannon.pathTo(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_불가능한_위치") + void 차는_궁성_교차점이_아닌_곳에서_대각선으로_이동할_수_없다(Team team, Position current, Position next) { + // given + Piece cannon = new Chariot(team); + + // when & then + assertThatThrownBy(() -> cannon.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file From c0f119938e2bb5bc871e79f25acccf7a0fdc2f5f Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 14:23:53 +0900 Subject: [PATCH 09/32] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/piece/Elephant.java | 2 +- src/main/java/model/piece/Horse.java | 2 +- .../java/model/piece/LinearMovePiece.java | 2 +- src/main/java/model/piece/Soldier.java | 2 +- .../fixture/PieceMovePositionFixture.java | 57 ------------------- src/test/java/model/piece/CannonTest.java | 14 +---- src/test/java/model/piece/ChariotTest.java | 12 ---- src/test/java/model/piece/ElephantTest.java | 15 +---- src/test/java/model/piece/HorseTest.java | 14 +---- .../java/model/piece/PalacePieceTest.java | 13 +++-- src/test/java/model/piece/SoldierTest.java | 14 +---- 11 files changed, 17 insertions(+), 130 deletions(-) diff --git a/src/main/java/model/piece/Elephant.java b/src/main/java/model/piece/Elephant.java index 173d321e29..db31288db2 100644 --- a/src/main/java/model/piece/Elephant.java +++ b/src/main/java/model/piece/Elephant.java @@ -19,7 +19,7 @@ public Elephant(Team team) { protected void validateMove(Position current, Position next) { Displacement displacement = next.toDisplacement(current); if (displacement.isNotStepCombination(ELEPHANT_LONG_STEP, ELEPHANT_SHORT_STEP)) { - throw new IllegalArgumentException("상이 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); } } diff --git a/src/main/java/model/piece/Horse.java b/src/main/java/model/piece/Horse.java index 0137dbd9f1..653f8207bd 100644 --- a/src/main/java/model/piece/Horse.java +++ b/src/main/java/model/piece/Horse.java @@ -19,7 +19,7 @@ public Horse(Team team) { protected void validateMove(Position current, Position next) { Displacement displacement = next.toDisplacement(current); if (displacement.isNotStepCombination(HORSE_LONG_STEP, HORSE_SHORT_STEP)) { - throw new IllegalArgumentException("마가 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); } } diff --git a/src/main/java/model/piece/LinearMovePiece.java b/src/main/java/model/piece/LinearMovePiece.java index ac94eabd49..33e5fd714a 100644 --- a/src/main/java/model/piece/LinearMovePiece.java +++ b/src/main/java/model/piece/LinearMovePiece.java @@ -28,7 +28,7 @@ protected List extractPath(Position start, Position end) { private void validateStraight(Position current, Displacement displacement) { if (!isPalaceDiagonal(current) && displacement.isNotStraight()) { - throw new IllegalArgumentException("차가 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); } } diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java index 034c0ba3b4..545f0a468b 100644 --- a/src/main/java/model/piece/Soldier.java +++ b/src/main/java/model/piece/Soldier.java @@ -24,7 +24,7 @@ private static int resolveForwardDirection(Team team) { protected void validateMove(Position current, Position next) { Displacement displacement = next.toDisplacement(current); if (!(displacement.isForwardBy(forwardDirection) || displacement.isSideOneStep())) { - throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); } } } diff --git a/src/test/java/model/fixture/PieceMovePositionFixture.java b/src/test/java/model/fixture/PieceMovePositionFixture.java index 2bb9f48605..cb70e8188e 100644 --- a/src/test/java/model/fixture/PieceMovePositionFixture.java +++ b/src/test/java/model/fixture/PieceMovePositionFixture.java @@ -7,24 +7,10 @@ public class PieceMovePositionFixture { - // 공통 - public static Stream 제자리_이동_케이스() { - return Stream.of(Arguments.of(new Position(0, 0), new Position(0, 0))); - } - // ============================ // 직선 이동 (車, 包 공통) // ============================ - public static Stream 사방위_이동_방향_케이스() { - return Stream.of( - Arguments.of(new Position(0, 0), new Position(0, 5)), // 우 이동 - Arguments.of(new Position(0, 0), new Position(5, 0)), // 하 이동 - Arguments.of(new Position(5, 5), new Position(0, 5)), // 상 이동 - Arguments.of(new Position(5, 5), new Position(5, 0)) // 좌 이동 - ); - } - public static Stream 사간방_대각선_이동_방향_케이스() { return Stream.of( Arguments.of(new Position(2, 2), new Position(4, 4)), // 1. 북동 (NE): rowDiff 증가, colDiff 증가 @@ -38,19 +24,6 @@ public class PieceMovePositionFixture { // 馬 // ============================ - public static Stream 마_이동_가능한_위치() { - return Stream.of( - Arguments.of(new Position(5, 5), new Position(3, 4)), // 상+좌 (rowDiff-2, colDiff-1) - Arguments.of(new Position(5, 5), new Position(3, 6)), // 상+우 (rowDiff-2, colDiff+1) - Arguments.of(new Position(5, 5), new Position(7, 4)), // 하+좌 (rowDiff+2, colDiff-1) - Arguments.of(new Position(5, 5), new Position(7, 6)), // 하+우 (rowDiff+2, colDiff+1) - Arguments.of(new Position(5, 5), new Position(4, 3)), // 좌+상 (rowDiff-1, colDiff-2) - Arguments.of(new Position(5, 5), new Position(6, 3)), // 좌+하 (rowDiff+1, colDiff-2) - Arguments.of(new Position(5, 5), new Position(4, 7)), // 우+상 (rowDiff-1, colDiff+2) - Arguments.of(new Position(5, 5), new Position(6, 7)) // 우+하 (rowDiff+1, colDiff+2) - ); - } - public static Stream 마_이동_불가능한_위치() { return Stream.of( Arguments.of(new Position(5, 5), new Position(5, 7)), // 직선 이동 @@ -65,19 +38,6 @@ public class PieceMovePositionFixture { // 象 // ============================ - public static Stream 상_이동_가능한_위치() { - return Stream.of( - Arguments.of(new Position(5, 5), new Position(2, 3)), // 상+좌 (rowDiff-3, colDiff-2) - Arguments.of(new Position(5, 5), new Position(2, 7)), // 상+우 (rowDiff-3, colDiff+2) - Arguments.of(new Position(5, 5), new Position(8, 3)), // 하+좌 (rowDiff+3, colDiff-2) - Arguments.of(new Position(5, 5), new Position(8, 7)), // 하+우 (rowDiff+3, colDiff+2) - Arguments.of(new Position(5, 5), new Position(3, 2)), // 좌+상 (rowDiff-2, colDiff-3) - Arguments.of(new Position(5, 5), new Position(7, 2)), // 좌+하 (rowDiff+2, colDiff-3) - Arguments.of(new Position(5, 5), new Position(3, 8)), // 우+상 (rowDiff-2, colDiff+3) - Arguments.of(new Position(5, 5), new Position(7, 8)) // 우+하 (rowDiff+2, colDiff+3) - ); - } - public static Stream 상_이동_불가능한_위치() { return Stream.of( Arguments.of(new Position(5, 5), new Position(3, 4)), // 마 이동 (rowDiff-2, colDiff-1) @@ -91,23 +51,6 @@ public class PieceMovePositionFixture { // ============================ // 兵 & 卒 (Soldier) 통합 데이터 // ============================ - public static Stream 졸_병_이동_가능한_위치() { - return Stream.concat( - // 한나라 (HAN): 전진(rowDiff+1), 좌우 - Stream.of( - Arguments.of(Team.HAN, new Position(3, 2), new Position(4, 2)), // 전진 - Arguments.of(Team.HAN, new Position(3, 2), new Position(3, 1)), // 좌 - Arguments.of(Team.HAN, new Position(3, 2), new Position(3, 3)) // 우 - ), - // 초나라 (CHO): 전진(rowDiff-1), 좌우 - Stream.of( - Arguments.of(Team.CHO, new Position(6, 2), new Position(5, 2)), // 전진 - Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 1)), // 좌 - Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 3)) // 우 - ) - ); - } - public static Stream 졸_병_이동_불가능한_위치() { return Stream.concat( // 한나라 (HAN) 불가능 diff --git a/src/test/java/model/piece/CannonTest.java b/src/test/java/model/piece/CannonTest.java index 92d92c7fe6..c0cd90d3a6 100644 --- a/src/test/java/model/piece/CannonTest.java +++ b/src/test/java/model/piece/CannonTest.java @@ -1,7 +1,6 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -13,17 +12,6 @@ public class CannonTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#사방위_이동_방향_케이스") - void 포는_직선으로_이동할_수_있다(Position current, Position next) { - // given - Piece cannon = new Cannon(Team.HAN); - - // when & then - assertThatCode(() -> cannon.pathTo(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#사간방_대각선_이동_방향_케이스") void 포는_대각선이나_제자리로_이동할_수_없다(Position current, Position next) { @@ -31,7 +19,7 @@ public class CannonTest { Piece cannon = new Cannon(Team.HAN); // when & then - assertThatThrownBy(() -> cannon.validateMove(current, next)) + assertThatThrownBy(() -> cannon.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } diff --git a/src/test/java/model/piece/ChariotTest.java b/src/test/java/model/piece/ChariotTest.java index 638d8947da..2ada118414 100644 --- a/src/test/java/model/piece/ChariotTest.java +++ b/src/test/java/model/piece/ChariotTest.java @@ -1,7 +1,6 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -14,17 +13,6 @@ public class ChariotTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#사방위_이동_방향_케이스") - void 차는_직선으로_이동할_수_있다(Position current, Position next) { - // given - Piece chariot = new Chariot(Team.HAN); - - // when & then - assertThatCode(() -> chariot.pathTo(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#사간방_대각선_이동_방향_케이스") void 차는_대각선_또는_제자리로_이동할_수_없다(Position current, Position next) { diff --git a/src/test/java/model/piece/ElephantTest.java b/src/test/java/model/piece/ElephantTest.java index d58859230d..0dea94ac0f 100644 --- a/src/test/java/model/piece/ElephantTest.java +++ b/src/test/java/model/piece/ElephantTest.java @@ -1,7 +1,6 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -14,17 +13,6 @@ public class ElephantTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#상_이동_가능한_위치") - void 상은_두칸_직진_후_대각선으로_이동할_수_있다(Position current, Position next) { - // given - Piece elephant = new Elephant(Team.HAN); - - // when & then - assertThatCode(() -> elephant.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#상_이동_불가능한_위치") void 상은_이동_규칙에_맞지_않으면_이동할_수_없다(Position current, Position next) { @@ -32,7 +20,8 @@ public class ElephantTest { Piece elephant = new Elephant(Team.HAN); // when & then - assertThatThrownBy(() -> elephant.validateMove(current, next)); + assertThatThrownBy(() -> elephant.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); } @ParameterizedTest diff --git a/src/test/java/model/piece/HorseTest.java b/src/test/java/model/piece/HorseTest.java index 9e98ee90e8..cf4eb615c5 100644 --- a/src/test/java/model/piece/HorseTest.java +++ b/src/test/java/model/piece/HorseTest.java @@ -1,7 +1,6 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -14,17 +13,6 @@ public class HorseTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#마_이동_가능한_위치") - void 마는_한칸_직진_후_대각선으로_이동할_수_있다(Position current, Position next) { - // given - Piece horse = new Horse(Team.HAN); - - // when & then - assertThatCode(() -> horse.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#마_이동_불가능한_위치") void 마는_이동_규칙에_맞지_않으면_이동할_수_없다(Position current, Position next) { @@ -32,7 +20,7 @@ public class HorseTest { Piece horse = new Horse(Team.HAN); // when & then - assertThatThrownBy(() -> horse.validateMove(current, next)) + assertThatThrownBy(() -> horse.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } diff --git a/src/test/java/model/piece/PalacePieceTest.java b/src/test/java/model/piece/PalacePieceTest.java index 460bf345be..105fa6a0a3 100644 --- a/src/test/java/model/piece/PalacePieceTest.java +++ b/src/test/java/model/piece/PalacePieceTest.java @@ -1,8 +1,9 @@ package model.piece; -import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; import model.Team; import model.coordinate.Position; import org.junit.jupiter.params.ParameterizedTest; @@ -16,9 +17,11 @@ public class PalacePieceTest { // given Piece piece = createPiece(team, type); - // when & then - assertThatCode(() -> piece.validateMove(current, next)) - .doesNotThrowAnyException(); + // when + List result = piece.pathTo(current, next); + + // then + assertThat(result).isEmpty(); } @ParameterizedTest(name = "{0}팀 {3}이 {1}에서 {2}로 이동 실패") @@ -28,7 +31,7 @@ public class PalacePieceTest { Piece piece = createPiece(team, type); // when & then - assertThatThrownBy(() -> piece.validateMove(current, next)) + assertThatThrownBy(() -> piece.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } diff --git a/src/test/java/model/piece/SoldierTest.java b/src/test/java/model/piece/SoldierTest.java index 2a65426e0d..d9ed46b0d8 100644 --- a/src/test/java/model/piece/SoldierTest.java +++ b/src/test/java/model/piece/SoldierTest.java @@ -1,7 +1,6 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -14,17 +13,6 @@ public class SoldierTest { - @ParameterizedTest(name = "{0}나라 졸 일 때, {1}에서 {2}로 이동할 수 있다.") - @MethodSource("model.fixture.PieceMovePositionFixture#졸_병_이동_가능한_위치") - void 졸_병_이동_성공_테스트(Team team, Position current, Position next) { - // given - Piece soldier = new Soldier(team); - - // when & then - assertThatCode(() -> soldier.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest(name = "{0}나라 졸 일 때, {1}에서 {2}로 이동할 수 없다.") @MethodSource("model.fixture.PieceMovePositionFixture#졸_병_이동_불가능한_위치") void 졸_병_이동_실패_테스트(Team team, Position current, Position next) { @@ -32,7 +20,7 @@ public class SoldierTest { Piece soldier = new Soldier(team); // when & then - assertThatThrownBy(() -> soldier.validateMove(current, next)) + assertThatThrownBy(() -> soldier.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } From 938d08943ad3519a2aeeff69be518b271e8393fe Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 14:52:53 +0900 Subject: [PATCH 10/32] =?UTF-8?q?fix:=20=EC=A7=81=EC=A7=84=20=EA=B8=B0?= =?UTF-8?q?=EB=AC=BC=20=EA=B5=90=EC=B0=A8=EC=A0=90=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 교차점에서 수평/수직 이동 실패하는 문제 해결 --- src/main/java/model/piece/LinearMovePiece.java | 15 +++++---------- src/main/java/model/piece/Piece.java | 7 +++++++ .../java/model/fixture/PieceMovePathFixture.java | 6 ++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/java/model/piece/LinearMovePiece.java b/src/main/java/model/piece/LinearMovePiece.java index 33e5fd714a..55c5c1cea6 100644 --- a/src/main/java/model/piece/LinearMovePiece.java +++ b/src/main/java/model/piece/LinearMovePiece.java @@ -14,8 +14,9 @@ protected LinearMovePiece(Team team, PieceType type) { @Override protected void validateMove(Position current, Position next) { Displacement displacement = next.toDisplacement(current); - validatePalaceDiagonal(current, next); - validateStraight(current, displacement); + if (displacement.isNotStraight()) { + validatePalaceDiagonal(current, next); + } } @Override @@ -26,15 +27,9 @@ protected List extractPath(Position start, Position end) { return extractLinearPath(start, end); } - private void validateStraight(Position current, Displacement displacement) { - if (!isPalaceDiagonal(current) && displacement.isNotStraight()) { - throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); - } - } - private void validatePalaceDiagonal(Position current, Position next) { - if (isPalaceDiagonal(current) && !isPalaceDiagonal(next)) { - throw new IllegalArgumentException("궁성 영역 대각선에서 이동 할 수 없는 위치입니다."); + if (!isPalaceDiagonal(current) || !isPalaceDiagonal(next)) { + throw new IllegalArgumentException("대각선 이동은 궁성 교차점에서만 가능합니다."); } } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index c3f46e3cdf..05e42dc9f8 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -14,7 +14,14 @@ protected Piece(Team team, PieceType type) { this.type = type; } + private static void validatePosition(Position current, Position next) { + if (current.equals(next)) { + throw new IllegalArgumentException("기물은 제자리로 이동할 수 없습니다."); + } + } + public List pathTo(Position current, Position next) { + validatePosition(current, next); validateMove(current, next); return extractPath(current, next); } diff --git a/src/test/java/model/fixture/PieceMovePathFixture.java b/src/test/java/model/fixture/PieceMovePathFixture.java index ab953b76a8..8da4596782 100644 --- a/src/test/java/model/fixture/PieceMovePathFixture.java +++ b/src/test/java/model/fixture/PieceMovePathFixture.java @@ -27,6 +27,12 @@ public class PieceMovePathFixture { new Position(0, 0), new Position(3, 0), List.of(new Position(1, 0), new Position(2, 0)) + ), + // 4. 궁성 교차점에서 수평 이동 + Arguments.of( + new Position(2, 5), + new Position(2, 8), + List.of(new Position(2, 6), new Position(2, 7)) ) ); } From f0c0e845e89c855855dd2e5904a8917b63fd793e Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 17:42:16 +0900 Subject: [PATCH 11/32] =?UTF-8?q?feat:=20=EC=A1=B8=20=EA=B6=81=EC=84=B1=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/JanggiGame.java | 2 +- src/main/java/model/Team.java | 2 +- .../java/model/coordinate/Displacement.java | 8 +++-- src/main/java/model/coordinate/Palace.java | 3 ++ src/main/java/model/coordinate/Position.java | 10 +----- .../java/model/piece/LinearMovePiece.java | 15 ++++++--- src/main/java/model/piece/PalacePiece.java | 9 +++-- src/main/java/model/piece/Piece.java | 13 ++------ src/main/java/model/piece/Soldier.java | 33 ++++++++++++++++++- .../java/view/formater/BoardFormatter.java | 4 +-- src/test/java/model/JanggiGameTest.java | 2 +- src/test/java/model/TeamTest.java | 4 +-- .../fixture/PalaceMovePositionFixture.java | 9 +++++ .../model/fixture/PieceMovePathFixture.java | 6 +++- src/test/java/model/piece/SoldierTest.java | 11 +++++++ 15 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index 28af47d51d..6831ca9dba 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -24,7 +24,7 @@ public void movePiece(Position current, Position next) { piece.validatePathCondition(pieces); board.move(current, next); - this.turn = turn.next(); + this.turn = turn.opposite(); } public Piece selectPiece(Position position) { diff --git a/src/main/java/model/Team.java b/src/main/java/model/Team.java index 7ba69b1f45..9b54c4975e 100644 --- a/src/main/java/model/Team.java +++ b/src/main/java/model/Team.java @@ -13,7 +13,7 @@ public boolean isHan() { return this == HAN; } - public Team next() { + public Team opposite() { if (isHan()) { return CHO; } diff --git a/src/main/java/model/coordinate/Displacement.java b/src/main/java/model/coordinate/Displacement.java index 4d38c50624..8011457f4c 100644 --- a/src/main/java/model/coordinate/Displacement.java +++ b/src/main/java/model/coordinate/Displacement.java @@ -22,8 +22,12 @@ public boolean isNotStepCombination(int longStep, int shortStep) { (absRowDiff() != shortStep || absColDiff() != longStep); } - public boolean isForwardBy(int forwardCount) { - return rowDiff == forwardCount && colDiff == 0; + public boolean isForwardBy(int forwardDirection) { + return rowDiff == forwardDirection && colDiff == 0; + } + + public boolean isSameForwardDirection(int forwardDirection) { + return rowDiff == forwardDirection; } public boolean isSideOneStep() { diff --git a/src/main/java/model/coordinate/Palace.java b/src/main/java/model/coordinate/Palace.java index 13dd5fa86b..1574c94597 100644 --- a/src/main/java/model/coordinate/Palace.java +++ b/src/main/java/model/coordinate/Palace.java @@ -10,16 +10,19 @@ public class Palace { new Position(1, 3), new Position(1, 4), new Position(1, 5), new Position(2, 3), new Position(2, 4), new Position(2, 5) ); + private static final Set CHO_PALACE = Set.of( new Position(7, 3), new Position(7, 4), new Position(7, 5), new Position(8, 3), new Position(8, 4), new Position(8, 5), new Position(9, 3), new Position(9, 4), new Position(9, 5) ); + private static final Set HAN_DIAGONAL_POINTS = Set.of( new Position(0, 3), new Position(0, 5), new Position(1, 4), new Position(2, 3), new Position(2, 5) ); + private static final Set CHO_DIAGONAL_POINTS = Set.of( new Position(7, 3), new Position(7, 5), new Position(8, 4), diff --git a/src/main/java/model/coordinate/Position.java b/src/main/java/model/coordinate/Position.java index 6fc4b74705..daf2469eb3 100644 --- a/src/main/java/model/coordinate/Position.java +++ b/src/main/java/model/coordinate/Position.java @@ -16,15 +16,7 @@ public record Position(int row, int col) { } public Displacement toDisplacement(Position other) { - return new Displacement(calculateRowDiff(other), calculateColDiff(other)); - } - - public int calculateRowDiff(Position other) { - return this.row() - other.row(); - } - - public int calculateColDiff(Position other) { - return this.col - other.col(); + return new Displacement(this.row() - other.row(), this.col - other.col()); } public Position resolveNext(int rowDistance, int colDistance) { diff --git a/src/main/java/model/piece/LinearMovePiece.java b/src/main/java/model/piece/LinearMovePiece.java index 55c5c1cea6..3f193ce097 100644 --- a/src/main/java/model/piece/LinearMovePiece.java +++ b/src/main/java/model/piece/LinearMovePiece.java @@ -4,6 +4,7 @@ import model.Team; import model.coordinate.Direction; import model.coordinate.Displacement; +import model.coordinate.Palace; import model.coordinate.Position; public abstract class LinearMovePiece extends Piece { @@ -20,19 +21,23 @@ protected void validateMove(Position current, Position next) { } @Override - protected List extractPath(Position start, Position end) { - if (isPalaceDiagonal(start)) { - return extractDiagonalPath(start, end); + protected List extractPath(Position current, Position next) { + if (Palace.isDiagonalPoint(team(), current)) { + return extractDiagonalPath(current, next); } - return extractLinearPath(start, end); + return extractLinearPath(current, next); } private void validatePalaceDiagonal(Position current, Position next) { - if (!isPalaceDiagonal(current) || !isPalaceDiagonal(next)) { + if (isNotInPalaceDiagonal(current, next)) { throw new IllegalArgumentException("대각선 이동은 궁성 교차점에서만 가능합니다."); } } + private boolean isNotInPalaceDiagonal(Position current, Position next) { + return !Palace.isDiagonalPoint(team(), current) || !Palace.isDiagonalPoint(team(), next); + } + private List extractLinearPath(Position start, Position end) { Displacement displacement = end.toDisplacement(start); Direction direction = displacement.extractCardinal(); diff --git a/src/main/java/model/piece/PalacePiece.java b/src/main/java/model/piece/PalacePiece.java index 5698a126f8..f7e1b277bd 100644 --- a/src/main/java/model/piece/PalacePiece.java +++ b/src/main/java/model/piece/PalacePiece.java @@ -2,6 +2,7 @@ import model.Team; import model.coordinate.Displacement; +import model.coordinate.Palace; import model.coordinate.Position; public abstract class PalacePiece extends Piece { @@ -18,11 +19,15 @@ protected void validateMove(Position current, Position next) { } private void validatePalacePosition(Position current, Position next) { - if (isNotPalace(current) || isNotPalace(next)) { + if (isNotInPalace(current, next)) { throw new IllegalArgumentException("궁성 영역 내부에서만 이동할 수 있습니다."); } } + private boolean isNotInPalace(Position current, Position next) { + return !Palace.contains(team(), current) || !Palace.contains(team(), next); + } + private void validateOneStep(Displacement displacement) { if (!displacement.isOneStepInRange()) { throw new IllegalArgumentException("궁성 내부에서 이동할 수 없는 위치입니다."); @@ -30,7 +35,7 @@ private void validateOneStep(Displacement displacement) { } private void validateDiagonal(Position current, Displacement displacement) { - if (displacement.isNotStraight() && !isPalaceDiagonal(current)) { + if (displacement.isNotStraight() && !Palace.isDiagonalPoint(team(), current)) { throw new IllegalArgumentException("궁성 대각선 이동은 특정 지점에서만 가능합니다."); } } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index 05e42dc9f8..ccab835d2c 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -2,7 +2,6 @@ import java.util.List; import model.Team; -import model.coordinate.Palace; import model.coordinate.Position; public abstract class Piece { @@ -37,7 +36,7 @@ public void validatePathCondition(List pieces) { } public void validateTarget(Piece otherPiece) { - if (getTeam() == otherPiece.team) { + if (team() == otherPiece.team) { throw new IllegalArgumentException("아군이 있는 위치로 이동할 수 없습니다."); } } @@ -52,15 +51,7 @@ protected List extractPath(Position current, Position next) { return List.of(); } - protected boolean isNotPalace(Position position) { - return !Palace.contains(team, position); - } - - protected boolean isPalaceDiagonal(Position position) { - return Palace.isDiagonalPoint(team, position); - } - - public Team getTeam() { + public Team team() { return team; } diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java index 545f0a468b..ffe575655a 100644 --- a/src/main/java/model/piece/Soldier.java +++ b/src/main/java/model/piece/Soldier.java @@ -2,6 +2,7 @@ import model.Team; import model.coordinate.Displacement; +import model.coordinate.Palace; import model.coordinate.Position; public class Soldier extends Piece { @@ -23,8 +24,38 @@ private static int resolveForwardDirection(Team team) { @Override protected void validateMove(Position current, Position next) { Displacement displacement = next.toDisplacement(current); + if (displacement.isNotStraight()) { + validateDiagonalOneStep(displacement); + validateEnemyPalaceDiagonal(current, next); + return; + } + validateForwardOneStep(displacement); + } + + private void validateEnemyPalaceDiagonal(Position current, Position next) { + if (isNotInEnemyPalaceDiagonal(current, next)) { + throw new IllegalArgumentException("졸은 궁성 교차점에서만 대각선 이동 가능합니다."); + } + } + + private boolean isNotInEnemyPalaceDiagonal(Position current, Position next) { + Team enemy = team().opposite(); + return !Palace.isDiagonalPoint(enemy, current) || !Palace.isDiagonalPoint(enemy, next); + } + + private void validateForwardOneStep(Displacement displacement) { if (!(displacement.isForwardBy(forwardDirection) || displacement.isSideOneStep())) { - throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("졸은 전진 또는 좌우로만 이동할 수 있습니다."); + } + } + + private void validateDiagonalOneStep(Displacement displacement) { + if (!displacement.isOneStepInRange()) { + throw new IllegalArgumentException("졸은 한 칸만 이동할 수 있습니다."); + } + + if (!displacement.isSameForwardDirection(forwardDirection)) { + throw new IllegalArgumentException("졸은 전진 대각선만 이동 가능합니다."); } } } diff --git a/src/main/java/view/formater/BoardFormatter.java b/src/main/java/view/formater/BoardFormatter.java index a01a712350..da6bc9e664 100644 --- a/src/main/java/view/formater/BoardFormatter.java +++ b/src/main/java/view/formater/BoardFormatter.java @@ -32,8 +32,8 @@ public static String formatSymbol(Piece piece) { if (piece == null) { return EMPTY; } - String color = extractColor(piece.getTeam()); - String symbol = SYMBOL_MAP.get(piece.getType()).get(piece.getTeam()); + String color = extractColor(piece.team()); + String symbol = SYMBOL_MAP.get(piece.getType()).get(piece.team()); return color + symbol + RESET; } diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java index 01e281e097..d636472e82 100644 --- a/src/test/java/model/JanggiGameTest.java +++ b/src/test/java/model/JanggiGameTest.java @@ -28,7 +28,7 @@ class JanggiGameTest { // then assertThat(board.pickPiece(destination)).isEqualTo(piece); - assertThat(janggiGame.getTurn()).isEqualTo(prevTurn.next()); + assertThat(janggiGame.getTurn()).isEqualTo(prevTurn.opposite()); } @Test diff --git a/src/test/java/model/TeamTest.java b/src/test/java/model/TeamTest.java index bdfb889af7..5dc14457df 100644 --- a/src/test/java/model/TeamTest.java +++ b/src/test/java/model/TeamTest.java @@ -12,7 +12,7 @@ class TeamTest { Team han = Team.HAN; // when - Team next = han.next(); + Team next = han.opposite(); // then assertThat(next).isEqualTo(Team.CHO); @@ -24,7 +24,7 @@ class TeamTest { Team cho = Team.CHO; // when - Team next = cho.next(); + Team next = cho.opposite(); // then assertThat(next).isEqualTo(Team.HAN); diff --git a/src/test/java/model/fixture/PalaceMovePositionFixture.java b/src/test/java/model/fixture/PalaceMovePositionFixture.java index 45dbbdf7e5..71f17994bf 100644 --- a/src/test/java/model/fixture/PalaceMovePositionFixture.java +++ b/src/test/java/model/fixture/PalaceMovePositionFixture.java @@ -130,4 +130,13 @@ public class PalaceMovePositionFixture { Arguments.of(Team.CHO, new Position(9, 5), new Position(6, 2)) ); } + + public static Stream 졸_병_궁성_이동_불가능한_위치() { + return Stream.of( + // 적군 궁성 내 '후퇴' 대각선 시도 + Arguments.of(Team.CHO, new Position(2, 4), new Position(3, 5)), + // 적군 궁성 내 대각선 2칸 이동 시도 + Arguments.of(Team.HAN, new Position(7, 3), new Position(9, 5)) + ); + } } \ No newline at end of file diff --git a/src/test/java/model/fixture/PieceMovePathFixture.java b/src/test/java/model/fixture/PieceMovePathFixture.java index 8da4596782..eb33316ab2 100644 --- a/src/test/java/model/fixture/PieceMovePathFixture.java +++ b/src/test/java/model/fixture/PieceMovePathFixture.java @@ -82,7 +82,11 @@ public class PieceMovePathFixture { // 초나라 전진/좌/우 Arguments.of(Team.CHO, new Position(6, 2), new Position(5, 2)), Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 1)), - Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 3)) + Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 3)), + // 한나라 적진 궁성 이동 + Arguments.of(Team.HAN, new Position(7, 5), new Position(8, 4)), // 대각선 + // 초나라 적진 궁성 이동 + Arguments.of(Team.CHO, new Position(2, 4), new Position(1, 4)) // 대각선 ); } } diff --git a/src/test/java/model/piece/SoldierTest.java b/src/test/java/model/piece/SoldierTest.java index d9ed46b0d8..159c5da4aa 100644 --- a/src/test/java/model/piece/SoldierTest.java +++ b/src/test/java/model/piece/SoldierTest.java @@ -47,4 +47,15 @@ public class SoldierTest { assertThatThrownBy(() -> soldier.validatePathCondition(obstacles)) .isInstanceOf(IllegalArgumentException.class); } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#졸_병_궁성_이동_불가능한_위치") + void 졸은_궁성_교차점이_아닌_곳에서_대각선으로_이동할_수_없다(Team team, Position current, Position next) { + // given + Piece cannon = new Soldier(team); + + // when & then + assertThatThrownBy(() -> cannon.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file From 69bbb8d3b404e18a9fc3702544339b63df4c376c Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 17:55:22 +0900 Subject: [PATCH 12/32] =?UTF-8?q?test:=20=ED=8F=AC=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=BB=A4=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EB=B3=B4=EC=99=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/model/piece/CannonTest.java | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/model/piece/CannonTest.java b/src/test/java/model/piece/CannonTest.java index c0cd90d3a6..50d3b4848c 100644 --- a/src/test/java/model/piece/CannonTest.java +++ b/src/test/java/model/piece/CannonTest.java @@ -1,6 +1,7 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -73,6 +74,18 @@ public class CannonTest { .isInstanceOf(IllegalArgumentException.class); } + @Test + void 포가_정상적으로_하나의_기물을_넘어가는_경우_예외가_발생하지_않는다() { + // given + Piece cannon = new Cannon(Team.HAN); + Piece hurdle = new Soldier(Team.CHO); + List piecesOnPath = List.of(hurdle); + + // when & then + assertThatCode(() -> cannon.validatePathCondition(piecesOnPath)) + .doesNotThrowAnyException(); + } + @Test void 포가_상대_포를_잡으려_하면_예외가_발생한다() { // given @@ -84,6 +97,17 @@ public class CannonTest { .isInstanceOf(IllegalArgumentException.class); } + @Test + void 포가_상대_포가_아닌_기물을_잡으려_하면_예외가_발생하지_않는다() { + // given + Piece cannon = new Cannon(Team.HAN); + Piece target = new Soldier(Team.CHO); + + // when & then + assertThatCode(() -> cannon.validateTarget(target)) + .doesNotThrowAnyException(); + } + @ParameterizedTest @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_경로") void 포는_궁성_교차점에서_대각선으로_이동_경로를_구할_수_있다(Team team, Position current, Position next, List expectedPath) { From 3b59fb75d212fa87c64e0fb65264cc537a3097aa Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 17:59:10 +0900 Subject: [PATCH 13/32] =?UTF-8?q?test:=20=EA=B6=81=EC=84=B1=20=EA=B8=B0?= =?UTF-8?q?=EB=AC=BC=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=BB=A4=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EB=B3=B4=EC=99=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/model/fixture/PalaceMovePositionFixture.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/model/fixture/PalaceMovePositionFixture.java b/src/test/java/model/fixture/PalaceMovePositionFixture.java index 71f17994bf..c17e501312 100644 --- a/src/test/java/model/fixture/PalaceMovePositionFixture.java +++ b/src/test/java/model/fixture/PalaceMovePositionFixture.java @@ -66,7 +66,10 @@ public class PalaceMovePositionFixture { // 4. 거리 초과 및 특수 실패 Arguments.of(Team.HAN, new Position(0, 4), new Position(2, 4), PieceType.GENERAL), // 직선 2칸 Arguments.of(Team.CHO, new Position(7, 3), new Position(9, 5), PieceType.GUARD), // 대각선 2칸 - Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 4), PieceType.GENERAL) // 제자리 이동 + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 4), PieceType.GENERAL), // 제자리 이동 + + // 5. 출발지가 궁성 밖인 경우 + Arguments.of(Team.HAN, new Position(3, 4), new Position(2, 4), PieceType.GENERAL) ); } From 15e95845dbbf6da933965b655a4f12cea652746f Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Tue, 7 Apr 2026 23:57:11 +0900 Subject: [PATCH 14/32] =?UTF-8?q?feat:=20=EC=99=95=EC=9D=B4=20=EC=9E=A1?= =?UTF-8?q?=ED=98=94=EC=9D=84=20=EB=95=8C=20=EA=B2=8C=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=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 --- .../java/controller/JanggiController.java | 8 +- src/main/java/model/JanggiGame.java | 32 ++++++- src/main/java/model/board/Board.java | 16 +++- src/main/java/model/piece/Piece.java | 10 +- src/main/java/view/OutputView.java | 5 + .../java/view/formater/BoardFormatter.java | 2 +- src/test/java/model/JanggiGameTest.java | 93 ++++++++++++++++--- src/test/java/model/board/BoardTest.java | 26 ++++++ src/test/java/model/testdouble/SpyBoard.java | 31 ++++--- 9 files changed, 184 insertions(+), 39 deletions(-) diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java index 2f126ec386..26c0b0f05c 100644 --- a/src/main/java/controller/JanggiController.java +++ b/src/main/java/controller/JanggiController.java @@ -29,10 +29,16 @@ public void run() { outputView.displayBoard(board.board()); JanggiGame janggiGame = new JanggiGame(board); - while (true) { + while (janggiGame.isNotDone()) { retry(() -> playByTurn(janggiGame), processError()); outputView.displayBoard(board.board()); } + printResult(janggiGame); + } + + private void printResult(JanggiGame janggiGame) { + Team winner = janggiGame.resolveWinner(); + outputView.displayWinner(winner); } private Board createBoardByFormation() { diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index 6831ca9dba..66d1163b71 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -16,29 +16,57 @@ public JanggiGame(Board board) { } public void movePiece(Position current, Position next) { + validateGamePlay(); Piece piece = selectPiece(current); List path = piece.pathTo(current, next); List pieces = board.extractPiecesByPath(path); - piece.validatePathCondition(pieces); - board.move(current, next); + board.move(current, next); this.turn = turn.opposite(); } + public boolean isNotDone() { + return board.isAliveGeneral(Team.HAN) && board.isAliveGeneral(Team.CHO); + } + + public Team resolveWinner() { + validateGameDone(); + if (board.isAliveGeneral(Team.HAN)) { + return Team.HAN; + } + return Team.CHO; + } + public Piece selectPiece(Position position) { Piece piece = board.pickPiece(position); validateAlly(piece); return piece; } + private boolean isDone() { + return !isNotDone(); + } + + private void validateGamePlay() { + if (isDone()) { + throw new IllegalStateException("게임이 종료되었으므로 이동할 수 없습니다."); + } + } + private void validateAlly(Piece piece) { if (!piece.isSameTeam(turn)) { throw new IllegalArgumentException(turn.getName() + "의 기물이 아닙니다."); } } + private void validateGameDone() { + if (isNotDone()) { + throw new IllegalArgumentException("아직 게임이 진행중입니다."); + } + } + public Team getTurn() { return turn; } diff --git a/src/main/java/model/board/Board.java b/src/main/java/model/board/Board.java index 991f82befd..54127df887 100644 --- a/src/main/java/model/board/Board.java +++ b/src/main/java/model/board/Board.java @@ -4,8 +4,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import model.Team; import model.coordinate.Position; import model.piece.Piece; +import model.piece.PieceType; public class Board { @@ -31,6 +33,12 @@ public Piece pickPiece(Position position) { .orElseThrow(() -> new IllegalArgumentException("해당 위치에 존재하는 장기말이 없습니다.")); } + public boolean isAliveGeneral(Team team) { + return board.values() + .stream() + .anyMatch(piece -> piece.isSameType(PieceType.GENERAL) && piece.isSameTeam(team)); + } + public void arrangePieces(Map pieces) { board.putAll(pieces); } @@ -46,11 +54,11 @@ private Optional findByPosition(Position position) { return Optional.ofNullable(board.get(position)); } - public Map board() { - return Map.copyOf(board); - } - private boolean hasPieceAt(Position position) { return board.containsKey(position); } + + public Map board() { + return Map.copyOf(board); + } } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index ccab835d2c..7fe88babab 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -36,15 +36,19 @@ public void validatePathCondition(List pieces) { } public void validateTarget(Piece otherPiece) { - if (team() == otherPiece.team) { + if (team() == otherPiece.team()) { throw new IllegalArgumentException("아군이 있는 위치로 이동할 수 없습니다."); } } + public boolean isSameType(PieceType type) { + return type() == type; + } + protected abstract void validateMove(Position current, Position next); protected boolean isSameType(Piece piece) { - return type == piece.type; + return isSameType(piece.type()); } protected List extractPath(Position current, Position next) { @@ -55,7 +59,7 @@ public Team team() { return team; } - public PieceType getType() { + public PieceType type() { return type; } } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index 4edb0391f8..b9ce90b749 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -10,6 +10,7 @@ import static view.formater.BoardFormatter.formatSymbol; import java.util.Map; +import model.Team; import model.board.Board; import model.coordinate.Position; import model.piece.Piece; @@ -49,4 +50,8 @@ public void displayBoard(Map board) { public void displayError(String message) { System.out.println(RED + "[ERROR] " + message + RESET); } + + public void displayWinner(Team winner) { + System.out.println(winner.getName() + " 승"); + } } \ No newline at end of file diff --git a/src/main/java/view/formater/BoardFormatter.java b/src/main/java/view/formater/BoardFormatter.java index da6bc9e664..b028f5d48d 100644 --- a/src/main/java/view/formater/BoardFormatter.java +++ b/src/main/java/view/formater/BoardFormatter.java @@ -33,7 +33,7 @@ public static String formatSymbol(Piece piece) { return EMPTY; } String color = extractColor(piece.team()); - String symbol = SYMBOL_MAP.get(piece.getType()).get(piece.team()); + String symbol = SYMBOL_MAP.get(piece.type()).get(piece.team()); return color + symbol + RESET; } diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java index d636472e82..3bd638e85b 100644 --- a/src/test/java/model/JanggiGameTest.java +++ b/src/test/java/model/JanggiGameTest.java @@ -3,8 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Map; import model.coordinate.Position; -import model.piece.Horse; import model.piece.Piece; import model.testdouble.FakePiece; import model.testdouble.SpyBoard; @@ -12,34 +12,35 @@ class JanggiGameTest { + private Position current = new Position(1, 1); + private Position next = new Position(2, 1); + private FakePiece piece = FakePiece.createFake(Team.CHO); + @Test void 장기말을_정상적으로_옮기면_보드에서_이동하고_다음_차례가_된다() { // given: (1,1)에 CHO의 이동 가능한 기물이 있고, 경로가 비어있는 상황 시뮬레이션 - Position source = new Position(1, 1); - Position destination = new Position(2, 2); - FakePiece piece = FakePiece.createFake(Team.CHO); - - SpyBoard board = new SpyBoard(piece); + SpyBoard board = SpyBoard.withBothGenerals(); + board.arrangePieces(Map.of(current, piece)); JanggiGame janggiGame = new JanggiGame(board); + Team prevTurn = janggiGame.getTurn(); // when - janggiGame.movePiece(source, destination); + janggiGame.movePiece(current, next); // then - assertThat(board.pickPiece(destination)).isEqualTo(piece); + assertThat(board.pickPiece(next)).isEqualTo(piece); assertThat(janggiGame.getTurn()).isEqualTo(prevTurn.opposite()); } @Test void 현재_턴의_팀에_맞는_기물을_선택할_수_있다() { // given - Piece piece = new Horse(Team.CHO); - SpyBoard board = new SpyBoard(piece); + SpyBoard board = new SpyBoard(Map.of(current, piece)); JanggiGame janggiGame = new JanggiGame(board); // when - Piece selectedPiece = janggiGame.selectPiece(new Position(1, 1)); + Piece selectedPiece = janggiGame.selectPiece(current); // then assertThat(selectedPiece).isSameAs(piece); @@ -48,11 +49,77 @@ class JanggiGameTest { @Test void 다른_팀의_기물을_선택하면_예외가_발생한다() { // given: CHO 턴인데 HAN 기물 배치 - SpyBoard board = new SpyBoard(new Horse(Team.HAN)); + SpyBoard board = new SpyBoard(Map.of(current, FakePiece.createFake(Team.HAN))); + JanggiGame janggiGame = new JanggiGame(board); + + // when & then + assertThatThrownBy(() -> janggiGame.selectPiece(current)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 왕이_모두_살아있으면_게임이_진행중이다() { + // given + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + + // when + boolean result = janggiGame.isNotDone(); + + // then + assertThat(result).isTrue(); + } + + @Test + void 한나라_왕만_존재할_때_게임은_종료되고_승리팀은_초나라가_된다() { + // given + SpyBoard board = SpyBoard.withOnlyChoGeneral(); + JanggiGame janggiGame = new JanggiGame(board); + + // when + boolean result = janggiGame.isNotDone(); + Team winner = janggiGame.resolveWinner(); + + // then + assertThat(result).isFalse(); + assertThat(winner).isEqualTo(Team.CHO); + } + + @Test + void 초나라_왕만_존재할_때_게임은_종료되고_승리팀은_한나라가_된다() { + // given + SpyBoard board = SpyBoard.withOnlyHanGeneral(); + JanggiGame janggiGame = new JanggiGame(board); + + // when + boolean result = janggiGame.isNotDone(); + Team winner = janggiGame.resolveWinner(); + + // then + assertThat(result).isFalse(); + assertThat(winner).isEqualTo(Team.HAN); + } + + @Test + void 게임이_종료되었을_때_기물을_이동하면_예외가_발생한다() { + // given + SpyBoard board = SpyBoard.withOnlyHanGeneral(); + board.arrangePieces(Map.of(current, piece)); + JanggiGame janggiGame = new JanggiGame(board); + + // when & then + assertThatThrownBy(() -> janggiGame.movePiece(current, next)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 게임이_종료되지_않았을_때_승자를_요청하면_예외가_발생한다() { + // given + SpyBoard board = SpyBoard.withBothGenerals(); JanggiGame janggiGame = new JanggiGame(board); // when & then - assertThatThrownBy(() -> janggiGame.selectPiece(new Position(1, 1))) + assertThatThrownBy(janggiGame::resolveWinner) .isInstanceOf(IllegalArgumentException.class); } } \ No newline at end of file diff --git a/src/test/java/model/board/BoardTest.java b/src/test/java/model/board/BoardTest.java index 66cd5c0c2b..765097125e 100644 --- a/src/test/java/model/board/BoardTest.java +++ b/src/test/java/model/board/BoardTest.java @@ -7,6 +7,7 @@ import java.util.Map; import model.Team; import model.coordinate.Position; +import model.piece.General; import model.piece.Piece; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; @@ -117,6 +118,31 @@ class BoardTest { .containsExactly(hurdle1, hurdle2); } + @Test + void 팀의_왕이_살아있는지_알_수_있다() { + // given + Position generalPos = new Position(1, 4); + Board board = new Board(Map.of(generalPos, new General(Team.HAN))); + + // when + boolean alive = board.isAliveGeneral(Team.HAN); + + // then + assertThat(alive).isTrue(); + } + + @Test + void 해당_팀의_왕이_죽었는지_알_수_잇다() { + // given + Board board = new Board(Map.of()); + + // when + boolean alive = board.isAliveGeneral(Team.HAN); + + // then + assertThat(alive).isFalse(); + } + private void assertSuccessMoved(Board board, Position source, FakePiece piece, Position destination) { assertThat(board.pickPiece(source)).isEqualTo(piece); assertThat(board.board()).doesNotContainKey(destination); diff --git a/src/test/java/model/testdouble/SpyBoard.java b/src/test/java/model/testdouble/SpyBoard.java index 9749cdd165..55a4c78328 100644 --- a/src/test/java/model/testdouble/SpyBoard.java +++ b/src/test/java/model/testdouble/SpyBoard.java @@ -4,35 +4,36 @@ import model.Team; import model.board.Board; import model.coordinate.Position; -import model.piece.Horse; +import model.piece.General; import model.piece.Piece; public class SpyBoard extends Board { - // pickPiece 반환용 - private final Piece piece; public boolean isMoved = false; - public SpyBoard(Piece piece) { - super(Map.of()); - this.piece = piece; + public SpyBoard(Map pieces) { + super(pieces); } - public static SpyBoard cho() { - return new SpyBoard(new Horse(Team.CHO)); + public static SpyBoard withBothGenerals() { + Map pieces = Map.of( + new Position(1, 4), new General(Team.HAN), + new Position(8, 4), new General(Team.CHO) + ); + return new SpyBoard(pieces); } - public static SpyBoard han() { - return new SpyBoard(new Horse(Team.HAN)); + public static SpyBoard withOnlyHanGeneral() { + return new SpyBoard(Map.of(new Position(1, 4), new General(Team.HAN))); } - @Override - public void move(Position current, Position next) { - isMoved = true; + public static SpyBoard withOnlyChoGeneral() { + return new SpyBoard(Map.of(new Position(8, 4), new General(Team.CHO))); } @Override - public Piece pickPiece(Position position) { - return piece; + public void move(Position current, Position next) { + isMoved = true; + super.move(current, next); } } From 69c10bc40a116d4cad8252b93465e9ae01fe0e92 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Wed, 8 Apr 2026 19:45:25 +0900 Subject: [PATCH 15/32] =?UTF-8?q?feat:=20=EB=B9=85=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/controller/JanggiController.java | 52 +++-- src/main/java/model/GameStatus.java | 8 + src/main/java/model/JanggiGame.java | 116 +++++++++-- src/main/java/model/board/Board.java | 23 +- src/main/java/model/piece/Piece.java | 6 +- src/main/java/model/piece/PieceType.java | 24 ++- src/main/java/view/InputCommand.java | 13 ++ src/main/java/view/InputView.java | 7 + src/main/java/view/OutputView.java | 6 + src/test/java/model/JanggiGameTest.java | 196 +++++++++++++----- src/test/java/model/board/BoardTest.java | 46 +++- src/test/java/model/testdouble/FakePiece.java | 16 +- src/test/java/model/testdouble/SpyBoard.java | 17 +- 13 files changed, 420 insertions(+), 110 deletions(-) create mode 100644 src/main/java/model/GameStatus.java create mode 100644 src/main/java/view/InputCommand.java diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java index 26c0b0f05c..3d11c54da4 100644 --- a/src/main/java/controller/JanggiController.java +++ b/src/main/java/controller/JanggiController.java @@ -4,6 +4,7 @@ import static model.Team.CHO; import static model.Team.HAN; +import java.util.Map; import java.util.function.Consumer; import model.JanggiGame; import model.Team; @@ -25,34 +26,36 @@ public JanggiController(InputView inputView, OutputView outputView) { } public void run() { - Board board = createBoardByFormation(); - outputView.displayBoard(board.board()); + JanggiGame janggiGame = createJanggiGame(); + outputView.displayBoard(janggiGame.getBoard()); - JanggiGame janggiGame = new JanggiGame(board); - while (janggiGame.isNotDone()) { - retry(() -> playByTurn(janggiGame), processError()); - outputView.displayBoard(board.board()); + while (janggiGame.canPlaying()) { + retry(() -> checkBigJang(janggiGame), processError()); + play(janggiGame); } - printResult(janggiGame); - } - private void printResult(JanggiGame janggiGame) { - Team winner = janggiGame.resolveWinner(); - outputView.displayWinner(winner); + printResult(janggiGame); } - private Board createBoardByFormation() { + private JanggiGame createJanggiGame() { TeamFormation hanFormation = retry(() -> inputView.readFormationByTeam(HAN), processError()); TeamFormation choFormation = retry(() -> inputView.readFormationByTeam(CHO), processError()); Board board = BoardFactory.generateDefaultPieces(); board.arrangePieces(hanFormation.generate()); board.arrangePieces(choFormation.generate()); - return board; + return new JanggiGame(board); + } + + private void play(JanggiGame janggiGame) { + if (janggiGame.canPlaying()) { + retry(() -> playByTurn(janggiGame), processError()); + } + outputView.displayBoard(janggiGame.getBoard()); } private void playByTurn(JanggiGame janggiGame) { - Team currentTurn = janggiGame.getTurn(); + Team currentTurn = janggiGame.turn(); Position current = inputView.readPiecePositionForMove(currentTurn); Piece piece = janggiGame.selectPiece(current); @@ -61,6 +64,27 @@ private void playByTurn(JanggiGame janggiGame) { janggiGame.movePiece(current, next); } + private void checkBigJang(JanggiGame janggiGame) { + if (!janggiGame.isBigJang()) { + return; + } + + boolean bigJang = inputView.readBigJangStatus(janggiGame.turn()); + if (bigJang) { + janggiGame.finishByBigJang(); + } + } + + private void printResult(JanggiGame janggiGame) { + Team winner = janggiGame.resolveWinner(); + outputView.displayWinner(winner); + + if (janggiGame.isBigJangDone()) { + Map finalScore = janggiGame.calculateFinalScore(); + outputView.displayScore(finalScore); + } + } + private Consumer processError() { return (e) -> outputView.displayError(e.getMessage()); } diff --git a/src/main/java/model/GameStatus.java b/src/main/java/model/GameStatus.java new file mode 100644 index 0000000000..ab42d80536 --- /dev/null +++ b/src/main/java/model/GameStatus.java @@ -0,0 +1,8 @@ +package model; + +public enum GameStatus { + PLAYING, + BIG_JANG, + BIG_JANG_DONE, + DONE +} diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index 66d1163b71..53be6a5c96 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -1,22 +1,29 @@ package model; import java.util.List; +import java.util.Map; import model.board.Board; +import model.coordinate.Direction; +import model.coordinate.Displacement; import model.coordinate.Position; import model.piece.Piece; public class JanggiGame { + private static final double AFTER_TURN_BONUS_SCORE = 1.5; + private static final Team START_TURN = Team.CHO; + private final Board board; private Team turn; + private GameStatus status; public JanggiGame(Board board) { this.board = board; - this.turn = Team.CHO; + this.turn = START_TURN; + this.status = GameStatus.PLAYING; } public void movePiece(Position current, Position next) { - validateGamePlay(); Piece piece = selectPiece(current); List path = piece.pathTo(current, next); @@ -24,34 +31,101 @@ public void movePiece(Position current, Position next) { piece.validatePathCondition(pieces); board.move(current, next); - this.turn = turn.opposite(); + changeGameState(); } - public boolean isNotDone() { - return board.isAliveGeneral(Team.HAN) && board.isAliveGeneral(Team.CHO); + public Piece selectPiece(Position position) { + validateGamePlay(); + Piece piece = board.pickPiece(position); + validateAlly(piece); + return piece; } public Team resolveWinner() { validateGameDone(); - if (board.isAliveGeneral(Team.HAN)) { - return Team.HAN; + if (isBigJangDone()) { + return resolveBigJangWinner(); } - return Team.CHO; + return resolveNormalWinner(); } - public Piece selectPiece(Position position) { - Piece piece = board.pickPiece(position); - validateAlly(piece); - return piece; + public Map calculateFinalScore() { + Team afterTurn = START_TURN.opposite(); + return Map.of( + START_TURN, board.calculateBaseScore(START_TURN), + afterTurn, board.calculateBaseScore(afterTurn) + AFTER_TURN_BONUS_SCORE + ); + } + + public boolean canPlaying() { + return this.status != GameStatus.DONE && !isBigJangDone(); + } + + public void finishByBigJang() { + if (!isBigJang()) { + throw new IllegalStateException("현재 빅장 상태가 아닙니다."); + } + this.status = GameStatus.BIG_JANG_DONE; } - private boolean isDone() { - return !isNotDone(); + public boolean isBigJang() { + return this.status == GameStatus.BIG_JANG; + } + + public boolean isBigJangDone() { + return this.status == GameStatus.BIG_JANG_DONE; + } + + private boolean checkBigJang() { + Position allyGeneralPosition = board.findGeneralPositionByTeam(turn); + Position enemyGeneralPosition = board.findGeneralPositionByTeam(turn.opposite()); + return determineBigJang(enemyGeneralPosition, allyGeneralPosition); + } + + private boolean determineBigJang(Position enemyGeneralPosition, Position allyGeneralPosition) { + Displacement displacement = enemyGeneralPosition.toDisplacement(allyGeneralPosition); + if (displacement.isNotStraight()) { + return false; + } + + Direction direction = displacement.extractCardinal(); + List path = direction.pathTo(allyGeneralPosition, enemyGeneralPosition); + List pieces = board.extractPiecesByPath(path); + return pieces.isEmpty(); + } + + private void changeGameState() { + this.turn = turn.opposite(); + if (!board.isAliveGeneral(turn)) { + this.status = GameStatus.DONE; + return; + } + if (checkBigJang()) { + this.status = GameStatus.BIG_JANG; + return; + } + this.status = GameStatus.PLAYING; + } + + private Team resolveNormalWinner() { + if (board.isAliveGeneral(START_TURN)) { + return START_TURN; + } + return START_TURN.opposite(); + } + + private Team resolveBigJangWinner() { + Map scores = calculateFinalScore(); + Team afterTurn = START_TURN.opposite(); + if (scores.get(START_TURN) > scores.get(afterTurn)) { + return START_TURN; + } + return afterTurn; } private void validateGamePlay() { - if (isDone()) { - throw new IllegalStateException("게임이 종료되었으므로 이동할 수 없습니다."); + if (!canPlaying()) { + throw new IllegalStateException("게임이 종료되었으므로 장기 게임을 진행할 수 없습니다."); } } @@ -62,12 +136,16 @@ private void validateAlly(Piece piece) { } private void validateGameDone() { - if (isNotDone()) { - throw new IllegalArgumentException("아직 게임이 진행중입니다."); + if (canPlaying()) { + throw new IllegalStateException("아직 게임이 진행중입니다."); } } - public Team getTurn() { + public Team turn() { return turn; } + + public Map getBoard() { + return board.board(); + } } diff --git a/src/main/java/model/board/Board.java b/src/main/java/model/board/Board.java index 54127df887..7e552540c2 100644 --- a/src/main/java/model/board/Board.java +++ b/src/main/java/model/board/Board.java @@ -36,7 +36,7 @@ public Piece pickPiece(Position position) { public boolean isAliveGeneral(Team team) { return board.values() .stream() - .anyMatch(piece -> piece.isSameType(PieceType.GENERAL) && piece.isSameTeam(team)); + .anyMatch(piece -> isTargetGeneral(piece, team)); } public void arrangePieces(Map pieces) { @@ -50,6 +50,27 @@ public List extractPiecesByPath(List path) { .toList(); } + public Position findGeneralPositionByTeam(Team team) { + return board.entrySet() + .stream() + .filter(data -> isTargetGeneral(data.getValue(), team)) + .map(Map.Entry::getKey) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(team.getName() + "의 왕이 없습니다.")); + } + + public double calculateBaseScore(Team team) { + return board.values() + .stream() + .filter(piece -> piece.isSameTeam(team)) + .mapToDouble(Piece::score) + .sum(); + } + + private boolean isTargetGeneral(Piece piece, Team team) { + return piece.isSameType(PieceType.GENERAL) && piece.isSameTeam(team); + } + private Optional findByPosition(Position position) { return Optional.ofNullable(board.get(position)); } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index 7fe88babab..79714fcae8 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -26,7 +26,7 @@ public List pathTo(Position current, Position next) { } public boolean isSameTeam(Team team) { - return this.team == team; + return team() == team; } public void validatePathCondition(List pieces) { @@ -45,6 +45,10 @@ public boolean isSameType(PieceType type) { return type() == type; } + public double score() { + return type().getScore(); + } + protected abstract void validateMove(Position current, Position next); protected boolean isSameType(Piece piece) { diff --git a/src/main/java/model/piece/PieceType.java b/src/main/java/model/piece/PieceType.java index d6ae24449e..4dede4f787 100644 --- a/src/main/java/model/piece/PieceType.java +++ b/src/main/java/model/piece/PieceType.java @@ -1,11 +1,21 @@ package model.piece; public enum PieceType { - CANNON, - CHARIOT, - ELEPHANT, - GENERAL, - GUARD, - HORSE, - SOLDIER; + GENERAL(0), + CHARIOT(13), + CANNON(7), + HORSE(5), + ELEPHANT(3), + GUARD(3), + SOLDIER(2); + + private final double score; + + PieceType(double score) { + this.score = score; + } + + public double getScore() { + return score; + } } diff --git a/src/main/java/view/InputCommand.java b/src/main/java/view/InputCommand.java new file mode 100644 index 0000000000..1a356c442d --- /dev/null +++ b/src/main/java/view/InputCommand.java @@ -0,0 +1,13 @@ +package view; + +public enum InputCommand { + Y, N; + + public static InputCommand parse(String input) { + try { + return InputCommand.valueOf(input.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("잘못된 입력입니다."); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 494c04cf0b..c2a76b0ff3 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -55,4 +55,11 @@ private int readFormationOrder(Team team) { String input = SCANNER.nextLine(); return PARSER.parseNumber(input); } + + public boolean readBigJangStatus(Team turn) { + System.out.printf("%n[%s] 빅장입니다! 종료하시겠습니까? (Y, N)%n", turn.getName()); + String input = SCANNER.nextLine(); + InputCommand command = InputCommand.parse(input); + return command == InputCommand.Y; + } } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index b9ce90b749..d1417d7cd3 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -9,6 +9,7 @@ import static view.formater.BoardFormatter.formatHorizon; import static view.formater.BoardFormatter.formatSymbol; +import java.text.DecimalFormat; import java.util.Map; import model.Team; import model.board.Board; @@ -54,4 +55,9 @@ public void displayError(String message) { public void displayWinner(Team winner) { System.out.println(winner.getName() + " 승"); } + + public void displayScore(Map finalScore) { + DecimalFormat formatter = new DecimalFormat("#.#"); + finalScore.forEach((team, score) -> System.out.printf("%s: %s점%n", team.getName(), formatter.format(score))); + } } \ No newline at end of file diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java index 3bd638e85b..eac0034894 100644 --- a/src/test/java/model/JanggiGameTest.java +++ b/src/test/java/model/JanggiGameTest.java @@ -2,45 +2,57 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import java.util.Map; import model.coordinate.Position; +import model.piece.Chariot; import model.piece.Piece; -import model.testdouble.FakePiece; import model.testdouble.SpyBoard; import org.junit.jupiter.api.Test; class JanggiGameTest { - private Position current = new Position(1, 1); - private Position next = new Position(2, 1); - private FakePiece piece = FakePiece.createFake(Team.CHO); - @Test - void 장기말을_정상적으로_옮기면_보드에서_이동하고_다음_차례가_된다() { - // given: (1,1)에 CHO의 이동 가능한 기물이 있고, 경로가 비어있는 상황 시뮬레이션 + void 장기_게임을_시작하면_초나라부터_시작하고_이어서_게임을_진행할_수_있다() { + // given: 왕만 배치 SpyBoard board = SpyBoard.withBothGenerals(); - board.arrangePieces(Map.of(current, piece)); + + // when JanggiGame janggiGame = new JanggiGame(board); - Team prevTurn = janggiGame.getTurn(); + // then + assertThat(janggiGame) + .extracting(JanggiGame::turn, JanggiGame::canPlaying) + .containsExactly(Team.CHO, true); + } - // when - janggiGame.movePiece(current, next); + @Test + void 장기게임_시작_후_정상적으로_기물을_옮기면_다음_차례가_된다() { + // given: 왕만 배치 + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + Team prevTurn = janggiGame.turn(); + + // when: 초나라 왕만 옆으로 이동 + janggiGame.movePiece(new Position(8, 4), new Position(8, 5)); // then - assertThat(board.pickPiece(next)).isEqualTo(piece); - assertThat(janggiGame.getTurn()).isEqualTo(prevTurn.opposite()); + assertThat(janggiGame) + .extracting(JanggiGame::turn, JanggiGame::canPlaying) + .containsExactly(prevTurn.opposite(), true); } @Test void 현재_턴의_팀에_맞는_기물을_선택할_수_있다() { // given - SpyBoard board = new SpyBoard(Map.of(current, piece)); + Position position = new Position(1, 1); + Piece piece = new Chariot(Team.CHO); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(position, piece); JanggiGame janggiGame = new JanggiGame(board); // when - Piece selectedPiece = janggiGame.selectPiece(current); + Piece selectedPiece = janggiGame.selectPiece(position); // then assertThat(selectedPiece).isSameAs(piece); @@ -49,77 +61,167 @@ class JanggiGameTest { @Test void 다른_팀의_기물을_선택하면_예외가_발생한다() { // given: CHO 턴인데 HAN 기물 배치 - SpyBoard board = new SpyBoard(Map.of(current, FakePiece.createFake(Team.HAN))); + Position position = new Position(1, 1); + Piece piece = new Chariot(Team.HAN); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(position, piece); JanggiGame janggiGame = new JanggiGame(board); // when & then - assertThatThrownBy(() -> janggiGame.selectPiece(current)) + assertThatThrownBy(() -> janggiGame.selectPiece(position)) .isInstanceOf(IllegalArgumentException.class); } @Test - void 왕이_모두_살아있으면_게임이_진행중이다() { - // given - SpyBoard board = SpyBoard.withBothGenerals(); + void 초나라_기물이_한나라_왕을_잡을_경우_게임은_종료되고_승자는_초나라가된다() { + // given: 차가 왕을 잡을 수 있는 위치에 존재 + Position killPosition = new Position(6, 4); + Position hanGeneralPosition = new Position(1, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(killPosition, new Chariot(Team.CHO)); JanggiGame janggiGame = new JanggiGame(board); - // when - boolean result = janggiGame.isNotDone(); + // when: 초나라 차로 한나라 왕 잡기 + janggiGame.movePiece(killPosition, hanGeneralPosition); // then - assertThat(result).isTrue(); + assertThat(janggiGame) + .extracting(JanggiGame::canPlaying, JanggiGame::resolveWinner) + .containsExactly(false, Team.CHO); } @Test - void 한나라_왕만_존재할_때_게임은_종료되고_승리팀은_초나라가_된다() { - // given - SpyBoard board = SpyBoard.withOnlyChoGeneral(); + void 한나라_기물이_초나라_왕을_잡을_경우_게임은_종료되고_승자는_한나라가된다() { + // given: 차가 왕을 잡을 수 있는 위치에 존재 + 다음 차례를 위해 초나라 왕을 일직선 상으로 한칸 아래 이동 + Position killPosition = new Position(3, 4); + Position choGeneralPosition = new Position(9, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(killPosition, new Chariot(Team.HAN)); JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(new Position(8, 4), choGeneralPosition); - // when - boolean result = janggiGame.isNotDone(); - Team winner = janggiGame.resolveWinner(); + // when: 한나라 차로 초나라 왕 잡기 + janggiGame.movePiece(killPosition, choGeneralPosition); // then - assertThat(result).isFalse(); - assertThat(winner).isEqualTo(Team.CHO); + assertThat(janggiGame) + .extracting(JanggiGame::canPlaying, JanggiGame::resolveWinner) + .containsExactly(false, Team.HAN); } @Test - void 초나라_왕만_존재할_때_게임은_종료되고_승리팀은_한나라가_된다() { + void 게임이_종료되지_않았을_때_승자를_요청하면_예외가_발생한다() { // given - SpyBoard board = SpyBoard.withOnlyHanGeneral(); + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + + // when & then + assertThatThrownBy(janggiGame::resolveWinner) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 정상적으로_게임이_종료되었을_때_게임을_진행할_수_없다() { + // given: 초나라가 승리한 상황 + Position killPosition = new Position(6, 4); + Position hanGeneralPosition = new Position(1, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(killPosition, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(killPosition, hanGeneralPosition); + + // when & then + assertAll( + () -> assertThatThrownBy(() -> janggiGame.selectPiece(hanGeneralPosition)) + .isInstanceOf(IllegalStateException.class), + () -> assertThatThrownBy(() -> janggiGame.movePiece(hanGeneralPosition, killPosition)) + .isInstanceOf(IllegalStateException.class) + ); + } + + @Test + void 기물_이동_후_왕이_마주보고_있는_상황이_되면_빅장_상태가_되고_게임은_유지된다() { + // given: 왕 사이를 막고 있는 기물이 있는 보드 제공 + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + + // when: 왕 사이를 막고 있는 차를 이동 + janggiGame.movePiece(barrierPos, new Position(6, 0)); + + // then + assertThat(janggiGame) + .extracting(JanggiGame::isBigJang, JanggiGame::canPlaying) + .containsExactly(true, true); + } + + @Test + void 빅장_상태가_되면_게임을_빅장으로_종료_할_수_있다() { + // given: 빅장 상태 + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(barrierPos, new Position(6, 0)); // when - boolean result = janggiGame.isNotDone(); - Team winner = janggiGame.resolveWinner(); + janggiGame.finishByBigJang(); // then - assertThat(result).isFalse(); - assertThat(winner).isEqualTo(Team.HAN); + assertThat(janggiGame) + .extracting(JanggiGame::isBigJangDone, JanggiGame::canPlaying, JanggiGame::isBigJang) + .containsExactly(true, false, false); } @Test - void 게임이_종료되었을_때_기물을_이동하면_예외가_발생한다() { - // given - SpyBoard board = SpyBoard.withOnlyHanGeneral(); - board.arrangePieces(Map.of(current, piece)); + void 빅장_상태가_아닐_때_빅장으로_종료_할_수_없다() { + // given: 빅장 상태가 아님 + SpyBoard board = SpyBoard.withBothGenerals(); JanggiGame janggiGame = new JanggiGame(board); // when & then - assertThatThrownBy(() -> janggiGame.movePiece(current, next)) + assertThatThrownBy(janggiGame::finishByBigJang) .isInstanceOf(IllegalStateException.class); } @Test - void 게임이_종료되지_않았을_때_승자를_요청하면_예외가_발생한다() { - // given + void 장기에서_기물의_점수를_계산하면_후차례에게_가점을_준다() { + // given: 양 팀 다 0점인 상황 SpyBoard board = SpyBoard.withBothGenerals(); JanggiGame janggiGame = new JanggiGame(board); - // when & then - assertThatThrownBy(janggiGame::resolveWinner) - .isInstanceOf(IllegalArgumentException.class); + // when + Map finalScore = janggiGame.calculateFinalScore(); + + // then: 후 턴은 1.5점을 더한 결과를 받는다. + double startTurnScore = finalScore.get(Team.CHO); + double afterTurnScore = finalScore.get(Team.HAN); + assertThat(afterTurnScore).isEqualTo(startTurnScore + 1.5); + } + + @Test + void 빅장으로_게임이_종료되었을_때_초나라의_점수가_더_높다면_초나라가_승리한다() { + // given: 빅장으로 종료한 상황 + 초나라 기물 하나 남아있음. + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(barrierPos, new Position(6, 0)); + janggiGame.finishByBigJang(); + + // when + Team winner = janggiGame.resolveWinner(); + + // then + assertThat(winner).isEqualTo(Team.CHO); + } + + @Test + void 빅장으로_게임이_종료되었을_때_두나라의_점수가_동일하다면_후턴인_한나라가_승리한다() { + // given: 두 나라 모두 장만 남고 + 빅장으로 종료한 상황 + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(new Position(8, 4), new Position(9, 4)); // 아래로만 이동 + janggiGame.finishByBigJang(); + + // when + Team winner = janggiGame.resolveWinner(); + + // then + assertThat(winner).isEqualTo(Team.HAN); } } \ No newline at end of file diff --git a/src/test/java/model/board/BoardTest.java b/src/test/java/model/board/BoardTest.java index 765097125e..81402188ac 100644 --- a/src/test/java/model/board/BoardTest.java +++ b/src/test/java/model/board/BoardTest.java @@ -51,7 +51,7 @@ class BoardTest { board.move(source, destination); // then - assertSuccessMoved(board, destination, piece, source); + assertSuccessMoved(board, source, piece, destination); } @Test @@ -77,7 +77,7 @@ class BoardTest { board.move(source, destination); // then - assertSuccessMoved(board, destination, piece, source); + assertSuccessMoved(board, source, piece, destination); } @Test @@ -143,8 +143,46 @@ class BoardTest { assertThat(alive).isFalse(); } + @Test + void 팀의_왕의_위치를_찾을_수_있다() { + // given + Position generalPos = new Position(1, 4); + General general = new General(Team.HAN); + Board board = new Board(Map.of(generalPos, general)); + + // when + Position foundPosition = board.findGeneralPositionByTeam(Team.HAN); + + // then + assertThat(foundPosition).isEqualTo(generalPos); + } + + @Test + void 팀의_왕이_없으면_위치를_찾을_때_예외가_발생한다() { + // given + Board board = new Board(Map.of()); + + // when & then + assertThatThrownBy(() -> board.findGeneralPositionByTeam(Team.HAN)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 특정_팀의_기물_점수_합계를_계산한다() { + // given + Board board = BoardFactory.generateDefaultPieces(); + Map choPieces = FormationType.SANG_MA_MA_SANG.generateByTeam(Team.CHO); + board.arrangePieces(choPieces); + + // when + double score = board.calculateBaseScore(Team.CHO); + + // then: 기본 총 점수는 72점이다 + assertThat(score).isEqualTo(72); + } + private void assertSuccessMoved(Board board, Position source, FakePiece piece, Position destination) { - assertThat(board.pickPiece(source)).isEqualTo(piece); - assertThat(board.board()).doesNotContainKey(destination); + assertThat(board.pickPiece(destination)).isEqualTo(piece); + assertThat(board.board()).doesNotContainKey(source); } } \ No newline at end of file diff --git a/src/test/java/model/testdouble/FakePiece.java b/src/test/java/model/testdouble/FakePiece.java index 863f406756..842cd4cb77 100644 --- a/src/test/java/model/testdouble/FakePiece.java +++ b/src/test/java/model/testdouble/FakePiece.java @@ -8,17 +8,21 @@ public class FakePiece extends Piece { - private final boolean movable; private final List path; + private final double score; - FakePiece(Team team, PieceType type, boolean movable, List path) { + FakePiece(Team team, PieceType type, List path, double score) { super(team, type); - this.movable = movable; this.path = path; + this.score = score; } public static FakePiece createFake(Team team) { - return new FakePiece(team, PieceType.SOLDIER, true, List.of()); + return new FakePiece(team, PieceType.SOLDIER, List.of(), 0.0); + } + + public static FakePiece createFakeWithScore(Team team, double score) { + return new FakePiece(team, PieceType.SOLDIER, List.of(), score); } @Override @@ -32,7 +36,7 @@ protected void validateMove(Position current, Position next) { } @Override - protected List extractPath(Position current, Position next) { - return List.of(); + public double score() { + return score; } } diff --git a/src/test/java/model/testdouble/SpyBoard.java b/src/test/java/model/testdouble/SpyBoard.java index 55a4c78328..059318d670 100644 --- a/src/test/java/model/testdouble/SpyBoard.java +++ b/src/test/java/model/testdouble/SpyBoard.java @@ -1,5 +1,6 @@ package model.testdouble; +import java.util.HashMap; import java.util.Map; import model.Team; import model.board.Board; @@ -9,31 +10,25 @@ public class SpyBoard extends Board { - public boolean isMoved = false; - public SpyBoard(Map pieces) { super(pieces); } public static SpyBoard withBothGenerals() { - Map pieces = Map.of( + Map pieces = new HashMap<>(Map.of( new Position(1, 4), new General(Team.HAN), new Position(8, 4), new General(Team.CHO) - ); + )); return new SpyBoard(pieces); } - public static SpyBoard withOnlyHanGeneral() { - return new SpyBoard(Map.of(new Position(1, 4), new General(Team.HAN))); - } - - public static SpyBoard withOnlyChoGeneral() { - return new SpyBoard(Map.of(new Position(8, 4), new General(Team.CHO))); + public SpyBoard addPiece(Position position, Piece piece) { + arrangePieces(Map.of(position, piece)); + return new SpyBoard(this.board()); } @Override public void move(Position current, Position next) { - isMoved = true; super.move(current, next); } } From 5978b5fbdc6cc683840b818e3c1f845cf2b074e6 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 01:01:48 +0900 Subject: [PATCH 16/32] =?UTF-8?q?refactor:=20=EC=9E=A5=EA=B8=B0=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20state=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9C=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 --- src/main/java/model/JanggiGame.java | 111 ++++----------------- src/main/java/model/JanggiReferee.java | 33 ++++++ src/main/java/model/JanggiState.java | 61 +++++++++++ src/main/java/model/Team.java | 12 ++- src/main/java/model/state/BigJang.java | 28 ++++++ src/main/java/model/state/BigJangDone.java | 31 ++++++ src/main/java/model/state/Finished.java | 28 ++++++ src/main/java/model/state/Running.java | 28 ++++++ 8 files changed, 239 insertions(+), 93 deletions(-) create mode 100644 src/main/java/model/JanggiReferee.java create mode 100644 src/main/java/model/JanggiState.java create mode 100644 src/main/java/model/state/BigJang.java create mode 100644 src/main/java/model/state/BigJangDone.java create mode 100644 src/main/java/model/state/Finished.java create mode 100644 src/main/java/model/state/Running.java diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index 53be6a5c96..d48b10a939 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -3,24 +3,18 @@ import java.util.List; import java.util.Map; import model.board.Board; -import model.coordinate.Direction; -import model.coordinate.Displacement; import model.coordinate.Position; import model.piece.Piece; +import model.state.BigJangDone; +import model.state.Running; public class JanggiGame { - - private static final double AFTER_TURN_BONUS_SCORE = 1.5; - private static final Team START_TURN = Team.CHO; - private final Board board; - private Team turn; - private GameStatus status; + private JanggiState state; public JanggiGame(Board board) { this.board = board; - this.turn = START_TURN; - this.status = GameStatus.PLAYING; + this.state = new Running(Team.startTurn()); } public void movePiece(Position current, Position next) { @@ -31,118 +25,51 @@ public void movePiece(Position current, Position next) { piece.validatePathCondition(pieces); board.move(current, next); - changeGameState(); + this.state = state.next(board); } public Piece selectPiece(Position position) { - validateGamePlay(); + state.validateGamePlay(); Piece piece = board.pickPiece(position); validateAlly(piece); return piece; } public Team resolveWinner() { - validateGameDone(); - if (isBigJangDone()) { - return resolveBigJangWinner(); - } - return resolveNormalWinner(); + return state.resolveWinner(board); } public Map calculateFinalScore() { - Team afterTurn = START_TURN.opposite(); - return Map.of( - START_TURN, board.calculateBaseScore(START_TURN), - afterTurn, board.calculateBaseScore(afterTurn) + AFTER_TURN_BONUS_SCORE - ); - } - - public boolean canPlaying() { - return this.status != GameStatus.DONE && !isBigJangDone(); + return JanggiReferee.collectScores(board); } public void finishByBigJang() { - if (!isBigJang()) { + if (state.status() != GameStatus.BIG_JANG) { throw new IllegalStateException("현재 빅장 상태가 아닙니다."); } - this.status = GameStatus.BIG_JANG_DONE; - } - - public boolean isBigJang() { - return this.status == GameStatus.BIG_JANG; - } - - public boolean isBigJangDone() { - return this.status == GameStatus.BIG_JANG_DONE; - } - - private boolean checkBigJang() { - Position allyGeneralPosition = board.findGeneralPositionByTeam(turn); - Position enemyGeneralPosition = board.findGeneralPositionByTeam(turn.opposite()); - return determineBigJang(enemyGeneralPosition, allyGeneralPosition); - } - - private boolean determineBigJang(Position enemyGeneralPosition, Position allyGeneralPosition) { - Displacement displacement = enemyGeneralPosition.toDisplacement(allyGeneralPosition); - if (displacement.isNotStraight()) { - return false; - } - - Direction direction = displacement.extractCardinal(); - List path = direction.pathTo(allyGeneralPosition, enemyGeneralPosition); - List pieces = board.extractPiecesByPath(path); - return pieces.isEmpty(); - } - - private void changeGameState() { - this.turn = turn.opposite(); - if (!board.isAliveGeneral(turn)) { - this.status = GameStatus.DONE; - return; - } - if (checkBigJang()) { - this.status = GameStatus.BIG_JANG; - return; - } - this.status = GameStatus.PLAYING; + this.state = new BigJangDone(state.turn()); } - private Team resolveNormalWinner() { - if (board.isAliveGeneral(START_TURN)) { - return START_TURN; - } - return START_TURN.opposite(); + public boolean canPlaying() { + return state.canPlaying(); } - private Team resolveBigJangWinner() { - Map scores = calculateFinalScore(); - Team afterTurn = START_TURN.opposite(); - if (scores.get(START_TURN) > scores.get(afterTurn)) { - return START_TURN; - } - return afterTurn; + public boolean isBigJang() { + return state.status() == GameStatus.BIG_JANG; } - private void validateGamePlay() { - if (!canPlaying()) { - throw new IllegalStateException("게임이 종료되었으므로 장기 게임을 진행할 수 없습니다."); - } + public boolean isBigJangDone() { + return state.status() == GameStatus.BIG_JANG_DONE; } private void validateAlly(Piece piece) { - if (!piece.isSameTeam(turn)) { - throw new IllegalArgumentException(turn.getName() + "의 기물이 아닙니다."); - } - } - - private void validateGameDone() { - if (canPlaying()) { - throw new IllegalStateException("아직 게임이 진행중입니다."); + if (!piece.isSameTeam(turn())) { + throw new IllegalArgumentException(turn().getName() + "의 기물이 아닙니다."); } } public Team turn() { - return turn; + return state.turn(); } public Map getBoard() { diff --git a/src/main/java/model/JanggiReferee.java b/src/main/java/model/JanggiReferee.java new file mode 100644 index 0000000000..4f3df01ce1 --- /dev/null +++ b/src/main/java/model/JanggiReferee.java @@ -0,0 +1,33 @@ +package model; + +import java.util.Map; +import model.board.Board; + +public class JanggiReferee { + + private static final Team START_TURN = Team.startTurn(); + private static final Team AFTER_TURN = Team.afterTurn(); + + private static final double AFTER_TURN_BONUS_SCORE = 1.5; + + private JanggiReferee() { + } + + public static Map collectScores(Board board) { + return Map.of( + START_TURN, board.calculateBaseScore(START_TURN), + AFTER_TURN, board.calculateBaseScore(AFTER_TURN) + AFTER_TURN_BONUS_SCORE + ); + } + + public static Team judgeWinner(Board board) { + Map scores = collectScores(board); + double startTurnScore = scores.get(START_TURN); + double afterTurnScore = scores.get(AFTER_TURN); + + if (startTurnScore > afterTurnScore) { + return START_TURN; + } + return AFTER_TURN; + } +} diff --git a/src/main/java/model/JanggiState.java b/src/main/java/model/JanggiState.java new file mode 100644 index 0000000000..4ead04da83 --- /dev/null +++ b/src/main/java/model/JanggiState.java @@ -0,0 +1,61 @@ +package model; + +import java.util.List; +import model.board.Board; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; +import model.piece.Piece; +import model.state.BigJang; +import model.state.Finished; +import model.state.Running; + +public abstract class JanggiState { + private final Team turn; + + protected JanggiState(Team turn) { + this.turn = turn; + } + + public void validateGamePlay() { + if (!canPlaying()) { + throw new IllegalStateException("게임이 종료되었으므로 장기 게임을 진행할 수 없습니다."); + } + } + + public JanggiState next(Board board) { + Team nextTurn = this.turn.opposite(); + + if (!board.isAliveGeneral(nextTurn)) { + return new Finished(this.turn); + } + if (checkBigJang(board, nextTurn)) { + return new BigJang(nextTurn); + } + return new Running(nextTurn); + } + + private boolean checkBigJang(Board board, Team nextTurn) { + Position allyGeneralPosition = board.findGeneralPositionByTeam(nextTurn); + Position enemyGeneralPosition = board.findGeneralPositionByTeam(nextTurn.opposite()); + Displacement displacement = enemyGeneralPosition.toDisplacement(allyGeneralPosition); + if (displacement.isNotStraight()) { + return false; + } + + Direction direction = displacement.extractCardinal(); + List path = direction.pathTo(allyGeneralPosition, enemyGeneralPosition); + List pieces = board.extractPiecesByPath(path); + return pieces.isEmpty(); + } + + public abstract Team resolveWinner(Board board); + + public abstract boolean canPlaying(); + + public abstract GameStatus status(); + + public Team turn() { + return turn; + } +} \ No newline at end of file diff --git a/src/main/java/model/Team.java b/src/main/java/model/Team.java index 9b54c4975e..4b66d75639 100644 --- a/src/main/java/model/Team.java +++ b/src/main/java/model/Team.java @@ -1,7 +1,9 @@ package model; public enum Team { - HAN("한나라"), CHO("초나라"); + + HAN("한나라"), + CHO("초나라"); private final String name; @@ -9,6 +11,14 @@ public enum Team { this.name = name; } + public static Team startTurn() { + return CHO; + } + + public static Team afterTurn() { + return startTurn().opposite(); + } + public boolean isHan() { return this == HAN; } diff --git a/src/main/java/model/state/BigJang.java b/src/main/java/model/state/BigJang.java new file mode 100644 index 0000000000..b439c1556e --- /dev/null +++ b/src/main/java/model/state/BigJang.java @@ -0,0 +1,28 @@ +package model.state; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class BigJang extends JanggiState { + + public BigJang(Team turn) { + super(turn); + } + + @Override + public Team resolveWinner(Board board) { + throw new IllegalStateException("빅장 합의가 필요합니다."); + } + + @Override + public boolean canPlaying() { + return true; + } + + @Override + public GameStatus status() { + return GameStatus.BIG_JANG; + } +} \ No newline at end of file diff --git a/src/main/java/model/state/BigJangDone.java b/src/main/java/model/state/BigJangDone.java new file mode 100644 index 0000000000..a22589af9e --- /dev/null +++ b/src/main/java/model/state/BigJangDone.java @@ -0,0 +1,31 @@ +package model.state; + +import model.GameStatus; +import model.JanggiReferee; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class BigJangDone extends JanggiState { + + private static final double AFTER_TURN_BONUS_SCORE = 1.5; + + public BigJangDone(Team turn) { + super(turn); + } + + @Override + public Team resolveWinner(Board board) { + return JanggiReferee.judgeWinner(board); + } + + @Override + public boolean canPlaying() { + return false; + } + + @Override + public GameStatus status() { + return GameStatus.BIG_JANG_DONE; + } +} \ No newline at end of file diff --git a/src/main/java/model/state/Finished.java b/src/main/java/model/state/Finished.java new file mode 100644 index 0000000000..0c5295b9ee --- /dev/null +++ b/src/main/java/model/state/Finished.java @@ -0,0 +1,28 @@ +package model.state; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class Finished extends JanggiState { + + public Finished(Team winner) { + super(winner); + } + + @Override + public Team resolveWinner(Board board) { + return turn(); + } + + @Override + public boolean canPlaying() { + return false; + } + + @Override + public GameStatus status() { + return GameStatus.DONE; + } +} \ No newline at end of file diff --git a/src/main/java/model/state/Running.java b/src/main/java/model/state/Running.java new file mode 100644 index 0000000000..4f939a711f --- /dev/null +++ b/src/main/java/model/state/Running.java @@ -0,0 +1,28 @@ +package model.state; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class Running extends JanggiState { + + public Running(Team turn) { + super(turn); + } + + @Override + public Team resolveWinner(Board board) { + throw new IllegalStateException("아직 게임이 진행중입니다."); + } + + @Override + public boolean canPlaying() { + return true; + } + + @Override + public GameStatus status() { + return GameStatus.PLAYING; + } +} \ No newline at end of file From f0a01c895dd0936bb7c09a02432699054a1defba Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 01:05:02 +0900 Subject: [PATCH 17/32] =?UTF-8?q?test:=20=EB=B9=85=EC=9E=A5=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=EC=84=9C=20=EC=8A=B9=EC=9E=90=20=EA=B5=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=BB=A4=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/state/BigJangDone.java | 2 -- src/test/java/model/JanggiGameTest.java | 13 +++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/model/state/BigJangDone.java b/src/main/java/model/state/BigJangDone.java index a22589af9e..faddc586e6 100644 --- a/src/main/java/model/state/BigJangDone.java +++ b/src/main/java/model/state/BigJangDone.java @@ -8,8 +8,6 @@ public class BigJangDone extends JanggiState { - private static final double AFTER_TURN_BONUS_SCORE = 1.5; - public BigJangDone(Team turn) { super(turn); } diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java index eac0034894..58380ef2a3 100644 --- a/src/test/java/model/JanggiGameTest.java +++ b/src/test/java/model/JanggiGameTest.java @@ -179,6 +179,19 @@ class JanggiGameTest { .isInstanceOf(IllegalStateException.class); } + @Test + void 빅장_상태에서_승자를_요청하면_예외가_발생한다() { + // given + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(barrierPos, new Position(6, 0)); + + // when & then + assertThatThrownBy(janggiGame::resolveWinner) + .isInstanceOf(IllegalStateException.class); + } + @Test void 장기에서_기물의_점수를_계산하면_후차례에게_가점을_준다() { // given: 양 팀 다 0점인 상황 From 9d13096729ccb766fe7d9f6af7ed939c159b02c7 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 01:11:25 +0900 Subject: [PATCH 18/32] =?UTF-8?q?refactor:=20Console=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EC=99=80=20=EA=B0=95=EA=B2=B0=ED=95=A9=20?= =?UTF-8?q?=EB=90=9C=20View=20static=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Application.java | 4 +- .../java/controller/JanggiController.java | 34 +++++--------- src/main/java/controller/Retrier.java | 8 ++-- src/main/java/view/InputView.java | 40 ++++++++-------- src/main/java/view/OutputView.java | 46 +++++++++---------- src/main/java/view/parser/InputParser.java | 7 ++- 6 files changed, 64 insertions(+), 75 deletions(-) diff --git a/src/main/java/Application.java b/src/main/java/Application.java index c4e80ce0a4..15c8f03f9c 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,10 +1,8 @@ import controller.JanggiController; -import view.InputView; -import view.OutputView; public class Application { public static void main(String[] args) { - JanggiController janggiController = new JanggiController(new InputView(), new OutputView()); + JanggiController janggiController = new JanggiController(); janggiController.run(); } } diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java index 3d11c54da4..84df17bbf4 100644 --- a/src/main/java/controller/JanggiController.java +++ b/src/main/java/controller/JanggiController.java @@ -5,7 +5,6 @@ import static model.Team.HAN; import java.util.Map; -import java.util.function.Consumer; import model.JanggiGame; import model.Team; import model.board.Board; @@ -17,20 +16,13 @@ import view.OutputView; public class JanggiController { - private final InputView inputView; - private final OutputView outputView; - - public JanggiController(InputView inputView, OutputView outputView) { - this.inputView = inputView; - this.outputView = outputView; - } public void run() { JanggiGame janggiGame = createJanggiGame(); - outputView.displayBoard(janggiGame.getBoard()); + OutputView.displayBoard(janggiGame.getBoard()); while (janggiGame.canPlaying()) { - retry(() -> checkBigJang(janggiGame), processError()); + retry(() -> checkBigJang(janggiGame), OutputView::displayError); play(janggiGame); } @@ -38,8 +30,8 @@ public void run() { } private JanggiGame createJanggiGame() { - TeamFormation hanFormation = retry(() -> inputView.readFormationByTeam(HAN), processError()); - TeamFormation choFormation = retry(() -> inputView.readFormationByTeam(CHO), processError()); + TeamFormation hanFormation = retry(() -> InputView.readFormationByTeam(HAN), OutputView::displayError); + TeamFormation choFormation = retry(() -> InputView.readFormationByTeam(CHO), OutputView::displayError); Board board = BoardFactory.generateDefaultPieces(); board.arrangePieces(hanFormation.generate()); @@ -49,18 +41,18 @@ private JanggiGame createJanggiGame() { private void play(JanggiGame janggiGame) { if (janggiGame.canPlaying()) { - retry(() -> playByTurn(janggiGame), processError()); + retry(() -> playByTurn(janggiGame), OutputView::displayError); } - outputView.displayBoard(janggiGame.getBoard()); + OutputView.displayBoard(janggiGame.getBoard()); } private void playByTurn(JanggiGame janggiGame) { Team currentTurn = janggiGame.turn(); - Position current = inputView.readPiecePositionForMove(currentTurn); + Position current = InputView.readPiecePositionForMove(currentTurn); Piece piece = janggiGame.selectPiece(current); - Position next = inputView.readPiecePositionForArrange(currentTurn, piece); + Position next = InputView.readPiecePositionForArrange(currentTurn, piece); janggiGame.movePiece(current, next); } @@ -69,7 +61,7 @@ private void checkBigJang(JanggiGame janggiGame) { return; } - boolean bigJang = inputView.readBigJangStatus(janggiGame.turn()); + boolean bigJang = InputView.readBigJangStatus(janggiGame.turn()); if (bigJang) { janggiGame.finishByBigJang(); } @@ -77,15 +69,11 @@ private void checkBigJang(JanggiGame janggiGame) { private void printResult(JanggiGame janggiGame) { Team winner = janggiGame.resolveWinner(); - outputView.displayWinner(winner); + OutputView.displayWinner(winner); if (janggiGame.isBigJangDone()) { Map finalScore = janggiGame.calculateFinalScore(); - outputView.displayScore(finalScore); + OutputView.displayScore(finalScore); } } - - private Consumer processError() { - return (e) -> outputView.displayError(e.getMessage()); - } } diff --git a/src/main/java/controller/Retrier.java b/src/main/java/controller/Retrier.java index 76e3f8e2c5..8a576262a3 100644 --- a/src/main/java/controller/Retrier.java +++ b/src/main/java/controller/Retrier.java @@ -7,24 +7,24 @@ public final class Retrier { private Retrier() { } - public static T retry(Supplier task, Consumer consumer) { + public static T retry(Supplier task, Consumer consumer) { while (true) { try { return task.get(); } catch (IllegalArgumentException e) { - consumer.accept(e); + consumer.accept(e.getMessage()); } } } - public static void retry(Runnable task, Consumer consumer) { + public static void retry(Runnable task, Consumer consumer) { while (true) { try { task.run(); return; } catch (IllegalArgumentException e) { - consumer.accept(e); + consumer.accept(e.getMessage()); } } } diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index c2a76b0ff3..f0926785a9 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -15,10 +15,10 @@ import view.parser.InputParser; public class InputView { + private static final Scanner SCANNER = new Scanner(System.in); - private static final InputParser PARSER = new InputParser(); - public TeamFormation readFormationByTeam(Team team) { + public static TeamFormation readFormationByTeam(Team team) { int order = readFormationOrder(team); FormationType formationType = Optional.ofNullable(FORMATION_ORDER_MAPPER.get(order)) .orElseThrow(() -> new IllegalArgumentException("올바른 상차림을 선택해주세요.")); @@ -26,40 +26,40 @@ public TeamFormation readFormationByTeam(Team team) { return new TeamFormation(team, formationType); } - public Position readPiecePositionForMove(Team turn) { + public static Position readPiecePositionForMove(Team turn) { System.out.println(); System.out.printf("[%s] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리)%n", turn.getName()); System.out.print("기물: "); return extractPosition(); } - private Position extractPosition() { - List tokens = PARSER.parseToken(SCANNER.nextLine(), ","); - int row = PARSER.parseNumber(tokens.get(0)); - int col = PARSER.parseNumber(tokens.get(1)); - return new Position(row, col); - } - - public Position readPiecePositionForArrange(Team turn, Piece piece) { + public static Position readPiecePositionForArrange(Team turn, Piece piece) { System.out.println(); System.out.printf("[%s] 기물 %s의 다음 위치를 선택해주세요. (쉼표 기준으로 분리)%n", turn.getName(), formatSymbol(piece)); System.out.print("기물: "); return extractPosition(); } - private int readFormationOrder(Team team) { + public static boolean readBigJangStatus(Team turn) { + System.out.printf("%n[%s] 빅장입니다! 종료하시겠습니까? (Y, N)%n", turn.getName()); + String input = SCANNER.nextLine(); + InputCommand command = InputCommand.parse(input); + return command == InputCommand.Y; + } + + private static Position extractPosition() { + List tokens = InputParser.parseToken(SCANNER.nextLine(), ","); + int row = InputParser.parseNumber(tokens.get(0)); + int col = InputParser.parseNumber(tokens.get(1)); + return new Position(row, col); + } + + private static int readFormationOrder(Team team) { System.out.printf("%n%s의 상차림을 선택해주세요.%n", team.getName()); FORMATION_ORDER_MAPPER.forEach((order, formation) -> System.out.printf("%d. %s%n", order, FORMATION_DISPLAY_MAPPER.get(formation))); String input = SCANNER.nextLine(); - return PARSER.parseNumber(input); - } - - public boolean readBigJangStatus(Team turn) { - System.out.printf("%n[%s] 빅장입니다! 종료하시겠습니까? (Y, N)%n", turn.getName()); - String input = SCANNER.nextLine(); - InputCommand command = InputCommand.parse(input); - return command == InputCommand.Y; + return InputParser.parseNumber(input); } } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index d1417d7cd3..9151e84075 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -18,6 +18,29 @@ public class OutputView { + public static void displayBoard(Map board) { + displayColIndex(); + String border = formatHorizon(Board.BOARD_COL); + System.out.println(border); + + displayPositionByPiece(board); + + System.out.println(border); + } + + public static void displayError(String message) { + System.out.println(RED + "[ERROR] " + message + RESET); + } + + public static void displayWinner(Team winner) { + System.out.println(winner.getName() + " 승"); + } + + public static void displayScore(Map finalScore) { + DecimalFormat formatter = new DecimalFormat("#.#"); + finalScore.forEach((team, score) -> System.out.printf("%s: %s점%n", team.getName(), formatter.format(score))); + } + private static void displayPositionByPiece(Map board) { for (int row = 0; row < Board.BOARD_ROW; row++) { System.out.print(ROW_NUM[row] + " " + VERTICAL_LINE); @@ -37,27 +60,4 @@ private static void displayColIndex() { } System.out.println(); } - - public void displayBoard(Map board) { - displayColIndex(); - String border = formatHorizon(Board.BOARD_COL); - System.out.println(border); - - displayPositionByPiece(board); - - System.out.println(border); - } - - public void displayError(String message) { - System.out.println(RED + "[ERROR] " + message + RESET); - } - - public void displayWinner(Team winner) { - System.out.println(winner.getName() + " 승"); - } - - public void displayScore(Map finalScore) { - DecimalFormat formatter = new DecimalFormat("#.#"); - finalScore.forEach((team, score) -> System.out.printf("%s: %s점%n", team.getName(), formatter.format(score))); - } } \ No newline at end of file diff --git a/src/main/java/view/parser/InputParser.java b/src/main/java/view/parser/InputParser.java index 8eeb8874f1..08e0adf1de 100644 --- a/src/main/java/view/parser/InputParser.java +++ b/src/main/java/view/parser/InputParser.java @@ -5,7 +5,10 @@ public class InputParser { - public int parseNumber(String input) { + private InputParser() { + } + + public static int parseNumber(String input) { try { return Integer.parseInt(input.strip()); } catch (NumberFormatException e) { @@ -13,7 +16,7 @@ public int parseNumber(String input) { } } - public List parseToken(String input, String delimiter) { + public static List parseToken(String input, String delimiter) { return Arrays.stream(input.strip() .split(delimiter)) .map(String::strip) From d1ef51495e2aa115017a1cfd443a6a28b98cf945 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 01:14:31 +0900 Subject: [PATCH 19/32] =?UTF-8?q?refactor:=20presentation=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Application.java | 2 +- src/main/java/{view => ui}/InputCommand.java | 2 +- .../{controller => ui}/JanggiController.java | 8 ++++---- src/main/java/{controller => ui}/Retrier.java | 2 +- .../{view => ui}/formater/BoardFormatter.java | 4 ++-- .../java/{view => ui}/mapper/ViewMapper.java | 2 +- .../java/{view => ui}/parser/InputParser.java | 2 +- src/main/java/{ => ui}/view/InputView.java | 11 +++++----- src/main/java/{ => ui}/view/OutputView.java | 20 +++++++++---------- 9 files changed, 27 insertions(+), 26 deletions(-) rename src/main/java/{view => ui}/InputCommand.java (95%) rename src/main/java/{controller => ui}/JanggiController.java (95%) rename src/main/java/{controller => ui}/Retrier.java (97%) rename src/main/java/{view => ui}/formater/BoardFormatter.java (94%) rename src/main/java/{view => ui}/mapper/ViewMapper.java (98%) rename src/main/java/{view => ui}/parser/InputParser.java (96%) rename src/main/java/{ => ui}/view/InputView.java (90%) rename src/main/java/{ => ui}/view/OutputView.java (79%) diff --git a/src/main/java/Application.java b/src/main/java/Application.java index 15c8f03f9c..382e823d7f 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,4 +1,4 @@ -import controller.JanggiController; +import ui.JanggiController; public class Application { public static void main(String[] args) { diff --git a/src/main/java/view/InputCommand.java b/src/main/java/ui/InputCommand.java similarity index 95% rename from src/main/java/view/InputCommand.java rename to src/main/java/ui/InputCommand.java index 1a356c442d..1c025932b6 100644 --- a/src/main/java/view/InputCommand.java +++ b/src/main/java/ui/InputCommand.java @@ -1,4 +1,4 @@ -package view; +package ui; public enum InputCommand { Y, N; diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/ui/JanggiController.java similarity index 95% rename from src/main/java/controller/JanggiController.java rename to src/main/java/ui/JanggiController.java index 84df17bbf4..3b711308c5 100644 --- a/src/main/java/controller/JanggiController.java +++ b/src/main/java/ui/JanggiController.java @@ -1,8 +1,8 @@ -package controller; +package ui; -import static controller.Retrier.retry; import static model.Team.CHO; import static model.Team.HAN; +import static ui.Retrier.retry; import java.util.Map; import model.JanggiGame; @@ -12,8 +12,8 @@ import model.board.TeamFormation; import model.coordinate.Position; import model.piece.Piece; -import view.InputView; -import view.OutputView; +import ui.view.InputView; +import ui.view.OutputView; public class JanggiController { diff --git a/src/main/java/controller/Retrier.java b/src/main/java/ui/Retrier.java similarity index 97% rename from src/main/java/controller/Retrier.java rename to src/main/java/ui/Retrier.java index 8a576262a3..8493499631 100644 --- a/src/main/java/controller/Retrier.java +++ b/src/main/java/ui/Retrier.java @@ -1,4 +1,4 @@ -package controller; +package ui; import java.util.function.Consumer; import java.util.function.Supplier; diff --git a/src/main/java/view/formater/BoardFormatter.java b/src/main/java/ui/formater/BoardFormatter.java similarity index 94% rename from src/main/java/view/formater/BoardFormatter.java rename to src/main/java/ui/formater/BoardFormatter.java index b028f5d48d..555ab569f1 100644 --- a/src/main/java/view/formater/BoardFormatter.java +++ b/src/main/java/ui/formater/BoardFormatter.java @@ -1,6 +1,6 @@ -package view.formater; +package ui.formater; -import static view.mapper.ViewMapper.SYMBOL_MAP; +import static ui.mapper.ViewMapper.SYMBOL_MAP; import model.Team; import model.piece.Piece; diff --git a/src/main/java/view/mapper/ViewMapper.java b/src/main/java/ui/mapper/ViewMapper.java similarity index 98% rename from src/main/java/view/mapper/ViewMapper.java rename to src/main/java/ui/mapper/ViewMapper.java index 7db0206b93..0d7143d1a2 100644 --- a/src/main/java/view/mapper/ViewMapper.java +++ b/src/main/java/ui/mapper/ViewMapper.java @@ -1,4 +1,4 @@ -package view.mapper; +package ui.mapper; import static model.board.FormationType.MA_SANG_MA_SANG; import static model.board.FormationType.MA_SANG_SANG_MA; diff --git a/src/main/java/view/parser/InputParser.java b/src/main/java/ui/parser/InputParser.java similarity index 96% rename from src/main/java/view/parser/InputParser.java rename to src/main/java/ui/parser/InputParser.java index 08e0adf1de..95323dc5e0 100644 --- a/src/main/java/view/parser/InputParser.java +++ b/src/main/java/ui/parser/InputParser.java @@ -1,4 +1,4 @@ -package view.parser; +package ui.parser; import java.util.Arrays; import java.util.List; diff --git a/src/main/java/view/InputView.java b/src/main/java/ui/view/InputView.java similarity index 90% rename from src/main/java/view/InputView.java rename to src/main/java/ui/view/InputView.java index f0926785a9..36c2c56bdf 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/ui/view/InputView.java @@ -1,8 +1,8 @@ -package view; +package ui.view; -import static view.formater.BoardFormatter.formatSymbol; -import static view.mapper.ViewMapper.FORMATION_DISPLAY_MAPPER; -import static view.mapper.ViewMapper.FORMATION_ORDER_MAPPER; +import static ui.formater.BoardFormatter.formatSymbol; +import static ui.mapper.ViewMapper.FORMATION_DISPLAY_MAPPER; +import static ui.mapper.ViewMapper.FORMATION_ORDER_MAPPER; import java.util.List; import java.util.Optional; @@ -12,7 +12,8 @@ import model.board.TeamFormation; import model.coordinate.Position; import model.piece.Piece; -import view.parser.InputParser; +import ui.InputCommand; +import ui.parser.InputParser; public class InputView { diff --git a/src/main/java/view/OutputView.java b/src/main/java/ui/view/OutputView.java similarity index 79% rename from src/main/java/view/OutputView.java rename to src/main/java/ui/view/OutputView.java index 9151e84075..7e8c7404a6 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/ui/view/OutputView.java @@ -1,13 +1,13 @@ -package view; - -import static view.formater.BoardFormatter.COL_NUM; -import static view.formater.BoardFormatter.RED; -import static view.formater.BoardFormatter.RESET; -import static view.formater.BoardFormatter.ROW_NUM; -import static view.formater.BoardFormatter.SPACE; -import static view.formater.BoardFormatter.VERTICAL_LINE; -import static view.formater.BoardFormatter.formatHorizon; -import static view.formater.BoardFormatter.formatSymbol; +package ui.view; + +import static ui.formater.BoardFormatter.COL_NUM; +import static ui.formater.BoardFormatter.RED; +import static ui.formater.BoardFormatter.RESET; +import static ui.formater.BoardFormatter.ROW_NUM; +import static ui.formater.BoardFormatter.SPACE; +import static ui.formater.BoardFormatter.VERTICAL_LINE; +import static ui.formater.BoardFormatter.formatHorizon; +import static ui.formater.BoardFormatter.formatSymbol; import java.text.DecimalFormat; import java.util.Map; From a4d06c0c58b31416989f1e688dd13b19f4e7bf21 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 12:00:53 +0900 Subject: [PATCH 20/32] =?UTF-8?q?docs:=20=EC=9E=A5=EA=B8=B0=202=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=20DB=20=EC=A0=81=EC=9A=A9=20=EA=B8=B0=EB=8A=A5=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 | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 538a2a5706..8622db65e1 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ # Cycle2 -## 1단계 - 궁성 영역 구현 +## 1단계 - 기물의 확장 1. 궁성 영역을 정의한다. * 한나라 궁성: (0,3) ~ (2,5) 내 9개 위치 @@ -312,3 +312,112 @@ Y  +-------------------+ 초나라 승 +``` + +## 2단계 - DB 적용 + +1. 이전에 하던 게임을 다시 시작할 수 있어야 한다. + * 장기를 저장하고 그대로 꺼내온다. +2. 장기 게임방을 만들고 장기 게임방에 입장할 수 있는 기능을 추가한다. (선택) + * 추후 작성 + +### 잘못된 게임 메뉴 선택 + +```text +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +4 + +[ERROR] 잘못된 입력입니다. +``` + +### 종료된 게임 선택 + +```text + +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +1 + +> 이어서 진행할 게임을 선택해주세요. +1번 게임: 종료 상태 +2번 게임: 진행 상태 + +번호를 입력해주세요: +1 + +[ERROR] 이미 종료된 게임입니다. +``` + +### 존재하지 않는 게임 선택 + +```text + +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +1 + +> 이어서 진행할 게임을 선택해주세요. +1번 게임: 종료 상태 +2번 게임: 진행 상태 + +번호를 입력해주세요: +3 + +[ERROR] 존재하지 않는 게임입니다. +``` + +### 이전 게임 다시 시작 + +```text + +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +1 + +> 이어서 진행할 게임을 선택해주세요. +1번 게임: 종료 상태 +2번 게임: 진행 상태 + +번호를 입력해주세요: +2 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * 包 * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) + +... + +``` \ No newline at end of file From fb723c1dd602e97ab84c64f03c88cf255a9afab0 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 15:28:22 +0900 Subject: [PATCH 21/32] =?UTF-8?q?feat:=20=EC=9E=A5=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=ED=99=95=EC=9E=A5=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?front=20controller=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Application.java | 6 +- src/main/java/ui/ContinueController.java | 8 +++ src/main/java/ui/FrontController.java | 29 +++++++++ src/main/java/ui/GameMenu.java | 33 ++++++++++ src/main/java/ui/JanggiController.java | 77 +---------------------- src/main/java/ui/NewGameController.java | 80 ++++++++++++++++++++++++ src/main/java/ui/view/InputView.java | 14 +++++ src/main/java/ui/view/OutputView.java | 4 ++ 8 files changed, 173 insertions(+), 78 deletions(-) create mode 100644 src/main/java/ui/ContinueController.java create mode 100644 src/main/java/ui/FrontController.java create mode 100644 src/main/java/ui/GameMenu.java create mode 100644 src/main/java/ui/NewGameController.java diff --git a/src/main/java/Application.java b/src/main/java/Application.java index 382e823d7f..e875fb530f 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,8 +1,8 @@ -import ui.JanggiController; +import ui.FrontController; public class Application { public static void main(String[] args) { - JanggiController janggiController = new JanggiController(); - janggiController.run(); + FrontController frontController = new FrontController(); + frontController.run(); } } diff --git a/src/main/java/ui/ContinueController.java b/src/main/java/ui/ContinueController.java new file mode 100644 index 0000000000..48debd64e4 --- /dev/null +++ b/src/main/java/ui/ContinueController.java @@ -0,0 +1,8 @@ +package ui; + +public class ContinueController implements JanggiController { + @Override + public void run() { + + } +} diff --git a/src/main/java/ui/FrontController.java b/src/main/java/ui/FrontController.java new file mode 100644 index 0000000000..5848c1c84e --- /dev/null +++ b/src/main/java/ui/FrontController.java @@ -0,0 +1,29 @@ +package ui; + +import static ui.Retrier.retry; + +import java.util.Map; +import java.util.Optional; +import ui.view.InputView; +import ui.view.OutputView; + +public class FrontController { + + private final Map controllers; + + public FrontController() { + controllers = Map.of( + GameMenu.CONTINUE, new ContinueController(), + GameMenu.NEW_GAME, new NewGameController() + ); + } + + public void run() { + GameMenu gameMenu; + do { + gameMenu = retry(InputView::readGameMenu, OutputView::displayError); + Optional.ofNullable(controllers.get(gameMenu)) + .ifPresent(JanggiController::run); + } while (gameMenu != GameMenu.END); + } +} diff --git a/src/main/java/ui/GameMenu.java b/src/main/java/ui/GameMenu.java new file mode 100644 index 0000000000..ecd25e9dbf --- /dev/null +++ b/src/main/java/ui/GameMenu.java @@ -0,0 +1,33 @@ +package ui; + +import java.util.stream.Stream; + +public enum GameMenu { + + CONTINUE(1, "게임 이어서 진행하기"), + NEW_GAME(2, "새로 시작하기"), + END(3, "종료하기"); + + private final int value; + private final String description; + + GameMenu(int value, String description) { + this.value = value; + this.description = description; + } + + public static GameMenu select(int number) { + return Stream.of(values()) + .filter(v -> v.value == number) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("잘못된 입력입니다.")); + } + + public int getValue() { + return value; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/ui/JanggiController.java b/src/main/java/ui/JanggiController.java index 3b711308c5..431a1c38ae 100644 --- a/src/main/java/ui/JanggiController.java +++ b/src/main/java/ui/JanggiController.java @@ -1,79 +1,6 @@ package ui; -import static model.Team.CHO; -import static model.Team.HAN; -import static ui.Retrier.retry; +public interface JanggiController { -import java.util.Map; -import model.JanggiGame; -import model.Team; -import model.board.Board; -import model.board.BoardFactory; -import model.board.TeamFormation; -import model.coordinate.Position; -import model.piece.Piece; -import ui.view.InputView; -import ui.view.OutputView; - -public class JanggiController { - - public void run() { - JanggiGame janggiGame = createJanggiGame(); - OutputView.displayBoard(janggiGame.getBoard()); - - while (janggiGame.canPlaying()) { - retry(() -> checkBigJang(janggiGame), OutputView::displayError); - play(janggiGame); - } - - printResult(janggiGame); - } - - private JanggiGame createJanggiGame() { - TeamFormation hanFormation = retry(() -> InputView.readFormationByTeam(HAN), OutputView::displayError); - TeamFormation choFormation = retry(() -> InputView.readFormationByTeam(CHO), OutputView::displayError); - - Board board = BoardFactory.generateDefaultPieces(); - board.arrangePieces(hanFormation.generate()); - board.arrangePieces(choFormation.generate()); - return new JanggiGame(board); - } - - private void play(JanggiGame janggiGame) { - if (janggiGame.canPlaying()) { - retry(() -> playByTurn(janggiGame), OutputView::displayError); - } - OutputView.displayBoard(janggiGame.getBoard()); - } - - private void playByTurn(JanggiGame janggiGame) { - Team currentTurn = janggiGame.turn(); - - Position current = InputView.readPiecePositionForMove(currentTurn); - Piece piece = janggiGame.selectPiece(current); - - Position next = InputView.readPiecePositionForArrange(currentTurn, piece); - janggiGame.movePiece(current, next); - } - - private void checkBigJang(JanggiGame janggiGame) { - if (!janggiGame.isBigJang()) { - return; - } - - boolean bigJang = InputView.readBigJangStatus(janggiGame.turn()); - if (bigJang) { - janggiGame.finishByBigJang(); - } - } - - private void printResult(JanggiGame janggiGame) { - Team winner = janggiGame.resolveWinner(); - OutputView.displayWinner(winner); - - if (janggiGame.isBigJangDone()) { - Map finalScore = janggiGame.calculateFinalScore(); - OutputView.displayScore(finalScore); - } - } + void run(); } diff --git a/src/main/java/ui/NewGameController.java b/src/main/java/ui/NewGameController.java new file mode 100644 index 0000000000..e90428e5f0 --- /dev/null +++ b/src/main/java/ui/NewGameController.java @@ -0,0 +1,80 @@ +package ui; + +import static model.Team.CHO; +import static model.Team.HAN; +import static ui.Retrier.retry; + +import java.util.Map; +import model.JanggiGame; +import model.Team; +import model.board.Board; +import model.board.BoardFactory; +import model.board.TeamFormation; +import model.coordinate.Position; +import model.piece.Piece; +import ui.view.InputView; +import ui.view.OutputView; + +public class NewGameController implements JanggiController { + + @Override + public void run() { + JanggiGame janggiGame = createJanggiGame(); + OutputView.displayBoard(janggiGame.getBoard()); + + while (janggiGame.canPlaying()) { + retry(() -> checkBigJang(janggiGame), OutputView::displayError); + play(janggiGame); + } + + printResult(janggiGame); + } + + private JanggiGame createJanggiGame() { + TeamFormation hanFormation = retry(() -> InputView.readFormationByTeam(HAN), OutputView::displayError); + TeamFormation choFormation = retry(() -> InputView.readFormationByTeam(CHO), OutputView::displayError); + + Board board = BoardFactory.generateDefaultPieces(); + board.arrangePieces(hanFormation.generate()); + board.arrangePieces(choFormation.generate()); + return new JanggiGame(board); + } + + private void play(JanggiGame janggiGame) { + if (janggiGame.canPlaying()) { + retry(() -> playByTurn(janggiGame), OutputView::displayError); + } + OutputView.displayBoard(janggiGame.getBoard()); + } + + private void playByTurn(JanggiGame janggiGame) { + Team currentTurn = janggiGame.turn(); + + Position current = InputView.readPiecePositionForMove(currentTurn); + Piece piece = janggiGame.selectPiece(current); + + Position next = InputView.readPiecePositionForArrange(currentTurn, piece); + janggiGame.movePiece(current, next); + } + + private void checkBigJang(JanggiGame janggiGame) { + if (!janggiGame.isBigJang()) { + return; + } + + boolean bigJang = InputView.readBigJangStatus(janggiGame.turn()); + if (bigJang) { + janggiGame.finishByBigJang(); + } + } + + private void printResult(JanggiGame janggiGame) { + Team winner = janggiGame.resolveWinner(); + OutputView.displayWinner(winner); + + if (janggiGame.isBigJangDone()) { + Map finalScore = janggiGame.calculateFinalScore(); + OutputView.displayScore(finalScore); + } + } +} diff --git a/src/main/java/ui/view/InputView.java b/src/main/java/ui/view/InputView.java index 36c2c56bdf..8ce7b1c4d6 100644 --- a/src/main/java/ui/view/InputView.java +++ b/src/main/java/ui/view/InputView.java @@ -4,6 +4,7 @@ import static ui.mapper.ViewMapper.FORMATION_DISPLAY_MAPPER; import static ui.mapper.ViewMapper.FORMATION_ORDER_MAPPER; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Scanner; @@ -12,6 +13,7 @@ import model.board.TeamFormation; import model.coordinate.Position; import model.piece.Piece; +import ui.GameMenu; import ui.InputCommand; import ui.parser.InputParser; @@ -63,4 +65,16 @@ private static int readFormationOrder(Team team) { String input = SCANNER.nextLine(); return InputParser.parseNumber(input); } + + public static GameMenu readGameMenu() { + System.out.println(); + System.out.println("> 장기 게임 메뉴"); + System.out.println(); + Arrays.stream(GameMenu.values()) + .forEach(gameMenu -> System.out.printf("%d. %s%n", gameMenu.getValue(), gameMenu.getDescription())); + + System.out.println(); + System.out.println("메뉴를 선택해주세요:"); + return GameMenu.select(InputParser.parseNumber(SCANNER.nextLine())); + } } diff --git a/src/main/java/ui/view/OutputView.java b/src/main/java/ui/view/OutputView.java index 7e8c7404a6..155716e788 100644 --- a/src/main/java/ui/view/OutputView.java +++ b/src/main/java/ui/view/OutputView.java @@ -60,4 +60,8 @@ private static void displayColIndex() { } System.out.println(); } + + public static void displayGameMenu() { + System.out.println("> 장기 게임 프로그램을 시작합니다."); + } } \ No newline at end of file From b7ae746cb0aea3072511e3134fdef1631916bd26 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 16:47:02 +0900 Subject: [PATCH 22/32] =?UTF-8?q?refactor:=20third=20party=EB=A5=BC=20?= =?UTF-8?q?=EB=8C=80=EB=B9=84=ED=95=9C=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/main/java/application/JanggiResult.java | 20 ++++++ src/main/java/application/JanggiService.java | 70 +++++++++++++++++++ src/main/java/application/MoveCommand.java | 6 ++ .../repository/InMemoryJanggiRepository.java | 28 ++++++++ .../java/repository/JanggiRepository.java | 13 ++++ src/main/java/ui/FrontController.java | 6 +- src/main/java/ui/NewGameController.java | 68 +++++++++--------- 8 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 src/main/java/application/JanggiResult.java create mode 100644 src/main/java/application/JanggiService.java create mode 100644 src/main/java/application/MoveCommand.java create mode 100644 src/main/java/repository/InMemoryJanggiRepository.java create mode 100644 src/main/java/repository/JanggiRepository.java diff --git a/README.md b/README.md index 8622db65e1..d46478d33e 100644 --- a/README.md +++ b/README.md @@ -379,7 +379,7 @@ Y 번호를 입력해주세요: 3 -[ERROR] 존재하지 않는 게임입니다. +[ERROR] 1번 장기 게임이 존재하지 않습니다. ``` ### 이전 게임 다시 시작 diff --git a/src/main/java/application/JanggiResult.java b/src/main/java/application/JanggiResult.java new file mode 100644 index 0000000000..874fb9cf5d --- /dev/null +++ b/src/main/java/application/JanggiResult.java @@ -0,0 +1,20 @@ +package application; + +import java.util.Map; +import model.JanggiGame; +import model.Team; + +public record JanggiResult( + Team winner, + boolean bigJangDone, + Map finalScore +) { + + public static JanggiResult from(JanggiGame janggiGame) { + return new JanggiResult( + janggiGame.resolveWinner(), + janggiGame.isBigJangDone(), + janggiGame.calculateFinalScore() + ); + } +} diff --git a/src/main/java/application/JanggiService.java b/src/main/java/application/JanggiService.java new file mode 100644 index 0000000000..67f789b964 --- /dev/null +++ b/src/main/java/application/JanggiService.java @@ -0,0 +1,70 @@ +package application; + +import java.util.Map; +import model.JanggiGame; +import model.Team; +import model.board.Board; +import model.board.BoardFactory; +import model.board.TeamFormation; +import model.coordinate.Position; +import model.piece.Piece; +import repository.JanggiRepository; + +public class JanggiService { + private final JanggiRepository janggiRepository; + + public JanggiService(JanggiRepository janggiRepository) { + this.janggiRepository = janggiRepository; + } + + public Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation) { + Board board = BoardFactory.generateDefaultPieces(); + board.arrangePieces(hanFormation.generate()); + board.arrangePieces(choFormation.generate()); + + JanggiGame janggiGame = new JanggiGame(board); + return janggiRepository.save(janggiGame); + } + + public void finishByBigJang(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + janggiGame.finishByBigJang(); + janggiRepository.update(janggiId, janggiGame); + } + + public void movePiece(Long janggiId, MoveCommand command) { + JanggiGame janggiGame = getGame(janggiId); + janggiGame.movePiece(command.current(), command.next()); + janggiRepository.update(janggiId, janggiGame); + } + + public JanggiResult getGameResult(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + return JanggiResult.from(janggiGame); + } + + public Map getBoardResponse(Long janggiId) { + return getGame(janggiId).getBoard(); + } + + public boolean canPlaying(Long janggiId) { + return getGame(janggiId).canPlaying(); + } + + public Team getTurn(Long janggiId) { + return getGame(janggiId).turn(); + } + + public Piece selectPiece(Long janggiId, Position current) { + return getGame(janggiId).selectPiece(current); + } + + public boolean isBigJang(Long janggiId) { + return getGame(janggiId).isBigJang(); + } + + private JanggiGame getGame(Long janggiId) { + return janggiRepository.findById(janggiId) + .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/application/MoveCommand.java b/src/main/java/application/MoveCommand.java new file mode 100644 index 0000000000..178639cafc --- /dev/null +++ b/src/main/java/application/MoveCommand.java @@ -0,0 +1,6 @@ +package application; + +import model.coordinate.Position; + +public record MoveCommand(Position current, Position next) { +} diff --git a/src/main/java/repository/InMemoryJanggiRepository.java b/src/main/java/repository/InMemoryJanggiRepository.java new file mode 100644 index 0000000000..7d9ad9904d --- /dev/null +++ b/src/main/java/repository/InMemoryJanggiRepository.java @@ -0,0 +1,28 @@ +package repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import model.JanggiGame; + +public class InMemoryJanggiRepository implements JanggiRepository { + + private final Map storage = new ConcurrentHashMap<>(); + private Long id = 0L; + + @Override + public Long save(JanggiGame janggiGame) { + storage.put(++id, janggiGame); + return id; + } + + @Override + public Optional findById(Long janggiId) { + return Optional.ofNullable(storage.get(janggiId)); + } + + @Override + public void update(Long janggiId, JanggiGame janggiGame) { + storage.put(janggiId, janggiGame); + } +} diff --git a/src/main/java/repository/JanggiRepository.java b/src/main/java/repository/JanggiRepository.java new file mode 100644 index 0000000000..02da1e270e --- /dev/null +++ b/src/main/java/repository/JanggiRepository.java @@ -0,0 +1,13 @@ +package repository; + +import java.util.Optional; +import model.JanggiGame; + +public interface JanggiRepository { + + Long save(JanggiGame janggiGame); + + Optional findById(Long janggiId); + + void update(Long janggiId, JanggiGame janggiGame); +} diff --git a/src/main/java/ui/FrontController.java b/src/main/java/ui/FrontController.java index 5848c1c84e..af5b7bf780 100644 --- a/src/main/java/ui/FrontController.java +++ b/src/main/java/ui/FrontController.java @@ -2,8 +2,10 @@ import static ui.Retrier.retry; +import application.JanggiService; import java.util.Map; import java.util.Optional; +import repository.InMemoryJanggiRepository; import ui.view.InputView; import ui.view.OutputView; @@ -12,9 +14,11 @@ public class FrontController { private final Map controllers; public FrontController() { + JanggiService janggiService = new JanggiService(new InMemoryJanggiRepository()); + controllers = Map.of( GameMenu.CONTINUE, new ContinueController(), - GameMenu.NEW_GAME, new NewGameController() + GameMenu.NEW_GAME, new NewGameController(janggiService) ); } diff --git a/src/main/java/ui/NewGameController.java b/src/main/java/ui/NewGameController.java index e90428e5f0..033e50d21c 100644 --- a/src/main/java/ui/NewGameController.java +++ b/src/main/java/ui/NewGameController.java @@ -4,11 +4,10 @@ import static model.Team.HAN; import static ui.Retrier.retry; -import java.util.Map; -import model.JanggiGame; +import application.JanggiResult; +import application.JanggiService; +import application.MoveCommand; import model.Team; -import model.board.Board; -import model.board.BoardFactory; import model.board.TeamFormation; import model.coordinate.Position; import model.piece.Piece; @@ -17,64 +16,65 @@ public class NewGameController implements JanggiController { + private final JanggiService janggiService; + + public NewGameController(JanggiService janggiService) { + this.janggiService = janggiService; + } + @Override public void run() { - JanggiGame janggiGame = createJanggiGame(); - OutputView.displayBoard(janggiGame.getBoard()); + Long janggiId = createJanggiGame(); + OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); - while (janggiGame.canPlaying()) { - retry(() -> checkBigJang(janggiGame), OutputView::displayError); - play(janggiGame); + while (janggiService.canPlaying(janggiId)) { + retry(() -> checkBigJang(janggiId), OutputView::displayError); + play(janggiId); } - printResult(janggiGame); + printResult(janggiId); } - private JanggiGame createJanggiGame() { + private Long createJanggiGame() { TeamFormation hanFormation = retry(() -> InputView.readFormationByTeam(HAN), OutputView::displayError); TeamFormation choFormation = retry(() -> InputView.readFormationByTeam(CHO), OutputView::displayError); - - Board board = BoardFactory.generateDefaultPieces(); - board.arrangePieces(hanFormation.generate()); - board.arrangePieces(choFormation.generate()); - return new JanggiGame(board); + return janggiService.createJanggiGame(hanFormation, choFormation); } - private void play(JanggiGame janggiGame) { - if (janggiGame.canPlaying()) { - retry(() -> playByTurn(janggiGame), OutputView::displayError); + private void play(Long janggiId) { + if (janggiService.canPlaying(janggiId)) { + retry(() -> playByTurn(janggiId), OutputView::displayError); } - OutputView.displayBoard(janggiGame.getBoard()); + OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); } - private void playByTurn(JanggiGame janggiGame) { - Team currentTurn = janggiGame.turn(); + private void playByTurn(Long janggiId) { + Team currentTurn = janggiService.getTurn(janggiId); Position current = InputView.readPiecePositionForMove(currentTurn); - Piece piece = janggiGame.selectPiece(current); + Piece piece = janggiService.selectPiece(janggiId, current); Position next = InputView.readPiecePositionForArrange(currentTurn, piece); - janggiGame.movePiece(current, next); + janggiService.movePiece(janggiId, new MoveCommand(current, next)); } - private void checkBigJang(JanggiGame janggiGame) { - if (!janggiGame.isBigJang()) { + private void checkBigJang(Long janggiId) { + if (!janggiService.isBigJang(janggiId)) { return; } - boolean bigJang = InputView.readBigJangStatus(janggiGame.turn()); + boolean bigJang = InputView.readBigJangStatus(janggiService.getTurn(janggiId)); if (bigJang) { - janggiGame.finishByBigJang(); + janggiService.finishByBigJang(janggiId); } } - private void printResult(JanggiGame janggiGame) { - Team winner = janggiGame.resolveWinner(); - OutputView.displayWinner(winner); + private void printResult(Long janggiId) { + JanggiResult gameResult = janggiService.getGameResult(janggiId); + OutputView.displayWinner(gameResult.winner()); - if (janggiGame.isBigJangDone()) { - Map finalScore = janggiGame.calculateFinalScore(); - OutputView.displayScore(finalScore); + if (gameResult.bigJangDone()) { + OutputView.displayScore(gameResult.finalScore()); } } } From cd7902505562262b9590b7d3bef63c603b753158 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 17:17:19 +0900 Subject: [PATCH 23/32] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EC=84=9C=20=EC=A7=84=ED=96=89=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++- ...JanggiResult.java => JanggiResultDto.java} | 6 +- src/main/java/application/JanggiService.java | 9 ++- src/main/java/model/JanggiGame.java | 4 ++ .../repository/InMemoryJanggiRepository.java | 9 +++ .../java/repository/JanggiRepository.java | 4 ++ src/main/java/ui/ContinueController.java | 31 +++++++- src/main/java/ui/FrontController.java | 2 +- src/main/java/ui/JanggiController.java | 71 ++++++++++++++++++- src/main/java/ui/NewGameController.java | 65 ++--------------- src/main/java/ui/mapper/ViewMapper.java | 8 +++ src/main/java/ui/parser/InputParser.java | 8 +++ src/main/java/ui/view/InputView.java | 18 +++++ src/main/java/ui/view/OutputView.java | 5 +- 14 files changed, 183 insertions(+), 73 deletions(-) rename src/main/java/application/{JanggiResult.java => JanggiResultDto.java} (72%) diff --git a/README.md b/README.md index d46478d33e..184a428efe 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,21 @@ Y 번호를 입력해주세요: 1 -[ERROR] 이미 종료된 게임입니다. +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 包 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * * * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +초나라 승 ``` ### 존재하지 않는 게임 선택 diff --git a/src/main/java/application/JanggiResult.java b/src/main/java/application/JanggiResultDto.java similarity index 72% rename from src/main/java/application/JanggiResult.java rename to src/main/java/application/JanggiResultDto.java index 874fb9cf5d..ab69d5482a 100644 --- a/src/main/java/application/JanggiResult.java +++ b/src/main/java/application/JanggiResultDto.java @@ -4,14 +4,14 @@ import model.JanggiGame; import model.Team; -public record JanggiResult( +public record JanggiResultDto( Team winner, boolean bigJangDone, Map finalScore ) { - public static JanggiResult from(JanggiGame janggiGame) { - return new JanggiResult( + public static JanggiResultDto from(JanggiGame janggiGame) { + return new JanggiResultDto( janggiGame.resolveWinner(), janggiGame.isBigJangDone(), janggiGame.calculateFinalScore() diff --git a/src/main/java/application/JanggiService.java b/src/main/java/application/JanggiService.java index 67f789b964..9a3c06b4ba 100644 --- a/src/main/java/application/JanggiService.java +++ b/src/main/java/application/JanggiService.java @@ -1,6 +1,7 @@ package application; import java.util.Map; +import model.GameStatus; import model.JanggiGame; import model.Team; import model.board.Board; @@ -38,9 +39,9 @@ public void movePiece(Long janggiId, MoveCommand command) { janggiRepository.update(janggiId, janggiGame); } - public JanggiResult getGameResult(Long janggiId) { + public JanggiResultDto getGameResult(Long janggiId) { JanggiGame janggiGame = getGame(janggiId); - return JanggiResult.from(janggiGame); + return JanggiResultDto.from(janggiGame); } public Map getBoardResponse(Long janggiId) { @@ -67,4 +68,8 @@ private JanggiGame getGame(Long janggiId) { return janggiRepository.findById(janggiId) .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); } + + public Map collectGameStatus() { + return janggiRepository.collectGameStatus(); + } } diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index d48b10a939..a88236ee7e 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -72,6 +72,10 @@ public Team turn() { return state.turn(); } + public GameStatus status() { + return state.status(); + } + public Map getBoard() { return board.board(); } diff --git a/src/main/java/repository/InMemoryJanggiRepository.java b/src/main/java/repository/InMemoryJanggiRepository.java index 7d9ad9904d..34173c2fe6 100644 --- a/src/main/java/repository/InMemoryJanggiRepository.java +++ b/src/main/java/repository/InMemoryJanggiRepository.java @@ -3,6 +3,8 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import model.GameStatus; import model.JanggiGame; public class InMemoryJanggiRepository implements JanggiRepository { @@ -25,4 +27,11 @@ public Optional findById(Long janggiId) { public void update(Long janggiId, JanggiGame janggiGame) { storage.put(janggiId, janggiGame); } + + @Override + public Map collectGameStatus() { + return storage.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().status())); + } } diff --git a/src/main/java/repository/JanggiRepository.java b/src/main/java/repository/JanggiRepository.java index 02da1e270e..73ca32f057 100644 --- a/src/main/java/repository/JanggiRepository.java +++ b/src/main/java/repository/JanggiRepository.java @@ -1,6 +1,8 @@ package repository; +import java.util.Map; import java.util.Optional; +import model.GameStatus; import model.JanggiGame; public interface JanggiRepository { @@ -10,4 +12,6 @@ public interface JanggiRepository { Optional findById(Long janggiId); void update(Long janggiId, JanggiGame janggiGame); + + Map collectGameStatus(); } diff --git a/src/main/java/ui/ContinueController.java b/src/main/java/ui/ContinueController.java index 48debd64e4..5d5b5e5135 100644 --- a/src/main/java/ui/ContinueController.java +++ b/src/main/java/ui/ContinueController.java @@ -1,8 +1,37 @@ package ui; -public class ContinueController implements JanggiController { +import static ui.Retrier.retry; + +import application.JanggiService; +import java.util.Map; +import model.GameStatus; +import ui.view.InputView; +import ui.view.OutputView; + +public class ContinueController extends JanggiController { + + protected ContinueController(JanggiService janggiService) { + super(janggiService); + } + @Override public void run() { + Map gameStatusById = janggiService.collectGameStatus(); + if (gameStatusById.isEmpty()) { + OutputView.displayNoGame(); + return; + } + + process(gameStatusById); + } + + private void process(Map gameStatusById) { + Long gameId = retry(() -> InputView.readPlayGameId(gameStatusById), OutputView::displayError); + try { + playGame(gameId); + } catch (IllegalArgumentException e) { + OutputView.displayError(e.getMessage()); + } } } diff --git a/src/main/java/ui/FrontController.java b/src/main/java/ui/FrontController.java index af5b7bf780..e8e67caddf 100644 --- a/src/main/java/ui/FrontController.java +++ b/src/main/java/ui/FrontController.java @@ -17,7 +17,7 @@ public FrontController() { JanggiService janggiService = new JanggiService(new InMemoryJanggiRepository()); controllers = Map.of( - GameMenu.CONTINUE, new ContinueController(), + GameMenu.CONTINUE, new ContinueController(janggiService), GameMenu.NEW_GAME, new NewGameController(janggiService) ); } diff --git a/src/main/java/ui/JanggiController.java b/src/main/java/ui/JanggiController.java index 431a1c38ae..400dcb8463 100644 --- a/src/main/java/ui/JanggiController.java +++ b/src/main/java/ui/JanggiController.java @@ -1,6 +1,71 @@ package ui; -public interface JanggiController { +import static ui.Retrier.retry; - void run(); -} +import application.JanggiResultDto; +import application.JanggiService; +import application.MoveCommand; +import model.Team; +import model.coordinate.Position; +import model.piece.Piece; +import ui.view.InputView; +import ui.view.OutputView; + +public abstract class JanggiController { + + protected final JanggiService janggiService; + + protected JanggiController(JanggiService janggiService) { + this.janggiService = janggiService; + } + + public abstract void run(); + + protected void playGame(Long janggiId) { + OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); + + while (janggiService.canPlaying(janggiId)) { + retry(() -> checkBigJang(janggiId), OutputView::displayError); + play(janggiId); + } + + printResult(janggiId); + } + + private void play(Long janggiId) { + if (janggiService.canPlaying(janggiId)) { + retry(() -> playByTurn(janggiId), OutputView::displayError); + } + OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); + } + + private void playByTurn(Long janggiId) { + Team currentTurn = janggiService.getTurn(janggiId); + + Position current = InputView.readPiecePositionForMove(currentTurn); + Piece piece = janggiService.selectPiece(janggiId, current); + + Position next = InputView.readPiecePositionForArrange(currentTurn, piece); + janggiService.movePiece(janggiId, new MoveCommand(current, next)); + } + + private void checkBigJang(Long janggiId) { + if (!janggiService.isBigJang(janggiId)) { + return; + } + + boolean bigJang = InputView.readBigJangStatus(janggiService.getTurn(janggiId)); + if (bigJang) { + janggiService.finishByBigJang(janggiId); + } + } + + private void printResult(Long janggiId) { + JanggiResultDto gameResult = janggiService.getGameResult(janggiId); + OutputView.displayWinner(gameResult.winner()); + + if (gameResult.bigJangDone()) { + OutputView.displayScore(gameResult.finalScore()); + } + } +} \ No newline at end of file diff --git a/src/main/java/ui/NewGameController.java b/src/main/java/ui/NewGameController.java index 033e50d21c..5b6863ce8c 100644 --- a/src/main/java/ui/NewGameController.java +++ b/src/main/java/ui/NewGameController.java @@ -4,77 +4,22 @@ import static model.Team.HAN; import static ui.Retrier.retry; -import application.JanggiResult; import application.JanggiService; -import application.MoveCommand; -import model.Team; import model.board.TeamFormation; -import model.coordinate.Position; -import model.piece.Piece; import ui.view.InputView; import ui.view.OutputView; -public class NewGameController implements JanggiController { +public class NewGameController extends JanggiController { - private final JanggiService janggiService; - - public NewGameController(JanggiService janggiService) { - this.janggiService = janggiService; + protected NewGameController(JanggiService janggiService) { + super(janggiService); } @Override public void run() { - Long janggiId = createJanggiGame(); - OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); - - while (janggiService.canPlaying(janggiId)) { - retry(() -> checkBigJang(janggiId), OutputView::displayError); - play(janggiId); - } - - printResult(janggiId); - } - - private Long createJanggiGame() { TeamFormation hanFormation = retry(() -> InputView.readFormationByTeam(HAN), OutputView::displayError); TeamFormation choFormation = retry(() -> InputView.readFormationByTeam(CHO), OutputView::displayError); - return janggiService.createJanggiGame(hanFormation, choFormation); - } - - private void play(Long janggiId) { - if (janggiService.canPlaying(janggiId)) { - retry(() -> playByTurn(janggiId), OutputView::displayError); - } - OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); - } - - private void playByTurn(Long janggiId) { - Team currentTurn = janggiService.getTurn(janggiId); - - Position current = InputView.readPiecePositionForMove(currentTurn); - Piece piece = janggiService.selectPiece(janggiId, current); - - Position next = InputView.readPiecePositionForArrange(currentTurn, piece); - janggiService.movePiece(janggiId, new MoveCommand(current, next)); - } - - private void checkBigJang(Long janggiId) { - if (!janggiService.isBigJang(janggiId)) { - return; - } - - boolean bigJang = InputView.readBigJangStatus(janggiService.getTurn(janggiId)); - if (bigJang) { - janggiService.finishByBigJang(janggiId); - } - } - - private void printResult(Long janggiId) { - JanggiResult gameResult = janggiService.getGameResult(janggiId); - OutputView.displayWinner(gameResult.winner()); - - if (gameResult.bigJangDone()) { - OutputView.displayScore(gameResult.finalScore()); - } + Long janggiId = janggiService.createJanggiGame(hanFormation, choFormation); + playGame(janggiId); } } diff --git a/src/main/java/ui/mapper/ViewMapper.java b/src/main/java/ui/mapper/ViewMapper.java index 0d7143d1a2..d772ab633f 100644 --- a/src/main/java/ui/mapper/ViewMapper.java +++ b/src/main/java/ui/mapper/ViewMapper.java @@ -8,6 +8,7 @@ import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; +import model.GameStatus; import model.Team; import model.board.FormationType; import model.piece.PieceType; @@ -24,6 +25,13 @@ public class ViewMapper { SANG_MA_MA_SANG, "상마마상" ); + public static Map GAME_STATUS_DISPLAY_MAPPER = Map.of( + GameStatus.PLAYING, "진행", + GameStatus.BIG_JANG, "진행", + GameStatus.BIG_JANG_DONE, "종료", + GameStatus.DONE, "종료" + ); + static { SYMBOL_MAP.put(PieceType.CHARIOT, Map.of(Team.HAN, "車", Team.CHO, "車")); SYMBOL_MAP.put(PieceType.CANNON, Map.of(Team.HAN, "包", Team.CHO, "包")); diff --git a/src/main/java/ui/parser/InputParser.java b/src/main/java/ui/parser/InputParser.java index 95323dc5e0..697ba99610 100644 --- a/src/main/java/ui/parser/InputParser.java +++ b/src/main/java/ui/parser/InputParser.java @@ -16,6 +16,14 @@ public static int parseNumber(String input) { } } + public static long parseLong(String input) { + try { + return Long.parseLong(input.strip()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("잘못된 입력입니다. 수를 입력해주세요: " + input); + } + } + public static List parseToken(String input, String delimiter) { return Arrays.stream(input.strip() .split(delimiter)) diff --git a/src/main/java/ui/view/InputView.java b/src/main/java/ui/view/InputView.java index 8ce7b1c4d6..15e4e2b011 100644 --- a/src/main/java/ui/view/InputView.java +++ b/src/main/java/ui/view/InputView.java @@ -3,11 +3,15 @@ import static ui.formater.BoardFormatter.formatSymbol; import static ui.mapper.ViewMapper.FORMATION_DISPLAY_MAPPER; import static ui.mapper.ViewMapper.FORMATION_ORDER_MAPPER; +import static ui.mapper.ViewMapper.GAME_STATUS_DISPLAY_MAPPER; +import static ui.parser.InputParser.parseLong; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Scanner; +import model.GameStatus; import model.Team; import model.board.FormationType; import model.board.TeamFormation; @@ -77,4 +81,18 @@ public static GameMenu readGameMenu() { System.out.println("메뉴를 선택해주세요:"); return GameMenu.select(InputParser.parseNumber(SCANNER.nextLine())); } + + public static Long readPlayGameId(Map gameStatusById) { + System.out.println(); + System.out.println("> 이어서 진행할 게임을 선택해주세요."); + gameStatusById.entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> System.out.printf("%d번 게임: %s 상태%n", + entry.getKey(), GAME_STATUS_DISPLAY_MAPPER.get(entry.getValue()))); + + System.out.println(); + System.out.println("번호를 입력해주세요:"); + return parseLong(SCANNER.nextLine()); + } } diff --git a/src/main/java/ui/view/OutputView.java b/src/main/java/ui/view/OutputView.java index 155716e788..4565f15166 100644 --- a/src/main/java/ui/view/OutputView.java +++ b/src/main/java/ui/view/OutputView.java @@ -61,7 +61,8 @@ private static void displayColIndex() { System.out.println(); } - public static void displayGameMenu() { - System.out.println("> 장기 게임 프로그램을 시작합니다."); + public static void displayNoGame() { + System.out.println(); + System.out.println("진행한 게임이 없습니다."); } } \ No newline at end of file From 5ce67f65bdddb4f663edfe24b7d656ddc822cbaa Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 17:32:16 +0900 Subject: [PATCH 24/32] =?UTF-8?q?chore:=20mysql=20docker=20compose=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 --- docker-compose.yml | 14 ++++++++++++++ init/v1_ddl.sql | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 docker-compose.yml create mode 100644 init/v1_ddl.sql diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..7696350216 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + db: + image: mysql:8.4 + container_name: janggi-db + restart: always + environment: + MYSQL_DATABASE: janggi_db + MYSQL_USER: janggi + MYSQL_PASSWORD: janggi1234 + MYSQL_ROOT_PASSWORD: root_password + ports: + - "3309:3306" + volumes: + - ./init:/docker-entrypoint-initdb.d \ No newline at end of file diff --git a/init/v1_ddl.sql b/init/v1_ddl.sql new file mode 100644 index 0000000000..7c6e01fcc6 --- /dev/null +++ b/init/v1_ddl.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS janggi_game ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + status VARCHAR(20) NOT NULL COMMENT '게임 상태', + turn VARCHAR(10) NOT NULL COMMENT '현재 게임 턴', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS piece ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + game_id BIGINT NOT NULL, + `row` INT NOT NULL COMMENT '행 좌표 (0~9)', + `col` INT NOT NULL COMMENT '열 좌표 (0~8)', + `type` VARCHAR(20) NOT NULL COMMENT '기물 종류)', + `team` VARCHAR(20) NOT NULL COMMENT '진영', + FOREIGN KEY (game_id) REFERENCES janggi_game(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE INDEX idx_game_id ON piece(game_id); \ No newline at end of file From 65e0c51d1eda5ef6ad113a19a312bff7301aedc0 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 18:04:49 +0900 Subject: [PATCH 25/32] =?UTF-8?q?feat:=20=EC=9E=A5=EA=B8=B0=20JDBC=20Repos?= =?UTF-8?q?itory=EB=A1=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + src/main/java/Application.java | 12 +- .../java/repository/JdbcJanggiRepository.java | 133 ++++++++++++++++++ src/main/java/repository/JdbcTemplate.java | 73 ++++++++++ src/main/java/ui/FrontController.java | 5 +- 5 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 src/main/java/repository/JdbcJanggiRepository.java create mode 100644 src/main/java/repository/JdbcTemplate.java diff --git a/build.gradle b/build.gradle index ce846f70cc..2ad58f2118 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,8 @@ repositories { } dependencies { + implementation 'com.mysql:mysql-connector-j:8.4.0' + 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/Application.java b/src/main/java/Application.java index e875fb530f..3e92cb84a6 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,8 +1,18 @@ +import application.JanggiService; +import com.mysql.cj.jdbc.MysqlDataSource; +import repository.JdbcJanggiRepository; import ui.FrontController; public class Application { + public static void main(String[] args) { - FrontController frontController = new FrontController(); + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setURL("jdbc:mysql://localhost:3309/janggi_db?serverTimezone=UTC&useSSL=false"); + dataSource.setUser("janggi"); + dataSource.setPassword("janggi1234"); + + JanggiService janggiService = new JanggiService(new JdbcJanggiRepository(dataSource)); + FrontController frontController = new FrontController(janggiService); frontController.run(); } } diff --git a/src/main/java/repository/JdbcJanggiRepository.java b/src/main/java/repository/JdbcJanggiRepository.java new file mode 100644 index 0000000000..5ef349d2d4 --- /dev/null +++ b/src/main/java/repository/JdbcJanggiRepository.java @@ -0,0 +1,133 @@ +package repository; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import javax.sql.DataSource; +import model.GameStatus; +import model.JanggiGame; +import model.JanggiState; +import model.Team; +import model.board.Board; +import model.coordinate.Position; +import model.piece.Cannon; +import model.piece.Chariot; +import model.piece.Elephant; +import model.piece.General; +import model.piece.Guard; +import model.piece.Horse; +import model.piece.Piece; +import model.piece.PieceType; +import model.piece.Soldier; +import model.state.BigJang; +import model.state.BigJangDone; +import model.state.Finished; +import model.state.Running; + +public class JdbcJanggiRepository extends JdbcTemplate implements JanggiRepository { + + public JdbcJanggiRepository(DataSource dataSource) { + super(dataSource); + } + + @Override + public Long save(JanggiGame janggiGame) { + String sql = "INSERT INTO janggi_game (status, turn) VALUES (?, ?)"; + Long gameId = executeInsert(sql, janggiGame.status().name(), janggiGame.turn().name()); + + savePieces(gameId, janggiGame.getBoard()); + return gameId; + } + + private void savePieces(Long gameId, Map board) { + String sql = "INSERT INTO piece (game_id, `row`, col, `type`, team) VALUES (?, ?, ?, ?, ?)"; + for (Map.Entry entry : board.entrySet()) { + Position pos = entry.getKey(); + Piece piece = entry.getValue(); + executeUpdate(sql, gameId, pos.row(), pos.col(), piece.type().name(), piece.team().name()); + } + } + + @Override + public Optional findById(Long janggiId) { + String gameSql = "SELECT * FROM janggi_game WHERE id = ?"; + return executeQuery(gameSql, rs -> { + if (!rs.next()) { + return Optional.empty(); + } + + Board board = fetchBoard(janggiId); + JanggiGame janggiGame = new JanggiGame(board); + try { + Field stateField = JanggiGame.class.getDeclaredField("state"); + stateField.setAccessible(true); + + GameStatus status = GameStatus.valueOf(rs.getString("status")); + Team turn = Team.valueOf(rs.getString("turn")); + JanggiState state = createState(status, turn); + stateField.set(janggiGame, state); + + return Optional.of(janggiGame); + } catch (Exception e) { + throw new RuntimeException("[ERROR] 복원 중 리플렉션 오류 발생", e); + } + }, janggiId); + } + + private Board fetchBoard(Long gameId) { + String pieceSql = "SELECT * FROM piece WHERE game_id = ?"; + return executeQuery(pieceSql, rs -> { + Map pieces = new HashMap<>(); + while (rs.next()) { + Piece piece = createPiece(rs.getString("type"), rs.getString("team")); + Position position = new Position(rs.getInt("row"), rs.getInt("col")); + pieces.put(position, piece); + } + return new Board(pieces); + }, gameId); + } + + @Override + public void update(Long janggiId, JanggiGame janggiGame) { + String updateGameSql = "UPDATE janggi_game SET status = ?, turn = ? WHERE id = ?"; + executeUpdate(updateGameSql, janggiGame.status().name(), janggiGame.turn().name(), janggiId); + + executeUpdate("DELETE FROM piece WHERE game_id = ?", janggiId); + savePieces(janggiId, janggiGame.getBoard()); + } + + @Override + public Map collectGameStatus() { + String sql = "SELECT id, status FROM janggi_game"; + return executeQuery(sql, rs -> { + Map statusMap = new HashMap<>(); + while (rs.next()) { + statusMap.put(rs.getLong("id"), GameStatus.valueOf(rs.getString("status"))); + } + return statusMap; + }); + } + + private JanggiState createState(GameStatus gameStatus, Team turn) { + return switch (gameStatus) { + case GameStatus.PLAYING -> new Running(turn); + case GameStatus.BIG_JANG -> new BigJang(turn); + case GameStatus.DONE -> new Finished(turn); + case GameStatus.BIG_JANG_DONE -> new BigJangDone(turn); + }; + } + + private Piece createPiece(String type, String teamName) { + Team team = Team.valueOf(teamName); + return switch (PieceType.valueOf(type)) { + case CHARIOT -> new Chariot(team); + case CANNON -> new Cannon(team); + case HORSE -> new Horse(team); + case ELEPHANT -> new Elephant(team); + case SOLDIER -> new Soldier(team); + case GUARD -> new Guard(team); + case GENERAL -> new General(team); + }; + } +} \ No newline at end of file diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java new file mode 100644 index 0000000000..8b31c53f29 --- /dev/null +++ b/src/main/java/repository/JdbcTemplate.java @@ -0,0 +1,73 @@ +package repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import javax.sql.DataSource; + +public abstract class JdbcTemplate { + + private final DataSource dataSource; + + protected JdbcTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public Long executeInsert(String sql, Object... parameters) { + try ( + Connection conn = dataSource.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) + ) { + setParameters(pstmt, parameters); + pstmt.executeUpdate(); + + try (ResultSet rs = pstmt.getGeneratedKeys()) { + if (rs.next()) { + return rs.getLong(1); + } + throw new SQLException("[ERROR] ID를 생성할 수 없습니다."); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void executeUpdate(String sql, Object... parameters) { + try ( + Connection conn = dataSource.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql) + ) { + setParameters(pstmt, parameters); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public T executeQuery(String sql, RowMapper mapper, Object... parameters) { + try ( + Connection conn = dataSource.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql) + ) { + setParameters(pstmt, parameters); + try (ResultSet rs = pstmt.executeQuery()) { + return mapper.map(rs); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private void setParameters(PreparedStatement pstmt, Object[] parameters) throws SQLException { + for (int i = 0; i < parameters.length; i++) { + pstmt.setObject(i + 1, parameters[i]); + } + } + + @FunctionalInterface + public interface RowMapper { + T map(ResultSet rs) throws SQLException; + } +} \ No newline at end of file diff --git a/src/main/java/ui/FrontController.java b/src/main/java/ui/FrontController.java index e8e67caddf..21e5f4cbad 100644 --- a/src/main/java/ui/FrontController.java +++ b/src/main/java/ui/FrontController.java @@ -5,7 +5,6 @@ import application.JanggiService; import java.util.Map; import java.util.Optional; -import repository.InMemoryJanggiRepository; import ui.view.InputView; import ui.view.OutputView; @@ -13,9 +12,7 @@ public class FrontController { private final Map controllers; - public FrontController() { - JanggiService janggiService = new JanggiService(new InMemoryJanggiRepository()); - + public FrontController(JanggiService janggiService) { controllers = Map.of( GameMenu.CONTINUE, new ContinueController(janggiService), GameMenu.NEW_GAME, new NewGameController(janggiService) From cb9865e403a46ed2850803131cbccc4a10da1eb1 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 21:40:41 +0900 Subject: [PATCH 26/32] =?UTF-8?q?refactor:=20jdbc=20repository=20type=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/repository/JdbcJanggiRepository.java | 101 ++++++------------ src/main/java/repository/JdbcTemplate.java | 10 +- .../java/repository/mapper/JanggiMapper.java | 47 ++++++++ 3 files changed, 87 insertions(+), 71 deletions(-) create mode 100644 src/main/java/repository/mapper/JanggiMapper.java diff --git a/src/main/java/repository/JdbcJanggiRepository.java b/src/main/java/repository/JdbcJanggiRepository.java index 5ef349d2d4..f36a5fc4ac 100644 --- a/src/main/java/repository/JdbcJanggiRepository.java +++ b/src/main/java/repository/JdbcJanggiRepository.java @@ -1,6 +1,7 @@ package repository; import java.lang.reflect.Field; +import java.sql.ResultSet; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -8,22 +9,10 @@ import model.GameStatus; import model.JanggiGame; import model.JanggiState; -import model.Team; import model.board.Board; import model.coordinate.Position; -import model.piece.Cannon; -import model.piece.Chariot; -import model.piece.Elephant; -import model.piece.General; -import model.piece.Guard; -import model.piece.Horse; import model.piece.Piece; -import model.piece.PieceType; -import model.piece.Soldier; -import model.state.BigJang; -import model.state.BigJangDone; -import model.state.Finished; -import model.state.Running; +import repository.mapper.JanggiMapper; public class JdbcJanggiRepository extends JdbcTemplate implements JanggiRepository { @@ -40,15 +29,6 @@ public Long save(JanggiGame janggiGame) { return gameId; } - private void savePieces(Long gameId, Map board) { - String sql = "INSERT INTO piece (game_id, `row`, col, `type`, team) VALUES (?, ?, ?, ?, ?)"; - for (Map.Entry entry : board.entrySet()) { - Position pos = entry.getKey(); - Piece piece = entry.getValue(); - executeUpdate(sql, gameId, pos.row(), pos.col(), piece.type().name(), piece.team().name()); - } - } - @Override public Optional findById(Long janggiId) { String gameSql = "SELECT * FROM janggi_game WHERE id = ?"; @@ -59,35 +39,10 @@ public Optional findById(Long janggiId) { Board board = fetchBoard(janggiId); JanggiGame janggiGame = new JanggiGame(board); - try { - Field stateField = JanggiGame.class.getDeclaredField("state"); - stateField.setAccessible(true); - - GameStatus status = GameStatus.valueOf(rs.getString("status")); - Team turn = Team.valueOf(rs.getString("turn")); - JanggiState state = createState(status, turn); - stateField.set(janggiGame, state); - - return Optional.of(janggiGame); - } catch (Exception e) { - throw new RuntimeException("[ERROR] 복원 중 리플렉션 오류 발생", e); - } + return resolveJanggiGame(rs, janggiGame); }, janggiId); } - private Board fetchBoard(Long gameId) { - String pieceSql = "SELECT * FROM piece WHERE game_id = ?"; - return executeQuery(pieceSql, rs -> { - Map pieces = new HashMap<>(); - while (rs.next()) { - Piece piece = createPiece(rs.getString("type"), rs.getString("team")); - Position position = new Position(rs.getInt("row"), rs.getInt("col")); - pieces.put(position, piece); - } - return new Board(pieces); - }, gameId); - } - @Override public void update(Long janggiId, JanggiGame janggiGame) { String updateGameSql = "UPDATE janggi_game SET status = ?, turn = ? WHERE id = ?"; @@ -109,25 +64,39 @@ public Map collectGameStatus() { }); } - private JanggiState createState(GameStatus gameStatus, Team turn) { - return switch (gameStatus) { - case GameStatus.PLAYING -> new Running(turn); - case GameStatus.BIG_JANG -> new BigJang(turn); - case GameStatus.DONE -> new Finished(turn); - case GameStatus.BIG_JANG_DONE -> new BigJangDone(turn); - }; + private void savePieces(Long gameId, Map board) { + String sql = "INSERT INTO piece (game_id, `row`, col, `type`, team) VALUES (?, ?, ?, ?, ?)"; + for (Map.Entry entry : board.entrySet()) { + Position pos = entry.getKey(); + Piece piece = entry.getValue(); + executeUpdate(sql, gameId, pos.row(), pos.col(), piece.type().name(), piece.team().name()); + } } - private Piece createPiece(String type, String teamName) { - Team team = Team.valueOf(teamName); - return switch (PieceType.valueOf(type)) { - case CHARIOT -> new Chariot(team); - case CANNON -> new Cannon(team); - case HORSE -> new Horse(team); - case ELEPHANT -> new Elephant(team); - case SOLDIER -> new Soldier(team); - case GUARD -> new Guard(team); - case GENERAL -> new General(team); - }; + private Board fetchBoard(Long gameId) { + String pieceSql = "SELECT * FROM piece WHERE game_id = ?"; + return executeQuery(pieceSql, rs -> { + Map pieces = new HashMap<>(); + while (rs.next()) { + Piece piece = JanggiMapper.createPiece(rs.getString("type"), rs.getString("team")); + Position position = new Position(rs.getInt("row"), rs.getInt("col")); + pieces.put(position, piece); + } + return new Board(pieces); + }, gameId); + } + + private Optional resolveJanggiGame(ResultSet rs, JanggiGame janggiGame) { + try { + JanggiState state = JanggiMapper.createState(rs.getString("status"), rs.getString("turn")); + + Field stateField = JanggiGame.class.getDeclaredField("state"); + stateField.setAccessible(true); + stateField.set(janggiGame, state); + + return Optional.of(janggiGame); + } catch (Exception e) { + throw new RuntimeException("[ERROR] 복원 중 리플렉션 오류 발생", e); + } } } \ No newline at end of file diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java index 8b31c53f29..30052aacb1 100644 --- a/src/main/java/repository/JdbcTemplate.java +++ b/src/main/java/repository/JdbcTemplate.java @@ -49,10 +49,10 @@ public void executeUpdate(String sql, Object... parameters) { public T executeQuery(String sql, RowMapper mapper, Object... parameters) { try ( Connection conn = dataSource.getConnection(); - PreparedStatement pstmt = conn.prepareStatement(sql) + PreparedStatement statement = conn.prepareStatement(sql) ) { - setParameters(pstmt, parameters); - try (ResultSet rs = pstmt.executeQuery()) { + setParameters(statement, parameters); + try (ResultSet rs = statement.executeQuery()) { return mapper.map(rs); } } catch (SQLException e) { @@ -60,9 +60,9 @@ public T executeQuery(String sql, RowMapper mapper, Object... parameters) } } - private void setParameters(PreparedStatement pstmt, Object[] parameters) throws SQLException { + private void setParameters(PreparedStatement statement, Object[] parameters) throws SQLException { for (int i = 0; i < parameters.length; i++) { - pstmt.setObject(i + 1, parameters[i]); + statement.setObject(i + 1, parameters[i]); } } diff --git a/src/main/java/repository/mapper/JanggiMapper.java b/src/main/java/repository/mapper/JanggiMapper.java new file mode 100644 index 0000000000..a1719befbf --- /dev/null +++ b/src/main/java/repository/mapper/JanggiMapper.java @@ -0,0 +1,47 @@ +package repository.mapper; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.piece.Cannon; +import model.piece.Chariot; +import model.piece.Elephant; +import model.piece.General; +import model.piece.Guard; +import model.piece.Horse; +import model.piece.Piece; +import model.piece.PieceType; +import model.piece.Soldier; +import model.state.BigJang; +import model.state.BigJangDone; +import model.state.Finished; +import model.state.Running; + +public class JanggiMapper { + + private JanggiMapper() { + } + + public static Piece createPiece(String type, String teamName) { + Team team = Team.valueOf(teamName); + return switch (PieceType.valueOf(type)) { + case CHARIOT -> new Chariot(team); + case CANNON -> new Cannon(team); + case HORSE -> new Horse(team); + case ELEPHANT -> new Elephant(team); + case SOLDIER -> new Soldier(team); + case GUARD -> new Guard(team); + case GENERAL -> new General(team); + }; + } + + public static JanggiState createState(String statusName, String turnName) { + Team turn = Team.valueOf(turnName); + return switch (GameStatus.valueOf(statusName)) { + case GameStatus.PLAYING -> new Running(turn); + case GameStatus.BIG_JANG -> new BigJang(turn); + case GameStatus.DONE -> new Finished(turn); + case GameStatus.BIG_JANG_DONE -> new BigJangDone(turn); + }; + } +} From 8abfc7cd5b4ccb463c848d88fc08a866f98e77fd Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 22:55:28 +0900 Subject: [PATCH 27/32] =?UTF-8?q?refactor:=20=ED=84=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20&=20=EA=B8=B0=EB=AC=BC=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=9B=90=EC=9E=90=EC=84=B1=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Application.java | 16 ++-- src/main/java/application/JanggiService.java | 66 +++----------- .../java/application/JanggiServiceImpl.java | 75 ++++++++++++++++ .../java/application/JanggiTxService.java | 70 +++++++++++++++ src/main/java/repository/JdbcTemplate.java | 87 +++++++++++++------ .../java/repository/db/ConnectionManager.java | 28 ++++++ .../java/repository/db/DataSourceManager.java | 22 +++++ .../repository/db/TransactionTemplate.java | 57 ++++++++++++ 8 files changed, 334 insertions(+), 87 deletions(-) create mode 100644 src/main/java/application/JanggiServiceImpl.java create mode 100644 src/main/java/application/JanggiTxService.java create mode 100644 src/main/java/repository/db/ConnectionManager.java create mode 100644 src/main/java/repository/db/DataSourceManager.java create mode 100644 src/main/java/repository/db/TransactionTemplate.java diff --git a/src/main/java/Application.java b/src/main/java/Application.java index 3e92cb84a6..5f693e14cf 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,17 +1,21 @@ import application.JanggiService; -import com.mysql.cj.jdbc.MysqlDataSource; +import application.JanggiServiceImpl; +import application.JanggiTxService; +import javax.sql.DataSource; import repository.JdbcJanggiRepository; +import repository.db.DataSourceManager; +import repository.db.TransactionTemplate; import ui.FrontController; public class Application { public static void main(String[] args) { - MysqlDataSource dataSource = new MysqlDataSource(); - dataSource.setURL("jdbc:mysql://localhost:3309/janggi_db?serverTimezone=UTC&useSSL=false"); - dataSource.setUser("janggi"); - dataSource.setPassword("janggi1234"); + DataSource dataSource = DataSourceManager.getDataSource(); + TransactionTemplate transactionTemplate = new TransactionTemplate(dataSource); + + JanggiServiceImpl janggiServiceImpl = new JanggiServiceImpl(new JdbcJanggiRepository(dataSource)); + JanggiService janggiService = new JanggiTxService(transactionTemplate, janggiServiceImpl); - JanggiService janggiService = new JanggiService(new JdbcJanggiRepository(dataSource)); FrontController frontController = new FrontController(janggiService); frontController.run(); } diff --git a/src/main/java/application/JanggiService.java b/src/main/java/application/JanggiService.java index 9a3c06b4ba..9e0a3ebf91 100644 --- a/src/main/java/application/JanggiService.java +++ b/src/main/java/application/JanggiService.java @@ -2,74 +2,30 @@ import java.util.Map; import model.GameStatus; -import model.JanggiGame; import model.Team; -import model.board.Board; -import model.board.BoardFactory; import model.board.TeamFormation; import model.coordinate.Position; import model.piece.Piece; -import repository.JanggiRepository; -public class JanggiService { - private final JanggiRepository janggiRepository; +public interface JanggiService { - public JanggiService(JanggiRepository janggiRepository) { - this.janggiRepository = janggiRepository; - } + Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation); - public Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation) { - Board board = BoardFactory.generateDefaultPieces(); - board.arrangePieces(hanFormation.generate()); - board.arrangePieces(choFormation.generate()); + void finishByBigJang(Long janggiId); - JanggiGame janggiGame = new JanggiGame(board); - return janggiRepository.save(janggiGame); - } + void movePiece(Long janggiId, MoveCommand command); - public void finishByBigJang(Long janggiId) { - JanggiGame janggiGame = getGame(janggiId); - janggiGame.finishByBigJang(); - janggiRepository.update(janggiId, janggiGame); - } + JanggiResultDto getGameResult(Long janggiId); - public void movePiece(Long janggiId, MoveCommand command) { - JanggiGame janggiGame = getGame(janggiId); - janggiGame.movePiece(command.current(), command.next()); - janggiRepository.update(janggiId, janggiGame); - } + Map getBoardResponse(Long janggiId); - public JanggiResultDto getGameResult(Long janggiId) { - JanggiGame janggiGame = getGame(janggiId); - return JanggiResultDto.from(janggiGame); - } + boolean canPlaying(Long janggiId); - public Map getBoardResponse(Long janggiId) { - return getGame(janggiId).getBoard(); - } + Team getTurn(Long janggiId); - public boolean canPlaying(Long janggiId) { - return getGame(janggiId).canPlaying(); - } + Piece selectPiece(Long janggiId, Position current); - public Team getTurn(Long janggiId) { - return getGame(janggiId).turn(); - } + boolean isBigJang(Long janggiId); - public Piece selectPiece(Long janggiId, Position current) { - return getGame(janggiId).selectPiece(current); - } - - public boolean isBigJang(Long janggiId) { - return getGame(janggiId).isBigJang(); - } - - private JanggiGame getGame(Long janggiId) { - return janggiRepository.findById(janggiId) - .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); - } - - public Map collectGameStatus() { - return janggiRepository.collectGameStatus(); - } + Map collectGameStatus(); } diff --git a/src/main/java/application/JanggiServiceImpl.java b/src/main/java/application/JanggiServiceImpl.java new file mode 100644 index 0000000000..98328135fb --- /dev/null +++ b/src/main/java/application/JanggiServiceImpl.java @@ -0,0 +1,75 @@ +package application; + +import java.util.Map; +import model.GameStatus; +import model.JanggiGame; +import model.Team; +import model.board.Board; +import model.board.BoardFactory; +import model.board.TeamFormation; +import model.coordinate.Position; +import model.piece.Piece; +import repository.JanggiRepository; + +public class JanggiServiceImpl implements JanggiService { + private final JanggiRepository janggiRepository; + + public JanggiServiceImpl(JanggiRepository janggiRepository) { + this.janggiRepository = janggiRepository; + } + + public Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation) { + Board board = BoardFactory.generateDefaultPieces(); + board.arrangePieces(hanFormation.generate()); + board.arrangePieces(choFormation.generate()); + + JanggiGame janggiGame = new JanggiGame(board); + return janggiRepository.save(janggiGame); + } + + public void finishByBigJang(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + janggiGame.finishByBigJang(); + janggiRepository.update(janggiId, janggiGame); + } + + public void movePiece(Long janggiId, MoveCommand command) { + JanggiGame janggiGame = getGame(janggiId); + janggiGame.movePiece(command.current(), command.next()); + janggiRepository.update(janggiId, janggiGame); + } + + public JanggiResultDto getGameResult(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + return JanggiResultDto.from(janggiGame); + } + + public Map getBoardResponse(Long janggiId) { + return getGame(janggiId).getBoard(); + } + + public boolean canPlaying(Long janggiId) { + return getGame(janggiId).canPlaying(); + } + + public Team getTurn(Long janggiId) { + return getGame(janggiId).turn(); + } + + public Piece selectPiece(Long janggiId, Position current) { + return getGame(janggiId).selectPiece(current); + } + + public boolean isBigJang(Long janggiId) { + return getGame(janggiId).isBigJang(); + } + + public Map collectGameStatus() { + return janggiRepository.collectGameStatus(); + } + + private JanggiGame getGame(Long janggiId) { + return janggiRepository.findById(janggiId) + .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/application/JanggiTxService.java b/src/main/java/application/JanggiTxService.java new file mode 100644 index 0000000000..c8c4d160e8 --- /dev/null +++ b/src/main/java/application/JanggiTxService.java @@ -0,0 +1,70 @@ +package application; + +import java.util.Map; +import model.GameStatus; +import model.Team; +import model.board.TeamFormation; +import model.coordinate.Position; +import model.piece.Piece; +import repository.db.TransactionTemplate; + +public class JanggiTxService implements JanggiService { + + private final TransactionTemplate transactionTemplate; + private final JanggiService janggiService; + + public JanggiTxService(TransactionTemplate transactionTemplate, JanggiService janggiService) { + this.transactionTemplate = transactionTemplate; + this.janggiService = janggiService; + } + + @Override + public Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation) { + return transactionTemplate.execute(() -> janggiService.createJanggiGame(hanFormation, choFormation)); + } + + @Override + public void finishByBigJang(Long janggiId) { + transactionTemplate.execute(() -> janggiService.finishByBigJang(janggiId)); + } + + @Override + public void movePiece(Long janggiId, MoveCommand command) { + transactionTemplate.execute(() -> janggiService.movePiece(janggiId, command)); + } + + @Override + public JanggiResultDto getGameResult(Long janggiId) { + return janggiService.getGameResult(janggiId); + } + + @Override + public Map getBoardResponse(Long janggiId) { + return janggiService.getBoardResponse(janggiId); + } + + @Override + public boolean canPlaying(Long janggiId) { + return janggiService.canPlaying(janggiId); + } + + @Override + public Team getTurn(Long janggiId) { + return janggiService.getTurn(janggiId); + } + + @Override + public Piece selectPiece(Long janggiId, Position current) { + return janggiService.selectPiece(janggiId, current); + } + + @Override + public boolean isBigJang(Long janggiId) { + return janggiService.isBigJang(janggiId); + } + + @Override + public Map collectGameStatus() { + return janggiService.collectGameStatus(); + } +} diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java index 30052aacb1..8d0fc35b6d 100644 --- a/src/main/java/repository/JdbcTemplate.java +++ b/src/main/java/repository/JdbcTemplate.java @@ -6,6 +6,7 @@ import java.sql.SQLException; import java.sql.Statement; import javax.sql.DataSource; +import repository.db.ConnectionManager; public abstract class JdbcTemplate { @@ -16,51 +17,85 @@ protected JdbcTemplate(DataSource dataSource) { } public Long executeInsert(String sql, Object... parameters) { - try ( - Connection conn = dataSource.getConnection(); - PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) - ) { - setParameters(pstmt, parameters); - pstmt.executeUpdate(); - - try (ResultSet rs = pstmt.getGeneratedKeys()) { - if (rs.next()) { - return rs.getLong(1); - } - throw new SQLException("[ERROR] ID를 생성할 수 없습니다."); + Connection conn = null; + try { + conn = getConnection(); + try (PreparedStatement statement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + setParameters(statement, parameters); + statement.executeUpdate(); + return extractGeneratedKey(statement); } } catch (SQLException e) { throw new RuntimeException(e); + } finally { + closeConnection(conn); } } public void executeUpdate(String sql, Object... parameters) { - try ( - Connection conn = dataSource.getConnection(); - PreparedStatement pstmt = conn.prepareStatement(sql) - ) { - setParameters(pstmt, parameters); - pstmt.executeUpdate(); + Connection conn = null; + try { + conn = getConnection(); + try (PreparedStatement statement = conn.prepareStatement(sql)) { + setParameters(statement, parameters); + statement.executeUpdate(); + } } catch (SQLException e) { throw new RuntimeException(e); + } finally { + closeConnection(conn); } } public T executeQuery(String sql, RowMapper mapper, Object... parameters) { - try ( - Connection conn = dataSource.getConnection(); - PreparedStatement statement = conn.prepareStatement(sql) - ) { - setParameters(statement, parameters); - try (ResultSet rs = statement.executeQuery()) { - return mapper.map(rs); + Connection conn = null; + try { + conn = getConnection(); + try (PreparedStatement statement = conn.prepareStatement(sql)) { + setParameters(statement, parameters); + try (ResultSet rs = statement.executeQuery()) { + return mapper.map(rs); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + closeConnection(conn); + } + } + + private long extractGeneratedKey(PreparedStatement statement) throws SQLException { + try (ResultSet rs = statement.getGeneratedKeys()) { + if (rs.next()) { + return rs.getLong(1); + } + throw new SQLException("[ERROR] ID를 생성할 수 없습니다."); + } + } + + private Connection getConnection() { + return ConnectionManager.get().orElseGet(() -> { + try { + return dataSource.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); } + }); + } + + private void closeConnection(Connection conn) { + if (ConnectionManager.isPresent() || conn == null) { + return; + } + + try { + conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } - private void setParameters(PreparedStatement statement, Object[] parameters) throws SQLException { + private void setParameters(PreparedStatement statement, Object... parameters) throws SQLException { for (int i = 0; i < parameters.length; i++) { statement.setObject(i + 1, parameters[i]); } diff --git a/src/main/java/repository/db/ConnectionManager.java b/src/main/java/repository/db/ConnectionManager.java new file mode 100644 index 0000000000..b6887ae1ea --- /dev/null +++ b/src/main/java/repository/db/ConnectionManager.java @@ -0,0 +1,28 @@ +package repository.db; + +import java.sql.Connection; +import java.util.Optional; + +public class ConnectionManager { + + private static final ThreadLocal THREAD_LOCAL_CONNECTION = new ThreadLocal<>(); + + private ConnectionManager() { + } + + public static void set(Connection connection) { + THREAD_LOCAL_CONNECTION.set(connection); + } + + public static Optional get() { + return Optional.ofNullable(THREAD_LOCAL_CONNECTION.get()); + } + + public static boolean isPresent() { + return get().isPresent(); + } + + public static void remove() { + THREAD_LOCAL_CONNECTION.remove(); + } +} \ No newline at end of file diff --git a/src/main/java/repository/db/DataSourceManager.java b/src/main/java/repository/db/DataSourceManager.java new file mode 100644 index 0000000000..cf2cbad25a --- /dev/null +++ b/src/main/java/repository/db/DataSourceManager.java @@ -0,0 +1,22 @@ +package repository.db; + +import com.mysql.cj.jdbc.MysqlDataSource; +import javax.sql.DataSource; + +public class DataSourceManager { + + private static final String URL = "jdbc:mysql://localhost:3309/janggi_db?serverTimezone=UTC&useSSL=false"; + private static final String USER = "janggi"; + private static final String PASSWORD = "janggi1234"; + + private DataSourceManager() { + } + + public static DataSource getDataSource() { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setURL(URL); + dataSource.setUser(USER); + dataSource.setPassword(PASSWORD); + return dataSource; + } +} diff --git a/src/main/java/repository/db/TransactionTemplate.java b/src/main/java/repository/db/TransactionTemplate.java new file mode 100644 index 0000000000..bb3c7666b3 --- /dev/null +++ b/src/main/java/repository/db/TransactionTemplate.java @@ -0,0 +1,57 @@ +package repository.db; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +public class TransactionTemplate { + private final DataSource dataSource; + + public TransactionTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(TransactionCallback callback) { + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(false); + ConnectionManager.set(conn); + + try { + T result = callback.execute(); + conn.commit(); + return result; + } catch (Exception e) { + conn.rollback(); + throw convertToRuntimeException(e); + } finally { + ConnectionManager.remove(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void execute(TransactionRunnable runnable) { + execute(() -> { + runnable.execute(); + return null; + }); + } + + private RuntimeException convertToRuntimeException(Exception e) { + if (e instanceof RuntimeException) { + return (RuntimeException) e; + } + return new RuntimeException(e); + } + + @FunctionalInterface + public interface TransactionCallback { + T execute() throws Exception; + } + + @FunctionalInterface + public interface TransactionRunnable { + void execute() throws Exception; + } +} \ No newline at end of file From a8ca96b18d2fd5718c486e880303f4904fcb21ba Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 23:09:43 +0900 Subject: [PATCH 28/32] =?UTF-8?q?refactor:=20=EC=9E=A5=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=AC=BC=20=EC=9D=B4=EB=8F=99=20=EC=8B=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EB=B2=8C=ED=81=AC=20=EC=97=B0?= =?UTF-8?q?=EC=82=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/model/JanggiGame.java | 6 ++- .../java/repository/JdbcJanggiRepository.java | 48 ++++++++----------- src/main/java/repository/JdbcTemplate.java | 19 ++++++++ .../java/repository/db/DataSourceManager.java | 2 +- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index a88236ee7e..04e28fc7a1 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -13,8 +13,12 @@ public class JanggiGame { private JanggiState state; public JanggiGame(Board board) { + this(board, new Running(Team.startTurn())); + } + + public JanggiGame(Board board, JanggiState state) { this.board = board; - this.state = new Running(Team.startTurn()); + this.state = state; } public void movePiece(Position current, Position next) { diff --git a/src/main/java/repository/JdbcJanggiRepository.java b/src/main/java/repository/JdbcJanggiRepository.java index f36a5fc4ac..675e5a858b 100644 --- a/src/main/java/repository/JdbcJanggiRepository.java +++ b/src/main/java/repository/JdbcJanggiRepository.java @@ -1,8 +1,7 @@ package repository; -import java.lang.reflect.Field; -import java.sql.ResultSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import javax.sql.DataSource; @@ -31,22 +30,22 @@ public Long save(JanggiGame janggiGame) { @Override public Optional findById(Long janggiId) { - String gameSql = "SELECT * FROM janggi_game WHERE id = ?"; - return executeQuery(gameSql, rs -> { + String sql = "SELECT * FROM janggi_game WHERE id = ?"; + return executeQuery(sql, rs -> { if (!rs.next()) { return Optional.empty(); } Board board = fetchBoard(janggiId); - JanggiGame janggiGame = new JanggiGame(board); - return resolveJanggiGame(rs, janggiGame); + JanggiState state = JanggiMapper.createState(rs.getString("status"), rs.getString("turn")); + return Optional.of(new JanggiGame(board, state)); }, janggiId); } @Override public void update(Long janggiId, JanggiGame janggiGame) { - String updateGameSql = "UPDATE janggi_game SET status = ?, turn = ? WHERE id = ?"; - executeUpdate(updateGameSql, janggiGame.status().name(), janggiGame.turn().name(), janggiId); + String sql = "UPDATE janggi_game SET status = ?, turn = ? WHERE id = ?"; + executeUpdate(sql, janggiGame.status().name(), janggiGame.turn().name(), janggiId); executeUpdate("DELETE FROM piece WHERE game_id = ?", janggiId); savePieces(janggiId, janggiGame.getBoard()); @@ -66,16 +65,21 @@ public Map collectGameStatus() { private void savePieces(Long gameId, Map board) { String sql = "INSERT INTO piece (game_id, `row`, col, `type`, team) VALUES (?, ?, ?, ?, ?)"; - for (Map.Entry entry : board.entrySet()) { - Position pos = entry.getKey(); - Piece piece = entry.getValue(); - executeUpdate(sql, gameId, pos.row(), pos.col(), piece.type().name(), piece.team().name()); - } + + List parameters = board.entrySet().stream() + .map(entry -> { + Position pos = entry.getKey(); + Piece piece = entry.getValue(); + return new Object[]{gameId, pos.row(), pos.col(), piece.type().name(), piece.team().name()}; + }) + .toList(); + + executeBatch(sql, parameters); } private Board fetchBoard(Long gameId) { - String pieceSql = "SELECT * FROM piece WHERE game_id = ?"; - return executeQuery(pieceSql, rs -> { + String sql = "SELECT * FROM piece WHERE game_id = ?"; + return executeQuery(sql, rs -> { Map pieces = new HashMap<>(); while (rs.next()) { Piece piece = JanggiMapper.createPiece(rs.getString("type"), rs.getString("team")); @@ -85,18 +89,4 @@ private Board fetchBoard(Long gameId) { return new Board(pieces); }, gameId); } - - private Optional resolveJanggiGame(ResultSet rs, JanggiGame janggiGame) { - try { - JanggiState state = JanggiMapper.createState(rs.getString("status"), rs.getString("turn")); - - Field stateField = JanggiGame.class.getDeclaredField("state"); - stateField.setAccessible(true); - stateField.set(janggiGame, state); - - return Optional.of(janggiGame); - } catch (Exception e) { - throw new RuntimeException("[ERROR] 복원 중 리플렉션 오류 발생", e); - } - } } \ No newline at end of file diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java index 8d0fc35b6d..06f81ebb90 100644 --- a/src/main/java/repository/JdbcTemplate.java +++ b/src/main/java/repository/JdbcTemplate.java @@ -5,6 +5,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.List; import javax.sql.DataSource; import repository.db.ConnectionManager; @@ -64,6 +65,24 @@ public T executeQuery(String sql, RowMapper mapper, Object... parameters) } } + public void executeBatch(String sql, List parameters) { + Connection conn = null; + try { + conn = getConnection(); + try (PreparedStatement statement = conn.prepareStatement(sql)) { + for (Object[] params : parameters) { + setParameters(statement, params); + statement.addBatch(); + } + statement.executeBatch(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + closeConnection(conn); + } + } + private long extractGeneratedKey(PreparedStatement statement) throws SQLException { try (ResultSet rs = statement.getGeneratedKeys()) { if (rs.next()) { diff --git a/src/main/java/repository/db/DataSourceManager.java b/src/main/java/repository/db/DataSourceManager.java index cf2cbad25a..e66596d003 100644 --- a/src/main/java/repository/db/DataSourceManager.java +++ b/src/main/java/repository/db/DataSourceManager.java @@ -5,7 +5,7 @@ public class DataSourceManager { - private static final String URL = "jdbc:mysql://localhost:3309/janggi_db?serverTimezone=UTC&useSSL=false"; + private static final String URL = "jdbc:mysql://localhost:3309/janggi_db?serverTimezone=UTC&useSSL=false&rewriteBatchedStatements=true"; private static final String USER = "janggi"; private static final String PASSWORD = "janggi1234"; From b5502ea9d33fee678d8ef7766c574eae8ca8fca3 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 23:17:24 +0900 Subject: [PATCH 29/32] =?UTF-8?q?refactor:=20jdbcTemplate=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/repository/JdbcTemplate.java | 49 ++++++++++------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java index 06f81ebb90..4137aa9b5f 100644 --- a/src/main/java/repository/JdbcTemplate.java +++ b/src/main/java/repository/JdbcTemplate.java @@ -18,64 +18,54 @@ protected JdbcTemplate(DataSource dataSource) { } public Long executeInsert(String sql, Object... parameters) { - Connection conn = null; - try { - conn = getConnection(); + return execute(conn -> { try (PreparedStatement statement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { setParameters(statement, parameters); statement.executeUpdate(); return extractGeneratedKey(statement); } - } catch (SQLException e) { - throw new RuntimeException(e); - } finally { - closeConnection(conn); - } + }); } public void executeUpdate(String sql, Object... parameters) { - Connection conn = null; - try { - conn = getConnection(); + execute(conn -> { try (PreparedStatement statement = conn.prepareStatement(sql)) { setParameters(statement, parameters); statement.executeUpdate(); + return null; } - } catch (SQLException e) { - throw new RuntimeException(e); - } finally { - closeConnection(conn); - } + }); } public T executeQuery(String sql, RowMapper mapper, Object... parameters) { - Connection conn = null; - try { - conn = getConnection(); + return execute(conn -> { try (PreparedStatement statement = conn.prepareStatement(sql)) { setParameters(statement, parameters); try (ResultSet rs = statement.executeQuery()) { return mapper.map(rs); } } - } catch (SQLException e) { - throw new RuntimeException(e); - } finally { - closeConnection(conn); - } + }); } public void executeBatch(String sql, List parameters) { - Connection conn = null; - try { - conn = getConnection(); + execute(conn -> { try (PreparedStatement statement = conn.prepareStatement(sql)) { for (Object[] params : parameters) { setParameters(statement, params); statement.addBatch(); } statement.executeBatch(); + return null; } + }); + } + + private T execute(PreparedStatementExecutor executor) { + Connection conn = null; + try { + conn = getConnection(); + return executor.execute(conn); } catch (SQLException e) { throw new RuntimeException(e); } finally { @@ -124,4 +114,9 @@ private void setParameters(PreparedStatement statement, Object... parameters) th public interface RowMapper { T map(ResultSet rs) throws SQLException; } + + @FunctionalInterface + private interface PreparedStatementExecutor { + T execute(Connection conn) throws SQLException; + } } \ No newline at end of file From eb0246bbd22d8e22cb1f0855361c86bcdd45751b Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 23:20:58 +0900 Subject: [PATCH 30/32] =?UTF-8?q?refactor:=20Connection=20=EA=B4=80?= =?UTF-8?q?=EC=8B=AC=EC=82=AC=20manager=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 --- src/main/java/repository/JdbcTemplate.java | 26 ++----------------- .../java/repository/db/ConnectionManager.java | 24 +++++++++++++++++ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java index 4137aa9b5f..2b1354dd57 100644 --- a/src/main/java/repository/JdbcTemplate.java +++ b/src/main/java/repository/JdbcTemplate.java @@ -64,12 +64,12 @@ public void executeBatch(String sql, List parameters) { private T execute(PreparedStatementExecutor executor) { Connection conn = null; try { - conn = getConnection(); + conn = ConnectionManager.getConnection(dataSource); return executor.execute(conn); } catch (SQLException e) { throw new RuntimeException(e); } finally { - closeConnection(conn); + ConnectionManager.releaseConnection(conn); } } @@ -82,28 +82,6 @@ private long extractGeneratedKey(PreparedStatement statement) throws SQLExceptio } } - private Connection getConnection() { - return ConnectionManager.get().orElseGet(() -> { - try { - return dataSource.getConnection(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - private void closeConnection(Connection conn) { - if (ConnectionManager.isPresent() || conn == null) { - return; - } - - try { - conn.close(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - private void setParameters(PreparedStatement statement, Object... parameters) throws SQLException { for (int i = 0; i < parameters.length; i++) { statement.setObject(i + 1, parameters[i]); diff --git a/src/main/java/repository/db/ConnectionManager.java b/src/main/java/repository/db/ConnectionManager.java index b6887ae1ea..9541f41d1f 100644 --- a/src/main/java/repository/db/ConnectionManager.java +++ b/src/main/java/repository/db/ConnectionManager.java @@ -1,7 +1,9 @@ package repository.db; import java.sql.Connection; +import java.sql.SQLException; import java.util.Optional; +import javax.sql.DataSource; public class ConnectionManager { @@ -25,4 +27,26 @@ public static boolean isPresent() { public static void remove() { THREAD_LOCAL_CONNECTION.remove(); } + + public static Connection getConnection(DataSource dataSource) { + return get().orElseGet(() -> { + try { + return dataSource.getConnection(); + } catch (SQLException e) { + throw new RuntimeException("[ERROR] 커넥션 획득 실패", e); + } + }); + } + + public static void releaseConnection(Connection conn) { + if (isPresent() || conn == null) { + return; + } + + try { + conn.close(); + } catch (SQLException e) { + throw new RuntimeException("[ERROR] 커넥션 반납 실패", e); + } + } } \ No newline at end of file From 0b127cf6d9ce93adb38b7248a581d09636385573 Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 23:22:51 +0900 Subject: [PATCH 31/32] =?UTF-8?q?docs:=20db=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EB=B0=A9=EB=B2=95=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 184a428efe..1ed5527795 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# 실행 방법 + +`docker-compose up -d`로 db 세팅 후 프로젝트 실행 + +--- + # Cycle 1 ## 1단계 - 보드 초기화 From e3b6123cd09f7aeb108f33767f7d3b81d18569ca Mon Sep 17 00:00:00 2001 From: jihwankim128 Date: Thu, 9 Apr 2026 23:34:55 +0900 Subject: [PATCH 32/32] =?UTF-8?q?refactor:=20=EC=A1=B0=ED=9A=8C=EC=9A=A9?= =?UTF-8?q?=20service=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Application.java | 9 ++-- .../java/application/JanggiQueryService.java | 52 +++++++++++++++++++ src/main/java/application/JanggiService.java | 18 ------- .../java/application/JanggiServiceImpl.java | 35 +------------ .../java/application/JanggiTxService.java | 40 -------------- src/main/java/ui/ContinueController.java | 9 ++-- src/main/java/ui/FrontController.java | 7 +-- src/main/java/ui/JanggiController.java | 23 ++++---- src/main/java/ui/NewGameController.java | 5 +- 9 files changed, 84 insertions(+), 114 deletions(-) create mode 100644 src/main/java/application/JanggiQueryService.java diff --git a/src/main/java/Application.java b/src/main/java/Application.java index 5f693e14cf..12142977a6 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,7 +1,9 @@ +import application.JanggiQueryService; import application.JanggiService; import application.JanggiServiceImpl; import application.JanggiTxService; import javax.sql.DataSource; +import repository.JanggiRepository; import repository.JdbcJanggiRepository; import repository.db.DataSourceManager; import repository.db.TransactionTemplate; @@ -12,11 +14,12 @@ public class Application { public static void main(String[] args) { DataSource dataSource = DataSourceManager.getDataSource(); TransactionTemplate transactionTemplate = new TransactionTemplate(dataSource); + JanggiRepository janggiRepository = new JdbcJanggiRepository(dataSource); - JanggiServiceImpl janggiServiceImpl = new JanggiServiceImpl(new JdbcJanggiRepository(dataSource)); + JanggiServiceImpl janggiServiceImpl = new JanggiServiceImpl(janggiRepository); JanggiService janggiService = new JanggiTxService(transactionTemplate, janggiServiceImpl); + JanggiQueryService janggiQueryService = new JanggiQueryService(janggiRepository); - FrontController frontController = new FrontController(janggiService); - frontController.run(); + new FrontController(janggiService, janggiQueryService).run(); } } diff --git a/src/main/java/application/JanggiQueryService.java b/src/main/java/application/JanggiQueryService.java new file mode 100644 index 0000000000..a25ab839f2 --- /dev/null +++ b/src/main/java/application/JanggiQueryService.java @@ -0,0 +1,52 @@ +package application; + +import java.util.Map; +import model.GameStatus; +import model.JanggiGame; +import model.Team; +import model.coordinate.Position; +import model.piece.Piece; +import repository.JanggiRepository; + +public class JanggiQueryService { + + private final JanggiRepository janggiRepository; + + public JanggiQueryService(JanggiRepository janggiRepository) { + this.janggiRepository = janggiRepository; + } + + public JanggiResultDto getGameResult(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + return JanggiResultDto.from(janggiGame); + } + + public Map getBoardResponse(Long janggiId) { + return getGame(janggiId).getBoard(); + } + + public boolean canPlaying(Long janggiId) { + return getGame(janggiId).canPlaying(); + } + + public Team getTurn(Long janggiId) { + return getGame(janggiId).turn(); + } + + public Piece selectPiece(Long janggiId, Position current) { + return getGame(janggiId).selectPiece(current); + } + + public boolean isBigJang(Long janggiId) { + return getGame(janggiId).isBigJang(); + } + + public Map collectGameStatus() { + return janggiRepository.collectGameStatus(); + } + + private JanggiGame getGame(Long janggiId) { + return janggiRepository.findById(janggiId) + .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/application/JanggiService.java b/src/main/java/application/JanggiService.java index 9e0a3ebf91..f8a73cb542 100644 --- a/src/main/java/application/JanggiService.java +++ b/src/main/java/application/JanggiService.java @@ -1,11 +1,6 @@ package application; -import java.util.Map; -import model.GameStatus; -import model.Team; import model.board.TeamFormation; -import model.coordinate.Position; -import model.piece.Piece; public interface JanggiService { @@ -15,17 +10,4 @@ public interface JanggiService { void movePiece(Long janggiId, MoveCommand command); - JanggiResultDto getGameResult(Long janggiId); - - Map getBoardResponse(Long janggiId); - - boolean canPlaying(Long janggiId); - - Team getTurn(Long janggiId); - - Piece selectPiece(Long janggiId, Position current); - - boolean isBigJang(Long janggiId); - - Map collectGameStatus(); } diff --git a/src/main/java/application/JanggiServiceImpl.java b/src/main/java/application/JanggiServiceImpl.java index 98328135fb..880a7adf32 100644 --- a/src/main/java/application/JanggiServiceImpl.java +++ b/src/main/java/application/JanggiServiceImpl.java @@ -1,17 +1,13 @@ package application; -import java.util.Map; -import model.GameStatus; import model.JanggiGame; -import model.Team; import model.board.Board; import model.board.BoardFactory; import model.board.TeamFormation; -import model.coordinate.Position; -import model.piece.Piece; import repository.JanggiRepository; public class JanggiServiceImpl implements JanggiService { + private final JanggiRepository janggiRepository; public JanggiServiceImpl(JanggiRepository janggiRepository) { @@ -39,35 +35,6 @@ public void movePiece(Long janggiId, MoveCommand command) { janggiRepository.update(janggiId, janggiGame); } - public JanggiResultDto getGameResult(Long janggiId) { - JanggiGame janggiGame = getGame(janggiId); - return JanggiResultDto.from(janggiGame); - } - - public Map getBoardResponse(Long janggiId) { - return getGame(janggiId).getBoard(); - } - - public boolean canPlaying(Long janggiId) { - return getGame(janggiId).canPlaying(); - } - - public Team getTurn(Long janggiId) { - return getGame(janggiId).turn(); - } - - public Piece selectPiece(Long janggiId, Position current) { - return getGame(janggiId).selectPiece(current); - } - - public boolean isBigJang(Long janggiId) { - return getGame(janggiId).isBigJang(); - } - - public Map collectGameStatus() { - return janggiRepository.collectGameStatus(); - } - private JanggiGame getGame(Long janggiId) { return janggiRepository.findById(janggiId) .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); diff --git a/src/main/java/application/JanggiTxService.java b/src/main/java/application/JanggiTxService.java index c8c4d160e8..e162755735 100644 --- a/src/main/java/application/JanggiTxService.java +++ b/src/main/java/application/JanggiTxService.java @@ -1,11 +1,6 @@ package application; -import java.util.Map; -import model.GameStatus; -import model.Team; import model.board.TeamFormation; -import model.coordinate.Position; -import model.piece.Piece; import repository.db.TransactionTemplate; public class JanggiTxService implements JanggiService { @@ -32,39 +27,4 @@ public void finishByBigJang(Long janggiId) { public void movePiece(Long janggiId, MoveCommand command) { transactionTemplate.execute(() -> janggiService.movePiece(janggiId, command)); } - - @Override - public JanggiResultDto getGameResult(Long janggiId) { - return janggiService.getGameResult(janggiId); - } - - @Override - public Map getBoardResponse(Long janggiId) { - return janggiService.getBoardResponse(janggiId); - } - - @Override - public boolean canPlaying(Long janggiId) { - return janggiService.canPlaying(janggiId); - } - - @Override - public Team getTurn(Long janggiId) { - return janggiService.getTurn(janggiId); - } - - @Override - public Piece selectPiece(Long janggiId, Position current) { - return janggiService.selectPiece(janggiId, current); - } - - @Override - public boolean isBigJang(Long janggiId) { - return janggiService.isBigJang(janggiId); - } - - @Override - public Map collectGameStatus() { - return janggiService.collectGameStatus(); - } } diff --git a/src/main/java/ui/ContinueController.java b/src/main/java/ui/ContinueController.java index 5d5b5e5135..1abc8127d5 100644 --- a/src/main/java/ui/ContinueController.java +++ b/src/main/java/ui/ContinueController.java @@ -2,6 +2,7 @@ import static ui.Retrier.retry; +import application.JanggiQueryService; import application.JanggiService; import java.util.Map; import model.GameStatus; @@ -10,18 +11,18 @@ public class ContinueController extends JanggiController { - protected ContinueController(JanggiService janggiService) { - super(janggiService); + protected ContinueController(JanggiService janggiService, JanggiQueryService janggiQueryService) { + super(janggiService, janggiQueryService); } @Override public void run() { - Map gameStatusById = janggiService.collectGameStatus(); + Map gameStatusById = janggiQueryService.collectGameStatus(); if (gameStatusById.isEmpty()) { OutputView.displayNoGame(); return; } - + process(gameStatusById); } diff --git a/src/main/java/ui/FrontController.java b/src/main/java/ui/FrontController.java index 21e5f4cbad..f186f9636e 100644 --- a/src/main/java/ui/FrontController.java +++ b/src/main/java/ui/FrontController.java @@ -2,6 +2,7 @@ import static ui.Retrier.retry; +import application.JanggiQueryService; import application.JanggiService; import java.util.Map; import java.util.Optional; @@ -12,10 +13,10 @@ public class FrontController { private final Map controllers; - public FrontController(JanggiService janggiService) { + public FrontController(JanggiService janggiService, JanggiQueryService janggiQueryService) { controllers = Map.of( - GameMenu.CONTINUE, new ContinueController(janggiService), - GameMenu.NEW_GAME, new NewGameController(janggiService) + GameMenu.CONTINUE, new ContinueController(janggiService, janggiQueryService), + GameMenu.NEW_GAME, new NewGameController(janggiService, janggiQueryService) ); } diff --git a/src/main/java/ui/JanggiController.java b/src/main/java/ui/JanggiController.java index 400dcb8463..673ee91e92 100644 --- a/src/main/java/ui/JanggiController.java +++ b/src/main/java/ui/JanggiController.java @@ -2,6 +2,7 @@ import static ui.Retrier.retry; +import application.JanggiQueryService; import application.JanggiResultDto; import application.JanggiService; import application.MoveCommand; @@ -14,17 +15,19 @@ public abstract class JanggiController { protected final JanggiService janggiService; + protected final JanggiQueryService janggiQueryService; - protected JanggiController(JanggiService janggiService) { + protected JanggiController(JanggiService janggiService, JanggiQueryService janggiQueryService) { this.janggiService = janggiService; + this.janggiQueryService = janggiQueryService; } public abstract void run(); protected void playGame(Long janggiId) { - OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); + OutputView.displayBoard(janggiQueryService.getBoardResponse(janggiId)); - while (janggiService.canPlaying(janggiId)) { + while (janggiQueryService.canPlaying(janggiId)) { retry(() -> checkBigJang(janggiId), OutputView::displayError); play(janggiId); } @@ -33,35 +36,35 @@ protected void playGame(Long janggiId) { } private void play(Long janggiId) { - if (janggiService.canPlaying(janggiId)) { + if (janggiQueryService.canPlaying(janggiId)) { retry(() -> playByTurn(janggiId), OutputView::displayError); } - OutputView.displayBoard(janggiService.getBoardResponse(janggiId)); + OutputView.displayBoard(janggiQueryService.getBoardResponse(janggiId)); } private void playByTurn(Long janggiId) { - Team currentTurn = janggiService.getTurn(janggiId); + Team currentTurn = janggiQueryService.getTurn(janggiId); Position current = InputView.readPiecePositionForMove(currentTurn); - Piece piece = janggiService.selectPiece(janggiId, current); + Piece piece = janggiQueryService.selectPiece(janggiId, current); Position next = InputView.readPiecePositionForArrange(currentTurn, piece); janggiService.movePiece(janggiId, new MoveCommand(current, next)); } private void checkBigJang(Long janggiId) { - if (!janggiService.isBigJang(janggiId)) { + if (!janggiQueryService.isBigJang(janggiId)) { return; } - boolean bigJang = InputView.readBigJangStatus(janggiService.getTurn(janggiId)); + boolean bigJang = InputView.readBigJangStatus(janggiQueryService.getTurn(janggiId)); if (bigJang) { janggiService.finishByBigJang(janggiId); } } private void printResult(Long janggiId) { - JanggiResultDto gameResult = janggiService.getGameResult(janggiId); + JanggiResultDto gameResult = janggiQueryService.getGameResult(janggiId); OutputView.displayWinner(gameResult.winner()); if (gameResult.bigJangDone()) { diff --git a/src/main/java/ui/NewGameController.java b/src/main/java/ui/NewGameController.java index 5b6863ce8c..91ffa7a3e0 100644 --- a/src/main/java/ui/NewGameController.java +++ b/src/main/java/ui/NewGameController.java @@ -4,6 +4,7 @@ import static model.Team.HAN; import static ui.Retrier.retry; +import application.JanggiQueryService; import application.JanggiService; import model.board.TeamFormation; import ui.view.InputView; @@ -11,8 +12,8 @@ public class NewGameController extends JanggiController { - protected NewGameController(JanggiService janggiService) { - super(janggiService); + protected NewGameController(JanggiService janggiService, JanggiQueryService janggiQueryService) { + super(janggiService, janggiQueryService); } @Override