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; + } + } +}