diff --git a/README.md b/README.md index 694745386c..63934eee49 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ - 시작 시 초/한 진영의 기본 기물 배치와 마상 배치를 반영해 초기화한다. - 장기판은 현재 위치의 기물 조회, 빈 칸 여부, 아군/적군 여부를 판단한다. - 장기판은 기물 이동 요청을 받아 실제 상태를 변경한다. +- 장기판은 상대 기물을 잡았을 때 해당 진영의 점수를 실시간으로 차감한다. +- 장기판은 어느 한 진영의 궁이 잡히면 즉시 게임 종료 상태로 변경한다. ### 기물 - 기물은 종류와 진영 정보를 가진다. @@ -23,20 +25,27 @@ - 좌표는 입력값을 검증하여 생성한다. - 좌표는 보드 범위(행 1~10, 열 1~9)를 벗어날 수 없다. - 좌표는 이동값(`Movement`)을 적용해 다음 위치를 계산할 수 있다. +- 좌표는 다른 위치에 대해서 선형(직선, 대각선)여부를 판단할 수 있다. +- 좌표는 특정 좌표 범위(한: 1~3행/4~6열, 초: 8~10행/4~6열)를 '궁성'으로 정의한다. ### 이동 전략 - 각 기물은 목적지까지의 경로를 계산한다. - 경로 검증은 `RoutePolicy`를 통해 수행한다. - 일반 기물은 경로가 비어 있고 도착 지점에 아군이 없을 때 이동할 수 있다. - 포는 이동 경로 중 정확히 하나의 기물을 넘어야 하며, 포를 넘을 수 없다. +- 궁성 내부에 위치한 기물(궁, 사, 차, 포, 졸)은 궁성 라인을 따른 대각선 이동이 가능하다. - 목적지 자체가 이동 규칙에 맞지 않으면 예외를 발생시킨다. ### 게임 진행 - 게임은 초 진영부터 시작한다. - 플레이어는 턴마다 시작 좌표와 도착 좌표를 입력한다. +- 사용자가 시작 좌표 입력 시 '종료'를 입력하면 게임 중단 여부를 재확인한다. + - 상대편이 동의(종료 입력)하면 게임을 즉시 종료한다. + - 동의하지 않으면(종료가 아닌 다른 값을 입력) 현재 턴을 유지하며 다시 입력을 받는다. - 현재 턴의 진영만 자신의 기물을 이동할 수 있다. - 이동이 완료되면 턴이 상대 진영으로 넘어간다. - 게임 상태는 `ChoTurn`, `HanTurn`, `Finish`로 구분된다. +- 200턴이 넘어가면 종료된다. ### 초기 배치 - 초와 한은 각각 마상 배치를 입력할 수 있다. @@ -46,7 +55,8 @@ ### 입력 - 한 진영의 마상 배치를 입력받는다. - 초 진영의 마상 배치를 입력받는다. -- 이동할 기물의 시작 좌표를 입력받는다. (예: `3,4`) +- 이동할 기물의 시작 좌표 또는 종료를 입력받는다. (예: `3,4` 또는 `종료` ) + - 종료 재확인 시 종료 동의 여부를 입력받는다. (예: `종료`) - 이동할 기물의 도착 좌표를 입력받는다. (예: `4,4`) - 잘못된 입력이 들어오면 예외 메시지를 출력하고 다시 입력받는다. @@ -54,8 +64,18 @@ - 현재 장기판 상태를 행/열 좌표와 함께 출력한다. - 초와 한 기물은 ANSI 색상으로 구분해 출력한다. - 현재 턴의 진영을 출력한다. +- 매 턴 이동이 성공하면 현재 양 진영의 점수 합계를 출력한다. +- 게임 종료 시 최종 점수와 함께 승리한 진영을 출력한다. - 예외가 발생하면 `[ERROR]` 형식의 메시지를 출력한다. +### 데이터베이스 +- 현재 DB에 저장된 전체 장기 게임방 목록을 조회한다. +- 선택한 게임방의 ID를 기반으로 해당 판의 모든 기물 정보를 불러와 보드를 복원한다. +- 새로운 게임 시작 시 입력받은 게임방 정보(이름, 생성 시간 등)를 DB에 저장한다. +- 새로운 게임의 초기 기물 배치 정보를 DB에 일괄 저장한다. +- 기물 이동 시, 이전 위치의 기물 데이터를 삭제하고 새로운 위치의 정보를 업데이트하여 상태를 영속화한다. +- 게임 종료 시(궁이 잡히거나 중단 동의 시, 또는 200턴을 넘길 시) 해당 게임방과 관련된 모든 데이터를 DB에서 삭제한다. + ### 예외 처리 - 존재하지 않는 마상 배치를 입력한 경우 - 숫자가 아닌 좌표를 입력한 경우 @@ -65,3 +85,4 @@ - 현재 턴의 진영이 아닌 기물을 움직이려는 경우 - 기물의 이동 규칙에 맞지 않는 목적지를 입력한 경우 - 이동 경로가 막혀 있거나 도착 지점에 아군 기물이 있는 경우 +- 점수 차감 시 유효하지 않은 점수값(음수 등)이 계산되는 경우 diff --git a/build.gradle b/build.gradle index ce846f70cc..baaafafa51 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'application' } version '1.0-SNAPSHOT' @@ -9,12 +10,19 @@ repositories { } dependencies { + implementation 'org.xerial:sqlite-jdbc:3.46.0.0' + implementation 'org.slf4j:slf4j-simple:2.0.9' + testImplementation platform('org.junit:junit-bom:5.11.4') testImplementation platform('org.assertj:assertj-bom:3.27.3') testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('org.assertj:assertj-core') } +application { + mainClass = 'janggi.Application' +} + java { toolchain { languageVersion = JavaLanguageVersion.of(21) diff --git a/janggi.db b/janggi.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/main/java/janggi/Application.java b/src/main/java/janggi/Application.java index c17454283c..d1f73320db 100644 --- a/src/main/java/janggi/Application.java +++ b/src/main/java/janggi/Application.java @@ -1,8 +1,31 @@ package janggi; +import janggi.dao.GameRoom; +import janggi.dao.Piece; +import janggi.db.SQLManager; +import janggi.db.TransactionManager; +import janggi.domain.Game; + public class Application { public static void main(String[] args) { - Runner runner = new Runner(); - runner.run(); + SQLManager sqlManager = new SQLManager("jdbc:sqlite:src/main/resources/janggi.db"); + TransactionManager transactionManager = new TransactionManager(sqlManager); + + GameRoom gameRoom = new GameRoom(sqlManager); + gameRoom.initTable(); + + Piece piece = new Piece(sqlManager); + piece.initTable(); + + JanggiService janggiService = new JanggiService(transactionManager, gameRoom, piece); + + Game game = new Game(); + + Runner runner = new Runner(janggiService, game); + + int gameId = runner.initBoard(); + runner.runJanggi(gameId); + + sqlManager.closeConnection(); } } diff --git a/src/main/java/janggi/JanggiService.java b/src/main/java/janggi/JanggiService.java new file mode 100644 index 0000000000..9ff4ae9268 --- /dev/null +++ b/src/main/java/janggi/JanggiService.java @@ -0,0 +1,59 @@ +package janggi; + +import janggi.dao.GameRoom; +import janggi.dao.Piece; +import janggi.db.TransactionManager; +import janggi.domain.Position; +import janggi.domain.Side; +import janggi.domain.piece.PieceType; +import janggi.dto.GameDto; +import janggi.dto.GameResponseDto; +import janggi.dto.PieceDto; +import janggi.dto.TurnDto; +import java.util.List; + +public class JanggiService { + private final TransactionManager transactionManager; + private final GameRoom gameRoom; + private final Piece piece; + + public JanggiService(TransactionManager transactionManager, GameRoom gameRoom, Piece piece) { + this.transactionManager = transactionManager; + this.gameRoom = gameRoom; + this.piece = piece; + } + + public List getEntireGame() { + return gameRoom.findAllGames(); + } + + public int addGameData(GameDto gameDto, List pieceDtos) { + return transactionManager.sync((connection) -> { + int gameId = gameRoom.insertGame(connection, gameDto); + piece.updatePieces(connection, gameId, pieceDtos); + + return gameId; + }); + } + + public void removeGame(int id) { + transactionManager.sync((connection) -> { + gameRoom.removeGame(connection, id); + }); + } + + public List getPieceInitInfos(int gameId) { + return piece.getAllPieces(gameId); + } + + + public void movePiece(int gameId, Position start, Position end, Side side, PieceType pieceType, TurnDto turnDto) { + transactionManager.sync(connection -> { + gameRoom.updateGameTurn(connection, gameId, turnDto); + piece.deletePiece(connection, gameId, start.getX(), start.getY()); + + PieceDto pieceDto = new PieceDto(end.getX(), end.getY(), pieceType.getName(), side.getName()); + piece.updatePiece(connection, gameId, pieceDto); + }); + } +} diff --git a/src/main/java/janggi/Runner.java b/src/main/java/janggi/Runner.java index 11fc0dffdb..4acfade4fb 100644 --- a/src/main/java/janggi/Runner.java +++ b/src/main/java/janggi/Runner.java @@ -2,45 +2,110 @@ import janggi.domain.Arrangement; import janggi.domain.Game; +import janggi.domain.GameInfo; +import janggi.domain.GameInfos; +import janggi.domain.GameName; +import janggi.domain.MoveResult; +import janggi.domain.PieceInitInfo; import janggi.domain.Position; +import janggi.domain.Side; +import janggi.domain.SideScore; +import janggi.domain.piece.PieceType; +import janggi.dto.GameDto; +import janggi.dto.PieceDto; +import janggi.dto.TurnDto; import janggi.view.InputView; import janggi.view.OutputView; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; public class Runner { + public static final String END_TEXT = "종료"; + private static final String UNEXPECTED_SERVER_ERROR_LOG_MESSAGE = "예측하지 못한 시스템 오류 발생"; private static final String SYSTEM_ERROR_MESSAGE = "시스템 오류가 발생하여 게임을 종료합니다."; private static final Logger logger = Logger.getLogger(Runner.class.getName()); - private Game game; - public void run() { - initArrangeGame(); - turnGame(); + private final JanggiService janggiService; + private final Game game; + + public Runner(JanggiService janggiService, Game game) { + this.janggiService = janggiService; + this.game = game; + } + + public int initBoard() { + Optional previousBoard = getPreviousBoard(); + return previousBoard.orElseGet(() -> createNewBoard().id()); } - private void initArrangeGame() { + public void runJanggi(int gameId) { + while (playTurnGame(gameId)) { + } + endGame(gameId); + } + + private Optional getPreviousBoard() { + GameInfos gameInfos = new GameInfos(janggiService.getEntireGame().stream() + .map(gameResponseDto -> new GameInfo(gameResponseDto.id(), gameResponseDto.name(), gameResponseDto.createdAt(), gameResponseDto.updatedAt(), Side.from(gameResponseDto.side()), gameResponseDto.turn())) + .toList()); + + + if(gameInfos.isEmpty()) { + return Optional.empty(); + } + + GameInfo selectedGame = askUntilValid(() -> getSelectedGame(gameInfos)); + + List pieceInitInfos = janggiService.getPieceInitInfos(selectedGame.id()).stream().map(pieceDto -> new PieceInitInfo(new Position(pieceDto.x(), pieceDto.y()), Side.from(pieceDto.side()), + PieceType.from(pieceDto.pieceType()))).toList(); + + game.init(pieceInitInfos, selectedGame.side(), selectedGame.turn()); + return Optional.of(selectedGame.id()); + } + + private GameInfo getSelectedGame(GameInfos gameInfos) { + OutputView.printGameRoom(gameInfos.getGameInfos()); + Optional input = InputView.askLoadGame(); + + if(input.isEmpty()) { + return createNewBoard(); + } + return gameInfos.getGameInfo(input.get()); + } + + private GameInfo createNewBoard() { + GameName gameName = new GameName(InputView.askGameName()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String formattedNow = LocalDateTime.now().format(formatter); + String hanArrangementInput = InputView.askHanArrangement(); Arrangement hanArrangement = Arrangement.from(hanArrangementInput); String choArrangementInput = InputView.askChoArrangement(); Arrangement choArrangement = Arrangement.from(choArrangementInput); - game = new Game(choArrangement, hanArrangement); - } + List pieceDtos = game.init(choArrangement, hanArrangement).stream() + .map(pieceInitInfo -> new PieceDto(pieceInitInfo.position().getX(), pieceInitInfo.position().getY(), pieceInitInfo.pieceType().getName(), pieceInitInfo.side().getName())) + .toList(); - private void turnGame() { - while (playTurnGame()) { - } + int id = janggiService.addGameData(new GameDto(gameName.name(), formattedNow, formattedNow, Side.CHO.getName(), 1), pieceDtos); + return new GameInfo(id, gameName.name(), formattedNow, formattedNow, Side.CHO, 1); } - private boolean playTurnGame() { + private boolean playTurnGame(int gameId) { try { - printCurrentStatus(); - movePiece(); - return isFinishedGame(); + OutputView.printLine(); + OutputView.printBoard(game.getCurrentBoardDto()); + OutputView.printTurn(game.getCurrentSide()); + + return executeTurn(gameId); } catch (IllegalArgumentException e) { OutputView.printErrorMessage(e.getMessage()); return true; @@ -51,26 +116,68 @@ private boolean playTurnGame() { } } - private void printCurrentStatus() { - OutputView.printBoard(game.getCurrentBoardDto()); - OutputView.printTurn(game.getCurrentSide()); - } + private boolean executeTurn(int gameId) { + Optional> startPositionInput = InputView.askStartPosition(); - private void movePiece() { - List startPositionInput = InputView.askStartPosition(); - Position startPosition = Position.from(startPositionInput); + if(startPositionInput.isEmpty()) { + return consentEndGame(gameId); + } + + Position startPosition = Position.from(startPositionInput.get()); List endPositionInput = InputView.askEndPosition(); Position endPosition = Position.from(endPositionInput); - game.move(startPosition, endPosition); + OutputView.printLine(); + + MoveResult moveResult = game.move(startPosition, endPosition); + TurnDto turnDto = new TurnDto(moveResult.turnAttribute().side().getName(), moveResult.turnAttribute().turn()); + + janggiService.movePiece(gameId, startPosition, endPosition, moveResult.pieceAttribute().side(), moveResult.pieceAttribute().pieceType(), turnDto); + + OutputView.printScore(game.getCurrentSideScore()); + return !game.isFinished(); } - private boolean isFinishedGame() { - if (game.isFinished()) { - OutputView.printWinner(game.getWinnerSide()); + private void endGame(int gameId) { + janggiService.removeGame(gameId); + + Side winnerSide = game.getWinnerSide(); + + if(winnerSide == null) { + OutputView.printScore(game.getCurrentSideScore()); + printScoreWinner(); + return; + } + OutputView.printWinner(winnerSide); + } + + private boolean consentEndGame(int gameId) { + String consentInput = InputView.consentEnd(); + + if(consentInput.equals(END_TEXT)) { return false; } - return true; + + return executeTurn(gameId); + } + + private void printScoreWinner() { + SideScore score = game.getCurrentSideScore(); + if(score.cho() > score.han()) { + OutputView.printWinner(Side.CHO); + return; + } + OutputView.printWinner(Side.HAN); + } + + private static T askUntilValid(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } } } diff --git a/src/main/java/janggi/dao/GameRoom.java b/src/main/java/janggi/dao/GameRoom.java new file mode 100644 index 0000000000..66e0a3ec32 --- /dev/null +++ b/src/main/java/janggi/dao/GameRoom.java @@ -0,0 +1,117 @@ +package janggi.dao; + +import janggi.db.SQLManager; +import janggi.dto.GameDto; +import janggi.dto.GameResponseDto; +import janggi.dto.TurnDto; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class GameRoom { + private final SQLManager sqlManager; + + public GameRoom(SQLManager sqlManager) { + this.sqlManager = sqlManager; + } + + public void initTable() { + String sql = + """ + CREATE TABLE IF NOT EXISTS GameRoom ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + turn INTEGER NOT NULL, + side TEXT NOT NULL + ) + """; + + try (Connection conn = sqlManager.ensureConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); + if (!conn.getAutoCommit()) conn.commit(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public int insertGame(Connection connection, GameDto gameDto){ + int generatedId = -1; + String sql = "INSERT INTO GameRoom (name, created_at, updated_at, turn, side) VALUES (?, ?, ?, ?, ?)"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + + pstmt.setString(1, gameDto.name()); + pstmt.setString(2, gameDto.createdAt()); + pstmt.setString(3, gameDto.updatedAt()); + pstmt.setInt(4, gameDto.turn()); + pstmt.setString(5, gameDto.side()); + + pstmt.executeUpdate(); + + try (ResultSet rs = pstmt.getGeneratedKeys()) { + if (rs.next()) { + generatedId = rs.getInt(1); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return generatedId; + } + + public List findAllGames() { + List gameInfos = new ArrayList<>(); + String sql = "SELECT id, name, created_at, updated_at, side, turn FROM GameRoom"; + try (Connection conn = sqlManager.ensureConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + gameInfos.add(new GameResponseDto( + rs.getInt("id"), + rs.getString("name"), + rs.getString("created_at"), + rs.getString("updated_at"), + rs.getString("side"), + rs.getInt("turn") + )); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return gameInfos; + } + + public void updateGameTurn(Connection connection, int gameId, TurnDto turnDto) { + String sql = "UPDATE GameRoom SET turn = ?, side = ?, updated_at = ? WHERE id = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setInt(1, turnDto.turn()); + pstmt.setString(2, turnDto.side()); + pstmt.setString(3, java.time.LocalDateTime.now().toString()); + pstmt.setInt(4, gameId); + + pstmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeGame(Connection connection, int id) { + String sql = "DELETE FROM GameRoom WHERE id = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setInt(1, id); + + pstmt.executeUpdate(); + + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/janggi/dao/Piece.java b/src/main/java/janggi/dao/Piece.java new file mode 100644 index 0000000000..3e71330bfc --- /dev/null +++ b/src/main/java/janggi/dao/Piece.java @@ -0,0 +1,133 @@ +package janggi.dao; + +import janggi.db.SQLManager; +import janggi.dto.PieceDto; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + + +public class Piece { + private final SQLManager sqlManager; + + public Piece(SQLManager sqlManager) { + this.sqlManager = sqlManager; + } + + public void initTable() { + String sql = + """ + CREATE TABLE IF NOT EXISTS Piece ( + game_id INTEGER NOT NULL, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + piece_type TEXT NOT NULL, + side TEXT NOT NULL, + PRIMARY KEY (game_id, x, y), + FOREIGN KEY (game_id) REFERENCES GameRoom(id) ON DELETE CASCADE + ) + """; + + try (Connection conn = sqlManager.ensureConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); + if (!conn.getAutoCommit()) conn.commit(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List getAllPieces(int gameId) { + List pieces = new ArrayList<>(); + String sql = "SELECT * FROM Piece WHERE game_id = ?"; + + try (Connection conn = sqlManager.ensureConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setInt(1, gameId); + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + PieceDto pieceDto = new PieceDto( + rs.getInt("x"), + rs.getInt("y"), + rs.getString("piece_type"), + rs.getString("side") + ); + pieces.add(pieceDto); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return pieces; + } + + public void updatePiece(Connection connection, int gameId, PieceDto pieceDto) { + String sql = """ + INSERT INTO Piece (game_id, x, y, piece_type, side) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(game_id, x, y) + DO UPDATE SET + piece_type = excluded.piece_type, + side = excluded.side + """; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setInt(1, gameId); + pstmt.setInt(2, pieceDto.x()); + pstmt.setInt(3, pieceDto.y()); + pstmt.setString(4, pieceDto.pieceType()); + pstmt.setString(5, pieceDto.side()); + + pstmt.executeUpdate(); + + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void updatePieces(Connection connection, int gameId, List pieceDtos) { + String sql = """ + INSERT INTO Piece (game_id, x, y, piece_type, side) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(game_id, x, y) + DO UPDATE SET + piece_type = excluded.piece_type, + side = excluded.side + """; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + for (PieceDto pieceDto : pieceDtos) { + pstmt.setInt(1, gameId); + pstmt.setInt(2, pieceDto.x()); + pstmt.setInt(3, pieceDto.y()); + pstmt.setString(4, pieceDto.pieceType()); + pstmt.setString(5, pieceDto.side()); + + pstmt.addBatch(); + } + + pstmt.executeBatch(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void deletePiece(Connection connection, int gameId, int x, int y) { + String sql = "DELETE FROM Piece WHERE game_id = ? AND x = ? AND y = ?"; + + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setInt(1, gameId); + pstmt.setInt(2, x); + pstmt.setInt(3, y); + + pstmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } +} + diff --git a/src/main/java/janggi/db/SQLManager.java b/src/main/java/janggi/db/SQLManager.java new file mode 100644 index 0000000000..0b260171e5 --- /dev/null +++ b/src/main/java/janggi/db/SQLManager.java @@ -0,0 +1,57 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +public class SQLManager { + private static final String SQLITE_JDBC_DRIVER = "org.sqlite.JDBC"; + private static final boolean OPT_AUTO_COMMIT = false; + private static final int OPT_VALID_TIMEOUT = 500; + + private Connection connection = null; + private final String url; + + public SQLManager(String url) { + this.url = url; + } + + public Connection createConnection() { + try { + Class.forName(SQLITE_JDBC_DRIVER); + this.connection = DriverManager.getConnection(this.url); + this.connection.setAutoCommit(OPT_AUTO_COMMIT); + + try (Statement stmt = connection.createStatement()) { + stmt.execute("PRAGMA foreign_keys = ON;"); + } + } catch (ClassNotFoundException | SQLException e) { + e.printStackTrace(); + } + return this.connection; + } + + public void closeConnection() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } finally { + connection = null; + } + } + + public Connection ensureConnection() { + try { + if (connection == null || connection.isClosed() || !connection.isValid(OPT_VALID_TIMEOUT)) { + createConnection(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return connection; + } +} diff --git a/src/main/java/janggi/db/TransactionManager.java b/src/main/java/janggi/db/TransactionManager.java new file mode 100644 index 0000000000..c1b0268e0d --- /dev/null +++ b/src/main/java/janggi/db/TransactionManager.java @@ -0,0 +1,45 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.Consumer; +import java.util.function.Function; + +public class TransactionManager { + private final SQLManager sqlManager; + + public TransactionManager(SQLManager sqlManager) { + this.sqlManager = sqlManager; + } + + public T sync(Function function) { + Connection conn = sqlManager.ensureConnection(); + try { + T result = function.apply(conn); + conn.commit(); + return result; + } catch (Exception e) { + rollback(conn); + throw new RuntimeException("Transaction failed", e); + } + } + + public void sync(Consumer consumer) { + Connection conn = sqlManager.ensureConnection(); + try { + consumer.accept(conn); + conn.commit(); + } catch (Exception e) { + rollback(conn); + throw new RuntimeException("트랜잭션 실행 중 오류 발생", e); + } + } + + private void rollback(Connection conn) { + try { + if (conn != null) conn.rollback(); + } catch (SQLException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/janggi/domain/BoardInitInfo.java b/src/main/java/janggi/domain/BoardInitInfo.java new file mode 100644 index 0000000000..628836cce5 --- /dev/null +++ b/src/main/java/janggi/domain/BoardInitInfo.java @@ -0,0 +1,4 @@ +package janggi.domain; + +public record BoardInitInfo(String name, String createdAt, String updatedAt) { +} diff --git a/src/main/java/janggi/domain/Game.java b/src/main/java/janggi/domain/Game.java index f4971804c2..d9f6d2f4cc 100644 --- a/src/main/java/janggi/domain/Game.java +++ b/src/main/java/janggi/domain/Game.java @@ -1,22 +1,55 @@ package janggi.domain; import janggi.domain.board.Board; -import janggi.domain.turn.ChoActionTurn; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceAttribute; +import janggi.domain.piece.PieceType; +import janggi.domain.turn.ChoTurn; +import janggi.domain.turn.HanTurn; import janggi.domain.turn.PlayerTurn; +import janggi.domain.turn.TurnState; import janggi.dto.BoardDto; import janggi.initializer.BoardInitializer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; public class Game { - private static final String INVALID_WINNER_SIDE = "잘못된 승자 진영입니다."; - private PlayerTurn playerTurn; - public Game(Arrangement choArrangement, Arrangement hanArrangement) { - this.playerTurn = new ChoActionTurn(new Board(BoardInitializer.createBoard(choArrangement, hanArrangement))); + public List init(Arrangement choArrangement, Arrangement hanArrangement) { + Map initBoard = BoardInitializer.createBoard(choArrangement, hanArrangement); + int hanScore = initBoard.values().stream() + .filter(piece -> piece.isEqualSide(Side.HAN)) + .mapToInt(Piece::getPieceScore) + .sum(); + int choScore = initBoard.values().stream() + .filter(piece -> piece.isEqualSide(Side.CHO)) + .mapToInt(Piece::getPieceScore) + .sum(); + + this.playerTurn = new ChoTurn(new Board(initBoard, hanScore, choScore), 0); + return getPieceInitInfo(initBoard); + } + + public void init(List pieceInitInfos, Side side, int turn) { + Map initBoard = BoardInitializer.createBoard(pieceInitInfos); + int hanScore = initBoard.values().stream() + .filter(piece -> piece.isEqualSide(Side.HAN)) + .mapToInt(Piece::getPieceScore) + .sum(); + int choScore = initBoard.values().stream() + .filter(piece -> piece.isEqualSide(Side.CHO)) + .mapToInt(Piece::getPieceScore) + .sum(); + + initTurn(new Board(initBoard, hanScore, choScore), side, turn); } - public void move(Position start, Position end) { - playerTurn = playerTurn.move(start, end); + public MoveResult move(Position start, Position end) { + TurnState turnState = playerTurn.move(start, end); + playerTurn = turnState.playerTurn(); + return new MoveResult(turnState.turnAttribute(), turnState.movedPiece()); } public boolean isFinished() { @@ -31,11 +64,29 @@ public Side getCurrentSide() { return playerTurn.getCurrentSide(); } + public SideScore getCurrentSideScore() { + return playerTurn.getCurrentScore(); + } + public Side getWinnerSide() { - Side winnerSide = playerTurn.getWinnerSide(); - if (winnerSide.equals(Side.EMPTY)) { - throw new IllegalStateException(INVALID_WINNER_SIDE); + return playerTurn.getWinnerSide(); + } + + private List getPieceInitInfo(Map board) { + List pieceInitInfos = new ArrayList<>(); + board.forEach(((position, piece) -> { + if(!piece.isEqualPieceType(PieceType.NONE)) { + pieceInitInfos.add(piece.getPieceInitInfo(position)); + } + })); + return pieceInitInfos; + } + + private void initTurn(Board board, Side side, int turn) { + if(side.equals(Side.CHO)) { + this.playerTurn = new ChoTurn(board, turn); + return; } - return winnerSide; + this.playerTurn = new HanTurn(board, turn); } } diff --git a/src/main/java/janggi/domain/GameInfo.java b/src/main/java/janggi/domain/GameInfo.java new file mode 100644 index 0000000000..0fc9872ab1 --- /dev/null +++ b/src/main/java/janggi/domain/GameInfo.java @@ -0,0 +1,4 @@ +package janggi.domain; + +public record GameInfo(int id, String name, String created, String recent, Side side, int turn) { +} diff --git a/src/main/java/janggi/domain/GameInfos.java b/src/main/java/janggi/domain/GameInfos.java new file mode 100644 index 0000000000..5e682cff04 --- /dev/null +++ b/src/main/java/janggi/domain/GameInfos.java @@ -0,0 +1,28 @@ +package janggi.domain; + +import java.util.List; + +public class GameInfos { + private static final String INVALID_NUMBER = "존재하지 않는 게임 번호입니다."; + + private List gameInfos; + + public GameInfos(List gameInfos) { + this.gameInfos = gameInfos; + } + + public GameInfo getGameInfo(int index) { + if(index < 0 || index > gameInfos.size() - 1) { + throw new IllegalArgumentException(INVALID_NUMBER); + } + return gameInfos.get(index); + } + + public boolean isEmpty() { + return gameInfos.isEmpty(); + } + + public List getGameInfos() { + return List.copyOf(gameInfos); + } +} diff --git a/src/main/java/janggi/domain/GameName.java b/src/main/java/janggi/domain/GameName.java new file mode 100644 index 0000000000..f280f5d8c5 --- /dev/null +++ b/src/main/java/janggi/domain/GameName.java @@ -0,0 +1,19 @@ +package janggi.domain; + +public record GameName(String name) { + private static final int MIN_NAME_SIZE = 2; + private static final int MAX_NAME_SIZE = 20; + + private static final String INVALID_NAME_SIZE_MESSAGE = "이름은 2글자 이상, 20글자 이하로만 가능합니다."; + + public GameName(String name) { + this.name = validate(name); + } + + private static String validate(String name) { + if(name.length() < MIN_NAME_SIZE || name.length() > MAX_NAME_SIZE) { + throw new IllegalArgumentException(INVALID_NAME_SIZE_MESSAGE); + } + return name; + } +} diff --git a/src/main/java/janggi/domain/MoveResult.java b/src/main/java/janggi/domain/MoveResult.java new file mode 100644 index 0000000000..98da8f7834 --- /dev/null +++ b/src/main/java/janggi/domain/MoveResult.java @@ -0,0 +1,7 @@ +package janggi.domain; + +import janggi.domain.piece.PieceAttribute; +import janggi.domain.turn.TurnAttribute; + +public record MoveResult(TurnAttribute turnAttribute, PieceAttribute pieceAttribute) { +} diff --git a/src/main/java/janggi/domain/Movement.java b/src/main/java/janggi/domain/Movement.java index ddc3fc681e..b8bf62b005 100644 --- a/src/main/java/janggi/domain/Movement.java +++ b/src/main/java/janggi/domain/Movement.java @@ -1,5 +1,7 @@ package janggi.domain; +import java.util.Arrays; + public enum Movement { UP(-1, 0), DOWN(1, 0), @@ -10,6 +12,8 @@ public enum Movement { DOWN_RIGHT(1, 1), DOWN_LEFT(1, -1); + private static final String INVALID_DELTA_DIRECTION_MESSAGE = "해당 dx,dy에 대한 movement가 없습니다."; + private final int dx; private final int dy; @@ -25,4 +29,11 @@ public int getDx() { public int getDy() { return dy; } + + public static Movement of(int dx, int dy) { + return Arrays.stream(Movement.values()) + .filter(movement -> movement.dx == dx && movement.dy == dy) + .findFirst() + .orElseThrow(() -> new IllegalStateException(INVALID_DELTA_DIRECTION_MESSAGE)); + } } diff --git a/src/main/java/janggi/domain/PieceInitInfo.java b/src/main/java/janggi/domain/PieceInitInfo.java new file mode 100644 index 0000000000..f4c2998935 --- /dev/null +++ b/src/main/java/janggi/domain/PieceInitInfo.java @@ -0,0 +1,6 @@ +package janggi.domain; + +import janggi.domain.piece.PieceType; + +public record PieceInitInfo(Position position, Side side, PieceType pieceType) { +} diff --git a/src/main/java/janggi/domain/Position.java b/src/main/java/janggi/domain/Position.java index cda97ddd5a..dec7765062 100644 --- a/src/main/java/janggi/domain/Position.java +++ b/src/main/java/janggi/domain/Position.java @@ -4,17 +4,16 @@ import java.util.Objects; public class Position { - public static final int POSITION_COMPONENTS_SIZE = 2; - public static final int BOARD_START_ROWS = 1; public static final int BOARD_START_COLS = 1; public static final int BOARD_END_ROWS = 10; public static final int BOARD_END_COLS = 9; + public static final int POSITION_COMPONENTS_SIZE = 2; + private static final String INVALID_POSITION_SIZE = "행과 열 두 개의 값만 입력하세요."; private static final String INVALID_ROW_RANGE = "유효하지 않은 위치입니다. 행은 1부터 10까지 가능합니다."; private static final String INVALID_COL_RANGE = "유효하지 않은 위치입니다. 열은 1부터 9까지 가능합니다."; - private static final String INVALID_HORIZON = "수직 또는 수평이 아닙니다."; private final int x; private final int y; @@ -77,19 +76,31 @@ public Position move(Movement movement) { return new Position(x + movement.getDx(), y + movement.getDy()); } - public int calculateRowDistance(Position position) { - return position.x - x; + public int getDeltaX(Position position) { + return Math.abs(position.x - x); + } + + public int getDeltaY(Position position) { + return Math.abs(position.y - y); + } + + public int compareX(Position position) { + return Integer.compare(position.x, x); + } + + public int compareY(Position position) { + return Integer.compare(position.y, y); } - public int calculateColumnDistance(Position position) { - return position.y - y; + public boolean isRange(int startX, int endX, int startY, int endY) { + return x >= startX && x <= endX && y >= startY && y <= endY; } - public boolean isHorizontal(Position position) { - return position.x == this.x; + public int getX() { + return x; } - public boolean isVertical(Position position) { - return position.y == this.y; + public int getY() { + return y; } } diff --git a/src/main/java/janggi/domain/Route.java b/src/main/java/janggi/domain/Route.java index b114ceeb15..196d21b3a7 100644 --- a/src/main/java/janggi/domain/Route.java +++ b/src/main/java/janggi/domain/Route.java @@ -1,7 +1,6 @@ package janggi.domain; import java.util.List; -import java.util.Objects; import java.util.function.Predicate; public record Route(List route) { diff --git a/src/main/java/janggi/domain/Side.java b/src/main/java/janggi/domain/Side.java index e0902b564d..0468f244fe 100644 --- a/src/main/java/janggi/domain/Side.java +++ b/src/main/java/janggi/domain/Side.java @@ -1,5 +1,6 @@ package janggi.domain; +import java.util.Arrays; import java.util.Map; public enum Side { @@ -8,6 +9,7 @@ public enum Side { EMPTY("없음"); private static final String INVALID_OPPOSITE_SIDE = "반대 진영이 없습니다."; + private static final String INVALID_SIDE_NAME = "해당 이름의 진영이 없습니다."; private static final Map oppositeSide = Map.of( Side.CHO, Side.HAN, @@ -33,4 +35,11 @@ public Side getOppositeSide() { return side; } + + public static Side from(String name) { + return Arrays.stream(Side.values()) + .filter(side -> side.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_SIDE_NAME)); + } } diff --git a/src/main/java/janggi/domain/SideScore.java b/src/main/java/janggi/domain/SideScore.java new file mode 100644 index 0000000000..69caa1caac --- /dev/null +++ b/src/main/java/janggi/domain/SideScore.java @@ -0,0 +1,4 @@ +package janggi.domain; + +public record SideScore(double han, double cho) { +} diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index cf0dc88909..aaa5e49dc3 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -2,13 +2,13 @@ import janggi.domain.Position; import janggi.domain.Side; +import janggi.domain.SideScore; import janggi.domain.piece.Empty; import janggi.domain.piece.Piece; -import janggi.domain.piece.PieceManifest; +import janggi.domain.piece.PieceAttribute; import janggi.domain.piece.PieceType; import janggi.domain.Route; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,13 +16,11 @@ public class Board implements BaseBoard { private static final String INVALID_PIECE_SIDE_MESSAGE = "자기 진영의 기물만 움직일 수 있습니다."; private final Map board; - private final Map isGungAlive = new HashMap<>(); + private final MaterialScore materialScore; - public Board(Map board) { + public Board(Map board, int hanScore, int choScore) { this.board = board; - - isGungAlive.put(Side.CHO, true); - isGungAlive.put(Side.HAN, true); + this.materialScore = new MaterialScore(hanScore, choScore); } @Override @@ -40,33 +38,44 @@ public boolean isAlly(Side side, Position position) { return board.get(position).isEqualSide(side); } - public List> getCurrentBoard() { + public List> getCurrentBoard() { CurrentBoard currentBoard = CurrentBoard.from(Collections.unmodifiableMap(board)); return currentBoard.getValues(); } - public void move(Position start, Position end, Side side) { + public PieceAttribute move(Position start, Position end, Side movableSide) { Piece piece = board.get(start); - if (!piece.isEqualSide(side)) { + if (!piece.isEqualSide(movableSide)) { throw new IllegalArgumentException(INVALID_PIECE_SIDE_MESSAGE); } Route route = piece.findRoute(start, end); piece.validateRoute(route, this); - movePiece(start, end, piece, side); + movePiece(start, end, piece, movableSide); + + return piece.getPieceInfo(); } public boolean isEndGame() { - return isGungAlive.values().stream().anyMatch(isCaptured -> !isCaptured); + return materialScore.isAnyGungDead(); + } + + public SideScore getScore() { + return materialScore.getCurrentScore(); } private void movePiece(Position start, Position end, Piece piece, Side side) { Piece destinationPiece = board.get(end); if(destinationPiece.isEqualPieceType(PieceType.GUNG)) { - isGungAlive.put(side.getOppositeSide(), false); + materialScore.updateGungDead(side.getOppositeSide()); } + + if(!destinationPiece.isEqualPieceType(PieceType.NONE)) { + materialScore.decreaseScore(side.getOppositeSide(), destinationPiece.getPieceScore()); + } + board.put(end, piece); board.put(start, new Empty()); } diff --git a/src/main/java/janggi/domain/board/CurrentBoard.java b/src/main/java/janggi/domain/board/CurrentBoard.java index a743d9550e..8456291934 100644 --- a/src/main/java/janggi/domain/board/CurrentBoard.java +++ b/src/main/java/janggi/domain/board/CurrentBoard.java @@ -7,24 +7,24 @@ import janggi.domain.Position; import janggi.domain.piece.Piece; -import janggi.domain.piece.PieceManifest; +import janggi.domain.piece.PieceAttribute; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; public class CurrentBoard { - private final List> currentBoard; + private final List> currentBoard; - public CurrentBoard(List> currentBoard) { + public CurrentBoard(List> currentBoard) { this.currentBoard = currentBoard; } public static CurrentBoard from(Map board) { - List> rows = new ArrayList<>(); + List> rows = new ArrayList<>(); for(int row = BOARD_START_ROWS; row <= BOARD_END_ROWS; row++) { - List currentRow = new ArrayList<>(); + List currentRow = new ArrayList<>(); for(int col = BOARD_START_COLS; col <= BOARD_END_COLS; col++) { Position position = new Position(row, col); currentRow.add(board.get(position).getPieceInfo()); @@ -34,7 +34,7 @@ public static CurrentBoard from(Map board) { return new CurrentBoard(Collections.unmodifiableList(rows)); } - public List> getValues() { + public List> getValues() { return currentBoard; } } diff --git a/src/main/java/janggi/domain/board/MaterialScore.java b/src/main/java/janggi/domain/board/MaterialScore.java new file mode 100644 index 0000000000..2ccd06f7d0 --- /dev/null +++ b/src/main/java/janggi/domain/board/MaterialScore.java @@ -0,0 +1,46 @@ +package janggi.domain.board; + +import janggi.domain.Side; +import janggi.domain.SideScore; +import java.util.HashMap; +import java.util.Map; + +public class MaterialScore { + private static final String INVALID_NEGATIVE_DECREASE_SCORE = "차감할 점수는 0보다 커야 합니다."; + private static final String INVALID_SCORE_SIDE = "해당 진영의 점수 정보가 존재하지 않습니다"; + + private static final double DUM = 1.5; + + private final Map isGungAlive; + private final Map score; + + public MaterialScore(int hanScore, int choScore) { + this.isGungAlive = new HashMap<>(Map.of(Side.HAN, true, Side.CHO, true)); + this.score = new HashMap<>(Map.of(Side.HAN, hanScore + DUM, Side.CHO, (double) choScore)); + } + + public SideScore getCurrentScore() { + return new SideScore(score.get(Side.HAN), score.get(Side.CHO)); + } + + public boolean isAnyGungDead() { + return isGungAlive.values().stream().anyMatch(isCaptured -> !isCaptured); + } + + public void updateGungDead(Side side) { + isGungAlive.put(side, false); + } + + public void decreaseScore(Side side, int pieceScore) { + if(pieceScore < 0) { + throw new IllegalArgumentException(INVALID_NEGATIVE_DECREASE_SCORE); + } + + Double currentScore = score.get(side); + if(currentScore == null) { + throw new IllegalStateException(INVALID_SCORE_SIDE); + } + + score.put(side, currentScore - pieceScore); + } +} diff --git a/src/main/java/janggi/domain/piece/ActivePiece.java b/src/main/java/janggi/domain/piece/ActivePiece.java index cde36d3b50..59fabb3499 100644 --- a/src/main/java/janggi/domain/piece/ActivePiece.java +++ b/src/main/java/janggi/domain/piece/ActivePiece.java @@ -1,6 +1,8 @@ package janggi.domain.piece; +import janggi.domain.Route; import janggi.domain.Side; +import janggi.domain.board.BaseBoard; import janggi.domain.policy.RoutePolicy; public abstract class ActivePiece extends BasePiece { @@ -13,4 +15,11 @@ public ActivePiece(RoutePolicy routePolicy, Side side, PieceType pieceType) { super(side, pieceType); this.routePolicy = routePolicy; } + + @Override + public void validateRoute(Route route, BaseBoard boardInfo) { + if (!routePolicy.isMovable(route, side, boardInfo)) { + throw new IllegalArgumentException(UNMOVABLE_ROUTE_MESSAGE); + } + } } diff --git a/src/main/java/janggi/domain/piece/BasePiece.java b/src/main/java/janggi/domain/piece/BasePiece.java index 2871c9513b..53f5b7fea5 100644 --- a/src/main/java/janggi/domain/piece/BasePiece.java +++ b/src/main/java/janggi/domain/piece/BasePiece.java @@ -1,5 +1,7 @@ package janggi.domain.piece; +import janggi.domain.PieceInitInfo; +import janggi.domain.Position; import janggi.domain.Side; public abstract class BasePiece implements Piece { @@ -22,7 +24,17 @@ public boolean isEqualSide(Side side) { } @Override - public PieceManifest getPieceInfo() { - return new PieceManifest(side, pieceType); + public PieceAttribute getPieceInfo() { + return new PieceAttribute(side, pieceType); + } + + @Override + public int getPieceScore() { + return this.pieceType.getScore(); + } + + @Override + public PieceInitInfo getPieceInitInfo(Position position) { + return new PieceInitInfo(position, side, pieceType); } } diff --git a/src/main/java/janggi/domain/piece/Gung.java b/src/main/java/janggi/domain/piece/Gung.java index 9cd23314fb..e6882ca307 100644 --- a/src/main/java/janggi/domain/piece/Gung.java +++ b/src/main/java/janggi/domain/piece/Gung.java @@ -1,9 +1,20 @@ package janggi.domain.piece; +import janggi.domain.Position; +import janggi.domain.Route; import janggi.domain.Side; public class Gung extends SingleLinearPiece { public Gung(Side side) { super(side, PieceType.GUNG); } + + @Override + public Route findRoute(Position start, Position end) { + if(!isGungSung(end)) { + throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); + } + + return super.findRoute(start, end); + } } diff --git a/src/main/java/janggi/domain/piece/LinearPiece.java b/src/main/java/janggi/domain/piece/LinearPiece.java index e5a2a59d96..6266b4e3b4 100644 --- a/src/main/java/janggi/domain/piece/LinearPiece.java +++ b/src/main/java/janggi/domain/piece/LinearPiece.java @@ -4,61 +4,77 @@ import janggi.domain.Position; import janggi.domain.Route; import janggi.domain.Side; -import janggi.domain.board.BaseBoard; import janggi.domain.policy.RoutePolicy; +import java.util.Set; import java.util.stream.Stream; public abstract class LinearPiece extends ActivePiece { + private static final Integer GUNG_SUNG_COL_START = 4; + private static final Integer GUNG_SUNG_COL_END = 6; + private static final Integer HAN_GUNG_SUNG_ROW_START = 1; + private static final Integer HAN_GUNG_SUNG_ROW_END = 3; + private static final Integer CHO_GUNG_SUNG_ROW_START = 8; + private static final Integer CHO_GUNG_SUNG_ROW_END = 10; + + private static final Set GungSungOrthogonalDirections = Set.of( + new Position(1, 5), new Position(2, 4), new Position(2, 6), new Position(3,5), + new Position(8, 5), new Position(9, 4), new Position(9, 6), new Position(10,5) + ); + public LinearPiece(RoutePolicy routePolicy, Side side, PieceType pieceType) { super(routePolicy, side, pieceType); } @Override public Route findRoute(Position start, Position end) { - boolean isVertical = start.isVertical(end); - boolean isHorizontal = start.isHorizontal(end); - - if (!isVertical && !isHorizontal) { + if(!isLinear(start, end)) { throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); } - int dist = start.calculateColumnDistance(end) + start.calculateRowDistance(end); - return calculatePath(start, isVertical, dist); + Movement direction = getLinearDirection(start, end); + int distance = calculateLinearDistance(start, end); + + return calculatePath(start, direction, distance); } - @Override - public void validateRoute(Route route, BaseBoard boardInfo) { - if (!routePolicy.isMovable(route, side, boardInfo)) { - throw new IllegalArgumentException(UNMOVABLE_ROUTE_MESSAGE); + protected boolean isLinear(Position start, Position end) { + if(start.compareX(end) == 0 && start.compareY(end) == 0) { + return false; } + + int dx = start.getDeltaX(end); + int dy = start.getDeltaY(end); + + return dx == 0 || dy == 0 || isGungSungDiagonal(start, end); + } + + private boolean isGungSungDiagonal(Position start, Position end ) { + return isGungSung(start) && isGungSung(end) && (!GungSungOrthogonalDirections.contains(start) && !GungSungOrthogonalDirections.contains(end)); + } + + protected static boolean isGungSung(Position position) { + return position.isRange(HAN_GUNG_SUNG_ROW_START, HAN_GUNG_SUNG_ROW_END, GUNG_SUNG_COL_START, GUNG_SUNG_COL_END) + || position.isRange(CHO_GUNG_SUNG_ROW_START, CHO_GUNG_SUNG_ROW_END, GUNG_SUNG_COL_START, GUNG_SUNG_COL_END); } - private Route calculatePath(Position start, boolean isVertical, int dist) { - Movement movement = resolveMovement(isVertical, dist); + protected Route calculatePath(Position start, Movement direction, int dist) { return new Route(Stream - .iterate(start, current -> current.move(movement)) + .iterate(start, current -> current.move(direction)) .limit(Math.abs(dist) + 1) .toList()); } - private Movement resolveMovement(boolean isVertical, int dist) { - if (isVertical) { - return resolveVerticalMovement(dist); - } - return resolveHorizontalMovement(dist); - } + protected Movement getLinearDirection(Position start, Position end) { + int dx = start.compareX(end); + int dy = start.compareY(end); - private Movement resolveVerticalMovement(int dist) { - if (dist < 0) { - return Movement.UP; - } - return Movement.DOWN; + return Movement.of(dx, dy); } - private Movement resolveHorizontalMovement(int dist) { - if (dist < 0) { - return Movement.LEFT; - } - return Movement.RIGHT; + protected int calculateLinearDistance(Position start, Position end) { + int dx = start.getDeltaX(end); + int dy = start.getDeltaY(end); + + return Math.max(dx, dy); } } diff --git a/src/main/java/janggi/domain/piece/Pawn.java b/src/main/java/janggi/domain/piece/Pawn.java index d8830ec1cb..b83b37361d 100644 --- a/src/main/java/janggi/domain/piece/Pawn.java +++ b/src/main/java/janggi/domain/piece/Pawn.java @@ -1,10 +1,18 @@ package janggi.domain.piece; +import janggi.domain.Movement; import janggi.domain.Position; import janggi.domain.Route; import janggi.domain.Side; +import java.util.Map; +import java.util.Set; public class Pawn extends SingleLinearPiece { + private Map> isBackward = Map.of( + Side.HAN, Set.of(Movement.UP, Movement.UP_LEFT, Movement.UP_RIGHT), + Side.CHO, Set.of(Movement.DOWN, Movement.DOWN_LEFT, Movement.DOWN_RIGHT) + ); + public Pawn(Side side) { super(side, PieceType.PAWN); } @@ -12,17 +20,19 @@ public Pawn(Side side) { @Override public Route findRoute(Position start, Position end) { - if(isBackward(start, end, side)) { + try { + Movement direction = getLinearDirection(start, end); + validateDirection(direction); + + return super.findRoute(start, end); + } catch (IllegalStateException e) { throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); } - return super.findRoute(start, end); - } - private boolean isBackward(Position start, Position end, Side side) { - if (side.equals(Side.CHO)) { - return start.calculateRowDistance(end) > 0; + private void validateDirection(Movement direction) { + if(isBackward.get(side).contains(direction)) { + throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); } - return start.calculateRowDistance(end) < 0; } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 9a97d9c3c7..682cac46b9 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -1,5 +1,6 @@ package janggi.domain.piece; +import janggi.domain.PieceInitInfo; import janggi.domain.Position; import janggi.domain.Route; import janggi.domain.Side; @@ -14,5 +15,9 @@ public interface Piece { boolean isEqualSide(Side side); - PieceManifest getPieceInfo(); + int getPieceScore(); + + PieceAttribute getPieceInfo(); + + PieceInitInfo getPieceInitInfo(Position position); } diff --git a/src/main/java/janggi/domain/piece/PieceAttribute.java b/src/main/java/janggi/domain/piece/PieceAttribute.java new file mode 100644 index 0000000000..70c6a176b0 --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceAttribute.java @@ -0,0 +1,6 @@ +package janggi.domain.piece; + +import janggi.domain.Side; + +public record PieceAttribute(Side side, PieceType pieceType) { +} diff --git a/src/main/java/janggi/domain/piece/PieceManifest.java b/src/main/java/janggi/domain/piece/PieceManifest.java deleted file mode 100644 index c2c7458001..0000000000 --- a/src/main/java/janggi/domain/piece/PieceManifest.java +++ /dev/null @@ -1,6 +0,0 @@ -package janggi.domain.piece; - -import janggi.domain.Side; - -public record PieceManifest(Side side, PieceType pieceType) { -} diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index 59710b76b2..a94a2ce922 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -1,22 +1,39 @@ package janggi.domain.piece; +import java.util.Arrays; + public enum PieceType { - CHA("CH"), - GUNG("GU"), - MA("MA"), - NONE("."), - PAWN("JO"), - PO("PO"), - SA("SA"), - SANG("SG"); + CHA("CH", 13), + GUNG("GU", 0), + MA("MA", 5), + NONE(".", 0), + PAWN("JO", 2), + PO("PO", 7), + SA("SA", 3), + SANG("SG", 3); + + private static final String INVALID_PIECE_TYPE_NAME = "해당 이름의 기물 종류가 없습니다."; 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; + } + + public static PieceType from(String name) { + return Arrays.stream(PieceType.values()) + .filter(pieceType -> pieceType.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_PIECE_TYPE_NAME)); + } } diff --git a/src/main/java/janggi/domain/piece/Sa.java b/src/main/java/janggi/domain/piece/Sa.java index 293060c37e..c45eb95b3c 100644 --- a/src/main/java/janggi/domain/piece/Sa.java +++ b/src/main/java/janggi/domain/piece/Sa.java @@ -1,9 +1,20 @@ package janggi.domain.piece; +import janggi.domain.Position; +import janggi.domain.Route; import janggi.domain.Side; public class Sa extends SingleLinearPiece { public Sa(Side side) { super( side, PieceType.SA); } + + @Override + public Route findRoute(Position start, Position end) { + if(!isGungSung(end)) { + throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); + } + + return super.findRoute(start, end); + } } diff --git a/src/main/java/janggi/domain/piece/SingleLinearPiece.java b/src/main/java/janggi/domain/piece/SingleLinearPiece.java index b7db6a4090..8c4336293c 100644 --- a/src/main/java/janggi/domain/piece/SingleLinearPiece.java +++ b/src/main/java/janggi/domain/piece/SingleLinearPiece.java @@ -14,9 +14,19 @@ public SingleLinearPiece(Side side, PieceType pieceType) { @Override public Route findRoute(Position start, Position end) { - if(Math.abs(start.calculateRowDistance(end) + start.calculateColumnDistance(end)) != RESTRICTED_DISTANCE) { + try { + int distance = calculateLinearDistance(start, end); + validateDistance(distance); + + return super.findRoute(start, end); + } catch (IllegalStateException e) { + throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); + } + } + + private void validateDistance(int distance) { + if(distance != RESTRICTED_DISTANCE) { throw new IllegalArgumentException(INVALID_DESTINATION_MESSAGE); } - return super.findRoute(start, end); } } diff --git a/src/main/java/janggi/domain/piece/StepPiece.java b/src/main/java/janggi/domain/piece/StepPiece.java index 17d2c735e1..cd539bcd96 100644 --- a/src/main/java/janggi/domain/piece/StepPiece.java +++ b/src/main/java/janggi/domain/piece/StepPiece.java @@ -4,7 +4,6 @@ import janggi.domain.Position; import janggi.domain.Route; import janggi.domain.Side; -import janggi.domain.board.BaseBoard; import janggi.domain.policy.RoutePolicy; import java.util.List; import java.util.Optional; @@ -26,11 +25,4 @@ public Route findRoute(Position start, Position end) { .findFirst() .orElseThrow(() -> new IllegalArgumentException(INVALID_DESTINATION_MESSAGE)); } - - @Override - public void validateRoute(Route route, BaseBoard boardInfo) { - if (!routePolicy.isMovable(route, side, boardInfo)) { - throw new IllegalArgumentException(UNMOVABLE_ROUTE_MESSAGE); - } - } } diff --git a/src/main/java/janggi/domain/turn/ActionTurn.java b/src/main/java/janggi/domain/turn/BaseTurn.java similarity index 50% rename from src/main/java/janggi/domain/turn/ActionTurn.java rename to src/main/java/janggi/domain/turn/BaseTurn.java index 576b0df3eb..75fc72dab5 100644 --- a/src/main/java/janggi/domain/turn/ActionTurn.java +++ b/src/main/java/janggi/domain/turn/BaseTurn.java @@ -1,17 +1,20 @@ package janggi.domain.turn; import janggi.domain.Side; +import janggi.domain.SideScore; import janggi.domain.board.Board; -import janggi.domain.piece.PieceManifest; +import janggi.domain.piece.PieceAttribute; import java.util.List; -public abstract class ActionTurn implements PlayerTurn { +public abstract class BaseTurn implements PlayerTurn { + protected static final int MAX_TURN = 200; + protected final Board board; - protected final Side side; + protected final int turn; - public ActionTurn(Board board, Side side) { + public BaseTurn(Board board, int turn) { this.board = board; - this.side = side; + this.turn = turn; } @Override @@ -20,17 +23,17 @@ public boolean isFinished() { } @Override - public List> getCurrentBoard() { + public List> getCurrentBoard() { return board.getCurrentBoard(); } @Override - public Side getCurrentSide() { - return side; + public Side getWinnerSide() { + return Side.EMPTY; } @Override - public Side getWinnerSide() { - return Side.EMPTY; + public SideScore getCurrentScore() { + return board.getScore(); } } diff --git a/src/main/java/janggi/domain/turn/ChoActionTurn.java b/src/main/java/janggi/domain/turn/ChoActionTurn.java deleted file mode 100644 index 9f1ed967d7..0000000000 --- a/src/main/java/janggi/domain/turn/ChoActionTurn.java +++ /dev/null @@ -1,20 +0,0 @@ -package janggi.domain.turn; - -import janggi.domain.Position; -import janggi.domain.Side; -import janggi.domain.board.Board; - -public class ChoActionTurn extends ActionTurn { - public ChoActionTurn(Board board) { - super(board, Side.CHO); - } - - @Override - public PlayerTurn move(Position start, Position end) { - board.move(start, end, side); - if (board.isEndGame()) { - return new Finish(board, side); - } - return new HanActionTurn(board); - } -} diff --git a/src/main/java/janggi/domain/turn/ChoTurn.java b/src/main/java/janggi/domain/turn/ChoTurn.java new file mode 100644 index 0000000000..79d3853387 --- /dev/null +++ b/src/main/java/janggi/domain/turn/ChoTurn.java @@ -0,0 +1,35 @@ +package janggi.domain.turn; + +import janggi.domain.Position; +import janggi.domain.Side; +import janggi.domain.board.Board; +import janggi.domain.piece.PieceAttribute; + +public class ChoTurn extends BaseTurn { + public ChoTurn(Board board, int turn) { + super(board, turn); + } + + @Override + public TurnState move(Position start, Position end) { + PieceAttribute pieceAttribute = board.move(start, end, Side.CHO); + + if(turn == MAX_TURN) { + TurnAttribute turnAttribute = new TurnAttribute(Side.EMPTY, turn + 1); + return new TurnState(new FinishTurn(board, Side.EMPTY, turn + 1), turnAttribute, pieceAttribute); + } + + if (board.isEndGame()) { + TurnAttribute turnAttribute = new TurnAttribute(Side.EMPTY, turn + 1); + return new TurnState(new FinishTurn(board, Side.CHO, turn + 1), turnAttribute, pieceAttribute); + } + + TurnAttribute turnAttribute = new TurnAttribute(Side.HAN, turn + 1); + return new TurnState(new HanTurn(board, turn + 1), turnAttribute, pieceAttribute); + } + + @Override + public Side getCurrentSide() { + return Side.CHO; + } +} diff --git a/src/main/java/janggi/domain/turn/Finish.java b/src/main/java/janggi/domain/turn/FinishTurn.java similarity index 66% rename from src/main/java/janggi/domain/turn/Finish.java rename to src/main/java/janggi/domain/turn/FinishTurn.java index 6800942d8c..138e6febb8 100644 --- a/src/main/java/janggi/domain/turn/Finish.java +++ b/src/main/java/janggi/domain/turn/FinishTurn.java @@ -4,18 +4,18 @@ import janggi.domain.Side; import janggi.domain.board.Board; -public class Finish extends ActionTurn { +public class FinishTurn extends BaseTurn { private static final String INVALID_MOVE = "게임 종료 상태에서는 이동할 수 없습니다."; private final Side winnerSide; - public Finish(Board board, Side winnerSide) { - super(board, Side.EMPTY); + public FinishTurn(Board board, Side winnerSide, int turn) { + super(board, turn); this.winnerSide = winnerSide; } @Override - public PlayerTurn move(Position start, Position end) { + public TurnState move(Position start, Position end) { throw new IllegalStateException(INVALID_MOVE); } @@ -28,4 +28,9 @@ public boolean isFinished() { public Side getWinnerSide() { return winnerSide; } + + @Override + public Side getCurrentSide() { + return Side.EMPTY; + } } diff --git a/src/main/java/janggi/domain/turn/HanActionTurn.java b/src/main/java/janggi/domain/turn/HanActionTurn.java deleted file mode 100644 index 684adbd513..0000000000 --- a/src/main/java/janggi/domain/turn/HanActionTurn.java +++ /dev/null @@ -1,20 +0,0 @@ -package janggi.domain.turn; - -import janggi.domain.Position; -import janggi.domain.Side; -import janggi.domain.board.Board; - -public class HanActionTurn extends ActionTurn { - public HanActionTurn(Board board) { - super(board, Side.HAN); - } - - @Override - public PlayerTurn move(Position start, Position end) { - board.move(start, end, side); - if (board.isEndGame()) { - return new Finish(board, side); - } - return new ChoActionTurn(board); - } -} diff --git a/src/main/java/janggi/domain/turn/HanTurn.java b/src/main/java/janggi/domain/turn/HanTurn.java new file mode 100644 index 0000000000..9de76ee8eb --- /dev/null +++ b/src/main/java/janggi/domain/turn/HanTurn.java @@ -0,0 +1,35 @@ +package janggi.domain.turn; + +import janggi.domain.Position; +import janggi.domain.Side; +import janggi.domain.board.Board; +import janggi.domain.piece.PieceAttribute; + +public class HanTurn extends BaseTurn { + public HanTurn(Board board, int turn) { + super(board, turn); + } + + @Override + public TurnState move(Position start, Position end) { + PieceAttribute pieceAttribute = board.move(start, end, Side.HAN); + + if(turn == MAX_TURN) { + TurnAttribute turnAttribute = new TurnAttribute(Side.EMPTY, turn + 1); + return new TurnState(new FinishTurn(board, Side.EMPTY, turn), turnAttribute, pieceAttribute); + } + + if (board.isEndGame()) { + TurnAttribute turnAttribute = new TurnAttribute(Side.EMPTY, turn + 1); + return new TurnState(new FinishTurn(board, Side.HAN, turn), turnAttribute, pieceAttribute); + } + + TurnAttribute turnAttribute = new TurnAttribute(Side.HAN, turn + 1); + return new TurnState(new ChoTurn(board, turn + 1), turnAttribute, pieceAttribute); + } + + @Override + public Side getCurrentSide() { + return Side.HAN; + } +} diff --git a/src/main/java/janggi/domain/turn/PlayerTurn.java b/src/main/java/janggi/domain/turn/PlayerTurn.java index 4184539908..2e0335d9bb 100644 --- a/src/main/java/janggi/domain/turn/PlayerTurn.java +++ b/src/main/java/janggi/domain/turn/PlayerTurn.java @@ -2,17 +2,20 @@ import janggi.domain.Position; import janggi.domain.Side; -import janggi.domain.piece.PieceManifest; +import janggi.domain.SideScore; +import janggi.domain.piece.PieceAttribute; import java.util.List; public interface PlayerTurn { - PlayerTurn move(Position start, Position end); + TurnState move(Position start, Position end); boolean isFinished(); - List> getCurrentBoard(); + List> getCurrentBoard(); Side getCurrentSide(); Side getWinnerSide(); + + SideScore getCurrentScore(); } diff --git a/src/main/java/janggi/domain/turn/TurnAttribute.java b/src/main/java/janggi/domain/turn/TurnAttribute.java new file mode 100644 index 0000000000..63f7c61b26 --- /dev/null +++ b/src/main/java/janggi/domain/turn/TurnAttribute.java @@ -0,0 +1,6 @@ +package janggi.domain.turn; + +import janggi.domain.Side; + +public record TurnAttribute(Side side, int turn) { +} diff --git a/src/main/java/janggi/domain/turn/TurnState.java b/src/main/java/janggi/domain/turn/TurnState.java new file mode 100644 index 0000000000..0e14505494 --- /dev/null +++ b/src/main/java/janggi/domain/turn/TurnState.java @@ -0,0 +1,6 @@ +package janggi.domain.turn; + +import janggi.domain.piece.PieceAttribute; + +public record TurnState(PlayerTurn playerTurn, TurnAttribute turnAttribute, PieceAttribute movedPiece) { +} diff --git a/src/main/java/janggi/dto/BoardDto.java b/src/main/java/janggi/dto/BoardDto.java index 82666beff0..72dbd065eb 100644 --- a/src/main/java/janggi/dto/BoardDto.java +++ b/src/main/java/janggi/dto/BoardDto.java @@ -1,7 +1,7 @@ package janggi.dto; -import janggi.domain.piece.PieceManifest; +import janggi.domain.piece.PieceAttribute; import java.util.List; -public record BoardDto(List> board) { +public record BoardDto(List> board) { } diff --git a/src/main/java/janggi/dto/GameDto.java b/src/main/java/janggi/dto/GameDto.java new file mode 100644 index 0000000000..8b2baa2458 --- /dev/null +++ b/src/main/java/janggi/dto/GameDto.java @@ -0,0 +1,4 @@ +package janggi.dto; + +public record GameDto(String name, String createdAt, String updatedAt, String side, int turn) { +} diff --git a/src/main/java/janggi/dto/GameResponseDto.java b/src/main/java/janggi/dto/GameResponseDto.java new file mode 100644 index 0000000000..8712a57f0c --- /dev/null +++ b/src/main/java/janggi/dto/GameResponseDto.java @@ -0,0 +1,4 @@ +package janggi.dto; + +public record GameResponseDto(int id, String name, String createdAt, String updatedAt, String side, int turn) { +} diff --git a/src/main/java/janggi/dto/PieceDto.java b/src/main/java/janggi/dto/PieceDto.java new file mode 100644 index 0000000000..142193a879 --- /dev/null +++ b/src/main/java/janggi/dto/PieceDto.java @@ -0,0 +1,4 @@ +package janggi.dto; + +public record PieceDto(int x, int y, String pieceType, String side) { +} diff --git a/src/main/java/janggi/dto/PiecePositionDto.java b/src/main/java/janggi/dto/PiecePositionDto.java new file mode 100644 index 0000000000..ceea459487 --- /dev/null +++ b/src/main/java/janggi/dto/PiecePositionDto.java @@ -0,0 +1,4 @@ +package janggi.dto; + +public record PiecePositionDto(int x, int y) { +} diff --git a/src/main/java/janggi/dto/TurnDto.java b/src/main/java/janggi/dto/TurnDto.java new file mode 100644 index 0000000000..01447c3e6c --- /dev/null +++ b/src/main/java/janggi/dto/TurnDto.java @@ -0,0 +1,4 @@ +package janggi.dto; + +public record TurnDto(String side, int turn) { +} diff --git a/src/main/java/janggi/initializer/BoardInitializer.java b/src/main/java/janggi/initializer/BoardInitializer.java index f5b6d74e15..7ecd8ee879 100644 --- a/src/main/java/janggi/initializer/BoardInitializer.java +++ b/src/main/java/janggi/initializer/BoardInitializer.java @@ -1,6 +1,7 @@ package janggi.initializer; import janggi.domain.Arrangement; +import janggi.domain.PieceInitInfo; import janggi.domain.Position; import janggi.domain.Side; import janggi.domain.piece.Cha; @@ -85,6 +86,12 @@ public static Map createBoard(Arrangement choArrangement, Arran return board; } + public static Map createBoard(List pieceInitInfos) { + Map board = initBoard(); + pieceInitInfos.forEach(pieceInitInfo -> board.put(pieceInitInfo.position(), pieceMap.get(pieceInitInfo.pieceType()).apply(pieceInitInfo.side()))); + return board; + } + private static Map initBoard() { Map pieces = new HashMap<>(); for (int i = Position.BOARD_START_ROWS; i <= Position.BOARD_END_ROWS; i++) { diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index fb7328f322..5ce74a983c 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -1,20 +1,40 @@ package janggi.view; +import static janggi.Runner.END_TEXT; + import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Scanner; +import javax.swing.text.html.Option; public class InputView { private static final String HAN_ARRANGEMENT_MESSAGE = "한 진영의 배치를 입력해주세요. (예: 마상마상, 마상상마, 상마상마, 상마마상)"; private static final String CHO_ARRANGEMENT_MESSAGE = "초 진영의 배치를 입력해주세요. (예: 마상마상, 마상상마, 상마상마, 상마마상)"; - private static final String START_POSITION_MESSAGE = "움직일 기물의 시작 좌표를 입력해주세요. (예: 3,4)"; + private static final String START_POSITION_MESSAGE = "움직일 기물의 시작 좌표를 입력하거나, 종료하려면 '종료'를 입력해주세요. (예: 3,4 또는 종료)"; private static final String END_POSITION_MESSAGE = "움직일 기물의 도착 좌표를 입력해주세요. (예: 4,4)"; private static final String BASE_DELIMITER = ","; + private static final String CONSENT_END_MESSAGE = "종료하는 데 동의하시면 '종료'를, 계속하시려면 아무 키나 입력해주세요."; private static final String INVALID_POSITION_TYPE = "숫자만 입력 가능합니다."; private static final Scanner scanner = new Scanner(System.in); + public static Optional askLoadGame() { + System.out.println("불러오려는 게임의 번호를, 또는 새로 생성하려면 '*'를 입력해주세요."); + String input = scanner.nextLine(); + System.out.println(); + if(input.trim().equals("*")) { + return Optional.empty(); + } + return Optional.of(parseInt(input)); + } + + public static String askGameName() { + System.out.println("생성하려는 게임의 이름을 입력해주세요."); + return scanner.nextLine(); + } + public static String askHanArrangement() { System.out.println(HAN_ARRANGEMENT_MESSAGE); return scanner.nextLine(); @@ -25,12 +45,22 @@ public static String askChoArrangement() { return scanner.nextLine(); } - public static List askStartPosition() { + public static Optional> askStartPosition() { System.out.println(START_POSITION_MESSAGE); String input = scanner.nextLine(); - return Arrays.stream(input.split(BASE_DELIMITER)) + + if(input.equals(END_TEXT)) { + return Optional.empty(); + } + + return Optional.of(Arrays.stream(input.split(BASE_DELIMITER)) .map(InputView::parseInt) - .toList(); + .toList()); + } + + public static String consentEnd() { + System.out.println(CONSENT_END_MESSAGE); + return scanner.nextLine(); } public static List askEndPosition() { diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index 0419928c3f..b32028d24a 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -1,9 +1,12 @@ package janggi.view; +import janggi.domain.GameInfo; import janggi.domain.Side; -import janggi.domain.piece.PieceManifest; +import janggi.domain.SideScore; +import janggi.domain.piece.PieceAttribute; import janggi.domain.piece.PieceType; import janggi.dto.BoardDto; +import java.time.format.DateTimeFormatter; import java.util.List; public class OutputView { @@ -14,10 +17,29 @@ public class OutputView { private static final String ANSI_GREEN = "\u001B[32m"; private static final String TURN_PREFIX = "현재 턴: "; + + private static final String HAN_SCORE_PREFIX = "한: "; + private static final String CHO_SCORE_PREFIX = "초: "; + private static final String SCORE_SUFFIX = "점"; + private static final String SCORE_DELIMITER = ", "; + private static final String ERROR_PREFIX = "[ERROR] "; + public static void printLine() { + System.out.println(); + } + + public static void printGameRoom(List gameInfos) { + System.out.println("진행 중인 장기 게임"); + for(int i = 0; i < gameInfos.size(); i++) { + GameInfo gameInfo = gameInfos.get(i); + System.out.println(i + 1 + ". 이름: " + gameInfo.name() + ", 생성 시간: " + gameInfo.created() + ", 가장 마지막 플레이 시간: " + gameInfo.recent()); + } + printLine(); + } + public static void printBoard(BoardDto boardDto) { - List> board = boardDto.board(); + List> board = boardDto.board(); StringBuilder result = new StringBuilder(); result.append(buildColumnHeader(board.getFirst().size())); appendBoardRows(board, result); @@ -28,7 +50,12 @@ public static void printTurn(Side side) { System.out.println(TURN_PREFIX + side.getName()); } + public static void printScore(SideScore score) { + System.out.println(CHO_SCORE_PREFIX + score.cho() + SCORE_SUFFIX + SCORE_DELIMITER + HAN_SCORE_PREFIX + score.han() + SCORE_SUFFIX); + } + public static void printWinner(Side winnerSide) { + printLine(); System.out.printf("%s 승리!%n", winnerSide.getName()); } @@ -45,7 +72,7 @@ private static String buildColumnHeader(int colSize) { return header.toString(); } - private static void appendBoardRows(List> board, StringBuilder result) { + private static void appendBoardRows(List> board, StringBuilder result) { result.append(buildTopBorder(board.getFirst().size())); for (int row = 0; row < board.size(); row++) { appendBoardRow(board.get(row), row, result); @@ -53,10 +80,10 @@ private static void appendBoardRows(List> board, StringBuild } } - private static void appendBoardRow(List row, int rowIndex, StringBuilder result) { + private static void appendBoardRow(List row, int rowIndex, StringBuilder result) { result.append(String.format(" %2d ┃", rowIndex + 1)); - for (PieceManifest pieceManifest : row) { - result.append(formatCell(pieceManifest)); + for (PieceAttribute pieceAttribute : row) { + result.append(formatCell(pieceAttribute)); result.append("┃"); } result.append(System.lineSeparator()); @@ -86,11 +113,11 @@ private static String buildBorder(String start, String middle, String end, int c return border.toString(); } - private static String formatCell(PieceManifest pieceManifest) { - if (isEmpty(pieceManifest)) { + private static String formatCell(PieceAttribute pieceAttribute) { + if (isEmpty(pieceAttribute)) { return " " + EMPTY_CELL + " "; } - return " " + colorize(pieceManifest.pieceType().getName(), pieceManifest.side()) + " "; + return " " + colorize(pieceAttribute.pieceType().getName(), pieceAttribute.side()) + " "; } private static String colorize(String pieceName, Side side) { @@ -103,10 +130,10 @@ private static String colorize(String pieceName, Side side) { return pieceName; } - private static boolean isEmpty(PieceManifest pieceManifest) { - if (pieceManifest == null) { + private static boolean isEmpty(PieceAttribute pieceAttribute) { + if (pieceAttribute == null) { return true; } - return pieceManifest.pieceType() == PieceType.NONE || pieceManifest.side() == Side.EMPTY; + return pieceAttribute.pieceType() == PieceType.NONE || pieceAttribute.side() == Side.EMPTY; } } diff --git a/src/main/resources/janggi.db b/src/main/resources/janggi.db new file mode 100644 index 0000000000..47141c7d11 Binary files /dev/null and b/src/main/resources/janggi.db differ diff --git a/src/test/java/janggi/domain/PositionTest.java b/src/test/java/janggi/domain/PositionTest.java index d6204bc5e3..e241305efa 100644 --- a/src/test/java/janggi/domain/PositionTest.java +++ b/src/test/java/janggi/domain/PositionTest.java @@ -9,8 +9,6 @@ import org.junit.jupiter.params.provider.ValueSource; class PositionTest { - - @ParameterizedTest @ValueSource(strings = {"0,7", "100,2", "-2,7"}) void 입력한_행이_유효_범위를_벗어나면_예외가_발생한다(String input) { diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 12d6812947..eb92f4ef10 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -6,18 +6,18 @@ import janggi.domain.Arrangement; import janggi.domain.Position; import janggi.domain.Side; +import janggi.domain.SideScore; +import janggi.domain.piece.Gung; +import janggi.domain.piece.Pawn; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; -import janggi.domain.piece.Po; import janggi.initializer.BoardInitializer; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; class BoardTest { - private Board board = new Board(BoardInitializer.createBoard(Arrangement.MA_SANG_MA_SANG, Arrangement.MA_SANG_MA_SANG)); + private Board board = new Board(BoardInitializer.createBoard(Arrangement.MA_SANG_MA_SANG, Arrangement.MA_SANG_MA_SANG), 72, 72); @Test void 빈_칸인지_여부를_제대로_반영한다() { @@ -42,7 +42,7 @@ class BoardTest { @Test void 자기_진영의_기물을_움직이면_정상_작동한다() { - Board board = new Board(BoardInitializer.createBoard(Arrangement.MA_SANG_MA_SANG, Arrangement.MA_SANG_MA_SANG)); + Board board = new Board(BoardInitializer.createBoard(Arrangement.MA_SANG_MA_SANG, Arrangement.MA_SANG_MA_SANG), 72, 72); board.move(new Position(1, 1), new Position(2, 1), Side.HAN); assertThat(board.getCurrentBoard().get(1).getFirst().pieceType()).isEqualTo(PieceType.CHA); @@ -51,8 +51,26 @@ class BoardTest { @Test void 다른_진영의_기물을_움직이면_예외_처리한다() { - Board board = new Board(BoardInitializer.createBoard(Arrangement.MA_SANG_MA_SANG, Arrangement.MA_SANG_MA_SANG)); + Board board = new Board(BoardInitializer.createBoard(Arrangement.MA_SANG_MA_SANG, Arrangement.MA_SANG_MA_SANG), 72, 72); assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(2, 1), Side.CHO)).isInstanceOf(IllegalArgumentException.class).hasMessage("자기 진영의 기물만 움직일 수 있습니다."); } + + @Test + void 기물을_잡으면_상대편_진영의_점수가_깎인다() { + Map customBoard = new HashMap<>(Map.of(new Position(1, 1), new Pawn(Side.HAN), new Position(1, 2), new Pawn(Side.CHO))); + Board board = new Board(customBoard, PieceType.PAWN.getScore(), PieceType.PAWN.getScore()); + + board.move(new Position(1, 2), new Position(1, 1), Side.CHO); + assertThat(board.getScore()).isEqualTo(new SideScore(1.5, PieceType.PAWN.getScore())); + } + + @Test + void 궁을_잡으면_게임_끝나는지_확인하는_메서드에서_참으로_리턴한다() { + Map customBoard = new HashMap<>(Map.of(new Position(1, 1), new Pawn(Side.HAN), new Position(1, 2), new Gung(Side.CHO))); + Board board = new Board(customBoard,0, 0); + + board.move(new Position(1, 1), new Position(1, 2), Side.HAN); + assertThat(board.isEndGame()).isTrue(); + } } diff --git a/src/test/java/janggi/domain/board/MaterialScoreTest.java b/src/test/java/janggi/domain/board/MaterialScoreTest.java new file mode 100644 index 0000000000..68a832243c --- /dev/null +++ b/src/test/java/janggi/domain/board/MaterialScoreTest.java @@ -0,0 +1,37 @@ +package janggi.domain.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.Side; +import janggi.domain.SideScore; +import org.junit.jupiter.api.Test; + +public class MaterialScoreTest { + @Test + void 초기_점수를_정확하게_반환한다() { + MaterialScore materialScore = new MaterialScore(54, 72); + SideScore currentScore = materialScore.getCurrentScore(); + + assertThat(currentScore.cho()).isEqualTo(72); + assertThat(currentScore.han()).isEqualTo(55.5); + } + + @Test + void 점수_감소_메서드에서_음수가_들어오면_예외가_발생한다() { + MaterialScore materialScore = new MaterialScore(72, 72); + + assertThatThrownBy(() -> materialScore.decreaseScore(Side.HAN, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("차감할 점수는 0보다 커야 합니다."); + } + + @Test + void 궁이_죽기_전까지는_isAnyGungDead가_거짓이다() { + MaterialScore materialScore = new MaterialScore(72, 72); + assertThat(materialScore.isAnyGungDead()).isFalse(); + + materialScore.updateGungDead(Side.CHO); + assertThat(materialScore.isAnyGungDead()).isTrue(); + } +} diff --git a/src/test/java/janggi/domain/piece/ChaTest.java b/src/test/java/janggi/domain/piece/ChaTest.java index 44b4119949..d9380f776e 100644 --- a/src/test/java/janggi/domain/piece/ChaTest.java +++ b/src/test/java/janggi/domain/piece/ChaTest.java @@ -8,6 +8,8 @@ import janggi.domain.Side; import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class ChaTest { @@ -15,7 +17,7 @@ class ChaTest { void 시작_좌표와_끝_좌표가_같은_선_상에_존재하지_않으면_에러가_발생한다() { Cha cha = new Cha(Side.CHO); Position start = new Position(2, 3); - Position end = new Position(3, 4); + Position end = new Position(5, 6); assertThatThrownBy(() -> cha.findRoute(start, end)) .isInstanceOf(IllegalArgumentException.class) @@ -81,4 +83,111 @@ class ChaTest { end ))); } + + @Test + void 궁성_위_오른쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Cha cha = new Cha(Side.CHO); + + Position start = new Position(3, 4); + Position end = new Position(1, 6); + + Route routes = cha.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 궁성_위_왼쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Cha cha = new Cha(Side.CHO); + + Position start = new Position(3, 6); + Position end = new Position(1, 4); + + Route routes = cha.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 궁성_아래_오른쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Cha cha = new Cha(Side.CHO); + + Position start = new Position(1, 4); + Position end = new Position(3, 6); + + Route routes = cha.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 궁성_아래_왼쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Cha cha = new Cha(Side.CHO); + + Position start = new Position(3, 4); + Position end = new Position(1, 6); + + Route routes = cha.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 대각선_이동이_궁성_밖을_포함할_때_에러가_발생한다() { + Cha cha = new Cha(Side.CHO); + + Position start = new Position(1, 4); + Position end = new Position(4, 7); + + assertThatThrownBy(() -> cha.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } + + @Test + void 궁성_내의_대각선_이동과_궁성_밖_직선_이동이_함께_있을_때_에러가_발생한다() { + Cha cha = new Cha(Side.CHO); + + Position start = new Position(1, 4); + Position end = new Position(4, 6); + + assertThatThrownBy(() -> cha.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } + + @ParameterizedTest + @CsvSource({ + "1, 5, 2, 4", + "1, 5, 2, 6", + "2, 4, 1, 5", + "2, 6, 3, 5", + "9, 4, 8, 5", + "9, 6, 10, 5" + }) + void 궁성_내_십자_위치에서의_대각선_이동이_있을_때_에러가_발생한다(int startX, int startY, int endX, int endY) { + Cha cha = new Cha(Side.CHO); + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + assertThatThrownBy(() -> cha.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } } diff --git a/src/test/java/janggi/domain/piece/GungTest.java b/src/test/java/janggi/domain/piece/GungTest.java index 7b329f859d..22f2565f4d 100644 --- a/src/test/java/janggi/domain/piece/GungTest.java +++ b/src/test/java/janggi/domain/piece/GungTest.java @@ -1,23 +1,23 @@ package janggi.domain.piece; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import janggi.domain.Position; import janggi.domain.Route; import janggi.domain.Side; +import org.assertj.core.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; public class GungTest { @ParameterizedTest @CsvSource({ - "2,3,2,4", - "2,3,2,2", - "2,3,1,3", - "2,3,3,3" + "2,5,1,5", + "2,5,2,4", + "2,5,2,6", + "2,5,3,5" }) - void 궁은_상하좌우로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { + void 궁은_궁성_안에서_상하좌우로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { Position startPosition = new Position(startX, startY); Position endPosition = new Position(endX, endY); @@ -30,18 +30,53 @@ public class GungTest { @ParameterizedTest @CsvSource({ - "2,3,2,5", - "2,3,2,1", - "2,3,1,6", - "2,3,3,1" + "2,5,1,4", + "2,5,1,6", + "2,5,3,4", + "2,5,3,6" }) - void 궁은_상하좌우가_아닌_좌표로는_이동할_수_없다(int startX, int startY, int endX, int endY) { + void 궁은_궁성_안에서_십자_위치를_제외한_곳에서_대각선으로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { Position startPosition = new Position(startX, startY); Position endPosition = new Position(endX, endY); Gung gung = new Gung(Side.CHO); - assertThatThrownBy(() -> gung.findRoute(startPosition, endPosition)) + Route actual = gung.findRoute(startPosition, endPosition); + + assertThat(actual.isDestinationSatisfied(position -> position.equals(endPosition))).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "1, 5, 2, 4", + "1, 5, 2, 6", + "2, 4, 1, 5", + "2, 6, 3, 5", + "9, 4, 8, 5", + "9, 6, 10, 5" + }) + void 궁은_궁성_내_십자_위치에서의_대각선_이동이_있을_때_에러가_발생한다(int startX, int startY, int endX, int endY) { + Gung gung = new Gung(Side.CHO); + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + Assertions.assertThatThrownBy(() -> gung.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } + + @ParameterizedTest + @CsvSource({ + "8,4,8,3", + "8,6,8,7", + "8,5,7,5" + }) + void 궁은_궁성_밖으로_이동이_있을_때_에러가_발생한다(int startX, int startY, int endX, int endY) { + Gung gung = new Gung(Side.CHO); + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + Assertions.assertThatThrownBy(() -> gung.findRoute(start, end)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("올바른 도착 지점이 아닙니다."); } diff --git a/src/test/java/janggi/domain/piece/PawnTest.java b/src/test/java/janggi/domain/piece/PawnTest.java index 5b4751b27c..f4f2914e82 100644 --- a/src/test/java/janggi/domain/piece/PawnTest.java +++ b/src/test/java/janggi/domain/piece/PawnTest.java @@ -14,7 +14,7 @@ class PawnTest { @CsvSource({ "2,3,3,3", "2,3,2,2", - "2,3,2,4" + "2,3,2,4", }) void 폰은_한_진영일때_하좌우로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { Position startPosition = new Position(startX, startY); @@ -27,6 +27,24 @@ class PawnTest { assertThat(actual.isDestinationSatisfied(position -> position.equals(endPosition))).isTrue(); } + @ParameterizedTest + @CsvSource({ + "9,5,10,4", + "9,5,10,6" + }) + void 폰은_한_진영일때_궁성에서_아래_대각선으로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { + Position startPosition = new Position(startX, startY); + Position endPosition = new Position(endX, endY); + + Pawn pawn = new Pawn(Side.HAN); + + Route actual = pawn.findRoute(startPosition, endPosition); + + assertThat(actual.isDestinationSatisfied(position -> position.equals(endPosition))).isTrue(); + } + + + @ParameterizedTest @CsvSource({ "2,3,1,3", @@ -44,6 +62,22 @@ class PawnTest { assertThat(actual.isDestinationSatisfied(position -> position.equals(endPosition))).isTrue(); } + @ParameterizedTest + @CsvSource({ + "2,5,1,4", + "2,5,1,6", + }) + void 폰은_초_진영일때_궁성에서_위_대각선으로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { + Position startPosition = new Position(startX, startY); + Position endPosition = new Position(endX, endY); + + Pawn pawn = new Pawn(Side.CHO); + + Route actual = pawn.findRoute(startPosition, endPosition); + + assertThat(actual.isDestinationSatisfied(position -> position.equals(endPosition))).isTrue(); + } + @ParameterizedTest @CsvSource({ "2,3,1,3", @@ -63,12 +97,26 @@ class PawnTest { @ParameterizedTest @CsvSource({ - "2,3,3,3", - "2,3,1,6", - "2,3,3,1", - "2,3,2,3" + "9,5,8,4", + "9,5,8,6", + }) + void 폰은_한_진영일_때_궁성에서_위_대각선으로_이동할_수_없다(int startX, int startY, int endX, int endY) { + Position startPosition = new Position(startX, startY); + Position endPosition = new Position(endX, endY); + + Pawn pawn = new Pawn(Side.HAN); + + assertThatThrownBy(() -> pawn.findRoute(startPosition, endPosition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } + + @ParameterizedTest + @CsvSource({ + "2,5,3,4", + "2,5,3,6", }) - void 폰은_초_진영일_때_상좌우가_아닌_좌표로는_이동할_수_없다(int startX, int startY, int endX, int endY) { + void 폰은_초_진영일_때_궁성에서_아래_대각선으로_이동할_수_없다(int startX, int startY, int endX, int endY) { Position startPosition = new Position(startX, startY); Position endPosition = new Position(endX, endY); diff --git a/src/test/java/janggi/domain/piece/PoTest.java b/src/test/java/janggi/domain/piece/PoTest.java index a6c5952bd9..5ca6979597 100644 --- a/src/test/java/janggi/domain/piece/PoTest.java +++ b/src/test/java/janggi/domain/piece/PoTest.java @@ -80,4 +80,92 @@ class PoTest { end ))); } + + @Test + void 궁성_위_오른쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Po po = new Po(Side.CHO); + + Position start = new Position(3, 4); + Position end = new Position(1, 6); + + Route routes = po.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 궁성_위_왼쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Po po = new Po(Side.CHO); + + Position start = new Position(3, 6); + Position end = new Position(1, 4); + + Route routes = po.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 궁성_아래_오른쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Po po = new Po(Side.CHO); + + Position start = new Position(1, 4); + Position end = new Position(3, 6); + + Route routes = po.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 궁성_아래_왼쪽_대각선에_해당되는_시작과_끝좌표에_올바른_경로를_생성한다() { + Po po = new Po(Side.CHO); + + Position start = new Position(3, 4); + Position end = new Position(1, 6); + + Route routes = po.findRoute(start, end); + + assertThat(routes).isEqualTo(new Route(List.of( + start, + new Position(2, 5), + end + ))); + } + + @Test + void 대각선_이동이_궁성_밖을_포함할_때_에러가_발생한다() { + Po po = new Po(Side.CHO); + + Position start = new Position(1, 4); + Position end = new Position(4, 7); + + assertThatThrownBy(() -> po.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } + + @Test + void 궁성_내의_대각선_이동과_궁성_밖_직선_이동이_함께_있을_때_에러가_발생한다() { + Po po = new Po(Side.CHO); + + Position start = new Position(1, 4); + Position end = new Position(4, 6); + + assertThatThrownBy(() -> po.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } } diff --git a/src/test/java/janggi/domain/piece/RouteTest.java b/src/test/java/janggi/domain/piece/RouteTest.java index 888ac185df..ecc5d0b2d6 100644 --- a/src/test/java/janggi/domain/piece/RouteTest.java +++ b/src/test/java/janggi/domain/piece/RouteTest.java @@ -24,7 +24,7 @@ public class RouteTest { new Position(1, 5) )); - assertThat(route.isEveryBetween(position -> position.isHorizontal(new Position(1, 6)))).isTrue(); + assertThat(route.isEveryBetween(position -> position.compareX(new Position(1, 6)) == 0)).isTrue(); } @Test @@ -37,7 +37,7 @@ public class RouteTest { new Position(1, 5) )); - assertThat(route.isEveryBetween(position -> position.isHorizontal(new Position(1, 6)))).isFalse(); + assertThat(route.isEveryBetween(position -> position.compareX(new Position(1, 1)) == 0)).isFalse(); } @Test @@ -50,7 +50,7 @@ public class RouteTest { new Position(5, 5) )); - assertThat(route.isAnyBetween(position -> position.isHorizontal(new Position(1, 6)))).isTrue(); + assertThat(route.isAnyBetween(position -> position.compareX(new Position(1, 1)) == 0)).isTrue(); } @Test @@ -63,7 +63,7 @@ public class RouteTest { new Position(5, 5) )); - assertThat(route.isAnyBetween(position -> position.isHorizontal(new Position(1, 6)))).isFalse(); + assertThat(route.isAnyBetween(position -> position.compareX(new Position(1, 1)) == 0)).isFalse(); } @Test @@ -76,7 +76,7 @@ public class RouteTest { new Position(3, 5) )); - assertThat(route.countBetween(position -> position.isHorizontal(new Position(1, 6)))).isEqualTo(2); + assertThat(route.countBetween(position -> position.compareX(new Position(1, 1)) == 0)).isEqualTo(2); } @Test @@ -103,7 +103,7 @@ public class RouteTest { new Position(5, 5) )); - assertThat(route.isDestinationSatisfied(position -> position.isVertical(new Position(2, 5)))).isTrue(); - assertThat(route.isDestinationSatisfied(position -> position.isHorizontal(new Position(2, 5)))).isFalse(); + assertThat(route.isDestinationSatisfied(position -> position.compareY(new Position(2, 5)) == 0)).isTrue(); + assertThat(route.isDestinationSatisfied(position -> position.compareX(new Position(6, 7)) == 0)).isFalse(); } } diff --git a/src/test/java/janggi/domain/piece/SaTest.java b/src/test/java/janggi/domain/piece/SaTest.java index 6145863eb7..331e831060 100644 --- a/src/test/java/janggi/domain/piece/SaTest.java +++ b/src/test/java/janggi/domain/piece/SaTest.java @@ -1,23 +1,23 @@ package janggi.domain.piece; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import janggi.domain.Position; import janggi.domain.Route; import janggi.domain.Side; +import org.assertj.core.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; class SaTest { @ParameterizedTest @CsvSource({ - "2,3,2,4", - "2,3,2,2", - "2,3,1,3", - "2,3,3,3" + "2,5,1,5", + "2,5,2,4", + "2,5,2,6", + "2,5,3,5" }) - void 사가_상하좌우로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { + void 사는_궁성_안에서_상하좌우로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { Position startPosition = new Position(startX, startY); Position endPosition = new Position(endX, endY); @@ -30,18 +30,55 @@ class SaTest { @ParameterizedTest @CsvSource({ - "2,3,2,5", - "2,3,2,1", - "2,3,1,6", - "2,3,3,1" + "2,5,1,4", + "2,5,1,6", + "2,5,3,4", + "2,5,3,6" }) - void 사가_상하좌우가_아닌_좌표로는_이동할_수_없다(int startX, int startY, int endX, int endY) { + void 사는_궁성_안에서_십자_위치를_제외한_곳에서_대각선으로_한_칸_이동할_수_있다(int startX, int startY, int endX, int endY) { Position startPosition = new Position(startX, startY); Position endPosition = new Position(endX, endY); Sa sa = new Sa(Side.CHO); - assertThatThrownBy(() -> sa.findRoute(startPosition, endPosition)) + Route actual = sa.findRoute(startPosition, endPosition); + + assertThat(actual.isDestinationSatisfied(position -> position.equals(endPosition))).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "1, 5, 2, 4", + "1, 5, 2, 6", + "2, 4, 1, 5", + "2, 6, 3, 5", + "9, 4, 8, 5", + "9, 6, 10, 5" + }) + void 사는_궁성_내_십자_위치에서의_대각선_이동이_있을_때_에러가_발생한다(int startX, int startY, int endX, int endY) { + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + Sa sa = new Sa(Side.CHO); + + Assertions.assertThatThrownBy(() -> sa.findRoute(start, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 도착 지점이 아닙니다."); + } + + @ParameterizedTest + @CsvSource({ + "8,4,8,3", + "8,6,8,7", + "8,5,7,5" + }) + void 사는_궁성_밖으로_이동이_있을_때_에러가_발생한다(int startX, int startY, int endX, int endY) { + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + Sa sa = new Sa(Side.CHO); + + Assertions.assertThatThrownBy(() -> sa.findRoute(start, end)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("올바른 도착 지점이 아닙니다."); }