diff --git a/README.md b/README.md index f069a20711..65d2b57927 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ ### 장기 게임 (JanggiGame) - [X] 장기 게임 흐름 관리 - [X] 게임 턴 관리 - - [X] 초기 장기판 셋업 (각 기물들을 정해진 초기 Point에 배치) ### ✉️ 도메인 (Domain) @@ -50,12 +49,63 @@ ### 📤 출력 (OutputView) #### 게임 진행 상태 출력 -- [ ] 게임 시작 안내 메시지 출력 -- [ ] 매 턴마다 현재 누구의 턴인지 출력 +- [X] 게임 시작 안내 메시지 출력 +- [X] 매 턴마다 현재 누구의 턴인지 출력 - [X] 매 턴마다 이동 결과가 반영된 현재 장기판의 상태를 시각적으로 출력 #### 게임 종료 출력 - [X] 승리 팀 안내 및 게임 종료 메시지 출력 #### 에러 출력 -- [ ] 예외 상황 발생 시 에러 문구 출력 (반드시 [ERROR]로 시작) +- [X] 예외 상황 발생 시 에러 문구 출력 (반드시 [ERROR]로 시작) + +## 추가 기능 목록 +### 궁성 규칙 +- [X] 장과 사는 궁성 안에서만 이동할 수 있다. +- [X] 장은 궁성 대각선 이동 규칙을 따른다. +- [X] 사는 궁성 대각선 이동 규칙을 따른다. +- [X] 차는 궁성 대각선 이동 규칙을 따른다. +- [X] 포는 궁성 대각선 이동 규칙을 따른다. +- [X] 졸은 궁성 안에서 대각선 이동 규칙을 따른다. + +### 게임 종료 +- [X] 상대 왕이 잡히면 게임을 종료한다. +- [X] 게임 종료 시 승자를 반환한다. + +### 점수 계산 +- [X] 현재 남아 있는 기물의 점수를 계산한다. +- [X] 팀별 점수를 계산할 수 있다. + +### 데이터베이스 초기화 +- [X] H2 데이터베이스에 연결한다. +- [X] 애플리케이션 시작 시 스키마를 초기화한다. +- [X] games 테이블을 생성한다. +- [X] game_pieces 테이블을 생성한다. + +### 게임 저장소 +- [X] 게임 목록을 조회할 수 있다. +- [X] 게임 id로 게임을 조회할 수 있다. +- [X] 새 게임을 저장할 수 있다. +- [X] 진행 중인 게임 상태를 수정할 수 있다. +- [X] 게임의 현재 턴 정보를 저장한다. +- [X] 게임의 종료 여부와 승자를 저장한다. +- [X] 현재 살아있는 기물의 위치를 저장한다. + +### 게임 복구 +- [X] 저장된 게임 목록을 출력한다. +- [X] 사용자가 이어서 할 게임을 선택할 수 있다. +- [X] 저장된 기물 위치로 장기판을 복구한다. +- [X] 저장된 현재 턴으로 게임을 복구한다. +- [X] 저장된 종료 상태로 게임을 복구한다. +- [X] 저장된 게임이 없으면 새 게임을 시작한다. + +### 게임 진행 흐름 +- [X] 새 게임을 생성할 수 있다. +- [X] 수를 둘 때마다 게임 상태를 저장한다. +- [X] 게임 종료 시 최종 상태를 저장한다. + +### 입/출력 +- [X] 게임 시작 시 새 게임과 이어하기 메뉴를 출력한다. +- [X] 저장된 게임 목록을 사용자에게 출력한다. +- [X] 사용자가 선택한 게임을 불러온다. +- [X] 현재 턴 정보를 출력한다. diff --git a/build.gradle b/build.gradle index ce846f70cc..9326c6225c 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ repositories { } dependencies { + implementation 'com.h2database:h2:2.3.232' testImplementation platform('org.junit:junit-bom:5.11.4') testImplementation platform('org.assertj:assertj-bom:3.27.3') testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java index a1e236e5f7..364ef74865 100644 --- a/src/main/java/janggi/JanggiApplication.java +++ b/src/main/java/janggi/JanggiApplication.java @@ -1,28 +1,70 @@ package janggi; -import janggi.domain.Board; +import janggi.domain.status.Team; +import janggi.repository.CsvInitialBoardProvider; +import janggi.repository.GameRepository; +import janggi.repository.InitialBoardProvider; +import janggi.service.JanggiGameService; +import janggi.dto.GameSummary; import janggi.domain.JanggiGame; import janggi.domain.Point; import janggi.dto.GameStatusInfo; -import janggi.dto.PositionInfo; +import janggi.util.DatabaseInitializer; +import janggi.util.JdbcConnectionManager; +import janggi.repository.JdbcGameRepository; import janggi.ui.InputView; import janggi.ui.OutputView; -import janggi.util.FileParser; import java.util.List; public class JanggiApplication { + public static void main(String[] args) { - List positionInfos = FileParser.readCsvFile("/janggi.csv"); - Board board = new Board(); - board.init(positionInfos); - JanggiGame game = new JanggiGame(board); - OutputView.printGameStatus(GameStatusInfo.from(game.getBoardStatus())); + JdbcConnectionManager connectionManager = new JdbcConnectionManager( + "jdbc:h2:file:./storage/janggi;AUTO_SERVER=TRUE", + "sa", + "" + ); + DatabaseInitializer databaseInitializer = new DatabaseInitializer(connectionManager); + databaseInitializer.init(); + + GameRepository gameRepository = new JdbcGameRepository(connectionManager); + InitialBoardProvider initialBoardProvider = new CsvInitialBoardProvider(); + JanggiGameService janggiGameService = + new JanggiGameService(gameRepository, initialBoardProvider); + + Long gameId = selectGame(janggiGameService); + playGame(janggiGameService, gameId); + } + + private static Long selectGame(JanggiGameService janggiGameService) { + String command = InputView.readGameCommand(); + if (InputView.isNewGameCommand(command)) { + return janggiGameService.createGame(); + } + if (InputView.isLoadGameCommand(command)) { + return loadGame(janggiGameService); + } + throw new IllegalArgumentException("[ERROR] 1 또는 2를 입력해 주세요."); + } + + private static Long loadGame(JanggiGameService janggiGameService) { + List gameSummaries = janggiGameService.findAllGames(); + OutputView.printSavedGames(gameSummaries); + return InputView.readGameId(); + } + + private static void playGame(JanggiGameService janggiGameService, Long gameId) { + JanggiGame janggiGame = janggiGameService.loadGame(gameId); + OutputView.printGameStatus(GameStatusInfo.from(janggiGame.getBoardStatus())); - while (!game.isFinished()) { + while (!janggiGame.isFinished()) { + OutputView.printCurrentTurn(janggiGame.currentTurn().getName()); List points = InputView.readPoints(); - game.play(points.get(0), points.get(1)); - OutputView.printGameStatus(GameStatusInfo.from(game.getBoardStatus())); + janggiGameService.play(gameId, points.get(0), points.get(1)); + janggiGame = janggiGameService.loadGame(gameId); + OutputView.printGameStatus(GameStatusInfo.from(janggiGame.getBoardStatus())); } - OutputView.printWinner(game.getWinner()); + OutputView.printWinner(janggiGame.getWinner()); + OutputView.printScore(janggiGame.scoreOf(Team.CHO), janggiGame.scoreOf(Team.HAN)); } } diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java index 0aa4372e9f..eb7bb4ea0e 100644 --- a/src/main/java/janggi/domain/Board.java +++ b/src/main/java/janggi/domain/Board.java @@ -12,79 +12,63 @@ public class Board { - private static final int BOARD_HEIGHT = 10; - - private final Map state; + private final Map piecesByPoint; public Board() { - this.state = new LinkedHashMap<>(); + this.piecesByPoint = new LinkedHashMap<>(); } public void init(List positionInfos) { - positionInfos.forEach(info -> state.put(info.point(), info.piece())); + positionInfos.forEach(info -> piecesByPoint.put(info.point(), info.piece())); } public void move(Point from, Point to, Team team) { validateFromPoint(from, team); validateToPoint(to, team); - Piece piece = state.get(from); + Piece piece = piecesByPoint.get(from); List route = piece.getRoute(from, to); - Piece targetPiece = state.get(to); + Piece targetPiece = piecesByPoint.get(to); if (targetPiece != null && !piece.canCapture(targetPiece)) { - throw new IllegalArgumentException("이 기물은 해당 타겟을 잡을 수 없습니다."); + throw new IllegalStateException("[ERROR] 이 기물은 해당 타겟을 잡을 수 없습니다."); } if (!piece.canMove(getPieces(route))) { - throw new IllegalArgumentException("해당 기물의 이동 경로에 장애물이 있거나 규칙에 어긋납니다."); - } - state.remove(from); - state.put(to, piece); - } - - public boolean isKingDie(Team team) { - return state.values().stream() - .noneMatch(piece -> piece.isSameType(PieceType.JANG) && - piece.isSameTeam(team)); - } - - public List> getPoints() { - List> pieces = new ArrayList<>(); - for (int i = 0; i < BOARD_HEIGHT; i++) { - pieces.add( - Point.getRow(i).stream() - .map(state::get) - .toList() - ); + throw new IllegalStateException("[ERROR] 해당 기물의 이동 경로에 장애물이 있거나 규칙에 어긋납니다."); } - return pieces; + piecesByPoint.remove(from); + piecesByPoint.put(to, piece); } public List getPieces(List point) { return point.stream() - .map(state::get) + .map(piecesByPoint::get) .filter(Objects::nonNull) .toList(); } + public Map getPiecesByPoint() { + return piecesByPoint; + } + private void validateFromPoint(Point from, Team team) { if (isEmptyPoint(from)) { - throw new IllegalArgumentException("출발지에 이동할 기물이 없습니다."); + throw new IllegalStateException("[ERROR] 출발지에 이동할 기물이 없습니다."); } if (!isSameTeam(from, team)) { - throw new IllegalArgumentException("상대방의 기물은 움직일 수 없습니다."); + throw new IllegalStateException("[ERROR] 상대방의 기물은 움직일 수 없습니다."); } } private void validateToPoint(Point to, Team team) { if (!isEmptyPoint(to) && isSameTeam(to, team)) { - throw new IllegalArgumentException("도착지에 본인의 기물이 있습니다."); + throw new IllegalStateException("[ERROR] 도착지에 본인의 기물이 있습니다."); } } private boolean isSameTeam(Point point, Team team) { - return state.get(point).isSameTeam(team); + return piecesByPoint.get(point).isSameTeam(team); } private boolean isEmptyPoint(Point point) { - return state.get(point) == null; + return piecesByPoint.get(point) == null; } } diff --git a/src/main/java/janggi/domain/Boards.java b/src/main/java/janggi/domain/Boards.java new file mode 100644 index 0000000000..dceb56d853 --- /dev/null +++ b/src/main/java/janggi/domain/Boards.java @@ -0,0 +1,52 @@ +package janggi.domain; + +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import java.util.ArrayList; +import java.util.List; + +public class Boards { + + private static final int BOARD_HEIGHT = 10; + + private final Board board; + + public Boards(Board board) { + this.board = board; + } + + public void move(Point from, Point to, Team team) { + board.move(from, to, team); + } + + public int scoreOf(Team team) { + return board.getPiecesByPoint().values().stream() + .filter(piece -> piece.isSameTeam(team)) + .mapToInt(Piece::getScore) + .sum(); + } + + public boolean isKingDie(Team team) { + return board.getPiecesByPoint().values().stream() + .noneMatch(piece -> piece.isSameType(PieceType.JANG) && + piece.isSameTeam(team)); + } + + public List getBoardStatus() { + return PositionInfo.from(board.getPiecesByPoint()); + } + + public List> getPoints() { + List> pieces = new ArrayList<>(); + for (int i = 0; i < BOARD_HEIGHT; i++) { + pieces.add( + Point.getPointsAtY(i).stream() + .map(board.getPiecesByPoint()::get) + .toList() + ); + } + return pieces; + } +} diff --git a/src/main/java/janggi/domain/JanggiGame.java b/src/main/java/janggi/domain/JanggiGame.java index 0c4dfb8afc..0e2812c4d4 100644 --- a/src/main/java/janggi/domain/JanggiGame.java +++ b/src/main/java/janggi/domain/JanggiGame.java @@ -4,16 +4,22 @@ import janggi.domain.status.ChoTurn; import janggi.domain.status.GameStatus; import janggi.domain.status.Team; +import janggi.dto.PositionInfo; import java.util.List; public class JanggiGame { - private final Board board; private GameStatus gameStatus; + private final Boards boards; public JanggiGame(Board board) { - this.board = board; this.gameStatus = new ChoTurn(); + this.boards = new Boards(board); + } + + public JanggiGame(Board board, GameStatus gameStatus) { + this.boards = new Boards(board); + this.gameStatus = gameStatus; } public boolean isFinished() { @@ -22,16 +28,28 @@ public boolean isFinished() { public Team getWinner() { if (!gameStatus.isFinished()) { - throw new RuntimeException("게임이 아직 끝나지 않았습니다."); + throw new IllegalStateException("[ERROR] 게임이 아직 끝나지 않았습니다."); } return gameStatus.getTeam(); } public List> getBoardStatus() { - return board.getPoints(); + return boards.getPoints(); } public void play(Point from, Point to) { - this.gameStatus = gameStatus.move(from, to, board); + this.gameStatus = gameStatus.move(from, to, boards); + } + + public Team currentTurn() { + return gameStatus.getTeam(); + } + + public List boardStatus() { + return boards.getBoardStatus(); + } + + public int scoreOf(Team team) { + return boards.scoreOf(team); } } diff --git a/src/main/java/janggi/domain/Point.java b/src/main/java/janggi/domain/Point.java index 9006d9b3b6..5712b51041 100644 --- a/src/main/java/janggi/domain/Point.java +++ b/src/main/java/janggi/domain/Point.java @@ -1,5 +1,7 @@ package janggi.domain; +import static java.lang.Math.abs; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -10,15 +12,24 @@ public class Point { private static final int BOARD_HEIGHT = 10; private static final List> CACHE; + private static final int PALACE_MIN_X = 3; + private static final int PALACE_MAX_X = 5; + + private static final int CHO_PALACE_MIN_Y = 0; + private static final int CHO_PALACE_MAX_Y = 2; + + private static final int HAN_PALACE_MIN_Y = 7; + private static final int HAN_PALACE_MAX_Y = 9; + private final int x; private final int y; static { List> points = new ArrayList<>(); - for(int i = 0; i < BOARD_HEIGHT; i++) { - List row = new ArrayList<>(); - addX(row, i); - points.add(row); + for (int i = 0; i < BOARD_HEIGHT; i++) { + List pointsAtY = new ArrayList<>(); + addPointsInX(pointsAtY, i); + points.add(pointsAtY); } CACHE = Collections.unmodifiableList(points); } @@ -34,7 +45,7 @@ public static Point of(int x, int y) { return CACHE.get(y).get(x); } - public static List getRow(int y) { + public static List getPointsAtY(int y) { return CACHE.get(y); } @@ -54,15 +65,62 @@ public int getPathY(Point from) { return this.y - from.y; } - private static void addX(List row, int y) { - for(int i = 0; i < BOARD_WIDTH; i++) { - row.add(new Point(i, y)); + public boolean isInSamePalace(Point to) { + return (isInChoPalace() && to.isInChoPalace()) + || (isInHanPalace() && to.isInHanPalace()); + } + + public boolean isPalaceDiagonalMove(Point to) { + if (!isInSamePalace(to)) { + return false; + } + return isChoPalaceDiagonalPair(to) || isHanPalaceDiagonalPair(to); + } + + private boolean isChoPalaceDiagonalPair(Point to) { + return isSamePointPair(to, Point.of(3, 0), Point.of(4, 1)) + || isSamePointPair(to, Point.of(4, 1), Point.of(5, 2)) + || isSamePointPair(to, Point.of(5, 0), Point.of(4, 1)) + || isSamePointPair(to, Point.of(4, 1), Point.of(3, 2)) + || isSamePointPair(to, Point.of(3, 0), Point.of(5, 2)) + || isSamePointPair(to, Point.of(5, 0), Point.of(3, 2)); + } + + private boolean isHanPalaceDiagonalPair(Point to) { + return isSamePointPair(to, Point.of(3, 7), Point.of(4, 8)) + || isSamePointPair(to, Point.of(4, 8), Point.of(5, 9)) + || isSamePointPair(to, Point.of(5, 7), Point.of(4, 8)) + || isSamePointPair(to, Point.of(4, 8), Point.of(3, 9)) + || isSamePointPair(to, Point.of(3, 7), Point.of(5, 9)) + || isSamePointPair(to, Point.of(5, 7), Point.of(3, 9)); + } + + private boolean isSamePointPair(Point to, Point first, Point second) { + return (this == first && to == second) || (this == second && to == first); + } + + private boolean isInChoPalace() { + return isInRange(CHO_PALACE_MIN_Y, CHO_PALACE_MAX_Y); + } + + private boolean isInHanPalace() { + return isInRange(HAN_PALACE_MIN_Y, HAN_PALACE_MAX_Y); + } + + private boolean isInRange(int minY, int maxY) { + return x >= PALACE_MIN_X && x <= PALACE_MAX_X + && y >= minY && y <= maxY; + } + + private static void addPointsInX(List pointsAtY, int y) { + for (int i = 0; i < BOARD_WIDTH; i++) { + pointsAtY.add(new Point(i, y)); } } private static void validateRange(int x, int y) { if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) { - throw new IllegalArgumentException("올바르지 않은 위치 범위입니다."); + throw new IllegalArgumentException("[ERROR] 올바르지 않은 위치 범위입니다."); } } } diff --git a/src/main/java/janggi/domain/piece/BasePiece.java b/src/main/java/janggi/domain/piece/BasePiece.java index d1d85602f8..05a53e9da2 100644 --- a/src/main/java/janggi/domain/piece/BasePiece.java +++ b/src/main/java/janggi/domain/piece/BasePiece.java @@ -27,4 +27,9 @@ public boolean isSameTeam(Team team) { public PieceType getType() { return type; } + + @Override + public Team getTeam() { + return team; + } } diff --git a/src/main/java/janggi/domain/piece/Cha.java b/src/main/java/janggi/domain/piece/Cha.java index eaf1478ecf..31415a7b48 100644 --- a/src/main/java/janggi/domain/piece/Cha.java +++ b/src/main/java/janggi/domain/piece/Cha.java @@ -15,6 +15,22 @@ public Cha(Team team) { @Override public List getRoute(Point from, Point to) { + if (isStraightMove(from, to)) { + return getNormalRoute(from, to); + } + if (from.isPalaceDiagonalMove(to)) { + return getPalaceRoute(from, to); + } + throw new IllegalArgumentException("[ERROR] 차는 직선 또는 궁성 대각선으로만 이동할 수 있습니다."); + } + + private boolean isStraightMove(Point from, Point to) { + int pathX = to.getPathX(from); + int pathY = to.getPathY(from); + return pathX == 0 || pathY == 0; + } + + private List getNormalRoute(Point from, Point to) { List route = new ArrayList<>(); int pathX = to.getPathX(from); int pathY = to.getPathY(from); @@ -32,9 +48,24 @@ public List getRoute(Point from, Point to) { return route; } + private List getPalaceRoute(Point from, Point to) { + int pathX = abs(to.getPathX(from)); + int pathY = abs(to.getPathY(from)); + + if (pathX == 1 && pathY == 1) { + return List.of(); + } + if (pathX == 2 && pathY == 2) { + int middleX = (from.getX() + to.getX()) / 2; + int middleY = (from.getY() + to.getY()) / 2; + return List.of(Point.of(middleX, middleY)); + } + throw new IllegalArgumentException("[ERROR] 궁성 대각선 경로가 아닙니다."); + } + private void validateDiagonalMove(int pathX, int pathY) { if (pathX != 0 && pathY != 0) { - throw new IllegalArgumentException("대각선 이동은 불가능합니다."); + throw new IllegalArgumentException("[ERROR] 대각선 이동은 불가능합니다."); } } } diff --git a/src/main/java/janggi/domain/piece/Jang.java b/src/main/java/janggi/domain/piece/Jang.java index 1ff066b47c..859c952506 100644 --- a/src/main/java/janggi/domain/piece/Jang.java +++ b/src/main/java/janggi/domain/piece/Jang.java @@ -16,25 +16,24 @@ public Jang(Team team) { @Override public List getRoute(Point from, Point to) { - int pathX = to.getPathX(from); - int pathY = to.getPathY(from); - int distanceX = abs(pathX); - int distanceY = abs(pathY); - - validateOverMove(distanceX, distanceY); - - return List.of(to); + if (!from.isInSamePalace(to)) { + throw new IllegalArgumentException("[ERROR] 장은 같은 궁성 안에서만 이동할 수 있습니다."); + } + if (isNormalMove(from, to) || isPalaceDiagonalMove(from, to)) { + return List.of(); + } + throw new IllegalArgumentException("[ERROR] 장은 궁성 안에서 한 칸만 이동할 수 있습니다."); } - @Override - public boolean canMove(List route) { - return route.stream() - .noneMatch(piece -> piece.isSameTeam(team)); + private boolean isNormalMove(Point from, Point to) { + int pathX = abs(to.getPathX(from)); + int pathY = abs(to.getPathY(from)); + return pathX + pathY == MAX_DISTANCE; } - private void validateOverMove(int distanceX, int distanceY) { - if (distanceX + distanceY != MAX_DISTANCE) { - throw new IllegalArgumentException("한 칸만 이동할 수 있습니다."); - } + private boolean isPalaceDiagonalMove(Point from, Point to) { + int pathX = abs(to.getPathX(from)); + int pathY = abs(to.getPathY(from)); + return pathX == 1 && pathY == 1 && from.isPalaceDiagonalMove(to); } } diff --git a/src/main/java/janggi/domain/piece/Jol.java b/src/main/java/janggi/domain/piece/Jol.java index 85353c0aae..1914ea9e97 100644 --- a/src/main/java/janggi/domain/piece/Jol.java +++ b/src/main/java/janggi/domain/piece/Jol.java @@ -21,28 +21,28 @@ public List getRoute(Point from, Point to) { int distanceX = abs(pathX); int distanceY = abs(pathY); - validateOverMove(distanceX, distanceY); validateBackMove(pathY); - return List.of(to); + if (isNormalMove(distanceX, distanceY) || isPalaceDiagonalMove(from, to, distanceX, distanceY)) { + return List.of(); + } + + throw new IllegalArgumentException("[ERROR] 졸은 앞으로, 좌우 한 칸 또는 궁성 안에서 대각선 한 칸만 이동할 수 있습니다."); } - @Override - public boolean canMove(List route) { - return route.stream() - .noneMatch(piece -> piece.isSameTeam(team)); + private boolean isNormalMove(int distanceX, int distanceY) { + return distanceX + distanceY == MAX_DISTANCE; } - private void validateOverMove(int distanceX, int distanceY) { - if (distanceX + distanceY != MAX_DISTANCE) { - throw new IllegalArgumentException("한 칸만 이동할 수 있습니다."); - } + private boolean isPalaceDiagonalMove(Point from, Point to, int distanceX, int distanceY) { + return distanceX == 1 + && distanceY == 1 + && from.isPalaceDiagonalMove(to); } private void validateBackMove(int pathY) { if ((team == Team.CHO && pathY < 0) || (team == Team.HAN && pathY > 0)) { - throw new IllegalArgumentException("뒤로 이동할 수 없습니다."); + throw new IllegalArgumentException("[ERROR] 뒤로 이동할 수 없습니다."); } } } - diff --git a/src/main/java/janggi/domain/piece/Ma.java b/src/main/java/janggi/domain/piece/Ma.java index 3775155e3b..9dca246457 100644 --- a/src/main/java/janggi/domain/piece/Ma.java +++ b/src/main/java/janggi/domain/piece/Ma.java @@ -31,9 +31,9 @@ public List getRoute(Point from, Point to) { } private void validateMove(int distanceX, int distanceY) { - if (!((distanceX == DISTANCE_MAX && distanceY == DISTANCE_MIN) || - (distanceX == DISTANCE_MIN && distanceY == DISTANCE_MAX))) { - throw new IllegalArgumentException("해당 기물의 이동 경로의 규칙에 어긋납니다."); + if (!((distanceX == DISTANCE_MAX && distanceY == DISTANCE_MIN) + || (distanceX == DISTANCE_MIN && distanceY == DISTANCE_MAX))) { + throw new IllegalArgumentException("[ERROR] 해당 기물의 이동 경로의 규칙에 어긋납니다."); } } } diff --git a/src/main/java/janggi/domain/piece/Pho.java b/src/main/java/janggi/domain/piece/Pho.java index 6e00abd738..741e2ca510 100644 --- a/src/main/java/janggi/domain/piece/Pho.java +++ b/src/main/java/janggi/domain/piece/Pho.java @@ -15,6 +15,22 @@ public Pho(Team team) { @Override public List getRoute(Point from, Point to) { + if (isStraightMove(from, to)) { + return getNormalRoute(from, to); + } + if (from.isPalaceDiagonalMove(to)) { + return getPalaceRoute(from, to); + } + throw new IllegalArgumentException("[ERROR] 포는 직선 또는 궁성 대각선으로만 이동할 수 있습니다."); + } + + private boolean isStraightMove(Point from, Point to) { + int pathX = to.getPathX(from); + int pathY = to.getPathY(from); + return pathX == 0 || pathY == 0; + } + + private List getNormalRoute(Point from, Point to) { List route = new ArrayList<>(); int pathX = to.getPathX(from); int pathY = to.getPathY(from); @@ -32,6 +48,21 @@ public List getRoute(Point from, Point to) { return route; } + private List getPalaceRoute(Point from, Point to) { + int pathX = abs(to.getPathX(from)); + int pathY = abs(to.getPathY(from)); + + if (pathX == 1 && pathY == 1) { + return List.of(); + } + if (pathX == 2 && pathY == 2) { + int middleX = (from.getX() + to.getX()) / 2; + int middleY = (from.getY() + to.getY()) / 2; + return List.of(Point.of(middleX, middleY)); + } + throw new IllegalArgumentException("[ERROR] 궁성 대각선 경로가 아닙니다."); + } + @Override public boolean canMove(List route) { if (route.size() > 1) { @@ -48,7 +79,7 @@ public boolean canCapture(Piece target) { private void validateDiagonalMove(int pathX, int pathY) { if (pathX != 0 && pathY != 0) { - throw new IllegalArgumentException("대각선 이동은 불가능합니다."); + throw new IllegalArgumentException("[ERROR] 대각선 이동은 불가능합니다."); } } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index c4802f0d83..0699059d8f 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -9,6 +9,7 @@ public interface Piece { boolean canMove(List route); boolean isSameTeam(Team team); PieceType getType(); + Team getTeam(); default boolean isSameType(PieceType type) { return getType() == type; @@ -17,4 +18,8 @@ default boolean isSameType(PieceType type) { default boolean canCapture(Piece targetPiece) { return true; } + + default int getScore() { + return getType().getScore(); + } } diff --git a/src/main/java/janggi/domain/piece/PieceFactory.java b/src/main/java/janggi/domain/piece/PieceFactory.java index 23fd146b49..587103bc01 100644 --- a/src/main/java/janggi/domain/piece/PieceFactory.java +++ b/src/main/java/janggi/domain/piece/PieceFactory.java @@ -18,7 +18,7 @@ public class PieceFactory { public static Piece initPiece(Team team, PieceType pieceType) { if (!FACTORY.containsKey(pieceType)) { - throw new IllegalArgumentException("존재하지 않는 기물입니다."); + throw new IllegalArgumentException("[ERROR] 존재하지 않는 기물입니다."); } return FACTORY.get(pieceType).apply(team); } diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index 0b878803c1..98aa729b26 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -1,21 +1,27 @@ package janggi.domain.piece; public enum PieceType { - CHA("차"), - PHO("포"), - MA("마"), - SANG("상"), - JANG("장"), - SA("사"), - JOL("졸"); + CHA("차", 3), + PHO("포", 3), + MA("마", 3), + SANG("상", 3), + JANG("장", 5), + SA("사", 1), + JOL("졸", 1); private final String name; + private final int score; - PieceType(String name) { + PieceType(String name, int score) { this.name = name; + this.score = score; } public String getName() { return name; } + + public int getScore() { + return score; + } } diff --git a/src/main/java/janggi/domain/piece/Sa.java b/src/main/java/janggi/domain/piece/Sa.java index 9d46638129..d190d3db7f 100644 --- a/src/main/java/janggi/domain/piece/Sa.java +++ b/src/main/java/janggi/domain/piece/Sa.java @@ -14,32 +14,26 @@ public Sa(Team team) { super(team, PieceType.SA); } - @Override - public boolean isSameTeam(Team team) { - return this.team.equals(team); - } - @Override public List getRoute(Point from, Point to) { - int pathX = to.getPathX(from); - int pathY = to.getPathY(from); - int distanceX = abs(pathX); - int distanceY = abs(pathY); - - validateOverMove(distanceX, distanceY); - - return List.of(to); + if (!from.isInSamePalace(to)) { + throw new IllegalArgumentException("[ERROR] 사는 같은 궁성 안에서만 이동할 수 있습니다."); + } + if (isNormalMove(from, to) || isPalaceDiagonalMove(from, to)) { + return List.of(); + } + throw new IllegalArgumentException("[ERROR] 사는 궁성 안에서 한 칸만 이동할 수 있습니다."); } - @Override - public boolean canMove(List route) { - return route.stream() - .noneMatch(piece -> piece.isSameTeam(team)); + private boolean isNormalMove(Point from, Point to) { + int pathX = abs(to.getPathX(from)); + int pathY = abs(to.getPathY(from)); + return pathX + pathY == MAX_DISTANCE; } - private void validateOverMove(int distanceX, int distanceY) { - if (distanceX + distanceY != MAX_DISTANCE) { - throw new IllegalArgumentException("한 칸만 이동할 수 있습니다."); - } + private boolean isPalaceDiagonalMove(Point from, Point to) { + int pathX = abs(to.getPathX(from)); + int pathY = abs(to.getPathY(from)); + return pathX == 1 && pathY == 1 && from.isPalaceDiagonalMove(to); } } diff --git a/src/main/java/janggi/domain/piece/Sang.java b/src/main/java/janggi/domain/piece/Sang.java index 08b35d8733..117c75b1a8 100644 --- a/src/main/java/janggi/domain/piece/Sang.java +++ b/src/main/java/janggi/domain/piece/Sang.java @@ -29,19 +29,19 @@ public List getRoute(Point from, Point to) { if (distanceX == DISTANCE_MAX) { return List.of( Point.of(from.getX() + (pathX / DISTANCE_MAX), from.getY()), - Point.of(from.getX() + signX * DISTANCE_MIN, from.getY() + signY) + Point.of(from.getX() + signX * DISTANCE_MIN, from.getY() + signY) ); } return List.of( Point.of(from.getX(), from.getY() + (pathY / DISTANCE_MAX)), - Point.of(from.getX() + signX, from.getY() + signY * DISTANCE_MIN) + Point.of(from.getX() + signX, from.getY() + signY * DISTANCE_MIN) ); } private void validateMove(int distanceX, int distanceY) { - if (!((distanceX == DISTANCE_MAX && distanceY == DISTANCE_MIN) || - (distanceX == DISTANCE_MIN && distanceY == DISTANCE_MAX))) { - throw new IllegalArgumentException("해당 기물의 이동 경로의 규칙에 어긋납니다."); + if (!((distanceX == DISTANCE_MAX && distanceY == DISTANCE_MIN) + || (distanceX == DISTANCE_MIN && distanceY == DISTANCE_MAX))) { + throw new IllegalArgumentException("[ERROR] 해당 기물의 이동 경로의 규칙에 어긋납니다."); } } } diff --git a/src/main/java/janggi/domain/status/ChoTurn.java b/src/main/java/janggi/domain/status/ChoTurn.java index b7b84d7221..1367f958e9 100644 --- a/src/main/java/janggi/domain/status/ChoTurn.java +++ b/src/main/java/janggi/domain/status/ChoTurn.java @@ -1,6 +1,6 @@ package janggi.domain.status; -import janggi.domain.Board; +import janggi.domain.Boards; import janggi.domain.Point; public class ChoTurn implements GameStatus { @@ -17,9 +17,9 @@ public Team getTeam() { } @Override - public GameStatus move(Point from, Point to, Board board) { - board.move(from, to, team); - if (board.isKingDie(Team.HAN)) { + public GameStatus move(Point from, Point to, Boards boards) { + boards.move(from, to, team); + if (boards.isKingDie(Team.HAN)) { return new FinishedGame(team); } return new HanTurn(); diff --git a/src/main/java/janggi/domain/status/FinishedGame.java b/src/main/java/janggi/domain/status/FinishedGame.java index a30cbc8d64..b1614fbb89 100644 --- a/src/main/java/janggi/domain/status/FinishedGame.java +++ b/src/main/java/janggi/domain/status/FinishedGame.java @@ -1,6 +1,6 @@ package janggi.domain.status; -import janggi.domain.Board; +import janggi.domain.Boards; import janggi.domain.Point; public class FinishedGame implements GameStatus { @@ -22,7 +22,7 @@ public Team getTeam() { } @Override - public GameStatus move(Point from, Point to, Board board) { - throw new RuntimeException("게임이 종료되었습니다.\n 승자는 "+ winner.name()); + public GameStatus move(Point from, Point to, Boards boards) { + throw new IllegalStateException("[ERROR] 게임이 종료되었습니다. 승자는 " + winner.name() + "입니다."); } } diff --git a/src/main/java/janggi/domain/status/GameStatus.java b/src/main/java/janggi/domain/status/GameStatus.java index 1661246ffc..b7632c42cb 100644 --- a/src/main/java/janggi/domain/status/GameStatus.java +++ b/src/main/java/janggi/domain/status/GameStatus.java @@ -1,11 +1,11 @@ package janggi.domain.status; -import janggi.domain.Board; +import janggi.domain.Boards; import janggi.domain.Point; public interface GameStatus { Team getTeam(); - GameStatus move(Point from, Point to, Board board); + GameStatus move(Point from, Point to, Boards boards); default boolean isFinished() { return false; diff --git a/src/main/java/janggi/domain/status/HanTurn.java b/src/main/java/janggi/domain/status/HanTurn.java index 9c11e612db..7aafa937c4 100644 --- a/src/main/java/janggi/domain/status/HanTurn.java +++ b/src/main/java/janggi/domain/status/HanTurn.java @@ -1,6 +1,6 @@ package janggi.domain.status; -import janggi.domain.Board; +import janggi.domain.Boards; import janggi.domain.Point; public class HanTurn implements GameStatus { @@ -17,9 +17,9 @@ public Team getTeam() { } @Override - public GameStatus move(Point from, Point to, Board board) { - board.move(from, to, team); - if (board.isKingDie(Team.CHO)) { + public GameStatus move(Point from, Point to, Boards boards) { + boards.move(from, to, team); + if (boards.isKingDie(Team.CHO)) { return new FinishedGame(team); } return new ChoTurn(); diff --git a/src/main/java/janggi/dto/GameSnapshot.java b/src/main/java/janggi/dto/GameSnapshot.java new file mode 100644 index 0000000000..9d3ce86c47 --- /dev/null +++ b/src/main/java/janggi/dto/GameSnapshot.java @@ -0,0 +1,13 @@ +package janggi.dto; + +import janggi.domain.status.Team; +import java.util.List; + +public record GameSnapshot( + Long id, + Team currentTurn, + boolean finished, + Team winner, + List positions +) { +} diff --git a/src/main/java/janggi/dto/GameStatusInfo.java b/src/main/java/janggi/dto/GameStatusInfo.java index e2d74fdda3..a5a8067d8b 100644 --- a/src/main/java/janggi/dto/GameStatusInfo.java +++ b/src/main/java/janggi/dto/GameStatusInfo.java @@ -17,10 +17,10 @@ public static GameStatusInfo from(List> board) { ); } - private static List toPieceInfos(List row) { + private static List toPieceInfos(List piecesAtY) { List pieceInfos = new ArrayList<>(); - for (Piece piece : row) { + for (Piece piece : piecesAtY) { pieceInfos.add(createPieceInfo(piece)); } return pieceInfos; @@ -30,13 +30,6 @@ private static PieceInfo createPieceInfo(Piece piece) { if (piece == null) { return new PieceInfo("+", null); } - return new PieceInfo(piece.getType().getName(), extractTeam(piece)); - } - - private static Team extractTeam(Piece piece) { - if (piece.isSameTeam(Team.HAN)) { - return Team.HAN; - } - return Team.CHO; + return new PieceInfo(piece.getType().getName(), piece.getTeam()); } } diff --git a/src/main/java/janggi/dto/GameSummary.java b/src/main/java/janggi/dto/GameSummary.java new file mode 100644 index 0000000000..0eec6722c8 --- /dev/null +++ b/src/main/java/janggi/dto/GameSummary.java @@ -0,0 +1,7 @@ +package janggi.dto; + +public record GameSummary( + Long id, + boolean finished +) { +} diff --git a/src/main/java/janggi/dto/PositionInfo.java b/src/main/java/janggi/dto/PositionInfo.java index 9e26f7ee3b..c3a73f533f 100644 --- a/src/main/java/janggi/dto/PositionInfo.java +++ b/src/main/java/janggi/dto/PositionInfo.java @@ -6,19 +6,13 @@ import janggi.domain.piece.PieceType; import janggi.domain.status.Team; import java.util.List; +import java.util.Map; +import java.util.Set; public record PositionInfo( Piece piece, Point point ) { - public static PositionInfo from(List data) { - return from( - Team.valueOf(data.get(0)), - data.get(1), - Integer.parseInt(data.get(2)), - Integer.parseInt(data.get(3)) - ); - } public static PositionInfo from(Team team, String pieceName, int x, int y) { return new PositionInfo( @@ -26,4 +20,11 @@ public static PositionInfo from(Team team, String pieceName, int x, int y) { Point.of(x, y) ); } + + public static List from(Map boardStatus) { + return boardStatus.entrySet().stream() + .filter(entry -> entry.getValue() != null) + .map(entry -> new PositionInfo(entry.getValue(), entry.getKey())) + .toList(); + } } diff --git a/src/main/java/janggi/repository/CsvInitialBoardProvider.java b/src/main/java/janggi/repository/CsvInitialBoardProvider.java new file mode 100644 index 0000000000..129dfddf1c --- /dev/null +++ b/src/main/java/janggi/repository/CsvInitialBoardProvider.java @@ -0,0 +1,13 @@ +package janggi.repository; + +import janggi.dto.PositionInfo; +import janggi.util.FileParser; +import java.util.List; + +public class CsvInitialBoardProvider implements InitialBoardProvider { + + @Override + public List load() { + return FileParser.readCsvFile("/janggi.csv"); + } +} diff --git a/src/main/java/janggi/repository/GameRepository.java b/src/main/java/janggi/repository/GameRepository.java new file mode 100644 index 0000000000..9205481edf --- /dev/null +++ b/src/main/java/janggi/repository/GameRepository.java @@ -0,0 +1,13 @@ +package janggi.repository; + +import janggi.dto.GameSnapshot; +import janggi.dto.GameSummary; +import java.util.List; +import java.util.Optional; + +public interface GameRepository { + List findAll(); + Optional findById(Long gameId); + Long save(GameSnapshot gameSnapshot); + void update(GameSnapshot gameSnapshot); +} diff --git a/src/main/java/janggi/repository/InitialBoardProvider.java b/src/main/java/janggi/repository/InitialBoardProvider.java new file mode 100644 index 0000000000..8abe5faa07 --- /dev/null +++ b/src/main/java/janggi/repository/InitialBoardProvider.java @@ -0,0 +1,8 @@ +package janggi.repository; + +import janggi.dto.PositionInfo; +import java.util.List; + +public interface InitialBoardProvider { + List load(); +} diff --git a/src/main/java/janggi/repository/JdbcGameRepository.java b/src/main/java/janggi/repository/JdbcGameRepository.java new file mode 100644 index 0000000000..facf251ca5 --- /dev/null +++ b/src/main/java/janggi/repository/JdbcGameRepository.java @@ -0,0 +1,236 @@ +package janggi.repository; + +import janggi.dto.GameSnapshot; +import janggi.dto.GameSummary; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import janggi.util.JdbcConnectionManager; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class JdbcGameRepository implements GameRepository { + + private JdbcConnectionManager connectionManager; + + public JdbcGameRepository(JdbcConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + @Override + public List findAll() { + try ( + Connection connection = connectionManager.getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT id, current_turn, finished FROM games ORDER BY id" + ); + ResultSet resultSet = statement.executeQuery() + ) { + return toGameSummaries(resultSet); + } catch (SQLException exception) { + throw new RuntimeException("[ERROR] 게임 목록 조회 중 데이터베이스 오류가 발생했습니다.", exception); + } + } + + @Override + public Optional findById(Long gameId) { + try ( + Connection connection = connectionManager.getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT id, current_turn, finished, winner FROM games WHERE id = ?" + ) + ) { + statement.setLong(1, gameId); + ResultSet resultSet = statement.executeQuery(); + if (!resultSet.next()) { + return Optional.empty(); + } + return Optional.of(toGameSnapshot(connection, resultSet)); + } catch (SQLException exception) { + throw new RuntimeException("[ERROR] 게임 조회 중 데이터베이스 오류가 발생했습니다.", exception); + } + + } + + @Override + public Long save(GameSnapshot gameSnapshot) { + try (Connection connection = connectionManager.getConnection()) { + Long gameId = insertGame(connection, gameSnapshot); + insertPieces(connection, gameId, gameSnapshot.positions()); + return gameId; + } catch (SQLException exception) { + throw new RuntimeException("[ERROR] 게임 저장 중 데이터베이스 오류가 발생했습니다.", exception); + } + } + + @Override + public void update(GameSnapshot gameSnapshot) { + try (Connection connection = connectionManager.getConnection()) { + updateGame(connection, gameSnapshot); + deletePieces(connection, gameSnapshot.id()); + insertPieces(connection, gameSnapshot.id(), gameSnapshot.positions()); + } catch (SQLException exception) { + throw new RuntimeException("[ERROR] 게임 수정 중 데이터베이스 오류가 발생했습니다.", exception); + } + } + + private void updateGame( + Connection connection, + GameSnapshot gameSnapshot + ) throws SQLException { + try ( + PreparedStatement statement = connection.prepareStatement( + "UPDATE games SET current_turn = ?, finished = ?, winner = ? WHERE id = ?" + ) + ) { + statement.setString(1, gameSnapshot.currentTurn().name()); + statement.setBoolean(2, gameSnapshot.finished()); + statement.setString(3, winnerName(gameSnapshot.winner())); + statement.setLong(4, gameSnapshot.id()); + statement.executeUpdate(); + } + } + + private void deletePieces( + Connection connection, + Long gameId + ) throws SQLException { + try ( + PreparedStatement statement = connection.prepareStatement( + "DELETE FROM game_pieces WHERE game_id = ?" + ) + ) { + statement.setLong(1, gameId); + statement.executeUpdate(); + } + } + + private Long insertGame(Connection connection, GameSnapshot gameSnapshot) throws SQLException { + try ( + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO games(current_turn, finished, winner) VALUES (?, ?, ?)", + PreparedStatement.RETURN_GENERATED_KEYS + ) + ) { + statement.setString(1, gameSnapshot.currentTurn().name()); + statement.setBoolean(2, gameSnapshot.finished()); + statement.setString(3, winnerName(gameSnapshot.winner())); + statement.executeUpdate(); + return generatedId(statement); + } + } + + private Long generatedId(PreparedStatement statement) throws SQLException { + ResultSet resultSet = statement.getGeneratedKeys(); + resultSet.next(); + return resultSet.getLong(1); + } + + private void insertPieces( + Connection connection, + Long gameId, + List positions + ) throws SQLException { + for (PositionInfo positionInfo : positions) { + insertPiece(connection, gameId, positionInfo); + } + } + + private void insertPiece( + Connection connection, + Long gameId, + PositionInfo position + ) throws SQLException { + try ( + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO game_pieces(game_id, team, piece_type, x_value, y_value) " + + "VALUES (?, ?, ?, ?, ?)" + ) + ) { + statement.setLong(1, gameId); + statement.setString(2, teamName(position)); + statement.setString(3, position.piece().getType().name()); + statement.setInt(4, position.point().getX()); + statement.setInt(5, position.point().getY()); + statement.executeUpdate(); + } + } + + private String teamName(PositionInfo position) { + if (position.piece().isSameTeam(Team.HAN)) { + return Team.HAN.name(); + } + return Team.CHO.name(); + } + + private String winnerName(Team winner) { + if (winner == null) { + return null; + } + return winner.name(); + } + + private Team winner(String winner) { + if (winner == null) { + return null; + } + return Team.valueOf(winner); + } + + private List toGameSummaries(ResultSet resultSet) throws SQLException { + List gameSummaries = new ArrayList<>(); + while (resultSet.next()) { + gameSummaries.add(new GameSummary( + resultSet.getLong("id"), + resultSet.getBoolean("finished") + )); + } + return gameSummaries; + } + + private List findPositions( + Connection connection, + Long gameId + ) throws SQLException { + try ( + PreparedStatement statement = connection.prepareStatement( + "SELECT team, piece_type, x_value, y_value FROM game_pieces WHERE game_id = ? ORDER BY id" + ) + ) { + statement.setLong(1, gameId); + ResultSet resultSet = statement.executeQuery(); + return toPositions(resultSet); + } + } + + private List toPositions(ResultSet resultSet) throws SQLException { + List positions = new ArrayList<>(); + while (resultSet.next()) { + positions.add(PositionInfo.from( + Team.valueOf(resultSet.getString("team")), + resultSet.getString("piece_type"), + resultSet.getInt("x_value"), + resultSet.getInt("y_value") + )); + } + return positions; + } + + private GameSnapshot toGameSnapshot( + Connection connection, + ResultSet resultSet + ) throws SQLException { + Long gameId = resultSet.getLong("id"); + return new GameSnapshot( + gameId, + Team.valueOf(resultSet.getString("current_turn")), + resultSet.getBoolean("finished"), + winner(resultSet.getString("winner")), + findPositions(connection, gameId) + ); + } +} diff --git a/src/main/java/janggi/service/JanggiGameService.java b/src/main/java/janggi/service/JanggiGameService.java new file mode 100644 index 0000000000..daf4306a65 --- /dev/null +++ b/src/main/java/janggi/service/JanggiGameService.java @@ -0,0 +1,89 @@ +package janggi.service; + +import janggi.dto.GameSnapshot; +import janggi.dto.GameSummary; +import janggi.domain.Board; +import janggi.domain.JanggiGame; +import janggi.domain.Point; +import janggi.domain.status.ChoTurn; +import janggi.domain.status.FinishedGame; +import janggi.domain.status.GameStatus; +import janggi.domain.status.HanTurn; +import janggi.domain.status.Team; +import janggi.repository.GameRepository; +import janggi.repository.InitialBoardProvider; +import java.util.List; + +public class JanggiGameService { + + private final GameRepository gameRepository; + private final InitialBoardProvider initialBoardProvider; + + public JanggiGameService(GameRepository gameRepository, InitialBoardProvider initialBoardProvider) { + this.gameRepository = gameRepository; + this.initialBoardProvider = initialBoardProvider; + } + + public List findAllGames() { + return validateGameList(gameRepository.findAll()); + } + + public JanggiGame startNewGame() { + Board board = new Board(); + board.init(initialBoardProvider.load()); + return new JanggiGame(board); + } + + public JanggiGame loadGame(Long gameId) { + GameSnapshot gameSnapshot = gameRepository.findById(gameId) + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 게임입니다.")); + Board board = new Board(); + board.init(gameSnapshot.positions()); + return new JanggiGame(board, gameStatus(gameSnapshot)); + } + + public void play(Long gameId, Point from, Point to) { + JanggiGame janggiGame = loadGame(gameId); + janggiGame.play(from, to); + gameRepository.update(toSnapshot(gameId, janggiGame)); + } + + public Long createGame() { + JanggiGame janggiGame = startNewGame(); + return gameRepository.save(toSnapshot(null, janggiGame)); + } + + private GameStatus gameStatus(GameSnapshot gameSnapshot) { + if (gameSnapshot.finished()) { + return new FinishedGame(gameSnapshot.winner()); + } + if (gameSnapshot.currentTurn() == Team.HAN) { + return new HanTurn(); + } + return new ChoTurn(); + } + + private GameSnapshot toSnapshot(Long gameId, JanggiGame janggiGame) { + return new GameSnapshot( + gameId, + janggiGame.currentTurn(), + janggiGame.isFinished(), + winner(janggiGame), + janggiGame.boardStatus() + ); + } + + private Team winner(JanggiGame janggiGame) { + if (!janggiGame.isFinished()) { + return null; + } + return janggiGame.getWinner(); + } + + private List validateGameList(List gameSummaries) { + if (gameSummaries.isEmpty()) { + throw new IllegalStateException("[ERROR] 저장된 게임이 없습니다."); + } + return gameSummaries; + } +} diff --git a/src/main/java/janggi/ui/InputView.java b/src/main/java/janggi/ui/InputView.java index ada1ce9c78..f407371a6d 100644 --- a/src/main/java/janggi/ui/InputView.java +++ b/src/main/java/janggi/ui/InputView.java @@ -7,15 +7,38 @@ public class InputView { + private static final String NEW_GAME_COMMAND = "1"; + private static final String LOAD_GAME_COMMAND = "2"; + private InputView() { } + public static String readGameCommand() { + System.out.println("장기 게임을 시작합니다."); + System.out.println("새 게임은 1, 이어하기는 2 입력해 주세요 : "); + return Console.readLine().trim(); + } + + public static Long readGameId() { + System.out.println("불러올 게임 id를 입력해 주세요 : "); + return Long.parseLong(Console.readLine().trim()); + } + public static List readPoints() { - return List.of(Parser.parsePoint(readFromPoint()), + return List.of( + Parser.parsePoint(readFromPoint()), Parser.parsePoint(readToPoint()) ); } + public static boolean isNewGameCommand(String command) { + return NEW_GAME_COMMAND.equals(command); + } + + public static boolean isLoadGameCommand(String command) { + return LOAD_GAME_COMMAND.equals(command); + } + private static String readFromPoint() { System.out.println("움직일 기물의 출발지를 입력해 주세요 (예 : 1,2) : "); return Console.readLine(); diff --git a/src/main/java/janggi/ui/OutputView.java b/src/main/java/janggi/ui/OutputView.java index b52efef781..4b8ca44f9d 100644 --- a/src/main/java/janggi/ui/OutputView.java +++ b/src/main/java/janggi/ui/OutputView.java @@ -1,8 +1,10 @@ package janggi.ui; -import janggi.domain.status.Team; import janggi.dto.GameStatusInfo; +import janggi.dto.GameSummary; import janggi.dto.PieceInfo; +import janggi.domain.status.Team; +import java.util.List; public class OutputView { @@ -10,20 +12,64 @@ public class OutputView { private static final String GREEN = "\u001B[32m"; private static final String RED = "\u001B[31m"; + public static void printSavedGames(List gameSummaries) { + System.out.println(); + System.out.println("저장된 게임 목록입니다."); + gameSummaries.forEach(OutputView::printGameSummary); + System.out.println(); + } + + public static void printCurrentTurn(String teamName) { + System.out.println("현재 턴 : " + teamName); + } + public static void printWinner(Team winner) { System.out.println("승자는 " + winner.getName()); } public static void printGameStatus(GameStatusInfo status) { System.out.println(); - status.pieces().stream() - .map(row -> row.stream() - .map(OutputView::formatPiece) - .toList()) - .forEach(System.out::println); + printHeader(); + printLinesByY(status.pieces()); System.out.println(); } + public static void printScore(int choScore, int hanScore) { + System.out.println("최종 점수"); + System.out.println("초 : " + choScore); + System.out.println("한 : " + hanScore); + } + + private static void printHeader() { + System.out.println(" " + List.of(0, 1, 2, 3, 4, 5, 6, 7, 8)); + } + + private static void printLinesByY(List> pieces) { + for (int y = pieces.size() - 1; y >= 0; y--) { + System.out.println(y + " " + formatLineAtY(pieces.get(y))); + } + } + + private static String formatLineAtY(List piecesAtY) { + return piecesAtY.stream() + .map(OutputView::formatPiece) + .toList() + .toString(); + } + + private static void printGameSummary(GameSummary gameSummary) { + System.out.println( + gameSummary.id() + "번 게임 - " + gameStatus(gameSummary) + ); + } + + private static String gameStatus(GameSummary gameSummary) { + if (gameSummary.finished()) { + return "종료"; + } + return "진행 중"; + } + private static String formatPiece(PieceInfo piece) { if (piece.team() == null) { return piece.name(); diff --git a/src/main/java/janggi/util/DatabaseInitializer.java b/src/main/java/janggi/util/DatabaseInitializer.java new file mode 100644 index 0000000000..f3a699b53c --- /dev/null +++ b/src/main/java/janggi/util/DatabaseInitializer.java @@ -0,0 +1,45 @@ +package janggi.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public class DatabaseInitializer { + + private final JdbcConnectionManager connectionManager; + + public DatabaseInitializer(JdbcConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + public void init() { + try ( + Connection connection = connectionManager.getConnection(); + Statement statement = connection.createStatement() + ) { + statement.execute(readSchema()); + } catch (SQLException | IOException exception) { + throw new RuntimeException("[ERROR] 데이터베이스 초기화 중 오류가 발생했습니다.", exception); + } + } + + private String readSchema() throws IOException { + BufferedReader reader = new BufferedReader( + new InputStreamReader(schemaStream(), StandardCharsets.UTF_8)); + return reader.lines() + .reduce("", (first, second) -> first + second + "\n"); + } + + private InputStream schemaStream() { + InputStream stream = getClass().getResourceAsStream("/schema.sql"); + if (stream == null) { + throw new IllegalArgumentException("[ERROR] schema.sql 파일을 찾을 수 없습니다."); + } + return stream; + } +} diff --git a/src/main/java/janggi/util/FileReader.java b/src/main/java/janggi/util/FileReader.java index 965397c4ea..65a5a6be62 100644 --- a/src/main/java/janggi/util/FileReader.java +++ b/src/main/java/janggi/util/FileReader.java @@ -16,7 +16,7 @@ public static List readFile(String resourcePath) { try (BufferedReader reader = createReader(resourcePath)) { return readLines(reader); } catch (IOException e) { - throw new IllegalStateException("[ERROR] 파일을 찾을 수 없습니다."); + throw new IllegalStateException("[ERROR] 파일을 읽을 수 없습니다.", e); } } @@ -36,4 +36,4 @@ private static List readLines(BufferedReader reader) throws IOException } return lines; } -} \ No newline at end of file +} diff --git a/src/main/java/janggi/util/JdbcConnectionManager.java b/src/main/java/janggi/util/JdbcConnectionManager.java new file mode 100644 index 0000000000..64c631d9ee --- /dev/null +++ b/src/main/java/janggi/util/JdbcConnectionManager.java @@ -0,0 +1,22 @@ +package janggi.util; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class JdbcConnectionManager { + + private final String URL; + private final String USER; + private final String PASSWORD; + + public JdbcConnectionManager(String url, String user, String password) { + this.URL = url; + this.USER = user; + this.PASSWORD = password; + } + + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(URL, USER, PASSWORD); + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000000..9df06562d9 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS games ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + current_turn VARCHAR(10) NOT NULL, + finished BOOLEAN NOT NULL, + winner VARCHAR(10) +); + +CREATE TABLE IF NOT EXISTS game_pieces ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + game_id BIGINT NOT NULL, + team VARCHAR(10) NOT NULL, + piece_type VARCHAR(10) NOT NULL, + x_value INT NOT NULL, + y_value INT NOT NULL, + FOREIGN KEY (game_id) REFERENCES games(id) +); diff --git a/src/test/java/janggi/domain/BoardTest.java b/src/test/java/janggi/domain/BoardTest.java new file mode 100644 index 0000000000..23c142ff5b --- /dev/null +++ b/src/test/java/janggi/domain/BoardTest.java @@ -0,0 +1,195 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class BoardTest { + + @Test + @DisplayName("출발지에 기물이 없으면 예외가 발생한다") + void moveFromEmptyPoint() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + assertThatThrownBy(() -> board.move(Point.of(0, 0), Point.of(0, 1), Team.CHO)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("출발지에 이동할 기물이 없습니다."); + } + + @Test + @DisplayName("상대 기물은 움직일 수 없다") + void moveOtherTeamPiece() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.HAN, "CHA", 0, 0) + ); + + assertThatThrownBy(() -> board.move(Point.of(0, 0), Point.of(0, 1), Team.CHO)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("상대방의 기물은 움직일 수 없습니다."); + } + + @Test + @DisplayName("도착지에 아군 기물이 있으면 예외가 발생한다") + void moveToSameTeamPiece() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.CHO, "CHA", 0, 0), + PositionInfo.from(Team.CHO, "JOL", 0, 1) + ); + + assertThatThrownBy(() -> board.move(Point.of(0, 0), Point.of(0, 1), Team.CHO)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("도착지에 본인의 기물이 있습니다."); + } + + @Test + @DisplayName("이동 경로에 장애물이 있으면 예외가 발생한다") + void moveBlockedRoute() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.CHO, "CHA", 0, 0), + PositionInfo.from(Team.CHO, "JOL", 0, 1) + ); + + assertThatThrownBy(() -> board.move(Point.of(0, 0), Point.of(0, 3), Team.CHO)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("이동 경로에 장애물이 있거나 규칙에 어긋납니다."); + } + + @Test + @DisplayName("포는 상대 포를 잡을 수 없다") + void phoCanNotCapturePho() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.CHO, "PHO", 1, 2), + PositionInfo.from(Team.HAN, "CHA", 1, 4), + PositionInfo.from(Team.HAN, "PHO", 1, 6) + ); + + assertThatThrownBy(() -> board.move(Point.of(1, 2), Point.of(1, 6), Team.CHO)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("해당 타겟을 잡을 수 없습니다."); + } + + @Test + @DisplayName("기물을 이동하면 출발지는 비고 도착지에 기물이 위치한다") + void movePiece() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.CHO, "CHA", 0, 0) + ); + + board.move(Point.of(0, 0), Point.of(0, 3), Team.CHO); + + assertThat(pieceAt(board, 0, 0)).isNull(); + assertThat(pieceAt(board, 0, 3).getType()).isEqualTo(PieceType.CHA); + assertThat(pieceAt(board, 0, 3).isSameTeam(Team.CHO)).isTrue(); + } + + @Test + @DisplayName("적 기물이 있는 위치로 이동하면 포획한다") + void capturePiece() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.CHO, "CHA", 0, 0), + PositionInfo.from(Team.HAN, "JOL", 0, 3) + ); + + board.move(Point.of(0, 0), Point.of(0, 3), Team.CHO); + + assertThat(pieceAt(board, 0, 0)).isNull(); + assertThat(pieceAt(board, 0, 3).getType()).isEqualTo(PieceType.CHA); + assertThat(pieceAt(board, 0, 3).isSameTeam(Team.CHO)).isTrue(); + } + + @Test + @DisplayName("팀의 왕이 살아있으면 죽지 않은 상태다") + void kingAlive() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + Boards boards = new Boards(board); + + assertThat(boards.isKingDie(Team.CHO)).isFalse(); + assertThat(boards.isKingDie(Team.HAN)).isFalse(); + } + + @Test + @DisplayName("팀의 왕이 없으면 죽은 상태다") + void kingDie() { + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1) + ); + Boards boards = new Boards(board); + + assertThat(boards.isKingDie(Team.HAN)).isTrue(); + } + + @Test + @DisplayName("보드의 현재 상태 반환") + void boardStatus() { + // given + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + Boards boards = new Boards(board); + + // when + List boardStatus = boards.getBoardStatus(); + + // then + assertThat(boardStatus.size()).isEqualTo(2); + } + + @Test + @DisplayName("팀별 현재 기물 점수 계산") + void score_of_team() { + // given + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.CHO, "CHA", 0, 0), + PositionInfo.from(Team.CHO, "JOL", 0, 3), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.HAN, "MA", 1, 9), + PositionInfo.from(Team.HAN, "SA", 3, 9) + ); + Boards boards = new Boards(board); + + // when + int choScore = boards.scoreOf(Team.CHO); + int hanScore = boards.scoreOf(Team.HAN); + + // then + assertThat(choScore).isEqualTo(9); + assertThat(hanScore).isEqualTo(9); + } + + private Board initBoard(PositionInfo... positionInfos) { + Board board = new Board(); + board.init(List.of(positionInfos)); + return board; + } + + private Piece pieceAt(Board board, int x, int y) { + return board.getPiecesByPoint().get(Point.of(x, y)); + } +} diff --git a/src/test/java/janggi/domain/BoardsTest.java b/src/test/java/janggi/domain/BoardsTest.java new file mode 100644 index 0000000000..b0c2226fa4 --- /dev/null +++ b/src/test/java/janggi/domain/BoardsTest.java @@ -0,0 +1,94 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class BoardsTest { + + @Test + @DisplayName("팀의 왕이 살아있으면 죽지 않은 상태다") + void kingAlive() { + Boards boards = initBoards( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + assertThat(boards.isKingDie(Team.CHO)).isFalse(); + assertThat(boards.isKingDie(Team.HAN)).isFalse(); + } + + @Test + @DisplayName("팀의 왕이 없으면 죽은 상태다") + void kingDie() { + Boards boards = initBoards( + PositionInfo.from(Team.CHO, "JANG", 4, 1) + ); + + assertThat(boards.isKingDie(Team.HAN)).isTrue(); + } + + @Test + @DisplayName("보드의 현재 상태를 저장용 목록으로 반환한다") + void boardStatus() { + Boards boards = initBoards( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + List boardStatus = boards.getBoardStatus(); + + assertThat(boardStatus).hasSize(2); + assertThat(boardStatus.get(0).point()).isEqualTo(Point.of(4, 1)); + assertThat(boardStatus.get(1).point()).isEqualTo(Point.of(4, 8)); + } + + @Test + @DisplayName("팀별 현재 기물 점수를 계산한다") + void scoreOfTeam() { + Boards boards = initBoards( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.CHO, "CHA", 0, 0), + PositionInfo.from(Team.CHO, "JOL", 0, 3), + PositionInfo.from(Team.HAN, "JANG", 4, 8), + PositionInfo.from(Team.HAN, "MA", 1, 9), + PositionInfo.from(Team.HAN, "SA", 3, 9) + ); + + int choScore = boards.scoreOf(Team.CHO); + int hanScore = boards.scoreOf(Team.HAN); + + assertThat(choScore).isEqualTo(9); + assertThat(hanScore).isEqualTo(9); + } + + @Test + @DisplayName("보드 현재 기물 배치를 조회한다") + void getPoints() { + Boards boards = initBoards( + PositionInfo.from(Team.CHO, "CHA", 0, 0), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + List> points = boards.getPoints(); + + assertThat(points).hasSize(10); + assertThat(points.get(0)).hasSize(9); + assertThat(points.get(0).get(0).getType()).isEqualTo(PieceType.CHA); + assertThat(points.get(8).get(4).getType()).isEqualTo(PieceType.JANG); + assertThat(points.get(1).get(0)).isNull(); + } + + private Boards initBoards(PositionInfo... positionInfos) { + Board board = new Board(); + board.init(List.of(positionInfos)); + return new Boards(board); + } +} diff --git a/src/test/java/janggi/domain/JanggiGameTest.java b/src/test/java/janggi/domain/JanggiGameTest.java new file mode 100644 index 0000000000..c4f7cea747 --- /dev/null +++ b/src/test/java/janggi/domain/JanggiGameTest.java @@ -0,0 +1,70 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.status.HanTurn; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class JanggiGameTest { + + @Test + @DisplayName("새 게임은 초나라 턴으로 시작") + void startChoTurn() { + // given + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + // when + JanggiGame janggiGame = new JanggiGame(board); + + // then + assertThat(janggiGame.currentTurn()).isEqualTo(Team.CHO); + } + + @Test + @DisplayName("현재 보드 상태를 저장용 목록으로 조회") + void getBoardStatus() { + // given + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + // when + JanggiGame janggiGame = new JanggiGame(board); + List boardStatus = janggiGame.boardStatus(); + + // then + assertThat(boardStatus).hasSize(2); + assertThat(boardStatus.get(0).point()).isEqualTo(Point.of(4, 1)); + assertThat(boardStatus.get(1).point()).isEqualTo(Point.of(4, 8)); + } + + @Test + @DisplayName("저장된 게임은 해당 턴 차례로 시작") + void restoreTurn() { + // given + Board board = initBoard( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ); + + // when + JanggiGame janggiGame = new JanggiGame(board, new HanTurn()); + + // then + assertThat(janggiGame.currentTurn()).isEqualTo(Team.HAN); + } + + private Board initBoard(PositionInfo... positionInfos) { + Board board = new Board(); + board.init(List.of(positionInfos)); + return board; + } +} diff --git a/src/test/java/janggi/domain/PointTest.java b/src/test/java/janggi/domain/PointTest.java index a9c721a888..9b976d1202 100644 --- a/src/test/java/janggi/domain/PointTest.java +++ b/src/test/java/janggi/domain/PointTest.java @@ -12,27 +12,55 @@ public class PointTest { @DisplayName("조회 시 해당 포인트가 나온다") void of() { // given - int column = 1; - int row = 2; + int x = 1; + int y = 2; // when - Point point = Point.of(column, row); + Point point = Point.of(x, y); // then - assertThat(point.getX()).isEqualTo(column); - assertThat(point.getY()).isEqualTo(row); + assertThat(point.getX()).isEqualTo(x); + assertThat(point.getY()).isEqualTo(y); } @Test @DisplayName("보드의 범위가 벗어난 곳에서 Point 생성시 예외 발생") void validate_of() { // given - int column = 10; - int row = 10; + int x = 10; + int y = 10; // when & then - assertThatThrownBy(() -> Point.of(column, row)) + assertThatThrownBy(() -> Point.of(x, y)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("올바르지 않은 위치 범위입니다."); } + + @Test + @DisplayName("from, to의 위치가 궁성 내에 있는지 판단") + void is_in_palace() { + // given + Point from = Point.of(3, 1); + Point to = Point.of(4, 1); + + // when + boolean result = from.isInSamePalace(to); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("궁성 내에서 대각선으로 이동하는지 판단") + void is_palace_diagonal_move() { + // given + Point from = Point.of(3, 0); + Point to = Point.of(5, 2); + + // when + boolean result = from.isPalaceDiagonalMove(to); + + // then + assertThat(result).isTrue(); + } } diff --git a/src/test/java/janggi/domain/piece/ChaTest.java b/src/test/java/janggi/domain/piece/ChaTest.java index 8b5e7e6da3..2843c87279 100644 --- a/src/test/java/janggi/domain/piece/ChaTest.java +++ b/src/test/java/janggi/domain/piece/ChaTest.java @@ -29,6 +29,21 @@ void straight_back_route(int x, int y, int result) { assertThat(route.size()).isEqualTo(result); } + @Test + @DisplayName("궁성 안에서 대각선으로 이동하는 기능") + void palace_diagonal_move() { + // given + Piece cha = new Cha(Team.CHO); + Point from = Point.of(3, 0); + Point to = Point.of(5, 2); + + // when + List route = cha.getRoute(from, to); + + // then + assertThat(route.size()).isEqualTo(1); + } + @Test @DisplayName("대각선으로 목적지로 할 경우 예외 발생") void destination_exception() { diff --git a/src/test/java/janggi/domain/piece/JangTest.java b/src/test/java/janggi/domain/piece/JangTest.java index c64b25acf3..1a27fecc52 100644 --- a/src/test/java/janggi/domain/piece/JangTest.java +++ b/src/test/java/janggi/domain/piece/JangTest.java @@ -21,4 +21,18 @@ void destination_exception() { assertThatThrownBy(() -> jang.getRoute(from, to)) .isInstanceOf(IllegalArgumentException.class); } + + @Test + @DisplayName("궁성 내에서 이동하지 않을 시 예외 발생") + void not_in_palace() { + // given + Piece jang = new Jang(Team.CHO); + Point from = Point.of(0,0); + Point to = Point.of(1, 1); + + // when & then + assertThatThrownBy(() -> jang.getRoute(from, to)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("궁성"); + } } diff --git a/src/test/java/janggi/domain/piece/JolTest.java b/src/test/java/janggi/domain/piece/JolTest.java index 82696bce45..e56573df81 100644 --- a/src/test/java/janggi/domain/piece/JolTest.java +++ b/src/test/java/janggi/domain/piece/JolTest.java @@ -1,9 +1,11 @@ package janggi.domain.piece; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import janggi.domain.Point; import janggi.domain.status.Team; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -50,4 +52,33 @@ void destination_exception() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("한 칸만"); } + + @Test + @DisplayName("대각선 이동 시 에외 발생") + void not_diagonal() { + // given + Piece jol = new Jol(Team.CHO); + Point from = Point.of(0,0); + Point to = Point.of(1, 1); + + // when & then + assertThatThrownBy(() -> jol.getRoute(from, to)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("대각선"); + } + + @Test + @DisplayName("궁성 내에서 대각선 이동") + void palace_diagonal_move() { + // given + Piece jol = new Jol(Team.HAN); + Point from = Point.of(3, 2); + Point to = Point.of(4, 1); + + // when + List result = jol.getRoute(from, to); + + // then + assertThat(result).isEmpty(); + } } diff --git a/src/test/java/janggi/domain/piece/PhoTest.java b/src/test/java/janggi/domain/piece/PhoTest.java index 721c782485..9ce0c2628b 100644 --- a/src/test/java/janggi/domain/piece/PhoTest.java +++ b/src/test/java/janggi/domain/piece/PhoTest.java @@ -7,6 +7,7 @@ import janggi.domain.Point; import janggi.domain.status.Team; import janggi.dto.PositionInfo; +import janggi.fixture.PositionInfoFixture; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -23,11 +24,13 @@ public class PhoTest { void setUp() { board = new Board(); List info = new ArrayList<>(); - info.add(PositionInfo.from(List.of("HAN", "PHO", "1", "1"))); - info.add(PositionInfo.from(List.of("HAN", "CHA", "1", "2"))); - info.add(PositionInfo.from(List.of("HAN", "CHA", "1", "3"))); - info.add(PositionInfo.from(List.of("CHO", "PHO", "1", "5"))); - info.add(PositionInfo.from(List.of("CHO", "PHO", "1", "6"))); + info.add(PositionInfoFixture.from(List.of("HAN", "PHO", "1", "1"))); + info.add(PositionInfoFixture.from(List.of("HAN", "CHA", "1", "2"))); + info.add(PositionInfoFixture.from(List.of("HAN", "CHA", "1", "3"))); + info.add(PositionInfoFixture.from(List.of("CHO", "PHO", "1", "5"))); + info.add(PositionInfoFixture.from(List.of("CHO", "PHO", "3", "0"))); + info.add(PositionInfoFixture.from(List.of("CHO", "JANG", "4", "1"))); + info.add(PositionInfoFixture.from(List.of("CHO", "PHO", "1", "6"))); board.init(info); } @@ -107,4 +110,19 @@ void huddle_is_pho_can_not_move() { // then assertThat(pho.canMove(pieces)).isFalse(); } + + @Test + @DisplayName("궁성 안에서 대각선으로 이동하는 기능") + void palace_diagonal_move() { + // given + Piece cha = new Pho(Team.CHO); + Point from = Point.of(3, 0); + Point to = Point.of(5, 2); + + // when + List route = cha.getRoute(from, to); + + // then + assertThat(route.size()).isEqualTo(1); + } } diff --git a/src/test/java/janggi/domain/piece/SaTest.java b/src/test/java/janggi/domain/piece/SaTest.java index f00f94c1c2..bbb0b55664 100644 --- a/src/test/java/janggi/domain/piece/SaTest.java +++ b/src/test/java/janggi/domain/piece/SaTest.java @@ -21,4 +21,18 @@ void destination_exception() { assertThatThrownBy(() -> sa.getRoute(from, to)) .isInstanceOf(IllegalArgumentException.class); } + + @Test + @DisplayName("궁성 내에서 이동하지 않을 시 예외 발생") + void not_in_palace() { + // given + Piece jang = new Sa(Team.CHO); + Point from = Point.of(0,0); + Point to = Point.of(1, 1); + + // when & then + assertThatThrownBy(() -> jang.getRoute(from, to)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("궁성"); + } } diff --git a/src/test/java/janggi/domain/piece/SangTest.java b/src/test/java/janggi/domain/piece/SangTest.java index 280375c683..37384fb686 100644 --- a/src/test/java/janggi/domain/piece/SangTest.java +++ b/src/test/java/janggi/domain/piece/SangTest.java @@ -16,11 +16,11 @@ public class SangTest { @ParameterizedTest @CsvSource(value = {"8:7", "8:3", "2:7", "2:3", "7:8", "3:8", "7:2", "3:2"}, delimiter = ':') @DisplayName("상이 움직일 때, 경유지는 두 곳이다.") - void straight_back_route(int column, int row) { + void straight_back_route(int x, int y) { // given Piece sang = new Sang(Team.CHO); Point from = Point.of(5,5); - Point to = Point.of(column, row); + Point to = Point.of(x, y); // when List route = sang.getRoute(from, to); diff --git a/src/test/java/janggi/domain/status/ChoTurnTest.java b/src/test/java/janggi/domain/status/ChoTurnTest.java index d8a3dad9eb..c1a3b2816e 100644 --- a/src/test/java/janggi/domain/status/ChoTurnTest.java +++ b/src/test/java/janggi/domain/status/ChoTurnTest.java @@ -4,8 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import janggi.domain.Board; +import janggi.domain.Boards; import janggi.domain.Point; import janggi.dto.PositionInfo; +import janggi.fixture.PositionInfoFixture; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -14,17 +16,18 @@ public class ChoTurnTest { - private Board board; + private Boards boards; @BeforeEach void setUp() { - board = new Board(); + Board board = new Board(); List info = new ArrayList<>(); - info.add(PositionInfo.from(List.of("HAN","JANG", "4", "1"))); - info.add(PositionInfo.from(List.of("CHO","JANG", "4", "8"))); - info.add(PositionInfo.from(List.of("HAN", "CHA", "1", "1"))); - info.add(PositionInfo.from(List.of("CHO", "CHA", "2", "3"))); + info.add(PositionInfoFixture.from(List.of("HAN","JANG", "4", "1"))); + info.add(PositionInfoFixture.from(List.of("CHO","JANG", "4", "8"))); + info.add(PositionInfoFixture.from(List.of("HAN", "CHA", "1", "1"))); + info.add(PositionInfoFixture.from(List.of("CHO", "CHA", "2", "3"))); board.init(info); + boards = new Boards(board); } @Test @@ -36,7 +39,7 @@ void turn_change() { // when GameStatus status = new ChoTurn(); - GameStatus gameStatus = status.move(from, to, board); + GameStatus gameStatus = status.move(from, to, boards); //then assertInstanceOf(HanTurn.class, gameStatus); @@ -53,7 +56,7 @@ void unavailable_move() { GameStatus status = new ChoTurn(); //then - assertThatThrownBy(() -> status.move(from, to, board)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> status.move(from, to, boards)) + .isInstanceOf(IllegalStateException.class); } } diff --git a/src/test/java/janggi/domain/status/HanTurnTest.java b/src/test/java/janggi/domain/status/HanTurnTest.java index 0ed00df0fd..d3e1235951 100644 --- a/src/test/java/janggi/domain/status/HanTurnTest.java +++ b/src/test/java/janggi/domain/status/HanTurnTest.java @@ -4,9 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import janggi.domain.Board; +import janggi.domain.Boards; import janggi.domain.Point; -import janggi.domain.piece.Piece; import janggi.dto.PositionInfo; +import janggi.fixture.PositionInfoFixture; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -15,17 +16,18 @@ public class HanTurnTest { - private Board board; + private Boards boards; @BeforeEach void setUp() { - board = new Board(); + Board board = new Board(); List info = new ArrayList<>(); - info.add(PositionInfo.from(List.of("HAN","JANG", "4", "1"))); - info.add(PositionInfo.from(List.of("CHO","JANG", "4", "8"))); - info.add(PositionInfo.from(List.of("HAN", "CHA", "1", "1"))); - info.add(PositionInfo.from(List.of("CHO", "CHA", "2", "3"))); + info.add(PositionInfoFixture.from(List.of("HAN","JANG", "4", "1"))); + info.add(PositionInfoFixture.from(List.of("CHO","JANG", "4", "8"))); + info.add(PositionInfoFixture.from(List.of("HAN", "CHA", "1", "1"))); + info.add(PositionInfoFixture.from(List.of("CHO", "CHA", "2", "3"))); board.init(info); + boards = new Boards(board); } @Test @@ -37,7 +39,7 @@ void turn_change() { // when GameStatus status = new HanTurn(); - GameStatus gameStatus = status.move(from, to, board); + GameStatus gameStatus = status.move(from, to, boards); //then assertInstanceOf(ChoTurn.class, gameStatus); @@ -49,8 +51,9 @@ void unavailable_move() { // given Board board = new Board(); List info = new ArrayList<>(); - info.add(PositionInfo.from(List.of("CHO", "CHA", "1", "1"))); + info.add(PositionInfoFixture.from(List.of("CHO", "CHA", "1", "1"))); board.init(info); + Boards boards = new Boards(board); Point from = Point.of(1, 1); Point to = Point.of(2, 3); @@ -58,7 +61,7 @@ void unavailable_move() { GameStatus status = new HanTurn(); //then - assertThatThrownBy(() -> status.move(from, to, board)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> status.move(from, to, boards)) + .isInstanceOf(IllegalStateException.class); } } diff --git a/src/test/java/janggi/fixture/PositionInfoFixture.java b/src/test/java/janggi/fixture/PositionInfoFixture.java new file mode 100644 index 0000000000..feec5dba68 --- /dev/null +++ b/src/test/java/janggi/fixture/PositionInfoFixture.java @@ -0,0 +1,20 @@ +package janggi.fixture; + +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import java.util.List; + +public class PositionInfoFixture { + + private PositionInfoFixture() { + } + + public static PositionInfo from(List data) { + return PositionInfo.from( + Team.valueOf(data.get(0)), + data.get(1), + Integer.parseInt(data.get(2)), + Integer.parseInt(data.get(3)) + ); + } +} diff --git a/src/test/java/janggi/repository/DatabaseInitializerTest.java b/src/test/java/janggi/repository/DatabaseInitializerTest.java new file mode 100644 index 0000000000..7070d71e25 --- /dev/null +++ b/src/test/java/janggi/repository/DatabaseInitializerTest.java @@ -0,0 +1,41 @@ +package janggi.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.util.DatabaseInitializer; +import janggi.util.JdbcConnectionManager; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DatabaseInitializerTest { + + @Test + @DisplayName("init 호출 시 테이블 생성") + void init() throws SQLException { + // given + JdbcConnectionManager connectionManager = new JdbcConnectionManager( + "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", + "sa", + "" + ); + DatabaseInitializer databaseInitializer = new DatabaseInitializer(connectionManager); + + // when + databaseInitializer.init(); + + //then + assertThat(existsTable(connectionManager, "GAMES")).isTrue(); + assertThat(existsTable(connectionManager, "GAME_PIECES")).isTrue(); + } + + private boolean existsTable(JdbcConnectionManager connectionManager, String tableName) throws SQLException { + try (Connection connection = connectionManager.getConnection()){ + ResultSet resultSet = connection.getMetaData() + .getTables(null, null, tableName, null); + return resultSet.next(); + } + } +} diff --git a/src/test/java/janggi/repository/JdbcGameRepositoryTest.java b/src/test/java/janggi/repository/JdbcGameRepositoryTest.java new file mode 100644 index 0000000000..68af92821b --- /dev/null +++ b/src/test/java/janggi/repository/JdbcGameRepositoryTest.java @@ -0,0 +1,158 @@ +package janggi.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.dto.GameSnapshot; +import janggi.dto.GameSummary; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import janggi.util.DatabaseInitializer; +import janggi.util.JdbcConnectionManager; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class JdbcGameRepositoryTest { + + private GameRepository repository; + private JdbcConnectionManager connectionManager; + + @BeforeEach + void setUp() { + String databaseName = "test" + System.nanoTime(); + connectionManager = new JdbcConnectionManager( + "jdbc:h2:mem:" + databaseName + ";DB_CLOSE_DELAY=-1", + "sa", + "" + ); + DatabaseInitializer databaseInitializer = new DatabaseInitializer(connectionManager); + databaseInitializer.init(); + repository = new JdbcGameRepository(connectionManager); + } + + @Test + @DisplayName("게임 저장 기능") + void save() { + // given + GameSnapshot gameSnapshot = new GameSnapshot( + null, + Team.CHO, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + + // when + repository.save(gameSnapshot); + + // then + assertThat(repository.findAll()).hasSize(1); + } + + @Test + @DisplayName("모든 게임 목록 조회") + void findAll() { + GameSnapshot firstGame = new GameSnapshot( + null, + Team.CHO, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + + GameSnapshot secondGame = new GameSnapshot( + null, + Team.HAN, + true, + Team.HAN, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1) + ) + ); + + repository.save(firstGame); + repository.save(secondGame); + + List gameSummaries = repository.findAll(); + + assertThat(gameSummaries).hasSize(2); + assertThat(gameSummaries.get(0).finished()).isFalse(); + assertThat(gameSummaries.get(1).finished()).isTrue(); + } + + @Test + @DisplayName("게임 id로 저장된 게임 조회") + void findById() { + // given + GameSnapshot gameSnapshot = new GameSnapshot( + 1L, + Team.CHO, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + repository.save(gameSnapshot); + + // when + Long gameId = repository.findAll().get(0).id(); + GameSnapshot foundGame = repository.findById(gameId).orElseThrow(); + + + // then + assertThat(foundGame.id()).isEqualTo(gameId); + assertThat(foundGame.currentTurn()).isEqualTo(Team.CHO); + assertThat(foundGame.finished()).isFalse(); + assertThat(foundGame.winner()).isNull(); + assertThat(foundGame.positions()).hasSize(2); + } + + @Test + @DisplayName("게임 상태 수정") + void update() { + // given + GameSnapshot savedGame = new GameSnapshot( + 1L, + Team.CHO, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + repository.save(savedGame); + Long gameId = repository.findAll().get(0).id(); + + // when + GameSnapshot updatedGame = new GameSnapshot( + gameId, + Team.HAN, + true, + Team.HAN, + List.of( + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + repository.update(updatedGame); + + // then + GameSnapshot foundGame = repository.findById(gameId).orElseThrow(); + + assertThat(foundGame.id()).isEqualTo(gameId); + assertThat(foundGame.currentTurn()).isEqualTo(Team.HAN); + assertThat(foundGame.finished()).isTrue(); + assertThat(foundGame.winner()).isEqualTo(Team.HAN); + assertThat(foundGame.positions()).hasSize(1); + + } +} diff --git a/src/test/java/janggi/service/FakeGameRepository.java b/src/test/java/janggi/service/FakeGameRepository.java new file mode 100644 index 0000000000..61ebc05973 --- /dev/null +++ b/src/test/java/janggi/service/FakeGameRepository.java @@ -0,0 +1,55 @@ +package janggi.service; + +import janggi.dto.GameSnapshot; +import janggi.dto.GameSummary; +import janggi.repository.GameRepository; +import java.util.List; +import java.util.Optional; + +public class FakeGameRepository implements GameRepository { + + private final List gameSummaries; + private final GameSnapshot gameSnapshot; + private GameSnapshot updatedGameSnapshot; + private GameSnapshot savedGameSnapshot; + + public FakeGameRepository(List gameSummaries, GameSnapshot gameSnapshot) { + this.gameSummaries = gameSummaries; + this.gameSnapshot = gameSnapshot; + } + + @Override + public List findAll() { + return gameSummaries; + } + + @Override + public Optional findById(Long gameId) { + if (gameSnapshot == null) { + return Optional.empty(); + } + if (!gameSnapshot.id().equals(gameId)) { + return Optional.empty(); + } + return Optional.of(gameSnapshot); + } + + @Override + public Long save(GameSnapshot gameSnapshot) { + this.savedGameSnapshot = gameSnapshot; + return 1L; + } + + @Override + public void update(GameSnapshot gameSnapshot) { + this.updatedGameSnapshot = gameSnapshot; + } + + public GameSnapshot updatedGameSnapshot() { + return updatedGameSnapshot; + } + + public GameSnapshot savedGameSnapshot() { + return savedGameSnapshot; + } +} diff --git a/src/test/java/janggi/service/FakeInitialBoardProvider.java b/src/test/java/janggi/service/FakeInitialBoardProvider.java new file mode 100644 index 0000000000..cbc97bd962 --- /dev/null +++ b/src/test/java/janggi/service/FakeInitialBoardProvider.java @@ -0,0 +1,19 @@ +package janggi.service; + +import janggi.dto.PositionInfo; +import janggi.repository.InitialBoardProvider; +import java.util.List; + +public class FakeInitialBoardProvider implements InitialBoardProvider { + + private final List positionInfos; + + public FakeInitialBoardProvider(List positionInfos) { + this.positionInfos = positionInfos; + } + + @Override + public List load() { + return positionInfos; + } +} diff --git a/src/test/java/janggi/service/JanggiGameServiceTest.java b/src/test/java/janggi/service/JanggiGameServiceTest.java new file mode 100644 index 0000000000..e981cc1cef --- /dev/null +++ b/src/test/java/janggi/service/JanggiGameServiceTest.java @@ -0,0 +1,188 @@ +package janggi.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.dto.GameSnapshot; +import janggi.dto.GameSummary; +import janggi.domain.JanggiGame; +import janggi.domain.Point; +import janggi.domain.status.Team; +import janggi.dto.PositionInfo; +import janggi.repository.GameRepository; +import janggi.repository.InitialBoardProvider; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class JanggiGameServiceTest { + + @Test + @DisplayName("저장된 게임 목록 조회") + void find_all_games() { + // given + GameRepository gameRepository = new FakeGameRepository( + List.of( + new GameSummary(1L, false), + new GameSummary(2L, true) + ), + null + ); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider(List.of()); + JanggiGameService janggiGameService = new JanggiGameService(gameRepository,initialBoardProvider); + + // when + List gameSummaries = janggiGameService.findAllGames(); + + // then + assertThat(gameSummaries).hasSize(2); + assertThat(gameSummaries.get(0).id()).isEqualTo(1L); + assertThat(gameSummaries.get(1).id()).isEqualTo(2L); + } + + @Test + @DisplayName("새 게임 시작") + void no_save_game_new_game_start() { + // given + GameRepository gameRepository = new FakeGameRepository(List.of(),null); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider( + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + JanggiGameService janggiGameService = new JanggiGameService(gameRepository, initialBoardProvider); + + // when + JanggiGame janggiGame = janggiGameService.startNewGame(); + + // then + assertThat(janggiGame.currentTurn()).isEqualTo(Team.CHO); + assertThat(janggiGame.boardStatus()).hasSize(2); + } + + @Test + @DisplayName("선택한 게임 재시작") + void load_game() { + // given + GameSnapshot gameSnapshot = new GameSnapshot( + 1L, + Team.HAN, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + GameRepository gameRepository = new FakeGameRepository( + List.of(new GameSummary(1L, false)), + gameSnapshot + ); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider(List.of()); + JanggiGameService janggiGameService = new JanggiGameService(gameRepository, initialBoardProvider); + + // when + JanggiGame janggiGame = janggiGameService.loadGame(1L); + + assertThat(janggiGame.currentTurn()).isEqualTo(Team.HAN); + assertThat(janggiGame.boardStatus()).hasSize(2); + } + + @Test + @DisplayName("선택한 게임이 없을 시 예외 발생") + void no_game_error() { + // given + GameSnapshot gameSnapshot = new GameSnapshot( + 1L, + Team.HAN, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + GameRepository gameRepository = new FakeGameRepository( + List.of(new GameSummary(1L, false)), + gameSnapshot + ); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider(List.of()); + JanggiGameService janggiGameService = new JanggiGameService(gameRepository, initialBoardProvider); + + // when & then + assertThatThrownBy(() -> janggiGameService.loadGame(2L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지"); + } + + @Test + @DisplayName("턴이 넘어가면 게임 상태 저장") + void play_turn_and_save() { + // given + GameSnapshot gameSnapshot = new GameSnapshot( + 1L, + Team.CHO, + false, + null, + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + FakeGameRepository gameRepository = new FakeGameRepository( + List.of(new GameSummary(1L, false)), + gameSnapshot + ); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider(List.of()); + JanggiGameService janggiGameService = + new JanggiGameService(gameRepository, initialBoardProvider); + + // when + janggiGameService.play(1L, Point.of(4, 1), Point.of(4, 2)); + + // then + assertThat(gameRepository.updatedGameSnapshot()).isNotNull(); + assertThat(gameRepository.updatedGameSnapshot().id()).isEqualTo(1L); + assertThat(gameRepository.updatedGameSnapshot().currentTurn()).isEqualTo(Team.HAN); + } + + @Test + @DisplayName("새 게임을 생성하고 저장") + void create_and_save_game() { + // given + FakeGameRepository gameRepository = new FakeGameRepository(List.of(),null); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider( + List.of( + PositionInfo.from(Team.CHO, "JANG", 4, 1), + PositionInfo.from(Team.HAN, "JANG", 4, 8) + ) + ); + JanggiGameService janggiGameService = new JanggiGameService(gameRepository, initialBoardProvider); + + // when + Long gameId = janggiGameService.createGame(); + + // then + assertThat(gameId).isEqualTo(1L); + assertThat(gameRepository.savedGameSnapshot()).isNotNull(); + assertThat(gameRepository.savedGameSnapshot().currentTurn()).isEqualTo(Team.CHO); + assertThat(gameRepository.savedGameSnapshot().finished()).isFalse(); + assertThat(gameRepository.savedGameSnapshot().winner()).isNull(); + assertThat(gameRepository.savedGameSnapshot().positions()).hasSize(2); + } + + @Test + @DisplayName("저장된 게임 목록이 없을 때 예외 발생") + void no_saved_games_error() { + // given + FakeGameRepository gameRepository = new FakeGameRepository(List.of(),null); + InitialBoardProvider initialBoardProvider = new FakeInitialBoardProvider(List.of()); + JanggiGameService janggiGameService = new JanggiGameService(gameRepository, initialBoardProvider); + + // when & then + assertThatThrownBy(janggiGameService::findAllGames) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("저장된"); + } +} diff --git a/storage/janggi.mv.db b/storage/janggi.mv.db new file mode 100644 index 0000000000..aa552211a2 Binary files /dev/null and b/storage/janggi.mv.db differ diff --git a/storage/janggi.trace.db b/storage/janggi.trace.db new file mode 100644 index 0000000000..22222e9c18 --- /dev/null +++ b/storage/janggi.trace.db @@ -0,0 +1,114 @@ +2026-03-31 14:29:41.363323Z jdbc[3]: exception +java.sql.SQLClientInfoException: Client info name 'ApplicationName' not supported. + at org.h2.jdbc.JdbcConnection.setClientInfo(JdbcConnection.java:1624) + at com.intellij.database.remote.jdbc.impl.RemoteConnectionImpl.setClientInfo(RemoteConnectionImpl.java:468) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +2026-03-31 14:29:45.415484Z jdbc[3]: exception +java.sql.SQLClientInfoException: Client info name 'ApplicationName' not supported. + at org.h2.jdbc.JdbcConnection.setClientInfo(JdbcConnection.java:1624) + at com.intellij.database.remote.jdbc.impl.RemoteConnectionImpl.setClientInfo(RemoteConnectionImpl.java:468) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +2026-03-31 14:29:45.701737Z jdbc[3]: exception +java.sql.SQLClientInfoException: Client info name 'ApplicationName' not supported. + at org.h2.jdbc.JdbcConnection.setClientInfo(JdbcConnection.java:1624) + at com.intellij.database.remote.jdbc.impl.RemoteConnectionImpl.setClientInfo(RemoteConnectionImpl.java:468) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +2026-03-31 14:29:56.780095Z jdbc[3]: exception +java.sql.SQLClientInfoException: Client info name 'ApplicationName' not supported. + at org.h2.jdbc.JdbcConnection.setClientInfo(JdbcConnection.java:1624) + at com.intellij.database.remote.jdbc.impl.RemoteConnectionImpl.setClientInfo(RemoteConnectionImpl.java:468) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +2026-03-31 14:30:03.489469Z jdbc[3]: exception +java.sql.SQLClientInfoException: Client info name 'ApplicationName' not supported. + at org.h2.jdbc.JdbcConnection.setClientInfo(JdbcConnection.java:1624) + at com.intellij.database.remote.jdbc.impl.RemoteConnectionImpl.setClientInfo(RemoteConnectionImpl.java:468) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +2026-03-31 14:30:10.396245Z jdbc[4]: exception +java.sql.SQLClientInfoException: Client info name 'ApplicationName' not supported. + at org.h2.jdbc.JdbcConnection.setClientInfo(JdbcConnection.java:1624) + at com.intellij.database.remote.jdbc.impl.RemoteConnectionImpl.setClientInfo(RemoteConnectionImpl.java:468) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583)