diff --git a/.codex/skills/SKILL.md b/.codex/skills/SKILL.md
deleted file mode 100644
index aa9541ad59..0000000000
--- a/.codex/skills/SKILL.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-description: Interview user in-depth to create a detailed spec
-allowed-tools: Write
----
-
-# 심층 인터뷰를 통한 요구사항 스펙 생성
-
-사용자를 인터뷰하여 암묵적 가정, 놓치기 쉬운 요구사항, 트레이드오프를 도출하고 상세한 스펙 문서를 생성합니다.
-
-## 인터뷰 원칙
-
-1. **역방향 요구사항 추출**: 사용자가 "당연하다고 생각해서 안 적는 것들"을 질문으로 끌어내기
-2. **깊이 있는 질문**: 뻔한 질문은 피하고, 구현 세부사항까지 파고들기
-3. **다각도 탐색**: 기술 구현, 우려 지점, 트레이드오프 등 다양한 관점에서 질문
-
-## 질문 영역 (참고용, 순서나 범위는 자유롭게)
-
-- 기술 구현 방식 및 아키텍처
-- 엣지 케이스 및 예외 상황
-- 성능, 확장성 고려사항
-- 기존 시스템과의 통합
-- 우려 지점 및 리스크
-- 트레이드오프 선택
-
-## 완료 조건
-
-다음 중 하나 이상 충족 시 인터뷰 종료:
-
-- 핵심 요구사항과 주요 엣지 케이스가 충분히 도출됨
-- 사용자가 "충분하다" 또는 "그만"이라고 표시
-- 새로운 질문에 대해 사용자가 "모르겠다" 또는 "나중에 결정"이 반복됨
-
-## 실행
-
-$ARGUMENTS
-
-위 주제에 대해 사용자에게 애매하다면 바로 질문을 하며 심층 인터뷰를 진행하세요.
-
-- 한 번에 1-2개의 연관된 질문을 던지세요
-- 이전 답변을 기반으로 후속 질문을 발전시키세요
-- 충분한 정보가 모이면 스펙 문서를 작성하세요
-
-## 출력
-
-인터뷰 완료 후 다음 위치에 스펙 문서 저장:
-
-```
-docs/codex/001_prd_v1.md
-```
-
-스펙 문서에 포함할 내용:
-
-- 기능 개요 및 목적
-- 핵심 요구사항 (Functional Requirements)
-- 비기능 요구사항 (Non-functional Requirements)
-- 엣지 케이스 및 예외 처리
-- 제약 조건 및 가정
-- 우선순위 및 트레이드오프 결정사항
diff --git a/build.gradle b/build.gradle
index ce846f70cc..8c48b612df 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,6 +13,9 @@ dependencies {
testImplementation platform('org.assertj:assertj-bom:3.27.3')
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation('org.assertj:assertj-core')
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ implementation 'org.xerial:sqlite-jdbc:3.46.1.3'
}
java {
diff --git a/src/main/java/janggi/Application.java b/src/main/java/janggi/Application.java
index eef32f322f..a120456d0d 100644
--- a/src/main/java/janggi/Application.java
+++ b/src/main/java/janggi/Application.java
@@ -1,16 +1,14 @@
package janggi;
-import janggi.view.output.ConsoleOutputView;
-import janggi.view.input.ConsoleInputView;
-import janggi.view.input.InputView;
-import janggi.view.output.OutputView;
+import janggi.config.AppConfig;
+import janggi.persistence.DatabaseInitializer;
public class Application {
public static void main(String[] args) {
- InputView inputView = new ConsoleInputView();
- OutputView outputView = new ConsoleOutputView();
- JanggiRunner janggiRunner = new JanggiRunner(inputView, outputView);
- janggiRunner.execute();
+ AppConfig appConfig = new AppConfig();
+ DatabaseInitializer databaseInitializer = appConfig.databaseInitializer();
+ databaseInitializer.initialize();
+ appConfig.janggiRunner().execute();
}
}
diff --git a/src/main/java/janggi/JanggiRunner.java b/src/main/java/janggi/JanggiRunner.java
index 5f08be9ad1..f904f1918d 100644
--- a/src/main/java/janggi/JanggiRunner.java
+++ b/src/main/java/janggi/JanggiRunner.java
@@ -2,36 +2,66 @@
import janggi.domain.JanggiGame;
import janggi.domain.Position;
+import janggi.domain.WinnerResult;
+import janggi.service.JanggiService;
import janggi.util.ActionExecutor;
import janggi.util.DelimiterParser;
+import janggi.util.PersistenceExecutor;
import janggi.view.input.InputView;
import janggi.view.output.OutputView;
import java.util.List;
+import java.util.Optional;
public class JanggiRunner {
private final InputView inputView;
private final OutputView outputView;
+ private final JanggiService janggiService;
- public JanggiRunner(InputView inputView, OutputView outputView) {
+ public JanggiRunner(InputView inputView, OutputView outputView, JanggiService janggiService) {
this.inputView = inputView;
this.outputView = outputView;
+ this.janggiService = janggiService;
}
public void execute() {
outputView.printStartMessage();
-
- JanggiGame janggiGame = JanggiGame.createInitialJanggiGame();
- while (true) {
+ JanggiGame janggiGame = PersistenceExecutor.retryOnTimeout(
+ janggiService::loadGame,
+ this::printPersistenceTimeoutMessage
+ );
+ while (isGameContinue(janggiGame)) {
outputView.printBoard(janggiGame.makeCurrentTurnBoardSnapShot());
Position startPosition = ActionExecutor.retryUntilSuccess(
() -> readValidStartPosition(janggiGame), outputView
);
- Position endPosition = ActionExecutor.retryUntilSuccess(
+ Optional endPosition = ActionExecutor.retryUntilSuccess(
() -> readValidEndPosition(janggiGame, startPosition), outputView
);
- janggiGame.doGame(startPosition, endPosition);
+ if (endPosition.isEmpty()) {
+ outputView.printMessage("말 선택을 취소했습니다. 다시 선택해주세요.");
+ continue;
+ }
+ PersistenceExecutor.retryOnTimeout(
+ () -> janggiService.play(janggiGame, startPosition, endPosition.get()),
+ this::printPersistenceTimeoutMessage
+ );
}
+ finish(janggiGame);
+ }
+
+ private void finish(JanggiGame janggiGame) {
+ PersistenceExecutor.retryOnTimeout(janggiService::finishGame, this::printPersistenceTimeoutMessage);
+ WinnerResult winnerResult = janggiGame.getWinnerResult();
+ outputView.printResult(
+ janggiGame.makeCurrentTurnBoardSnapShot(),
+ winnerResult.winner(),
+ winnerResult.winnerScore()
+ );
+ }
+
+ private boolean isGameContinue(JanggiGame janggiGame) {
+ return !janggiGame.isGameOver();
}
private Position readValidStartPosition(JanggiGame janggiGame) {
@@ -44,12 +74,19 @@ private Position readValidStartPosition(JanggiGame janggiGame) {
return startPosition;
}
- private Position readValidEndPosition(JanggiGame janggiGame, Position startPosition) {
+ private Optional readValidEndPosition(JanggiGame janggiGame, Position startPosition) {
outputView.printAskMovePosition(janggiGame.findPiece(startPosition).nickname());
- String rawMovePosition = inputView.readLine();
- List parsedMovePosition = DelimiterParser.parse(rawMovePosition);
+ Optional rawMovePosition = inputView.readCancelableLine();
+ if (rawMovePosition.isEmpty()) {
+ return Optional.empty();
+ }
+ List parsedMovePosition = DelimiterParser.parse(rawMovePosition.get());
Position endPosition = Position.makePosition(parsedMovePosition);
janggiGame.validateValidEndPosition(startPosition, endPosition);
- return endPosition;
+ return Optional.of(endPosition);
+ }
+
+ private void printPersistenceTimeoutMessage() {
+ outputView.printMessage("데이터베이스 응답이 지연되었습니다. 다시 시도합니다.");
}
}
diff --git a/src/main/java/janggi/config/AppConfig.java b/src/main/java/janggi/config/AppConfig.java
new file mode 100644
index 0000000000..4ddba3311e
--- /dev/null
+++ b/src/main/java/janggi/config/AppConfig.java
@@ -0,0 +1,38 @@
+package janggi.config;
+
+import janggi.JanggiRunner;
+import janggi.persistence.DatabaseInitializer;
+import janggi.persistence.repository.JanggiGameRepository;
+import janggi.persistence.repository.JdbcJanggiGameRepository;
+import janggi.service.JanggiService;
+import janggi.view.input.ConsoleInputView;
+import janggi.view.input.InputView;
+import janggi.view.output.ConsoleOutputView;
+import janggi.view.output.OutputView;
+
+public class AppConfig {
+
+ public DatabaseInitializer databaseInitializer() {
+ return new DatabaseInitializer();
+ }
+
+ public JanggiRunner janggiRunner() {
+ return new JanggiRunner(inputView(), outputView(), janggiService());
+ }
+
+ public JanggiService janggiService() {
+ return new JanggiService(janggiGameRepository());
+ }
+
+ public JanggiGameRepository janggiGameRepository() {
+ return new JdbcJanggiGameRepository();
+ }
+
+ public InputView inputView() {
+ return new ConsoleInputView();
+ }
+
+ public OutputView outputView() {
+ return new ConsoleOutputView();
+ }
+}
diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java
index 875f133337..31d0400a62 100644
--- a/src/main/java/janggi/domain/Board.java
+++ b/src/main/java/janggi/domain/Board.java
@@ -1,12 +1,16 @@
package janggi.domain;
+import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.piece.Piece;
+import janggi.domain.piece.PieceType;
import janggi.domain.team.Team;
import janggi.domain.team.TeamType;
import janggi.dto.BoardSpot;
import janggi.dto.BoardSpots;
+import janggi.dto.MoveRoute;
import java.util.EnumMap;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -55,11 +59,7 @@ public Optional findPiece(Position position) {
.findFirst();
}
- public boolean hasPiece(Position position) {
- return findPiece(position).isPresent();
- }
-
- public void canMove(Position startPosition, Position endPosition, TeamType playingTeam) {
+ public void validateMove(Position startPosition, Position endPosition, TeamType playingTeam) {
validateRange(startPosition);
validateRange(endPosition);
Piece piece = findTeamPiece(startPosition, currentTeam(playingTeam));
@@ -72,12 +72,33 @@ public Board move(
Position endPosition,
TeamType playingTeam
) {
- canMove(startPosition, endPosition, playingTeam);
+ validateMove(startPosition, endPosition, playingTeam);
Team movedCurrentTeam = currentTeam(playingTeam).move(startPosition, endPosition);
Team remainedOpponentTeam = removeOpponentPiece(playingTeam, endPosition);
return createMovedBoard(playingTeam, movedCurrentTeam, remainedOpponentTeam);
}
+ public boolean hasGung(TeamType teamType) {
+ return findSpecificTeam(teamType).hasPieceType(PieceType.GUNG);
+ }
+
+ public Optional findWinner() {
+ boolean chuHasGung = hasGung(TeamType.CHU);
+ boolean hanHasGung = hasGung(TeamType.HAN);
+ if (chuHasGung == hanHasGung) {
+ return Optional.empty();
+ }
+ if (chuHasGung) {
+ return Optional.of(TeamType.CHU);
+ }
+ return Optional.of(TeamType.HAN);
+ }
+
+ public int calculateScore(TeamType teamType) {
+ Team team = findSpecificTeam(teamType);
+ return team.calculatePiecesScore();
+ }
+
private Piece findTeamPiece(Position position, Team nowTeam) {
return nowTeam.findPiece(position)
.orElseThrow(() -> new IllegalArgumentException("입력한 위치에 기물이 없습니다."));
@@ -92,15 +113,52 @@ private Team opponentTeam(TeamType nowTurnTeamType) {
}
private void validateCanMove(Piece piece, Position piecePosition, Position targetPosition) {
- if (!piece.isValidMovePattern(piecePosition.getX(), piecePosition.getY(), targetPosition.getX(),
- targetPosition.getY())) {
+ Optional movePath = piece.findMovePath(
+ piecePosition.getX(),
+ piecePosition.getY(),
+ targetPosition.getX(),
+ targetPosition.getY()
+ );
+ if (movePath.isEmpty()) {
throw new IllegalArgumentException("이동할 수 없는 위치입니다.");
}
- if (!piece.isObstaclesNotExist(piecePosition, targetPosition, this)) {
+ MoveRoute moveRoute = createMoveRoute(piecePosition, targetPosition, movePath.get());
+ if (!piece.canMove(moveRoute)) {
throw new IllegalArgumentException("이동할 수 없는 위치입니다.");
}
}
+ private MoveRoute createMoveRoute(
+ Position startPosition,
+ Position targetPosition,
+ MovePathStrategy movePath
+ ) {
+ return new MoveRoute(
+ findIntermediatePieceTypes(startPosition, targetPosition, movePath),
+ findTargetPieceType(targetPosition)
+ );
+ }
+
+ private List findIntermediatePieceTypes(
+ Position startPosition,
+ Position targetPosition,
+ MovePathStrategy movePath
+ ) {
+ return movePath.intermediatePositions(startPosition, targetPosition).stream()
+ .map(this::findPieceType)
+ .flatMap(Optional::stream)
+ .toList();
+ }
+
+ private Optional findTargetPieceType(Position targetPosition) {
+ return findPieceType(targetPosition);
+ }
+
+ private Optional findPieceType(Position position) {
+ return findPiece(position)
+ .map(Piece::getPieceType);
+ }
+
private void validateTargetPosition(Team team, Position targetPosition) {
if (team.findPiece(targetPosition).isPresent()) {
throw new IllegalArgumentException("같은 팀의 기물이 있는 위치로는 이동할 수 없습니다.");
diff --git a/src/main/java/janggi/domain/Delta.java b/src/main/java/janggi/domain/Delta.java
index edf9e2494e..1dbe3c9f1c 100644
--- a/src/main/java/janggi/domain/Delta.java
+++ b/src/main/java/janggi/domain/Delta.java
@@ -42,6 +42,58 @@ public static Delta createRightDown() {
return new Delta(1, -1);
}
+ public static Delta from(int dx, int dy) {
+ if (isVertical(dx)) {
+ return vertical(dy);
+ }
+ if (isHorizontal(dy)) {
+ return horizontal(dx);
+ }
+ if (isDiagonal(dx, dy)) {
+ return diagonal(dx, dy);
+ }
+ throw new IllegalArgumentException("지원하지 않는 방향입니다.");
+ }
+
+ private static boolean isVertical(int dx) {
+ return dx == 0;
+ }
+
+ private static boolean isHorizontal(int dy) {
+ return dy == 0;
+ }
+
+ private static boolean isDiagonal(int dx, int dy) {
+ return Math.abs(dx) == 1 && Math.abs(dy) == 1;
+ }
+
+ private static Delta vertical(int dy) {
+ if (dy == 1) {
+ return createUp();
+ }
+ return createDown();
+ }
+
+ private static Delta horizontal(int dx) {
+ if (dx == 1) {
+ return createRight();
+ }
+ return createLeft();
+ }
+
+ private static Delta diagonal(int dx, int dy) {
+ if (dx == -1 && dy == 1) {
+ return createLeftUp();
+ }
+ if (dx == 1 && dy == 1) {
+ return createRightUp();
+ }
+ if (dx == -1 && dy == -1) {
+ return createLeftDown();
+ }
+ return createRightDown();
+ }
+
public int dx() {
return dx;
}
diff --git a/src/main/java/janggi/domain/JanggiGame.java b/src/main/java/janggi/domain/JanggiGame.java
index 8cdc1548e0..f66aa94acd 100644
--- a/src/main/java/janggi/domain/JanggiGame.java
+++ b/src/main/java/janggi/domain/JanggiGame.java
@@ -5,6 +5,7 @@
import janggi.dto.BoardSpots;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
public class JanggiGame {
@@ -53,6 +54,22 @@ public TeamType getCurrentTurnTeam() {
return lastTurn.nextTurnTeam();
}
+ public boolean isGameOver() {
+ return findWinner().isPresent();
+ }
+
+ public Optional findWinner() {
+ return getLastTurn().findWinner();
+ }
+
+ public WinnerResult getWinnerResult() {
+ return getLastTurn().getWinnerResult();
+ }
+
+ public int getTurnCount() {
+ return turns.size() - 1;
+ }
+
private Turn getLastTurn() {
return turns.getLast();
}
diff --git a/src/main/java/janggi/domain/Palace.java b/src/main/java/janggi/domain/Palace.java
new file mode 100644
index 0000000000..4452bb171a
--- /dev/null
+++ b/src/main/java/janggi/domain/Palace.java
@@ -0,0 +1,128 @@
+package janggi.domain;
+
+import janggi.domain.movepath.DirectionalMovePath;
+import janggi.domain.movepath.FixedMovePath;
+import janggi.domain.movepath.MovePathStrategy;
+import janggi.domain.team.TeamType;
+import java.util.List;
+import java.util.Optional;
+
+public class Palace {
+
+ private static final int MIN_X = 4;
+ private static final int MAX_X = 6;
+ private static final int CHU_MIN_Y = 1;
+ private static final int CHU_MAX_Y = 3;
+ private static final int HAN_MIN_Y = 8;
+ private static final int HAN_MAX_Y = 10;
+ private static final int CHU_CENTER_Y = 2;
+ private static final int HAN_CENTER_Y = 9;
+ private static final int CENTER_X = 5;
+
+ public boolean isInside(Position position) {
+ return isInside(position, TeamType.CHU) || isInside(position, TeamType.HAN);
+ }
+
+ public boolean isInside(Position position, TeamType teamType) {
+ if (teamType == TeamType.CHU) {
+ return isInsideRange(position, CHU_MIN_Y, CHU_MAX_Y);
+ }
+ return isInsideRange(position, HAN_MIN_Y, HAN_MAX_Y);
+ }
+
+ public Optional findOneStepMovePath(Position start, Position end) {
+ if (!isInside(start) || !isInside(end) || !isSamePalace(start, end)) {
+ return Optional.empty();
+ }
+
+ int dx = end.getX() - start.getX();
+ int dy = end.getY() - start.getY();
+ if (isStraightOneStep(dx, dy)) {
+ return Optional.of(new FixedMovePath(List.of(Delta.from(dx, dy))));
+ }
+ if (isConnectedDiagonal(start, end)) {
+ return Optional.of(new FixedMovePath(List.of(Delta.from(dx, dy))));
+ }
+ return Optional.empty();
+ }
+
+ public Optional findDiagonalMovePath(Position start, Position end) {
+ if (!isSamePalace(start, end) || !isDiagonalNode(start) || !isDiagonalNode(end)) {
+ return Optional.empty();
+ }
+ int dx = end.getX() - start.getX();
+ int dy = end.getY() - start.getY();
+ if (dx == 0 || Math.abs(dx) != Math.abs(dy) || Math.abs(dx) > 2) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new DirectionalMovePath(List.of(Delta.from(Integer.signum(dx), Integer.signum(dy))))
+ );
+ }
+
+ public Optional findForwardDiagonalStepPath(
+ Position start,
+ Position end,
+ TeamType teamType
+ ) {
+ int dx = end.getX() - start.getX();
+ int dy = end.getY() - start.getY();
+ if (Math.abs(dx) != 1 || Math.abs(dy) != 1) {
+ return Optional.empty();
+ }
+ if (!isForward(teamType, dy)) {
+ return Optional.empty();
+ }
+ return findOneStepMovePath(start, end)
+ .filter(path -> isOneStepDiagonal(dx, dy));
+ }
+
+ private boolean isConnectedDiagonal(Position start, Position end) {
+ int dx = end.getX() - start.getX();
+ int dy = end.getY() - start.getY();
+ return isOneStepDiagonal(dx, dy)
+ && isDiagonalNode(start)
+ && isDiagonalNode(end)
+ && (isCenter(start) || isCenter(end));
+ }
+
+ private boolean isOneStepDiagonal(int dx, int dy) {
+ return Math.abs(dx) == 1 && Math.abs(dy) == 1;
+ }
+
+ private boolean isSamePalace(Position start, Position end) {
+ return isInside(start, TeamType.CHU) && isInside(end, TeamType.CHU)
+ || isInside(start, TeamType.HAN) && isInside(end, TeamType.HAN);
+ }
+
+ private boolean isDiagonalNode(Position position) {
+ return isCenter(position) || isCorner(position);
+ }
+
+ private boolean isInsideRange(Position position, int minY, int maxY) {
+ return MIN_X <= position.getX() && position.getX() <= MAX_X
+ && minY <= position.getY() && position.getY() <= maxY;
+ }
+
+ private boolean isCenter(Position position) {
+ return position.getX() == CENTER_X
+ && (position.getY() == CHU_CENTER_Y || position.getY() == HAN_CENTER_Y);
+ }
+
+ private boolean isCorner(Position position) {
+ return (position.getX() == MIN_X || position.getX() == MAX_X)
+ && (position.getY() == CHU_MIN_Y || position.getY() == CHU_MAX_Y
+ || position.getY() == HAN_MIN_Y || position.getY() == HAN_MAX_Y);
+ }
+
+ private boolean isStraightOneStep(int dx, int dy) {
+ return Math.abs(dx) + Math.abs(dy) == 1;
+ }
+
+ private boolean isForward(TeamType teamType, int dy) {
+ if (teamType == TeamType.CHU) {
+ return dy == 1;
+ }
+ return dy == -1;
+ }
+}
diff --git a/src/main/java/janggi/domain/Pieces.java b/src/main/java/janggi/domain/Pieces.java
index 20bab88455..e87d580a79 100644
--- a/src/main/java/janggi/domain/Pieces.java
+++ b/src/main/java/janggi/domain/Pieces.java
@@ -5,6 +5,7 @@
import janggi.domain.piece.Jol;
import janggi.domain.piece.Ma;
import janggi.domain.piece.Piece;
+import janggi.domain.piece.PieceType;
import janggi.domain.piece.Po;
import janggi.domain.piece.Sa;
import janggi.domain.piece.Sang;
@@ -82,6 +83,11 @@ public Optional findPiece(Position position) {
return Optional.ofNullable(value.get(position));
}
+ public boolean hasPieceType(PieceType pieceType) {
+ return value.values().stream()
+ .anyMatch(piece -> piece.getPieceType() == pieceType);
+ }
+
private static void createChas(Map pieces, int indexY, TeamType teamType) {
pieces.put(new Position(1, indexY), new Cha(teamType));
pieces.put(new Position(9, indexY), new Cha(teamType));
@@ -116,4 +122,10 @@ private static void createJols(Map pieces, int indexY, TeamType
pieces.put(new Position(i, indexY), new Jol(teamType));
}
}
+
+ public int sumScore() {
+ return value.values().stream()
+ .mapToInt(Piece::getScore)
+ .sum();
+ }
}
diff --git a/src/main/java/janggi/domain/Turn.java b/src/main/java/janggi/domain/Turn.java
index fa94ca90a7..e2f3098c3c 100644
--- a/src/main/java/janggi/domain/Turn.java
+++ b/src/main/java/janggi/domain/Turn.java
@@ -3,6 +3,7 @@
import janggi.domain.piece.Piece;
import janggi.domain.team.TeamType;
import janggi.dto.BoardSpots;
+import java.util.Optional;
public class Turn {
@@ -18,6 +19,10 @@ public static Turn createInitialTurn() {
return new Turn(TeamType.HAN, Board.createInitialBoard());
}
+ public static Turn from(TeamType movedTeam, Board board) {
+ return new Turn(movedTeam, board);
+ }
+
public boolean isMyTeamPieceExist(Position position) {
return board.isPieceExist(position, movedTeam);
}
@@ -33,7 +38,7 @@ public Turn move(Position startPosition, Position endPosition) {
}
public void canMove(Position startPosition, Position endPosition) {
- board.canMove(startPosition, endPosition, playingTeamType());
+ board.validateMove(startPosition, endPosition, playingTeamType());
}
public TeamType nextTurnTeam() {
@@ -44,6 +49,19 @@ public BoardSpots makeBoardSnapShot() {
return board.makeSnapShot();
}
+ public Optional findWinner() {
+ return board.findWinner();
+ }
+
+ public WinnerResult getWinnerResult() {
+ Optional winnerCandidate = findWinner();
+ if (winnerCandidate.isEmpty()) {
+ throw new IllegalArgumentException("아직 승자가 존재하지 않습니다.");
+ }
+ TeamType winner = winnerCandidate.get();
+ return new WinnerResult(winner, board.calculateScore(winner));
+ }
+
private TeamType playingTeamType() {
if (movedTeam == TeamType.CHU) {
return TeamType.HAN;
diff --git a/src/main/java/janggi/domain/WinnerResult.java b/src/main/java/janggi/domain/WinnerResult.java
new file mode 100644
index 0000000000..64012da156
--- /dev/null
+++ b/src/main/java/janggi/domain/WinnerResult.java
@@ -0,0 +1,9 @@
+package janggi.domain;
+
+import janggi.domain.team.TeamType;
+
+public record WinnerResult(
+ TeamType winner,
+ int winnerScore
+) {
+}
diff --git a/src/main/java/janggi/domain/piece/Cha.java b/src/main/java/janggi/domain/piece/Cha.java
index 71bff3a44a..688d3b2593 100644
--- a/src/main/java/janggi/domain/piece/Cha.java
+++ b/src/main/java/janggi/domain/piece/Cha.java
@@ -1,11 +1,12 @@
package janggi.domain.piece;
-import janggi.domain.Board;
import janggi.domain.Delta;
+import janggi.domain.Palace;
import janggi.domain.Position;
-import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.movepath.DirectionalMovePath;
+import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
import java.util.List;
import java.util.Optional;
@@ -14,6 +15,7 @@ public class Cha implements Piece {
private final TeamType teamType;
private final PieceType pieceType;
private final List paths;
+ private final Palace palace;
public Cha(TeamType teamType) {
this.teamType = teamType;
@@ -24,6 +26,7 @@ public Cha(TeamType teamType) {
new DirectionalMovePath(List.of(Delta.createLeft())),
new DirectionalMovePath(List.of(Delta.createRight()))
);
+ palace = new Palace();
}
@Override
@@ -36,32 +39,23 @@ public Optional findMovePath(int startX, int startY, int endX,
if (isSamePosition(startX, startY, endX, endY)) {
return Optional.empty();
}
- if (!isStraightDirection(startX, startY, endX, endY)) {
- return Optional.empty();
+ if (isStraightDirection(startX, startY, endX, endY)) {
+ int dx = endX - startX;
+ int dy = endY - startY;
+ return paths.stream()
+ .filter(path -> path.matches(dx, dy))
+ .findFirst();
}
- int dx = endX - startX;
- int dy = endY - startY;
- return paths.stream()
- .filter(path -> path.matches(dx, dy))
- .findFirst();
- }
- @Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
- Optional movePath = findMovePath(start.getX(), start.getY(), end.getX(), end.getY());
- if (movePath.isEmpty()) {
- return false;
- }
- return movePath.get().intermediatePositions(start, end).stream()
- .noneMatch(board::hasPiece);
- }
-
- private boolean isSamePosition(int startX, int startY, int endX, int endY) {
- return startX == endX && startY == endY;
+ return palace.findDiagonalMovePath(
+ new Position(startX, startY),
+ new Position(endX, endY)
+ );
}
- private boolean isStraightDirection(int startX, int startY, int endX, int endY) {
- return startX == endX || startY == endY;
+ @Override
+ public boolean canMove(MoveRoute moveRoute) {
+ return moveRoute.intermediatePieceTypes().isEmpty();
}
@Override
@@ -78,4 +72,17 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
+
+ private boolean isSamePosition(int startX, int startY, int endX, int endY) {
+ return startX == endX && startY == endY;
+ }
+
+ private boolean isStraightDirection(int startX, int startY, int endX, int endY) {
+ return startX == endX || startY == endY;
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Gung.java b/src/main/java/janggi/domain/piece/Gung.java
index 67d8816f4f..7ba6628a7b 100644
--- a/src/main/java/janggi/domain/piece/Gung.java
+++ b/src/main/java/janggi/domain/piece/Gung.java
@@ -1,33 +1,22 @@
package janggi.domain.piece;
-import janggi.domain.Board;
-import janggi.domain.Delta;
+import janggi.domain.Palace;
import janggi.domain.Position;
import janggi.domain.movepath.MovePathStrategy;
-import janggi.domain.movepath.FixedMovePath;
import janggi.domain.team.TeamType;
-import java.util.List;
+import janggi.dto.MoveRoute;
import java.util.Optional;
public class Gung implements Piece {
private final TeamType teamType;
private final PieceType pieceType;
- private final List paths;
+ private final Palace palace;
public Gung(TeamType teamType) {
this.teamType = teamType;
pieceType = PieceType.GUNG;
- paths = List.of(
- new FixedMovePath(List.of(Delta.createUp())),
- new FixedMovePath(List.of(Delta.createDown())),
- new FixedMovePath(List.of(Delta.createLeft())),
- new FixedMovePath(List.of(Delta.createRight())),
- new FixedMovePath(List.of(Delta.createRightUp())),
- new FixedMovePath(List.of(Delta.createRightDown())),
- new FixedMovePath(List.of(Delta.createLeftUp())),
- new FixedMovePath(List.of(Delta.createLeftDown()))
- );
+ palace = new Palace();
}
@Override
@@ -37,34 +26,14 @@ public boolean isValidMovePattern(int startX, int startY, int endX, int endY) {
@Override
public Optional findMovePath(int startX, int startY, int endX, int endY) {
- int dx = endX - startX;
- int dy = endY - startY;
- int distanceX = Math.abs(dx);
- int distanceY = Math.abs(dy);
- if (isSamePosition(distanceX, distanceY)) {
- return Optional.empty();
- }
- if (!isOneStep(distanceX, distanceY)) {
- return Optional.empty();
- }
- return paths.stream()
- .filter(path -> path.matches(dx, dy))
- .findFirst();
+ return palace.findOneStepMovePath(new Position(startX, startY), new Position(endX, endY));
}
@Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
+ public boolean canMove(MoveRoute moveRoute) {
return true;
}
- private boolean isSamePosition(int distanceX, int distanceY) {
- return distanceX == 0 && distanceY == 0;
- }
-
- private boolean isOneStep(int distanceX, int distanceY) {
- return distanceX <= 1 && distanceY <= 1;
- }
-
@Override
public String nickname() {
return pieceType.getNickname();
@@ -79,4 +48,9 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Jol.java b/src/main/java/janggi/domain/piece/Jol.java
index aab4ba84de..8155618bfb 100644
--- a/src/main/java/janggi/domain/piece/Jol.java
+++ b/src/main/java/janggi/domain/piece/Jol.java
@@ -1,11 +1,12 @@
package janggi.domain.piece;
-import janggi.domain.Board;
import janggi.domain.Delta;
+import janggi.domain.Palace;
import janggi.domain.Position;
-import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.movepath.FixedMovePath;
+import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
import java.util.List;
import java.util.Optional;
@@ -14,11 +15,13 @@ public class Jol implements Piece {
private final TeamType teamType;
private final PieceType pieceType;
private final List paths;
+ private final Palace palace;
public Jol(TeamType teamType) {
this.teamType = teamType;
pieceType = PieceType.JOL;
paths = createPaths();
+ palace = new Palace();
}
@Override
@@ -33,13 +36,21 @@ public Optional findMovePath(int startX, int startY, int endX,
if (isSamePosition(dx, dy)) {
return Optional.empty();
}
- return paths.stream()
+ Optional normalPath = paths.stream()
.filter(path -> path.matches(dx, dy))
.findFirst();
+ if (normalPath.isPresent()) {
+ return normalPath;
+ }
+ return palace.findForwardDiagonalStepPath(
+ new Position(startX, startY),
+ new Position(endX, endY),
+ teamType
+ );
}
@Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
+ public boolean canMove(MoveRoute moveRoute) {
return true;
}
@@ -76,4 +87,9 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Ma.java b/src/main/java/janggi/domain/piece/Ma.java
index c1205bf186..cae94f7748 100644
--- a/src/main/java/janggi/domain/piece/Ma.java
+++ b/src/main/java/janggi/domain/piece/Ma.java
@@ -1,11 +1,10 @@
package janggi.domain.piece;
-import janggi.domain.Board;
import janggi.domain.Delta;
-import janggi.domain.Position;
-import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.movepath.FixedMovePath;
+import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
import java.util.List;
import java.util.Optional;
@@ -45,13 +44,8 @@ public Optional findMovePath(int startX, int startY, int endX,
}
@Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
- Optional movePath = findMovePath(start.getX(), start.getY(), end.getX(), end.getY());
- if (movePath.isEmpty()) {
- return false;
- }
- return movePath.get().intermediatePositions(start, end).stream()
- .noneMatch(board::hasPiece);
+ public boolean canMove(MoveRoute moveRoute) {
+ return moveRoute.intermediatePieceTypes().isEmpty();
}
@Override
@@ -68,4 +62,9 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java
index 65196e367b..ff54b4e92f 100644
--- a/src/main/java/janggi/domain/piece/Piece.java
+++ b/src/main/java/janggi/domain/piece/Piece.java
@@ -1,9 +1,8 @@
package janggi.domain.piece;
-import janggi.domain.Board;
-import janggi.domain.Position;
import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
import java.util.Optional;
public interface Piece {
@@ -12,11 +11,13 @@ public interface Piece {
Optional findMovePath(int startX, int startY, int endX, int endY);
- boolean isObstaclesNotExist(Position start, Position end, Board board);
+ boolean canMove(MoveRoute moveRoute);
String nickname();
PieceType getPieceType();
TeamType getTeamType();
+
+ int getScore();
}
diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java
index 2b6fc7d98f..1c25338efa 100644
--- a/src/main/java/janggi/domain/piece/PieceType.java
+++ b/src/main/java/janggi/domain/piece/PieceType.java
@@ -1,22 +1,28 @@
package janggi.domain.piece;
public enum PieceType {
- CHA("차"),
- GUNG("궁"),
- JOL("졸"),
- MA("마"),
- PO("포"),
- SA("사"),
- SANG("상"),
+ CHA("차", 13),
+ GUNG("궁", 0),
+ JOL("졸", 2),
+ MA("마", 5),
+ PO("포", 7),
+ SA("사", 3),
+ SANG("상", 3),
;
- PieceType(String nickname) {
+ PieceType(String nickname, int score) {
this.nickname = nickname;
+ this.score = score;
}
private final String nickname;
+ private final int score;
public String getNickname() {
return nickname;
}
+
+ public int getScore() {
+ return score;
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Po.java b/src/main/java/janggi/domain/piece/Po.java
index 3a86933376..6356ed1c44 100644
--- a/src/main/java/janggi/domain/piece/Po.java
+++ b/src/main/java/janggi/domain/piece/Po.java
@@ -1,11 +1,12 @@
package janggi.domain.piece;
-import janggi.domain.Board;
import janggi.domain.Delta;
+import janggi.domain.Palace;
import janggi.domain.Position;
-import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.movepath.DirectionalMovePath;
+import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
import java.util.List;
import java.util.Optional;
@@ -14,6 +15,7 @@ public class Po implements Piece {
private final TeamType teamType;
private final PieceType pieceType;
private final List paths;
+ private final Palace palace;
public Po(TeamType teamType) {
this.teamType = teamType;
@@ -24,6 +26,7 @@ public Po(TeamType teamType) {
new DirectionalMovePath(List.of(Delta.createLeft())),
new DirectionalMovePath(List.of(Delta.createRight()))
);
+ palace = new Palace();
}
@Override
@@ -38,31 +41,30 @@ public Optional findMovePath(int startX, int startY, int endX,
}
int dx = endX - startX;
int dy = endY - startY;
- return paths.stream()
+ Optional normalPath = paths.stream()
.filter(path -> path.matches(dx, dy))
.findFirst();
+ if (normalPath.isPresent()) {
+ return normalPath;
+ }
+ return palace.findDiagonalMovePath(
+ new Position(startX, startY),
+ new Position(endX, endY)
+ );
}
@Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
- Optional movePath = findMovePath(start.getX(), start.getY(), end.getX(), end.getY());
- if (movePath.isEmpty()) {
- return false;
- }
- List intermediatePositions = movePath.get().intermediatePositions(start, end);
- List obstacles = intermediatePositions.stream()
- .map(board::findPiece)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .toList();
+ public boolean canMove(MoveRoute moveRoute) {
+ List obstacles = moveRoute.intermediatePieceTypes();
if (obstacles.size() != 1) {
return false;
}
- if (obstacles.getFirst().getPieceType() == PieceType.PO) {
+ if (obstacles.getFirst() == PieceType.PO) {
return false;
}
- Optional targetPiece = board.findPiece(end);
- return targetPiece.isEmpty() || !(targetPiece.get().getPieceType() == PieceType.PO);
+ return moveRoute.targetPieceType()
+ .map(targetPieceType -> targetPieceType != PieceType.PO)
+ .orElse(true);
}
@Override
@@ -79,4 +81,9 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Sa.java b/src/main/java/janggi/domain/piece/Sa.java
index 2d6ac683d6..b89cea6435 100644
--- a/src/main/java/janggi/domain/piece/Sa.java
+++ b/src/main/java/janggi/domain/piece/Sa.java
@@ -1,33 +1,22 @@
package janggi.domain.piece;
-import janggi.domain.Board;
-import janggi.domain.Delta;
+import janggi.domain.Palace;
import janggi.domain.Position;
import janggi.domain.movepath.MovePathStrategy;
-import janggi.domain.movepath.FixedMovePath;
import janggi.domain.team.TeamType;
-import java.util.List;
+import janggi.dto.MoveRoute;
import java.util.Optional;
public class Sa implements Piece {
private final TeamType teamType;
private final PieceType pieceType;
- private final List paths;
+ private final Palace palace;
public Sa(TeamType teamType) {
this.teamType = teamType;
pieceType = PieceType.SA;
- paths = List.of(
- new FixedMovePath(List.of(Delta.createUp())),
- new FixedMovePath(List.of(Delta.createDown())),
- new FixedMovePath(List.of(Delta.createLeft())),
- new FixedMovePath(List.of(Delta.createRight())),
- new FixedMovePath(List.of(Delta.createRightUp())),
- new FixedMovePath(List.of(Delta.createRightDown())),
- new FixedMovePath(List.of(Delta.createLeftUp())),
- new FixedMovePath(List.of(Delta.createLeftDown()))
- );
+ palace = new Palace();
}
@Override
@@ -37,34 +26,14 @@ public boolean isValidMovePattern(int startX, int startY, int endX, int endY) {
@Override
public Optional findMovePath(int startX, int startY, int endX, int endY) {
- int dx = endX - startX;
- int dy = endY - startY;
- int distanceX = Math.abs(dx);
- int distanceY = Math.abs(dy);
- if (isSamePosition(distanceX, distanceY)) {
- return Optional.empty();
- }
- if (!isOneStep(distanceX, distanceY)) {
- return Optional.empty();
- }
- return paths.stream()
- .filter(path -> path.matches(dx, dy))
- .findFirst();
+ return palace.findOneStepMovePath(new Position(startX, startY), new Position(endX, endY));
}
@Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
+ public boolean canMove(MoveRoute moveRoute) {
return true;
}
- private boolean isSamePosition(int distanceX, int distanceY) {
- return distanceX == 0 && distanceY == 0;
- }
-
- private boolean isOneStep(int distanceX, int distanceY) {
- return distanceX <= 1 && distanceY <= 1;
- }
-
@Override
public String nickname() {
return pieceType.getNickname();
@@ -79,4 +48,9 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
}
diff --git a/src/main/java/janggi/domain/piece/Sang.java b/src/main/java/janggi/domain/piece/Sang.java
index 2c079c5070..d3a55f51e2 100644
--- a/src/main/java/janggi/domain/piece/Sang.java
+++ b/src/main/java/janggi/domain/piece/Sang.java
@@ -1,11 +1,10 @@
package janggi.domain.piece;
-import janggi.domain.Board;
import janggi.domain.Delta;
-import janggi.domain.Position;
-import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.movepath.FixedMovePath;
+import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
import java.util.List;
import java.util.Optional;
@@ -45,13 +44,8 @@ public Optional findMovePath(int startX, int startY, int endX,
}
@Override
- public boolean isObstaclesNotExist(Position start, Position end, Board board) {
- Optional movePath = findMovePath(start.getX(), start.getY(), end.getX(), end.getY());
- if (movePath.isEmpty()) {
- return false;
- }
- return movePath.get().intermediatePositions(start, end).stream()
- .noneMatch(board::hasPiece);
+ public boolean canMove(MoveRoute moveRoute) {
+ return moveRoute.intermediatePieceTypes().isEmpty();
}
@Override
@@ -68,4 +62,9 @@ public PieceType getPieceType() {
public TeamType getTeamType() {
return teamType;
}
+
+ @Override
+ public int getScore() {
+ return pieceType.getScore();
+ }
}
diff --git a/src/main/java/janggi/domain/team/Team.java b/src/main/java/janggi/domain/team/Team.java
index 15954aa5d5..c6b219f2d7 100644
--- a/src/main/java/janggi/domain/team/Team.java
+++ b/src/main/java/janggi/domain/team/Team.java
@@ -3,6 +3,7 @@
import janggi.domain.Pieces;
import janggi.domain.Position;
import janggi.domain.piece.Piece;
+import janggi.domain.piece.PieceType;
import janggi.dto.BoardSpots;
import java.util.Optional;
@@ -32,6 +33,10 @@ public Optional findPiece(Position position) {
return pieces.findPiece(position);
}
+ public boolean hasPieceType(PieceType pieceType) {
+ return pieces.hasPieceType(pieceType);
+ }
+
public Team remove(Position position) {
return new Team(teamType, pieces.remove(position));
}
@@ -39,4 +44,8 @@ public Team remove(Position position) {
public Team move(Position piecePosition, Position targetPosition) {
return new Team(teamType, pieces.move(piecePosition, targetPosition));
}
+
+ public int calculatePiecesScore() {
+ return pieces.sumScore();
+ }
}
diff --git a/src/main/java/janggi/dto/MoveRoute.java b/src/main/java/janggi/dto/MoveRoute.java
new file mode 100644
index 0000000000..bd95f91004
--- /dev/null
+++ b/src/main/java/janggi/dto/MoveRoute.java
@@ -0,0 +1,12 @@
+package janggi.dto;
+
+import janggi.domain.piece.PieceType;
+import java.util.List;
+import java.util.Optional;
+
+public record MoveRoute(
+ List intermediatePieceTypes,
+ Optional targetPieceType
+) {
+
+}
diff --git a/src/main/java/janggi/exception/PersistenceTimeoutException.java b/src/main/java/janggi/exception/PersistenceTimeoutException.java
new file mode 100644
index 0000000000..a20372b2fe
--- /dev/null
+++ b/src/main/java/janggi/exception/PersistenceTimeoutException.java
@@ -0,0 +1,8 @@
+package janggi.exception;
+
+public class PersistenceTimeoutException extends RuntimeException {
+
+ public PersistenceTimeoutException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/janggi/persistence/DatabaseInitializer.java b/src/main/java/janggi/persistence/DatabaseInitializer.java
new file mode 100644
index 0000000000..28e180c34e
--- /dev/null
+++ b/src/main/java/janggi/persistence/DatabaseInitializer.java
@@ -0,0 +1,44 @@
+package janggi.persistence;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class DatabaseInitializer {
+
+ private static final String JDBC_URL = "jdbc:sqlite:janggi.db";
+ private static final String SCHEMA_FILE_PATH = "schema.sql";
+
+ public void initialize() {
+ try (
+ Connection connection = DriverManager.getConnection(JDBC_URL);
+ Statement statement = connection.createStatement()
+ ) {
+ executeSchema(statement, readSchema());
+ } catch (SQLException | IOException e) {
+ throw new IllegalStateException("데이터베이스 초기화에 실패했습니다.", e);
+ }
+ }
+
+ private String readSchema() throws IOException {
+ try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(SCHEMA_FILE_PATH)) {
+ if (inputStream == null) {
+ throw new IllegalStateException("schema.sql 파일을 찾을 수 없습니다.");
+ }
+ return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+
+ private void executeSchema(Statement statement, String schema) throws SQLException {
+ for (String query : schema.split(";")) {
+ String trimmedQuery = query.trim();
+ if (!trimmedQuery.isEmpty()) {
+ statement.executeUpdate(trimmedQuery);
+ }
+ }
+ }
+}
diff --git a/src/main/java/janggi/persistence/GameStatus.java b/src/main/java/janggi/persistence/GameStatus.java
new file mode 100644
index 0000000000..130c41fa5a
--- /dev/null
+++ b/src/main/java/janggi/persistence/GameStatus.java
@@ -0,0 +1,7 @@
+package janggi.persistence;
+
+public enum GameStatus {
+ BEFORE_START,
+ IN_PROGRESS,
+ FINISHED,
+}
diff --git a/src/main/java/janggi/persistence/dto/MoveHistory.java b/src/main/java/janggi/persistence/dto/MoveHistory.java
new file mode 100644
index 0000000000..feabbba006
--- /dev/null
+++ b/src/main/java/janggi/persistence/dto/MoveHistory.java
@@ -0,0 +1,10 @@
+package janggi.persistence.dto;
+
+public record MoveHistory(
+ int turnNumber,
+ int startX,
+ int startY,
+ int endX,
+ int endY
+) {
+}
diff --git a/src/main/java/janggi/persistence/model/JanggiGameHistory.java b/src/main/java/janggi/persistence/model/JanggiGameHistory.java
new file mode 100644
index 0000000000..26cccae630
--- /dev/null
+++ b/src/main/java/janggi/persistence/model/JanggiGameHistory.java
@@ -0,0 +1,34 @@
+package janggi.persistence.model;
+
+import janggi.persistence.GameStatus;
+import janggi.persistence.dto.MoveHistory;
+import java.util.List;
+
+public class JanggiGameHistory {
+
+ private final long gameId;
+ private final List moveHistories;
+ private final GameStatus gameStatus;
+
+ public JanggiGameHistory(long gameId, List moveHistories, GameStatus gameStatus) {
+ this.gameId = gameId;
+ this.moveHistories = moveHistories;
+ this.gameStatus = gameStatus;
+ }
+
+ public static JanggiGameHistory createEmpty() {
+ return new JanggiGameHistory(0L, List.of(), GameStatus.BEFORE_START);
+ }
+
+ public long getGameId() {
+ return gameId;
+ }
+
+ public List getMoveHistories() {
+ return moveHistories;
+ }
+
+ public boolean isBeforeStart() {
+ return gameStatus == GameStatus.BEFORE_START;
+ }
+}
diff --git a/src/main/java/janggi/persistence/repository/JanggiGameRepository.java b/src/main/java/janggi/persistence/repository/JanggiGameRepository.java
new file mode 100644
index 0000000000..d8a420325a
--- /dev/null
+++ b/src/main/java/janggi/persistence/repository/JanggiGameRepository.java
@@ -0,0 +1,16 @@
+package janggi.persistence.repository;
+
+import janggi.domain.Position;
+import janggi.persistence.GameStatus;
+import janggi.persistence.model.JanggiGameHistory;
+
+public interface JanggiGameRepository {
+
+ long createNewGame();
+
+ void saveMove(long gameId, int turnNumber, Position startPosition, Position endPosition);
+
+ void update(GameStatus gameStatus, long gameId);
+
+ JanggiGameHistory findRecentGame();
+}
diff --git a/src/main/java/janggi/persistence/repository/JdbcJanggiGameRepository.java b/src/main/java/janggi/persistence/repository/JdbcJanggiGameRepository.java
new file mode 100644
index 0000000000..c956d0ec24
--- /dev/null
+++ b/src/main/java/janggi/persistence/repository/JdbcJanggiGameRepository.java
@@ -0,0 +1,154 @@
+package janggi.persistence.repository;
+
+import janggi.domain.Position;
+import janggi.exception.PersistenceTimeoutException;
+import janggi.persistence.GameStatus;
+import janggi.persistence.dto.MoveHistory;
+import janggi.persistence.model.JanggiGameHistory;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JdbcJanggiGameRepository implements JanggiGameRepository {
+
+ private static final String JDBC_URL = "jdbc:sqlite:janggi.db";
+ private static final int QUERY_TIMEOUT_SECONDS = 3;
+ private static final String FIND_RECENT_GAME_SQL =
+ """
+ SELECT id, status
+ FROM janggi_game
+ WHERE status = ?
+ ORDER BY id DESC
+ LIMIT 1
+ """;
+ private static final String FIND_MOVE_HISTORY_SQL =
+ """
+ SELECT turn_number, start_x, start_y, end_x, end_y
+ FROM move_history
+ WHERE game_id = ?
+ ORDER BY turn_number ASC
+ """;
+ private static final String UPDATE_GAME_STATUS_SQL =
+ """
+ UPDATE janggi_game
+ SET status = ?
+ WHERE id = ?
+ """;
+ private static final String CREATE_SQL =
+ """
+ INSERT INTO janggi_game(status)
+ VALUES (?)
+ """;
+ private static final String SAVE_MOVE_HISTORY_SQL =
+ """
+ INSERT INTO move_history(game_id, turn_number, start_x, start_y, end_x, end_y)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """;
+ private static final String FIND_LAST_INSERTED_ID_SQL = "SELECT last_insert_rowid()";
+
+ @Override
+ public long createNewGame() {
+ try (
+ Connection connection = DriverManager.getConnection(JDBC_URL);
+ PreparedStatement statement = connection.prepareStatement(CREATE_SQL)
+ ) {
+ statement.setString(1, GameStatus.IN_PROGRESS.name());
+ statement.executeUpdate();
+ return findLastInsertedId(connection);
+ } catch (SQLException e) {
+ throw new IllegalStateException("게임 생성에 실패했습니다.", e);
+ }
+ }
+
+ @Override
+ public void saveMove(long gameId, int turnNumber, Position startPosition, Position endPosition) {
+ try (
+ Connection connection = DriverManager.getConnection(JDBC_URL);
+ PreparedStatement statement = connection.prepareStatement(SAVE_MOVE_HISTORY_SQL)
+ ) {
+ statement.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
+ statement.setLong(1, gameId);
+ statement.setInt(2, turnNumber);
+ statement.setInt(3, startPosition.getX());
+ statement.setInt(4, startPosition.getY());
+ statement.setInt(5, endPosition.getX());
+ statement.setInt(6, endPosition.getY());
+ statement.executeUpdate();
+ } catch (SQLTimeoutException e) {
+ throw new PersistenceTimeoutException("타임아웃입니다. 재시도합니다.", e);
+ } catch (SQLException e) {
+ throw new IllegalStateException("수 저장에 실패했습니다.", e);
+ }
+ }
+
+ @Override
+ public void update(GameStatus gameStatus, long gameId) {
+ try (
+ Connection connection = DriverManager.getConnection(JDBC_URL);
+ PreparedStatement statement = connection.prepareStatement(UPDATE_GAME_STATUS_SQL)
+ ) {
+ statement.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
+ statement.setString(1, gameStatus.name());
+ statement.setLong(2, gameId);
+ statement.executeUpdate();
+ } catch (SQLTimeoutException e) {
+ throw new PersistenceTimeoutException("타임아웃입니다. 재시도합니다.", e);
+ } catch (SQLException e) {
+ throw new IllegalStateException("게임 상태 변경에 실패했습니다.", e);
+ }
+ }
+
+ @Override
+ public JanggiGameHistory findRecentGame() {
+ try (
+ Connection connection = DriverManager.getConnection(JDBC_URL);
+ PreparedStatement recentGameStatement = connection.prepareStatement(FIND_RECENT_GAME_SQL)
+ ) {
+ recentGameStatement.setString(1, GameStatus.IN_PROGRESS.name());
+ ResultSet recentGameResultSet = recentGameStatement.executeQuery();
+ if (!recentGameResultSet.next()) {
+ return JanggiGameHistory.createEmpty();
+ }
+ long gameId = recentGameResultSet.getLong("id");
+ GameStatus gameStatus = GameStatus.valueOf(recentGameResultSet.getString("status"));
+ List moveHistories = findMoveHistories(connection, FIND_MOVE_HISTORY_SQL, gameId);
+ return new JanggiGameHistory(gameId, moveHistories, gameStatus);
+ } catch (SQLException e) {
+ throw new IllegalStateException("최근 게임 조회에 실패했습니다.", e);
+ }
+ }
+
+ private long findLastInsertedId(Connection connection) throws SQLException {
+ try (
+ PreparedStatement statement = connection.prepareStatement(FIND_LAST_INSERTED_ID_SQL)
+ ) {
+ ResultSet resultSet = statement.executeQuery();
+ resultSet.next();
+ return resultSet.getLong(1);
+ }
+ }
+
+ private List findMoveHistories(Connection connection, String sql, long gameId) throws SQLException {
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ statement.setLong(1, gameId);
+ ResultSet resultSet = statement.executeQuery();
+
+ List moveHistories = new ArrayList<>();
+ while (resultSet.next()) {
+ moveHistories.add(new MoveHistory(
+ resultSet.getInt("turn_number"),
+ resultSet.getInt("start_x"),
+ resultSet.getInt("start_y"),
+ resultSet.getInt("end_x"),
+ resultSet.getInt("end_y")
+ ));
+ }
+ return moveHistories;
+ }
+ }
+}
diff --git a/src/main/java/janggi/service/JanggiService.java b/src/main/java/janggi/service/JanggiService.java
new file mode 100644
index 0000000000..4d2a83372c
--- /dev/null
+++ b/src/main/java/janggi/service/JanggiService.java
@@ -0,0 +1,52 @@
+package janggi.service;
+
+import janggi.domain.JanggiGame;
+import janggi.domain.Position;
+import janggi.persistence.GameStatus;
+import janggi.persistence.dto.MoveHistory;
+import janggi.persistence.model.JanggiGameHistory;
+import janggi.persistence.repository.JanggiGameRepository;
+
+public class JanggiService {
+
+ private final JanggiGameRepository janggiGameRepository;
+ private long currentGameId;
+
+ public JanggiService(JanggiGameRepository janggiGameRepository) {
+ this.janggiGameRepository = janggiGameRepository;
+ }
+
+ public JanggiGame loadGame() {
+ JanggiGameHistory recentGame = janggiGameRepository.findRecentGame();
+ if (recentGame.isBeforeStart()) {
+ currentGameId = janggiGameRepository.createNewGame();
+ return JanggiGame.createInitialJanggiGame();
+ }
+ currentGameId = recentGame.getGameId();
+ return rebuildJanggiGame(recentGame);
+ }
+
+ public void play(JanggiGame janggiGame, Position startPosition, Position endPosition) {
+ janggiGame.doGame(startPosition, endPosition);
+ saveMove(janggiGame, startPosition, endPosition);
+ }
+
+ public void saveMove(JanggiGame janggiGame, Position startPosition, Position endPosition) {
+ janggiGameRepository.saveMove(currentGameId, janggiGame.getTurnCount(), startPosition, endPosition);
+ }
+
+ public void finishGame() {
+ janggiGameRepository.update(GameStatus.FINISHED, currentGameId);
+ }
+
+ private JanggiGame rebuildJanggiGame(JanggiGameHistory recentGame) {
+ JanggiGame janggiGame = JanggiGame.createInitialJanggiGame();
+ for (MoveHistory moveHistory : recentGame.getMoveHistories()) {
+ janggiGame.doGame(
+ new Position(moveHistory.startX(), moveHistory.startY()),
+ new Position(moveHistory.endX(), moveHistory.endY())
+ );
+ }
+ return janggiGame;
+ }
+}
diff --git a/src/main/java/janggi/util/PersistenceExecutor.java b/src/main/java/janggi/util/PersistenceExecutor.java
new file mode 100644
index 0000000000..1632966530
--- /dev/null
+++ b/src/main/java/janggi/util/PersistenceExecutor.java
@@ -0,0 +1,37 @@
+package janggi.util;
+
+import janggi.exception.PersistenceTimeoutException;
+import java.util.function.Supplier;
+
+public class PersistenceExecutor {
+
+ private static final int MAX_RETRY_COUNT = 3;
+
+ public static T retryOnTimeout(Supplier supplier, Runnable onTimeout) {
+ return retryOnTimeout(supplier, onTimeout, 0);
+ }
+
+ public static void retryOnTimeout(Runnable action, Runnable onTimeout) {
+ retryOnTimeout(() -> {
+ action.run();
+ return null;
+ }, onTimeout, 0);
+ }
+
+ private static T retryOnTimeout(Supplier supplier, Runnable onTimeout, int retryCount) {
+ try {
+ return supplier.get();
+ } catch (PersistenceTimeoutException e) {
+ int nextRetryCount = retryCount + 1;
+ checkRetryCount(e, nextRetryCount);
+ onTimeout.run();
+ return retryOnTimeout(supplier, onTimeout, nextRetryCount);
+ }
+ }
+
+ private static void checkRetryCount(PersistenceTimeoutException e, int retryCount) {
+ if (retryCount >= MAX_RETRY_COUNT) {
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/janggi/view/input/ConsoleInputView.java b/src/main/java/janggi/view/input/ConsoleInputView.java
index e38b5d20af..d691a34a97 100644
--- a/src/main/java/janggi/view/input/ConsoleInputView.java
+++ b/src/main/java/janggi/view/input/ConsoleInputView.java
@@ -1,21 +1,29 @@
package janggi.view.input;
+import java.util.Optional;
import java.util.Scanner;
public class ConsoleInputView implements InputView {
+ private static final String CANCEL = "cancel";
+
private final Scanner scanner;
public ConsoleInputView() {
scanner = new Scanner(System.in);
}
- public ConsoleInputView(Scanner scanner) {
- this.scanner = scanner;
- }
-
@Override
public String readLine() {
return scanner.nextLine();
}
+
+ @Override
+ public Optional readCancelableLine() {
+ String command = readLine();
+ if (CANCEL.equalsIgnoreCase(command.trim())) {
+ return Optional.empty();
+ }
+ return Optional.of(command);
+ }
}
diff --git a/src/main/java/janggi/view/input/InputView.java b/src/main/java/janggi/view/input/InputView.java
index 762abdce6c..5d1e666dd0 100644
--- a/src/main/java/janggi/view/input/InputView.java
+++ b/src/main/java/janggi/view/input/InputView.java
@@ -1,7 +1,10 @@
package janggi.view.input;
+import java.util.Optional;
+
public interface InputView {
String readLine();
+ Optional readCancelableLine();
}
diff --git a/src/main/java/janggi/view/output/ConsoleOutputView.java b/src/main/java/janggi/view/output/ConsoleOutputView.java
index e4acf9e503..68d16e1a79 100644
--- a/src/main/java/janggi/view/output/ConsoleOutputView.java
+++ b/src/main/java/janggi/view/output/ConsoleOutputView.java
@@ -66,7 +66,17 @@ public void printAskPiecePosition() {
@Override
public void printAskMovePosition(String nickname) {
- printMessage(nickname + "의 목적 좌표를 입력해주세요. (ex. 1,3)");
+ printMessage(nickname + "의 목적 좌표를 입력해주세요. (ex. 1,3) / 말 선택을 취소하시려면 cancel을 입력해주세요.");
+ }
+
+ @Override
+ public void printResult(BoardSpots boardSpots, TeamType winner, int winnerScore) {
+ printBoard(boardSpots);
+ printWinner(winner, winnerScore);
+ }
+
+ private void printWinner(TeamType winner, int winnerScore) {
+ printMessage(formatTeamType(winner) + "의 승리입니다. 점수: " + winnerScore);
}
private void printHeader() {
diff --git a/src/main/java/janggi/view/output/OutputView.java b/src/main/java/janggi/view/output/OutputView.java
index 29b842e022..e7658a611c 100644
--- a/src/main/java/janggi/view/output/OutputView.java
+++ b/src/main/java/janggi/view/output/OutputView.java
@@ -20,4 +20,6 @@ public interface OutputView {
void printAskPiecePosition();
void printAskMovePosition(String nickname);
+
+ void printResult(BoardSpots boardSpots, TeamType winner, int winnerScore);
}
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 0000000000..3b27ecce18
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS janggi_game
+(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ status TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS move_history
+(
+ game_id INTEGER NOT NULL,
+ turn_number INTEGER NOT NULL,
+ start_x INTEGER NOT NULL,
+ start_y INTEGER NOT NULL,
+ end_x INTEGER NOT NULL,
+ end_y INTEGER NOT NULL,
+ PRIMARY KEY (game_id, turn_number),
+ FOREIGN KEY (game_id) REFERENCES janggi_game (id)
+);
diff --git a/src/test/java/janggi/domain/BoardTest.java b/src/test/java/janggi/domain/BoardTest.java
index c0b85687c7..3d1a263637 100644
--- a/src/test/java/janggi/domain/BoardTest.java
+++ b/src/test/java/janggi/domain/BoardTest.java
@@ -20,7 +20,7 @@ void cannotMoveWhenStartPositionIsOutOfRange() {
Position outOfBound = new Position(0, 0);
// when & then
- assertThatThrownBy(() -> board.canMove(outOfBound, new Position(1, 4), TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(outOfBound, new Position(1, 4), TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("입력한 좌표가 장기판 범위 밖입니다.");
}
@@ -33,7 +33,7 @@ void cannotMoveWhenEndPositionIsOutOfRange() {
Position outOfBound = new Position(1, 11);
// when & then
- assertThatThrownBy(() -> board.canMove(new Position(1, 4), outOfBound, TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(new Position(1, 4), outOfBound, TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("입력한 좌표가 장기판 범위 밖입니다.");
}
@@ -46,7 +46,7 @@ void cannotMoveWhenStartPositionHasNoCurrentTeamPiece() {
Position notExistPosition = new Position(2, 2);
// when & then
- assertThatThrownBy(() -> board.canMove(notExistPosition, new Position(1, 1), TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(notExistPosition, new Position(1, 1), TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("입력한 위치에 기물이 없습니다.");
}
@@ -59,7 +59,7 @@ void cannotStartAtOpponentTeamPosition() {
Position opponentTeamPosition = new Position(1, 7);
// when & then
- assertThatThrownBy(() -> board.canMove(opponentTeamPosition, new Position(1, 6), TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(opponentTeamPosition, new Position(1, 6), TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("입력한 위치에 기물이 없습니다.");
}
@@ -72,7 +72,7 @@ void cannotMoveToSameTeamPiecePosition() {
Position currentTeamPosition = new Position(2, 1);
// when & then
- assertThatThrownBy(() -> board.canMove(new Position(1, 1), currentTeamPosition, TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(new Position(1, 1), currentTeamPosition, TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("같은 팀의 기물이 있는 위치로는 이동할 수 없습니다.");
}
@@ -86,7 +86,30 @@ void cannotMoveWhenMovePatternIsInvalid() {
Position saEndPosition = new Position(4, 3);
// when & then
- assertThatThrownBy(() -> board.canMove(saStartPosition, saEndPosition, TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(saStartPosition, saEndPosition, TeamType.CHU))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("이동할 수 없는 위치입니다.");
+ }
+
+ @Test
+ @DisplayName("궁은 궁성 대각선으로 이동할 수 있다.")
+ void canMoveGungDiagonallyInsidePalace() {
+ // given
+ Board board = Board.createInitialBoard();
+
+ // when & then
+ assertThatCode(() -> board.validateMove(new Position(5, 2), new Position(4, 3), TeamType.CHU))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("궁은 궁성 밖으로 이동할 수 없다.")
+ void cannotMoveGungOutsidePalace() {
+ // given
+ Board board = Board.createInitialBoard();
+
+ // when & then
+ assertThatThrownBy(() -> board.validateMove(new Position(5, 2), new Position(3, 2), TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("이동할 수 없는 위치입니다.");
}
@@ -100,7 +123,7 @@ void cannotMoveChaWhenPathIsBlocked() {
Position chaEndPosition = new Position(1, 5);
// when & then
- assertThatThrownBy(() -> board.canMove(chaStartPosition, chaEndPosition, TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(chaStartPosition, chaEndPosition, TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("이동할 수 없는 위치입니다.");
}
@@ -114,7 +137,7 @@ void cannotMoveMaWhenLegIsBlocked() {
Position maEndPosition = new Position(4, 2);
// when & then
- assertThatThrownBy(() -> board.canMove(maStartPosition, maEndPosition, TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(maStartPosition, maEndPosition, TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("이동할 수 없는 위치입니다.");
}
@@ -128,11 +151,67 @@ void cannotMovePoWithoutBridge() {
Position poEndPosition = new Position(2, 6);
// when & then
- assertThatThrownBy(() -> board.canMove(poStartPosition, poEndPosition, TeamType.CHU))
+ assertThatThrownBy(() -> board.validateMove(poStartPosition, poEndPosition, TeamType.CHU))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("이동할 수 없는 위치입니다.");
}
+ @Test
+ @DisplayName("졸은 궁성 안에서 전진 대각선으로 이동할 수 있다.")
+ void canMoveJolDiagonallyInsidePalace() {
+ // given
+ Board board = Board.createInitialBoard();
+ Board firstMovedBoard = board.move(new Position(5, 7), new Position(5, 6), TeamType.HAN);
+ Board secondMovedBoard = firstMovedBoard.move(new Position(5, 6), new Position(5, 5), TeamType.HAN);
+ Board thirdMovedBoard = secondMovedBoard.move(new Position(5, 5), new Position(5, 4), TeamType.HAN);
+ Board fourthMovedBoard = thirdMovedBoard.move(new Position(5, 4), new Position(5, 3), TeamType.HAN);
+ Board movedBoard = fourthMovedBoard.move(new Position(5, 3), new Position(4, 3), TeamType.HAN);
+
+ // when & then
+ assertThatCode(() -> movedBoard.validateMove(new Position(4, 3), new Position(5, 2), TeamType.HAN))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("궁성 대각선 경로가 막혀 있으면 차는 이동할 수 없다.")
+ void cannotMoveChaDiagonallyInsidePalaceWhenRouteIsBlocked() {
+ // given
+ Board board = Board.createInitialBoard();
+ Board firstMovedBoard = board.move(new Position(5, 2), new Position(5, 3), TeamType.CHU);
+ Board secondMovedBoard = firstMovedBoard.move(new Position(4, 1), new Position(4, 2), TeamType.CHU);
+ Board thirdMovedBoard = secondMovedBoard.move(new Position(1, 4), new Position(2, 4), TeamType.CHU);
+ Board fourthMovedBoard = thirdMovedBoard.move(new Position(2, 3), new Position(2, 6), TeamType.CHU);
+ Board fifthMovedBoard = fourthMovedBoard.move(new Position(4, 2), new Position(5, 2), TeamType.CHU);
+ Board sixthMovedBoard = fifthMovedBoard.move(new Position(1, 1), new Position(1, 3), TeamType.CHU);
+ Board seventhMovedBoard = sixthMovedBoard.move(new Position(1, 3), new Position(4, 3), TeamType.CHU);
+ Board movedBoard = seventhMovedBoard.move(new Position(4, 3), new Position(4, 1), TeamType.CHU);
+
+ // when & then
+ assertThatThrownBy(() -> movedBoard.validateMove(new Position(4, 1), new Position(6, 3), TeamType.CHU))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("이동할 수 없는 위치입니다.");
+ }
+
+ @Test
+ @DisplayName("궁성 대각선 경로가 비어 있으면 차는 이동할 수 있다.")
+ void canMoveChaDiagonallyInsidePalaceWhenRouteIsEmpty() {
+ // given
+ Board board = Board.createInitialBoard();
+ Board firstMovedBoard = board.move(new Position(5, 2), new Position(5, 3), TeamType.CHU);
+ Board secondMovedBoard = firstMovedBoard.move(new Position(4, 1), new Position(4, 2), TeamType.CHU);
+ Board thirdMovedBoard = secondMovedBoard.move(new Position(1, 4), new Position(2, 4), TeamType.CHU);
+ Board fourthMovedBoard = thirdMovedBoard.move(new Position(2, 3), new Position(2, 6), TeamType.CHU);
+ Board fifthMovedBoard = fourthMovedBoard.move(new Position(4, 2), new Position(5, 2), TeamType.CHU);
+ Board sixthMovedBoard = fifthMovedBoard.move(new Position(1, 1), new Position(1, 3), TeamType.CHU);
+ Board seventhMovedBoard = sixthMovedBoard.move(new Position(1, 3), new Position(4, 3), TeamType.CHU);
+ Board eighthMovedBoard = seventhMovedBoard.move(new Position(4, 3), new Position(4, 1), TeamType.CHU);
+ Board movedBoard = eighthMovedBoard.move(new Position(5, 2), new Position(6, 2), TeamType.CHU);
+
+ // when & then
+ assertThatCode(() -> movedBoard.validateMove(new Position(4, 1), new Position(6, 3), TeamType.CHU))
+ .doesNotThrowAnyException();
+ }
+
@Test
@DisplayName("이동 패턴과 장애물 조건을 모두 만족하면 이동할 수 있다.")
void canMoveWhenPatternAndObstacleRulesAreSatisfied() {
@@ -148,9 +227,9 @@ void canMoveWhenPatternAndObstacleRulesAreSatisfied() {
// when & then
assertAll(
- () -> assertThatCode(() -> movedBoard.canMove(chaStartPosition, chaEndPosition, TeamType.CHU))
+ () -> assertThatCode(() -> movedBoard.validateMove(chaStartPosition, chaEndPosition, TeamType.CHU))
.doesNotThrowAnyException(),
- () -> assertThatCode(() -> movedBoard.canMove(poStartPosition, poEndPosition, TeamType.CHU))
+ () -> assertThatCode(() -> movedBoard.validateMove(poStartPosition, poEndPosition, TeamType.CHU))
.doesNotThrowAnyException()
);
}
@@ -193,4 +272,54 @@ void moveCapturesOpponentPiece() {
() -> assertThat(capturedBoard.findPiece(new Position(1, 7))).get().isInstanceOf(Jol.class)
);
}
+
+ @Test
+ @DisplayName("궁이 잡히면 승자를 확인할 수 있다.")
+ void findWinnerWhenGungIsCaptured() {
+ // given
+ Board board = Board.createInitialBoard();
+
+ // when
+ Board firstMovedBoard = board.move(new Position(1, 4), new Position(2, 4), TeamType.CHU);
+ Board secondMovedBoard = firstMovedBoard.move(new Position(1, 1), new Position(1, 4), TeamType.CHU);
+ Board thirdMovedBoard = secondMovedBoard.move(new Position(1, 4), new Position(1, 7), TeamType.CHU);
+ Board fourthMovedBoard = thirdMovedBoard.move(new Position(1, 7), new Position(1, 9), TeamType.CHU);
+ Board capturedBoard = fourthMovedBoard.move(new Position(1, 9), new Position(5, 9), TeamType.CHU);
+
+ // then
+ assertAll(
+ () -> assertThat(capturedBoard.hasGung(TeamType.HAN)).isFalse(),
+ () -> assertThat(capturedBoard.findWinner()).contains(TeamType.CHU)
+ );
+ }
+
+ @Test
+ @DisplayName("초기 보드에서 팀의 기물 점수를 계산할 수 있다.")
+ void calculateInitialScore() {
+ // given
+ Board board = Board.createInitialBoard();
+
+ // when & then
+ assertAll(
+ () -> assertThat(board.calculateScore(TeamType.CHU)).isEqualTo(72),
+ () -> assertThat(board.calculateScore(TeamType.HAN)).isEqualTo(72)
+ );
+ }
+
+ @Test
+ @DisplayName("상대 기물을 잡은 뒤 팀의 기물 점수를 계산할 수 있다.")
+ void calculateScoreAfterCapture() {
+ // given
+ Position hanJolPosition = new Position(1, 7);
+ Board board = Board.createInitialBoard();
+ Board firstMovedBoard = board.move(new Position(1, 4), new Position(1, 5), TeamType.CHU);
+ Board secondMovedBoard = firstMovedBoard.move(new Position(1, 5), new Position(1, 6), TeamType.CHU);
+ Board capturedBoard = secondMovedBoard.move(new Position(1, 6), hanJolPosition, TeamType.CHU);
+
+ // when & then
+ assertAll(
+ () -> assertThat(capturedBoard.calculateScore(TeamType.CHU)).isEqualTo(72),
+ () -> assertThat(capturedBoard.calculateScore(TeamType.HAN)).isEqualTo(70)
+ );
+ }
}
diff --git a/src/test/java/janggi/domain/JanggiGameTest.java b/src/test/java/janggi/domain/JanggiGameTest.java
new file mode 100644
index 0000000000..eddd273e38
--- /dev/null
+++ b/src/test/java/janggi/domain/JanggiGameTest.java
@@ -0,0 +1,35 @@
+package janggi.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import janggi.domain.team.TeamType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class JanggiGameTest {
+
+ @Test
+ @DisplayName("궁이 잡히면 게임이 종료되고 승자가 결정된다.")
+ void gameEndsWhenGungIsCaptured() {
+ // given
+ JanggiGame janggiGame = JanggiGame.createInitialJanggiGame();
+
+ // when
+ janggiGame.doGame(new Position(1, 4), new Position(2, 4));
+ janggiGame.doGame(new Position(9, 7), new Position(8, 7));
+ janggiGame.doGame(new Position(1, 1), new Position(1, 4));
+ janggiGame.doGame(new Position(7, 7), new Position(6, 7));
+ janggiGame.doGame(new Position(1, 4), new Position(1, 7));
+ janggiGame.doGame(new Position(8, 7), new Position(7, 7));
+ janggiGame.doGame(new Position(1, 7), new Position(1, 9));
+ janggiGame.doGame(new Position(2, 10), new Position(3, 8));
+ janggiGame.doGame(new Position(1, 9), new Position(5, 9));
+
+ // then
+ assertAll(
+ () -> assertThat(janggiGame.isGameOver()).isTrue(),
+ () -> assertThat(janggiGame.findWinner()).contains(TeamType.CHU)
+ );
+ }
+}
diff --git a/src/test/java/janggi/domain/PalaceTest.java b/src/test/java/janggi/domain/PalaceTest.java
new file mode 100644
index 0000000000..d4d5afac72
--- /dev/null
+++ b/src/test/java/janggi/domain/PalaceTest.java
@@ -0,0 +1,60 @@
+package janggi.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import janggi.domain.team.TeamType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class PalaceTest {
+
+ private final Palace palace = new Palace();
+
+ @Test
+ @DisplayName("궁성 내부 좌표를 판별할 수 있다.")
+ void isInside() {
+ assertAll(
+ () -> assertThat(palace.isInside(new Position(5, 2), TeamType.CHU)).isTrue(),
+ () -> assertThat(palace.isInside(new Position(5, 9), TeamType.HAN)).isTrue(),
+ () -> assertThat(palace.isInside(new Position(5, 5))).isFalse()
+ );
+ }
+
+ @Test
+ @DisplayName("궁성 한 칸 이동 경로를 찾을 수 있다.")
+ void findOneStepMovePath() {
+ assertAll(
+ () -> assertThat(palace.findOneStepMovePath(new Position(5, 2), new Position(5, 3))).isPresent(),
+ () -> assertThat(palace.findOneStepMovePath(new Position(5, 2), new Position(4, 1))).isPresent(),
+ () -> assertThat(palace.findOneStepMovePath(new Position(4, 2), new Position(5, 1))).isEmpty()
+ );
+ }
+
+ @Test
+ @DisplayName("궁성 대각선 경로를 찾을 수 있다.")
+ void findDiagonalMovePath() {
+ assertAll(
+ () -> assertThat(palace.findDiagonalMovePath(new Position(4, 1), new Position(5, 2))).isPresent(),
+ () -> assertThat(palace.findDiagonalMovePath(new Position(4, 1), new Position(6, 3))).isPresent(),
+ () -> assertThat(palace.findDiagonalMovePath(new Position(4, 2), new Position(5, 1))).isEmpty(),
+ () -> assertThat(palace.findDiagonalMovePath(new Position(4, 1), new Position(6, 10))).isEmpty()
+ );
+ }
+
+ @Test
+ @DisplayName("졸의 궁성 전진 대각선 경로를 찾을 수 있다.")
+ void findForwardDiagonalStepPath() {
+ assertAll(
+ () -> assertThat(
+ palace.findForwardDiagonalStepPath(new Position(4, 1), new Position(5, 2), TeamType.CHU)
+ ).isPresent(),
+ () -> assertThat(
+ palace.findForwardDiagonalStepPath(new Position(5, 2), new Position(4, 1), TeamType.CHU)
+ ).isEmpty(),
+ () -> assertThat(
+ palace.findForwardDiagonalStepPath(new Position(6, 10), new Position(5, 9), TeamType.HAN)
+ ).isPresent()
+ );
+ }
+}
diff --git a/src/test/java/janggi/domain/piece/ChaTest.java b/src/test/java/janggi/domain/piece/ChaTest.java
index 57113e4aa6..a62a60a12c 100644
--- a/src/test/java/janggi/domain/piece/ChaTest.java
+++ b/src/test/java/janggi/domain/piece/ChaTest.java
@@ -3,9 +3,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
-import janggi.domain.Board;
-import janggi.domain.Position;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
+import java.util.List;
+import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -27,7 +28,20 @@ void isValidMovePatternStraight() {
}
@Test
- @DisplayName("차는 대각선으로 이동할 수 없다.")
+ @DisplayName("차는 궁성 안에서 대각선으로 이동할 수 있다.")
+ void canMoveDiagonalInsidePalace() {
+ // given
+ Cha cha = new Cha(TeamType.CHU);
+
+ // when & then
+ assertAll(
+ () -> assertThat(cha.isValidMovePattern(4, 1, 5, 2)).isTrue(),
+ () -> assertThat(cha.isValidMovePattern(4, 1, 6, 3)).isTrue()
+ );
+ }
+
+ @Test
+ @DisplayName("차는 궁성 밖 대각선이나 연결되지 않은 궁성 대각선으로 이동할 수 없다.")
void cannotMoveDiagonal() {
// given
Cha cha = new Cha(TeamType.CHU);
@@ -35,7 +49,8 @@ void cannotMoveDiagonal() {
// when & then
assertAll(
() -> assertThat(cha.isValidMovePattern(4, 4, 5, 5)).isFalse(),
- () -> assertThat(cha.isValidMovePattern(4, 4, 2, 2)).isFalse()
+ () -> assertThat(cha.isValidMovePattern(4, 4, 2, 2)).isFalse(),
+ () -> assertThat(cha.isValidMovePattern(4, 2, 5, 1)).isFalse()
);
}
@@ -54,10 +69,10 @@ void cannotMoveSamePosition() {
void isValidPathWhenRouteIsEmpty() {
// given
Cha cha = new Cha(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(List.of(), Optional.empty());
// when
- boolean result = cha.isObstaclesNotExist(new Position(1, 1), new Position(1, 3), board);
+ boolean result = cha.canMove(moveRoute);
// then
assertThat(result).isTrue();
@@ -68,10 +83,38 @@ void isValidPathWhenRouteIsEmpty() {
void cannotMoveWhenRouteIsBlocked() {
// given
Cha cha = new Cha(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(List.of(PieceType.JOL), Optional.empty());
+
+ // when
+ boolean result = cha.canMove(moveRoute);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ @DisplayName("궁성 대각선 경로가 비어 있으면 차는 이동할 수 있다.")
+ void isValidDiagonalPathInsidePalaceWhenRouteIsEmpty() {
+ // given
+ Cha cha = new Cha(TeamType.CHU);
+ MoveRoute moveRoute = new MoveRoute(List.of(), Optional.empty());
+
+ // when
+ boolean result = cha.canMove(moveRoute);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("궁성 대각선 경로가 막혀 있으면 차는 이동할 수 없다.")
+ void cannotMoveDiagonalWhenPalaceRouteIsBlocked() {
+ // given
+ Cha cha = new Cha(TeamType.CHU);
+ MoveRoute moveRoute = new MoveRoute(List.of(PieceType.GUNG), Optional.empty());
// when
- boolean result = cha.isObstaclesNotExist(new Position(1, 1), new Position(1, 5), board);
+ boolean result = cha.canMove(moveRoute);
// then
assertThat(result).isFalse();
diff --git a/src/test/java/janggi/domain/piece/GungTest.java b/src/test/java/janggi/domain/piece/GungTest.java
index 86fe48cbbc..6855108d79 100644
--- a/src/test/java/janggi/domain/piece/GungTest.java
+++ b/src/test/java/janggi/domain/piece/GungTest.java
@@ -17,10 +17,10 @@ void isValidMovePatternStraightOneStep() {
// when & then
assertAll(
- () -> assertThat(gung.isValidMovePattern(4, 4, 4, 5)).isTrue(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 4, 3)).isTrue(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 5, 4)).isTrue(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 3, 4)).isTrue()
+ () -> assertThat(gung.isValidMovePattern(5, 2, 5, 3)).isTrue(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 5, 1)).isTrue(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 6, 2)).isTrue(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 4, 2)).isTrue()
);
}
@@ -32,25 +32,25 @@ void isValidMovePatternDiagonalOneStep() {
// when & then
assertAll(
- () -> assertThat(gung.isValidMovePattern(4, 4, 5, 5)).isTrue(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 5, 3)).isTrue(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 3, 5)).isTrue(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 3, 3)).isTrue()
+ () -> assertThat(gung.isValidMovePattern(5, 2, 6, 3)).isTrue(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 6, 1)).isTrue(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 4, 3)).isTrue(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 4, 1)).isTrue()
);
}
@Test
- @DisplayName("궁은 두 칸 이상 이동할 수 없다.")
+ @DisplayName("궁은 궁성 밖으로 나가거나 연결되지 않은 대각선으로 이동할 수 없다.")
void cannotMoveOverOneStep() {
// given
Gung gung = new Gung(TeamType.CHU);
// when & then
assertAll(
- () -> assertThat(gung.isValidMovePattern(4, 4, 6, 4)).isFalse(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 6, 6)).isFalse(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 4, 6)).isFalse(),
- () -> assertThat(gung.isValidMovePattern(4, 4, 2, 4)).isFalse()
+ () -> assertThat(gung.isValidMovePattern(5, 2, 5, 4)).isFalse(),
+ () -> assertThat(gung.isValidMovePattern(4, 1, 6, 3)).isFalse(),
+ () -> assertThat(gung.isValidMovePattern(4, 2, 5, 1)).isFalse(),
+ () -> assertThat(gung.isValidMovePattern(5, 2, 3, 2)).isFalse()
);
}
@@ -61,6 +61,6 @@ void cannotMoveSamePosition() {
Gung gung = new Gung(TeamType.CHU);
// when & then
- assertThat(gung.isValidMovePattern(4, 4, 4, 4)).isFalse();
+ assertThat(gung.isValidMovePattern(5, 2, 5, 2)).isFalse();
}
}
diff --git a/src/test/java/janggi/domain/piece/JolTest.java b/src/test/java/janggi/domain/piece/JolTest.java
index d12b9a87e1..3f3b672e2a 100644
--- a/src/test/java/janggi/domain/piece/JolTest.java
+++ b/src/test/java/janggi/domain/piece/JolTest.java
@@ -59,18 +59,38 @@ void cannotMoveBackward() {
);
}
+ @Test
+ @DisplayName("졸은 궁성 안에서 전진 대각선으로 한 칸 이동할 수 있다.")
+ void canMoveForwardDiagonallyInsidePalace() {
+ // given
+ Jol chuJol = new Jol(TeamType.CHU);
+ Jol hanJol = new Jol(TeamType.HAN);
+
+ // when & then
+ assertAll(
+ () -> assertThat(chuJol.isValidMovePattern(4, 1, 5, 2)).isTrue(),
+ () -> assertThat(chuJol.isValidMovePattern(5, 2, 6, 3)).isTrue(),
+ () -> assertThat(hanJol.isValidMovePattern(6, 10, 5, 9)).isTrue(),
+ () -> assertThat(hanJol.isValidMovePattern(5, 9, 4, 8)).isTrue()
+ );
+ }
+
@Test
@DisplayName("졸은 두 칸 이상 이동하거나 대각선으로 이동할 수 없다.")
void cannotMoveInvalidPath() {
// given
Jol jol = new Jol(TeamType.CHU);
+ Jol hanJol = new Jol(TeamType.HAN);
// when & then
assertAll(
() -> assertThat(jol.isValidMovePattern(4, 4, 4, 6)).isFalse(),
() -> assertThat(jol.isValidMovePattern(4, 4, 2, 4)).isFalse(),
() -> assertThat(jol.isValidMovePattern(4, 4, 5, 5)).isFalse(),
- () -> assertThat(jol.isValidMovePattern(4, 4, 3, 3)).isFalse()
+ () -> assertThat(jol.isValidMovePattern(4, 4, 3, 3)).isFalse(),
+ () -> assertThat(jol.isValidMovePattern(5, 2, 4, 1)).isFalse(),
+ () -> assertThat(hanJol.isValidMovePattern(5, 9, 6, 10)).isFalse(),
+ () -> assertThat(jol.isValidMovePattern(4, 2, 5, 1)).isFalse()
);
}
diff --git a/src/test/java/janggi/domain/piece/MaTest.java b/src/test/java/janggi/domain/piece/MaTest.java
index e2a3646449..dead28e153 100644
--- a/src/test/java/janggi/domain/piece/MaTest.java
+++ b/src/test/java/janggi/domain/piece/MaTest.java
@@ -3,9 +3,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
-import janggi.domain.Board;
-import janggi.domain.Position;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
+import java.util.List;
+import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -49,10 +50,10 @@ void cannotMoveInvalidPattern() {
void isValidPathWhenIntermediatePositionIsEmpty() {
// given
Ma ma = new Ma(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(List.of(), Optional.empty());
// when
- boolean result = ma.isObstaclesNotExist(new Position(2, 1), new Position(3, 3), board);
+ boolean result = ma.canMove(moveRoute);
// then
assertThat(result).isTrue();
@@ -63,10 +64,10 @@ void isValidPathWhenIntermediatePositionIsEmpty() {
void cannotMoveWhenIntermediatePositionIsBlocked() {
// given
Ma ma = new Ma(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(List.of(PieceType.JOL), Optional.empty());
// when
- boolean result = ma.isObstaclesNotExist(new Position(2, 1), new Position(4, 2), board);
+ boolean result = ma.canMove(moveRoute);
// then
assertThat(result).isFalse();
diff --git a/src/test/java/janggi/domain/piece/PoTest.java b/src/test/java/janggi/domain/piece/PoTest.java
index f87a14bd6d..0d4fa0f616 100644
--- a/src/test/java/janggi/domain/piece/PoTest.java
+++ b/src/test/java/janggi/domain/piece/PoTest.java
@@ -3,9 +3,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
-import janggi.domain.Board;
-import janggi.domain.Position;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
+import java.util.List;
+import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -27,7 +28,7 @@ void isValidMovePatternStraight() {
}
@Test
- @DisplayName("포는 대각선이나 제자리로 이동할 수 없다.")
+ @DisplayName("포는 궁성 밖 대각선이나 제자리로 이동할 수 없다.")
void cannotMoveInvalidPattern() {
// given
Po po = new Po(TeamType.CHU);
@@ -35,7 +36,21 @@ void cannotMoveInvalidPattern() {
// when & then
assertAll(
() -> assertThat(po.isValidMovePattern(4, 4, 5, 5)).isFalse(),
- () -> assertThat(po.isValidMovePattern(4, 4, 4, 4)).isFalse()
+ () -> assertThat(po.isValidMovePattern(4, 4, 4, 4)).isFalse(),
+ () -> assertThat(po.isValidMovePattern(4, 2, 5, 1)).isFalse()
+ );
+ }
+
+ @Test
+ @DisplayName("포는 궁성 안에서 대각선 이동할 수 있다.")
+ void canMoveDiagonalInsidePalace() {
+ // given
+ Po po = new Po(TeamType.CHU);
+
+ // when & then
+ assertAll(
+ () -> assertThat(po.isValidMovePattern(4, 1, 5, 2)).isTrue(),
+ () -> assertThat(po.isValidMovePattern(4, 1, 6, 3)).isTrue()
);
}
@@ -44,11 +59,13 @@ void cannotMoveInvalidPattern() {
void isObstaclesNotExistWhenExactlyOneBridgeExists() {
// given
Po po = new Po(TeamType.CHU);
- Board board = Board.createInitialBoard();
- Board movedBoard = board.move(new Position(1, 4), new Position(2, 4), TeamType.CHU);
+ MoveRoute moveRoute = new MoveRoute(
+ List.of(PieceType.JOL),
+ Optional.empty()
+ );
// when
- boolean result = po.isObstaclesNotExist(new Position(2, 3), new Position(2, 6), movedBoard);
+ boolean result = po.canMove(moveRoute);
// then
assertThat(result).isTrue();
@@ -59,10 +76,10 @@ void isObstaclesNotExistWhenExactlyOneBridgeExists() {
void cannotMoveWithoutBridge() {
// given
Po po = new Po(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(List.of(), Optional.empty());
// when
- boolean result = po.isObstaclesNotExist(new Position(2, 3), new Position(2, 6), board);
+ boolean result = po.canMove(moveRoute);
// then
assertThat(result).isFalse();
@@ -73,13 +90,48 @@ void cannotMoveWithoutBridge() {
void cannotUsePoAsBridgeOrTarget() {
// given
Po po = new Po(TeamType.CHU);
- Board board = Board.createInitialBoard();
- Board movedBoard = board.move(new Position(1, 4), new Position(2, 4), TeamType.CHU);
+ MoveRoute poBridgeRoute = new MoveRoute(
+ List.of(PieceType.PO),
+ Optional.empty()
+ );
+ MoveRoute poTargetRoute = new MoveRoute(
+ List.of(PieceType.JOL),
+ Optional.of(PieceType.PO)
+ );
// when & then
assertAll(
- () -> assertThat(po.isObstaclesNotExist(new Position(2, 3), new Position(2, 10), board)).isFalse(),
- () -> assertThat(po.isObstaclesNotExist(new Position(2, 3), new Position(2, 8), movedBoard)).isFalse()
+ () -> assertThat(po.canMove(poBridgeRoute)).isFalse(),
+ () -> assertThat(po.canMove(poTargetRoute)).isFalse()
);
}
+
+ @Test
+ @DisplayName("포는 궁성 대각선에 다리가 하나 있으면 이동할 수 있다.")
+ void canMoveDiagonallyInsidePalaceWithBridge() {
+ // given
+ Po po = new Po(TeamType.CHU);
+ MoveRoute moveRoute = new MoveRoute(List.of(PieceType.GUNG), Optional.empty());
+
+ // when
+ boolean result = po.canMove(moveRoute);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("포는 궁성 대각선에 다리가 없으면 이동할 수 없다.")
+ void cannotMoveDiagonallyInsidePalaceWithoutBridge() {
+ // given
+ Po po = new Po(TeamType.CHU);
+ MoveRoute moveRoute = new MoveRoute(List.of(), Optional.empty());
+
+ // when
+ boolean result = po.canMove(moveRoute);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
}
diff --git a/src/test/java/janggi/domain/piece/SaTest.java b/src/test/java/janggi/domain/piece/SaTest.java
index baea98b76c..4e801e5070 100644
--- a/src/test/java/janggi/domain/piece/SaTest.java
+++ b/src/test/java/janggi/domain/piece/SaTest.java
@@ -17,10 +17,10 @@ void isValidMovePatternStraightOneStep() {
// when & then
assertAll(
- () -> assertThat(sa.isValidMovePattern(4, 4, 4, 5)).isTrue(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 4, 3)).isTrue(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 5, 4)).isTrue(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 3, 4)).isTrue()
+ () -> assertThat(sa.isValidMovePattern(5, 2, 5, 3)).isTrue(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 5, 1)).isTrue(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 6, 2)).isTrue(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 4, 2)).isTrue()
);
}
@@ -32,25 +32,25 @@ void isValidMovePatternDiagonalOneStep() {
// when & then
assertAll(
- () -> assertThat(sa.isValidMovePattern(4, 4, 5, 5)).isTrue(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 5, 3)).isTrue(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 3, 5)).isTrue(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 3, 3)).isTrue()
+ () -> assertThat(sa.isValidMovePattern(5, 2, 6, 3)).isTrue(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 6, 1)).isTrue(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 4, 3)).isTrue(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 4, 1)).isTrue()
);
}
@Test
- @DisplayName("사는 두 칸 이상 이동하거나 제자리로 이동할 수 없다.")
+ @DisplayName("사는 궁성 밖으로 나가거나 연결되지 않은 대각선으로 이동할 수 없다.")
void cannotMoveInvalidPattern() {
// given
Sa sa = new Sa(TeamType.CHU);
// when & then
assertAll(
- () -> assertThat(sa.isValidMovePattern(4, 4, 6, 4)).isFalse(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 6, 6)).isFalse(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 4, 6)).isFalse(),
- () -> assertThat(sa.isValidMovePattern(4, 4, 4, 4)).isFalse()
+ () -> assertThat(sa.isValidMovePattern(5, 2, 5, 4)).isFalse(),
+ () -> assertThat(sa.isValidMovePattern(4, 1, 6, 3)).isFalse(),
+ () -> assertThat(sa.isValidMovePattern(4, 2, 5, 1)).isFalse(),
+ () -> assertThat(sa.isValidMovePattern(5, 2, 5, 2)).isFalse()
);
}
}
diff --git a/src/test/java/janggi/domain/piece/SangTest.java b/src/test/java/janggi/domain/piece/SangTest.java
index b2cf561576..6cb446c901 100644
--- a/src/test/java/janggi/domain/piece/SangTest.java
+++ b/src/test/java/janggi/domain/piece/SangTest.java
@@ -3,9 +3,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
-import janggi.domain.Board;
-import janggi.domain.Position;
import janggi.domain.team.TeamType;
+import janggi.dto.MoveRoute;
+import java.util.List;
+import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -49,10 +50,10 @@ void cannotMoveInvalidPattern() {
void isObstaclesNotExistWhenIntermediatePositionsAreEmpty() {
// given
Sang sang = new Sang(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(List.of(), Optional.empty());
// when
- boolean result = sang.isObstaclesNotExist(new Position(4, 5), new Position(7, 7), board);
+ boolean result = sang.canMove(moveRoute);
// then
assertThat(result).isTrue();
@@ -63,10 +64,13 @@ void isObstaclesNotExistWhenIntermediatePositionsAreEmpty() {
void cannotMoveWhenIntermediatePositionIsBlocked() {
// given
Sang sang = new Sang(TeamType.CHU);
- Board board = Board.createInitialBoard();
+ MoveRoute moveRoute = new MoveRoute(
+ List.of(PieceType.JOL),
+ Optional.empty()
+ );
// when
- boolean result = sang.isObstaclesNotExist(new Position(4, 2), new Position(7, 4), board);
+ boolean result = sang.canMove(moveRoute);
// then
assertThat(result).isFalse();
diff --git a/src/test/java/janggi/service/JanggiServiceTest.java b/src/test/java/janggi/service/JanggiServiceTest.java
new file mode 100644
index 0000000000..3568340cd9
--- /dev/null
+++ b/src/test/java/janggi/service/JanggiServiceTest.java
@@ -0,0 +1,143 @@
+package janggi.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import janggi.domain.JanggiGame;
+import janggi.domain.Position;
+import janggi.domain.team.TeamType;
+import janggi.persistence.GameStatus;
+import janggi.persistence.dto.MoveHistory;
+import janggi.persistence.model.JanggiGameHistory;
+import janggi.persistence.repository.JanggiGameRepository;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class JanggiServiceTest {
+
+ @Test
+ @DisplayName("진행 중인 게임이 없으면 새 게임을 생성하고 초기 장기 게임을 반환한다.")
+ void loadInitialGameWhenNoRecentGameExists() {
+ // given
+ FakeJanggiGameRepository repository = new FakeJanggiGameRepository(JanggiGameHistory.createEmpty());
+ JanggiService janggiService = new JanggiService(repository);
+
+ // when
+ JanggiGame janggiGame = janggiService.loadGame();
+
+ // then
+ assertAll(
+ () -> assertThat(repository.createNewGameCalled).isTrue(),
+ () -> assertThat(janggiGame.getTurnCount()).isZero(),
+ () -> assertThat(janggiGame.getCurrentTurnTeam()).isEqualTo(TeamType.CHU)
+ );
+ }
+
+ @Test
+ @DisplayName("진행 중인 게임이 있으면 최근 기록으로 장기 게임을 복원한다.")
+ void rebuildGameFromRecentGameHistory() {
+ // given
+ JanggiGameHistory gameHistory = new JanggiGameHistory(
+ 7L,
+ List.of(new MoveHistory(1, 1, 4, 2, 4)),
+ GameStatus.IN_PROGRESS
+ );
+ FakeJanggiGameRepository repository = new FakeJanggiGameRepository(gameHistory);
+ JanggiService janggiService = new JanggiService(repository);
+
+ // when
+ JanggiGame janggiGame = janggiService.loadGame();
+
+ // then
+ assertAll(
+ () -> assertThat(repository.createNewGameCalled).isFalse(),
+ () -> assertThat(janggiGame.getTurnCount()).isEqualTo(1),
+ () -> assertThat(janggiGame.getCurrentTurnTeam()).isEqualTo(TeamType.HAN)
+ );
+ }
+
+ @Test
+ @DisplayName("게임을 진행하면 이동 결과를 저장소에 저장한다.")
+ void playAndSaveMove() {
+ // given
+ FakeJanggiGameRepository repository = new FakeJanggiGameRepository(JanggiGameHistory.createEmpty());
+ JanggiService janggiService = new JanggiService(repository);
+ JanggiGame janggiGame = janggiService.loadGame();
+ Position startPosition = new Position(1, 4);
+ Position endPosition = new Position(2, 4);
+
+ // when
+ janggiService.play(janggiGame, startPosition, endPosition);
+
+ // then
+ assertAll(
+ () -> assertThat(janggiGame.getTurnCount()).isEqualTo(1),
+ () -> assertThat(janggiGame.getCurrentTurnTeam()).isEqualTo(TeamType.HAN),
+ () -> assertThat(repository.savedGameId).isEqualTo(repository.createdGameId),
+ () -> assertThat(repository.savedTurnNumber).isEqualTo(1),
+ () -> assertThat(repository.savedStartPosition).isEqualTo(startPosition),
+ () -> assertThat(repository.savedEndPosition).isEqualTo(endPosition)
+ );
+ }
+
+ @Test
+ @DisplayName("게임 종료 시 저장소에 종료 상태를 반영한다.")
+ void finishGame() {
+ // given
+ FakeJanggiGameRepository repository = new FakeJanggiGameRepository(JanggiGameHistory.createEmpty());
+ JanggiService janggiService = new JanggiService(repository);
+ janggiService.loadGame();
+
+ // when
+ janggiService.finishGame();
+
+ // then
+ assertAll(
+ () -> assertThat(repository.updatedGameStatus).isEqualTo(GameStatus.FINISHED),
+ () -> assertThat(repository.updatedGameId).isEqualTo(repository.createdGameId)
+ );
+ }
+
+ private static class FakeJanggiGameRepository implements JanggiGameRepository {
+
+ private final JanggiGameHistory recentGame;
+ private final long createdGameId = 1L;
+ private boolean createNewGameCalled;
+ private long savedGameId;
+ private int savedTurnNumber;
+ private Position savedStartPosition;
+ private Position savedEndPosition;
+ private GameStatus updatedGameStatus;
+ private long updatedGameId;
+
+ private FakeJanggiGameRepository(JanggiGameHistory recentGame) {
+ this.recentGame = recentGame;
+ }
+
+ @Override
+ public long createNewGame() {
+ createNewGameCalled = true;
+ return createdGameId;
+ }
+
+ @Override
+ public void saveMove(long gameId, int turnNumber, Position startPosition, Position endPosition) {
+ savedGameId = gameId;
+ savedTurnNumber = turnNumber;
+ savedStartPosition = startPosition;
+ savedEndPosition = endPosition;
+ }
+
+ @Override
+ public void update(GameStatus gameStatus, long gameId) {
+ updatedGameStatus = gameStatus;
+ updatedGameId = gameId;
+ }
+
+ @Override
+ public JanggiGameHistory findRecentGame() {
+ return recentGame;
+ }
+ }
+}