diff --git a/.gitignore b/.gitignore index 6c01878138..eee9400608 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ HELP.md .gradle +.gradle-local/ build/ +data/ +*.mv.db +*.trace.db !gradle/wrapper/gradle-wrapper.jar !**/src/main/** !**/src/test/** diff --git a/README.md b/README.md index 8204a7762d..bf62093344 100644 --- a/README.md +++ b/README.md @@ -99,27 +99,53 @@ - [x] 기물 규칙 및 제약에 맞게 이동한다. - [차(車)] - [x] 상하좌우로 장애물을 만날 때까지 칸 수 제한 없이 이동한다. + - [x] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. - [x] 이동하려는 경로 중간에 다른 기물이 **있**다면 `IllegalArgumentException`을 발생시킨다. - [포(包)] - [x] 상하좌우로 이동하되, 반드시 중간에 다른 기물을 하나 뛰어넘어야 합니다. + - [x] 궁성 내부(진영 상관 X)에서 궁성의 대각선을 이용해 이동할 수 있다. + - 이때에도 경로 상에 반드시 기물이 있어야 한다. - [x] 이동하려는 경로 중간에 다른 기물이 **없**다면 `IllegalArgumentException`을 발생시킨다. - [x] 넘으려는 기물이 포(包) 일 경우 `IllegalArgumentException`을 발생시킨다. - [x] 목적지 좌표의 기물이 포(包)일 경우 `IllegalArgumentException`을 발생시킨다. - [마(馬)] - [x] 직선 1칸 + 대각선 1칸(日) 이동한다. + - 궁성 내부 대각선 이용 불가 - [x] 목적지 좌표(직선 1칸 또는 대각선 1칸)에 다른 기물이 있으면 `IllegalArgumentException`을 발생시킨다. - [상(象)] - [x] 직선 1칸 + 대각선 2칸(用) 이동한다. + - 궁성 내부 대각선 이용 불가 - [x] 목적지 좌표(직선 1칸 또는 대각선 1칸)에 다른 기물이 있으면 `IllegalArgumentException`을 발생시킨다. - - [궁(楚/漢), 사(士)] + - [궁(楚/漢)] - [x] 상하좌우 직선 1칸 이동한다. + - [x] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - [x] 상대 기물에게 잡힐 경우 게임이 종료되며 상대방이 게임을 승리한다. + + - [사(士)] + - [x] 상하좌우 직선 1칸 이동한다. + - [x] 아군 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. - [졸/병(卒/兵)] - [x] 상좌우 직선 1칸 이동한다. 후진은 불가 주의 + - [x] 상대 궁성 내부에서만 대각선으로 1칸 이동할 수 있다. + - 졸/병은 규칙에 의해 애초에 아군 궁성으로 이동조차 불가능하다. + +### 기물 별 점수 + +『초』 총점 : 72.0점 +『한』 총점 : 73.5점 (후수이기 때문에 점수 + 1.5) + +- [차(車)] : 13점 +- [포(包)] : 7점 +- [마(馬)] : 5점 +- [상(象)] : 3점 +- [사(士)] : 3점 +- [졸/병(卒/兵)] : 2점 +- [궁(楚/漢)] : 점수 없음 -> 죽는 순간 상대방 🥇게임 승리🥇 --- diff --git a/build.gradle b/build.gradle index ce846f70cc..672be9de70 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ repositories { } dependencies { + runtimeOnly('com.h2database:h2:2.4.240') 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/AppConfig.java b/src/main/java/janggi/AppConfig.java deleted file mode 100644 index 54fa745eda..0000000000 --- a/src/main/java/janggi/AppConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package janggi; - -import janggi.view.InputView; -import janggi.view.OutputView; -import java.util.Scanner; - -public class AppConfig { - public JanggiGame janggi() { - return new JanggiGame(inputView(), outputView()); - } - - public InputView inputView() { - return new InputView(new Scanner(System.in)); - } - - public OutputView outputView() { - return new OutputView(); - } -} diff --git a/src/main/java/janggi/GameRunner.java b/src/main/java/janggi/GameRunner.java new file mode 100644 index 0000000000..b2cff0b6b3 --- /dev/null +++ b/src/main/java/janggi/GameRunner.java @@ -0,0 +1,110 @@ +package janggi; + +import janggi.domain.Game; +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.board.initializer.BoardInitializer; +import janggi.domain.board.initializer.ElephantSetUp; +import janggi.domain.board.initializer.StandardBoardInitializer; +import janggi.domain.piece.Camp; +import janggi.service.GameService; +import janggi.view.InputView; +import janggi.view.OutputView; +import janggi.view.dto.CampDto; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class GameRunner { + private static final String INVALID_GAME_ROOM = "[ERROR] 존재하지 않는 게임방 번호입니다."; + + private final InputView inputView; + private final OutputView outputView; + private final GameService gameService; + + public GameRunner(InputView inputView, OutputView outputView, GameService gameService) { + this.inputView = inputView; + this.outputView = outputView; + this.gameService = gameService; + } + + public void run() { + Game game = retryOnInvalidInput(this::loadOrCreateGame); + outputView.printBoard(game.boardSnapshot()); + play(game); + } + + private Game loadOrCreateGame() { + outputView.printExistGameRoom(gameService.findAllIds()); + long gameId = inputView.readSelectedGameRoom(); + + if (gameId == 0L) { + return createNewGame(); + } + + return gameService.findById(gameId) + .orElseThrow(() -> new IllegalArgumentException(INVALID_GAME_ROOM)); + } + + private Game createNewGame() { + Board board = createBoard(); + return gameService.create(board); + } + + private Board createBoard() { + Map elephantSetUps = new HashMap<>(); + readElephantSetUp(elephantSetUps, Camp.HAN); + readElephantSetUp(elephantSetUps, Camp.CHO); + + BoardInitializer initializer = new StandardBoardInitializer(elephantSetUps); + return new Board(initializer); + } + + private void readElephantSetUp(Map elephantSetUps, Camp camp) { + ElephantSetUp elephantSetUp = retryOnInvalidInput( + () -> inputView.readElephantSetting(CampDto.from(camp)) + ); + elephantSetUps.put(camp, elephantSetUp); + } + + private void play(Game game) { + boolean continueGame = true; + while (continueGame) { + outputView.printScore(game.calculateScore()); + continueGame = retryOnInvalidInput(() -> playTurn(game)); + outputView.printBoard(game.boardSnapshot()); + } + outputView.printWinner(game.currentTurn()); + } + + private boolean playTurn(Game game) { + Camp currentTurn = game.currentTurn(); + + Position source = retryOnInvalidInput(() -> readSource(game, currentTurn)); + Position destination = retryOnInvalidInput(inputView::readDestination); + + boolean gameEnded = game.play(source, destination); + if (gameEnded) { + gameService.deleteById(game.id()); + return false; + } + gameService.save(game); + return true; + } + + private Position readSource(Game game, Camp currentTurn) { + Position source = inputView.readSource(CampDto.from(currentTurn)); + game.validateSourceForCurrentTurn(source); + return source; + } + + private T retryOnInvalidInput(Supplier input) { + while (true) { + try { + return input.get(); + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java index 1e3c5416ee..6fce6b67cd 100644 --- a/src/main/java/janggi/JanggiApplication.java +++ b/src/main/java/janggi/JanggiApplication.java @@ -1,9 +1,26 @@ package janggi; +import janggi.db.DatabaseInitializer; +import janggi.db.H2ConnectionManager; +import janggi.db.TransactionManager; +import janggi.repository.GamePieceRepository; +import janggi.repository.GameStateRepository; +import janggi.service.GameService; +import janggi.view.InputView; +import janggi.view.OutputView; +import java.util.Scanner; + public class JanggiApplication { public static void main(String[] args) { - AppConfig app = new AppConfig(); - JanggiGame janggi = app.janggi(); + H2ConnectionManager connectionManager = new H2ConnectionManager(); + new DatabaseInitializer(connectionManager).initialize(); + TransactionManager transactionManager = new TransactionManager(connectionManager); + + GameRunner janggi = new GameRunner( + new InputView(new Scanner(System.in)), + new OutputView(), + new GameService(transactionManager, new GameStateRepository(), new GamePieceRepository()) + ); janggi.run(); } } diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java deleted file mode 100644 index 04a12fda8f..0000000000 --- a/src/main/java/janggi/JanggiGame.java +++ /dev/null @@ -1,90 +0,0 @@ -package janggi; - -import janggi.domain.Position; -import janggi.domain.Turn; -import janggi.domain.board.Board; -import janggi.domain.board.initializer.BoardInitializer; -import janggi.domain.board.initializer.ElephantSetUp; -import janggi.domain.board.initializer.StandardBoardInitializer; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; -import janggi.domain.piece.Camp; -import janggi.view.InputView; -import janggi.view.OutputView; -import janggi.view.dto.CampDto; -import java.util.function.Supplier; - -public class JanggiGame { - - private final InputView inputView; - private final OutputView outputView; - - public JanggiGame(InputView inputView, OutputView outputView) { - this.inputView = inputView; - this.outputView = outputView; - } - - public void run() { - Board board = createBoard(); - outputView.printBoard(board.getBoard()); - play(board); - } - - private Board createBoard() { - ElephantSetUpDto hanElephantSetUp = readElephantSetUp(Camp.HAN); - ElephantSetUpDto choElephantSetUp = readElephantSetUp(Camp.CHO); - - BoardInitializer initializer - = new StandardBoardInitializer(hanElephantSetUp, choElephantSetUp); - return new Board(initializer); - } - - private ElephantSetUpDto readElephantSetUp(Camp camp) { - ElephantSetUp elephantSetUp = retryOnInvalidInput( - () -> inputView.readElephantSetting(CampDto.from(camp)) - ); - return new ElephantSetUpDto(camp, elephantSetUp); - } - - private void play(Board board) { - Turn turn = new Turn(); - while (true) { - retryOnInvalidInput(() -> playTurn(board, turn)); - outputView.printBoard(board.getBoard()); - } - } - - private void playTurn(Board board, Turn turn) { - Camp camp = turn.currentTurn(); - Position source = retryOnInvalidInput(() -> readSource(board, camp)); - Position destination = retryOnInvalidInput(inputView::readDestination); - board.movePiece(source, destination, camp); - turn.finishTurn(); - } - - private Position readSource(Board board, Camp camp) { - Position source = inputView.readSource(CampDto.from(camp)); - board.validateCampTurn(source, camp); - return source; - } - - private T retryOnInvalidInput(Supplier input) { - while (true) { - try { - return input.get(); - } catch (IllegalArgumentException e) { - outputView.printError(e.getMessage()); - } - } - } - - private void retryOnInvalidInput(Runnable input) { - while (true) { - try { - input.run(); - return; - } catch (IllegalArgumentException e) { - outputView.printError(e.getMessage()); - } - } - } -} diff --git a/src/main/java/janggi/db/ConnectionManager.java b/src/main/java/janggi/db/ConnectionManager.java new file mode 100644 index 0000000000..334ecda106 --- /dev/null +++ b/src/main/java/janggi/db/ConnectionManager.java @@ -0,0 +1,8 @@ +package janggi.db; + +import java.sql.Connection; + +public interface ConnectionManager { + + Connection createConnection(); +} diff --git a/src/main/java/janggi/db/DatabaseInitializer.java b/src/main/java/janggi/db/DatabaseInitializer.java new file mode 100644 index 0000000000..1a75072556 --- /dev/null +++ b/src/main/java/janggi/db/DatabaseInitializer.java @@ -0,0 +1,49 @@ +package janggi.db; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public final class DatabaseInitializer { + + private static final String SCHEMA_PATH = "/schema.sql"; + private static final String SCHEMA_LOAD_FAILED = "[ERROR] DB 스키마를 읽을 수 없습니다."; + private static final String SCHEMA_INIT_FAILED = "[ERROR] DB 스키마를 초기화할 수 없습니다."; + + private final ConnectionManager connectionManager; + + public DatabaseInitializer(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + public void initialize() { + try (Connection connection = connectionManager.createConnection()) { + String schema = loadSchema(); + for (String statement : schema.split(";")) { + String sql = statement.trim(); + if (sql.isEmpty()) { + continue; + } + try (Statement jdbcStatement = connection.createStatement()) { + jdbcStatement.execute(sql); + } + } + } catch (SQLException e) { + throw new IllegalStateException(SCHEMA_INIT_FAILED, e); + } + } + + private String loadSchema() { + try (InputStream inputStream = DatabaseInitializer.class.getResourceAsStream(SCHEMA_PATH)) { + if (inputStream == null) { + throw new IllegalStateException(SCHEMA_LOAD_FAILED); + } + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException(SCHEMA_LOAD_FAILED, e); + } + } +} diff --git a/src/main/java/janggi/db/H2ConnectionManager.java b/src/main/java/janggi/db/H2ConnectionManager.java new file mode 100644 index 0000000000..27576116a0 --- /dev/null +++ b/src/main/java/janggi/db/H2ConnectionManager.java @@ -0,0 +1,23 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public final class H2ConnectionManager implements ConnectionManager { + + private static final String URL = "jdbc:h2:file:./data/janggi"; + private static final String USER = "stark"; + private static final String PASSWORD = "stark123!"; + + private static final String UNABLE_TO_ACCESS_DATABASE = "[ERROR] H2 데이터베이스에 연결할 수 없습니다."; + + @Override + public Connection createConnection() { + try { + return DriverManager.getConnection(URL, USER, PASSWORD); + } catch (SQLException e) { + throw new IllegalStateException(UNABLE_TO_ACCESS_DATABASE); + } + } +} diff --git a/src/main/java/janggi/db/TransactionManager.java b/src/main/java/janggi/db/TransactionManager.java new file mode 100644 index 0000000000..7fc56d128b --- /dev/null +++ b/src/main/java/janggi/db/TransactionManager.java @@ -0,0 +1,58 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.SQLException; + +public final class TransactionManager { + + private final ConnectionManager connectionManager; + + public TransactionManager(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + public T readOnly(SqlFunction action) throws SQLException { + try (Connection connection = connectionManager.createConnection()) { + return action.apply(connection); + } + } + + public T inTransaction(SqlFunction action) throws SQLException { + try (Connection connection = connectionManager.createConnection()) { + connection.setAutoCommit(false); + + try { + T result = action.apply(connection); + connection.commit(); + return result; + } catch (SQLException e) { + connection.rollback(); + throw e; + } + } + } + + public void inTransaction(SqlConsumer action) throws SQLException { + try (Connection connection = connectionManager.createConnection()) { + connection.setAutoCommit(false); + + try { + action.accept(connection); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw e; + } + } + } + + @FunctionalInterface + public interface SqlFunction { + T apply(Connection connection) throws SQLException; + } + + @FunctionalInterface + public interface SqlConsumer { + void accept(Connection connection) throws SQLException; + } +} diff --git a/src/main/java/janggi/domain/Game.java b/src/main/java/janggi/domain/Game.java new file mode 100644 index 0000000000..40e9d549e0 --- /dev/null +++ b/src/main/java/janggi/domain/Game.java @@ -0,0 +1,55 @@ +package janggi.domain; + +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import java.util.Map; + +public final class Game { + private final long id; + private final Board board; + private Camp currentTurn; + + private Game(long id, Board board, Camp currentTurn) { + this.id = id; + this.board = board; + this.currentTurn = currentTurn; + } + + public long id() { + return id; + } + + public static Game start(long id, Board board) { + return new Game(id, board, Camp.CHO); + } + + public static Game restore(long id, Board board, Camp currentTurn) { + return new Game(id, board, currentTurn); + } + + public Camp currentTurn() { + return currentTurn; + } + + public Map boardSnapshot() { + return board.getBoard(); + } + + public Map calculateScore() { + return board.calculateScore(); + } + + public void validateSourceForCurrentTurn(Position source) { + board.validateCampTurn(source, currentTurn()); + } + + public boolean play(Position source, Position destination) { + boolean gameEnded = board.movePieceAndCheckGameEnd(source, destination, currentTurn()); + if (!gameEnded) { + currentTurn = currentTurn.next(); + } + return gameEnded; + } +} diff --git a/src/main/java/janggi/domain/Turn.java b/src/main/java/janggi/domain/Turn.java deleted file mode 100644 index 2b6c85c1aa..0000000000 --- a/src/main/java/janggi/domain/Turn.java +++ /dev/null @@ -1,15 +0,0 @@ -package janggi.domain; - -import janggi.domain.piece.Camp; - -public class Turn { - private Camp currentCamp = Camp.CHO; - - public Camp currentTurn() { - return currentCamp; - } - - public void finishTurn() { - currentCamp = currentCamp.next(); - } -} diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 54000940df..82d71fb0df 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; @@ -32,20 +31,23 @@ public boolean isSameCampPieceAt(Position position, Camp camp) { } @Override - public boolean hasSamePieceRuleAt(Position position, PieceType pieceType) { - if (board.containsKey(position)) { - Piece foundPiece = board.get(position); - return foundPiece.isSamePieceRule(pieceType); + public boolean hasSamePieceTypeAt(Position position, PieceType pieceType) { + if (!board.containsKey(position)) { + return false; } - return false; + Piece foundPiece = board.get(position); + return foundPiece.isSamePieceType(pieceType); } - public void movePiece(Position source, Position destination, Camp turn) { + public boolean movePieceAndCheckGameEnd(Position source, Position destination, Camp turn) { validateCampTurn(source, turn); - Piece piece = board.get(source); - piece.validateMove(source, destination, this); - board.put(destination, piece); + Piece movingPiece = board.get(source); + movingPiece.validateMove(source, destination, this); + + boolean isDestinationGeneral = isGeneralAt(destination); board.remove(source); + board.put(destination, movingPiece); + return isDestinationGeneral; } public void validateCampTurn(Position source, Camp turn) { @@ -56,13 +58,36 @@ public void validateCampTurn(Position source, Camp turn) { } } - private void validateSource(Position source) { - if (!board.containsKey(source)) { - throw new IllegalArgumentException(SOURCE_NOT_EXISTS); - } + public Map calculateScore() { + Map resultScore = new HashMap<>(); + + Camp.getAllCamp().forEach(camp -> { + double totalScore = board.values().stream() + .filter(piece -> piece.isSameCamp(camp)) + .mapToDouble(Piece::score) + .sum(); + + totalScore += camp.getBonusScoreForSecondPlayer(); + resultScore.put(camp, totalScore); + }); + + return resultScore; } public Map getBoard() { return Map.copyOf(board); } + + private boolean isGeneralAt(Position position) { + if (!board.containsKey(position)) { + return false; + } + return board.get(position).isGeneral(); + } + + private void validateSource(Position source) { + if (!board.containsKey(source)) { + throw new IllegalArgumentException(SOURCE_NOT_EXISTS); + } + } } diff --git a/src/main/java/janggi/domain/board/BoardChecker.java b/src/main/java/janggi/domain/board/BoardChecker.java index 23cb0dcd6e..b8631979f7 100644 --- a/src/main/java/janggi/domain/board/BoardChecker.java +++ b/src/main/java/janggi/domain/board/BoardChecker.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; @@ -10,5 +9,5 @@ public interface BoardChecker { boolean isSameCampPieceAt(Position position, Camp camp); - boolean hasSamePieceRuleAt(Position position, PieceType pieceType); + boolean hasSamePieceTypeAt(Position position, PieceType pieceType); } diff --git a/src/main/java/janggi/domain/board/Palace.java b/src/main/java/janggi/domain/board/Palace.java new file mode 100644 index 0000000000..c8cb39a29f --- /dev/null +++ b/src/main/java/janggi/domain/board/Palace.java @@ -0,0 +1,54 @@ +package janggi.domain.board; + +import janggi.domain.piece.Camp; +import java.util.Arrays; + +public final class Palace { + + private static final String INVALID_PALACE_MOVEMENT = "[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."; + private static final int PALACE_CENTER_ROW_DISTANCE = 1; + private static final int FRIENDLY_PALACE_ROW_RANGE = 2; + private static final int PALACE_START_COLUMN = 3; + private static final int PALACE_CENTER_COLUMN = 4; + private static final int PALACE_END_COLUMN = 5; + + private Palace() { + } + + public static void validateFriendlyPalace(Camp camp, Position position) { + if (!(isFriendlyPalaceRow(camp, position) && isPalaceColumn(position))) { + throw new IllegalArgumentException(INVALID_PALACE_MOVEMENT); + } + } + + private static boolean isFriendlyPalaceRow(Camp camp, Position position) { + int absRowDifference = Math.abs(position.row() - camp.getStartRowPosition()); + return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; + } + + public static boolean isPalace(Position position) { + return isPalaceRow(position) && isPalaceColumn(position); + } + + private static boolean isPalaceRow(Position position) { + return Arrays.stream(Camp.values()) + .anyMatch(camp -> { + int absRowDifference = Math.abs(position.row() - camp.getStartRowPosition()); + return absRowDifference <= FRIENDLY_PALACE_ROW_RANGE; + }); + } + + private static boolean isPalaceColumn(Position position) { + return position.column() >= PALACE_START_COLUMN + && position.column() <= PALACE_END_COLUMN; + } + + public static boolean isPalaceCenter(Position position) { + return Arrays.stream(Camp.values()) + .anyMatch(camp -> { + int absRowDifference = Math.abs(position.row() - camp.getStartRowPosition()); + return position.column() == PALACE_CENTER_COLUMN + && absRowDifference == PALACE_CENTER_ROW_DISTANCE; + }); + } +} diff --git a/src/main/java/janggi/domain/Position.java b/src/main/java/janggi/domain/board/Position.java similarity index 89% rename from src/main/java/janggi/domain/Position.java rename to src/main/java/janggi/domain/board/Position.java index 18a6312957..0cc94eb748 100644 --- a/src/main/java/janggi/domain/Position.java +++ b/src/main/java/janggi/domain/board/Position.java @@ -1,10 +1,10 @@ -package janggi.domain; +package janggi.domain.board; public record Position(int row, int column) { - public static final int MIN_POSITION_INDEX = 0; - public static final int MAX_ROW_INDEX = 9; - public static final int MAX_COLUMN_INDEX = 8; + private static final int MIN_POSITION_INDEX = 0; + private static final int MAX_ROW_INDEX = 9; + private static final int MAX_COLUMN_INDEX = 8; private static final String ROW_OUT_OF_RANGE = String.format( "[ERROR] 행은 %d행 이상 %d행 이하여야 합니다.", MIN_POSITION_INDEX, diff --git a/src/main/java/janggi/domain/board/initializer/BoardInitializer.java b/src/main/java/janggi/domain/board/initializer/BoardInitializer.java index 3cb0e323d0..3d36b58962 100644 --- a/src/main/java/janggi/domain/board/initializer/BoardInitializer.java +++ b/src/main/java/janggi/domain/board/initializer/BoardInitializer.java @@ -1,6 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Piece; import java.util.Map; diff --git a/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java b/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java index 127ada10b9..41321fa78f 100644 --- a/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java +++ b/src/main/java/janggi/domain/board/initializer/ElephantSetUp.java @@ -1,6 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; @@ -24,7 +24,7 @@ public enum ElephantSetUp { } public Map settingUp(Camp camp) { - List settingColumns = camp.convertElephantColumns(SETTING_COLUMNS); + List settingColumns = settingColumnsOf(camp); Map map = new HashMap<>(); for (int i = 0; i < settingColumns.size(); i++) { @@ -36,4 +36,11 @@ public Map settingUp(Camp camp) { return map; } + + private List settingColumnsOf(Camp camp) { + if (camp == Camp.CHO) { + return SETTING_COLUMNS.reversed(); + } + return SETTING_COLUMNS; + } } diff --git a/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java b/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java index 14be6c0ffb..7426f6c595 100644 --- a/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java +++ b/src/main/java/janggi/domain/board/initializer/InitialPiecePlacement.java @@ -1,7 +1,6 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; @@ -44,15 +43,12 @@ public enum InitialPiecePlacement { this.piece = new Piece(pieceType, camp); } - public static Map init(ElephantSetUpDto firstChoice, ElephantSetUpDto secondChoice) { + public static Map initialize() { Map board = new HashMap<>(); for (InitialPiecePlacement placement : values()) { board.put(placement.position, placement.piece); } - - board.putAll(firstChoice.settingUp()); - board.putAll(secondChoice.settingUp()); return board; } } diff --git a/src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java b/src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java new file mode 100644 index 0000000000..0ef0e8da4c --- /dev/null +++ b/src/main/java/janggi/domain/board/initializer/SnapshotBoardInitializer.java @@ -0,0 +1,20 @@ +package janggi.domain.board.initializer; + +import janggi.domain.board.Position; +import janggi.domain.piece.Piece; +import java.util.HashMap; +import java.util.Map; + +public final class SnapshotBoardInitializer implements BoardInitializer { + + private final Map snapshot; + + public SnapshotBoardInitializer(Map snapshot) { + this.snapshot = Map.copyOf(snapshot); + } + + @Override + public Map initialize() { + return new HashMap<>(snapshot); + } +} diff --git a/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java b/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java index 5f1d352edc..6aaa9ea689 100644 --- a/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java +++ b/src/main/java/janggi/domain/board/initializer/StandardBoardInitializer.java @@ -1,31 +1,32 @@ package janggi.domain.board.initializer; -import janggi.domain.Position; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; +import java.util.HashMap; import java.util.Map; public class StandardBoardInitializer implements BoardInitializer { private static final String INVALID_ELEPHANT_SETUP = "[ERROR] 진영별 상차림은 각각 하나씩만 존재해야 합니다."; - private final ElephantSetUpDto firstElephantSetUp; - private final ElephantSetUpDto secondElephantSetUp; + private final Map elephantSetUps; - public StandardBoardInitializer(ElephantSetUpDto firstElephantSetUp, ElephantSetUpDto secondElephantSetUp) { - validateDifferentCamp(firstElephantSetUp, secondElephantSetUp); - this.firstElephantSetUp = firstElephantSetUp; - this.secondElephantSetUp = secondElephantSetUp; - } - - private void validateDifferentCamp(ElephantSetUpDto firstElephantSetUp, ElephantSetUpDto secondElephantSetUp) { - if (firstElephantSetUp.camp() == secondElephantSetUp.camp()) { - throw new IllegalArgumentException(INVALID_ELEPHANT_SETUP); - } + public StandardBoardInitializer(Map elephantSetUps) { + validate(elephantSetUps); + this.elephantSetUps = Map.copyOf(elephantSetUps); } @Override public Map initialize() { - return InitialPiecePlacement.init(firstElephantSetUp, secondElephantSetUp); + Map board = new HashMap<>(InitialPiecePlacement.initialize()); + elephantSetUps.forEach((camp, elephantSetUp) -> board.putAll(elephantSetUp.settingUp(camp))); + return board; + } + + private void validate(Map elephantSetUps) { + if (elephantSetUps.size() != Camp.getAllCamp().size()) { + throw new IllegalArgumentException(INVALID_ELEPHANT_SETUP); + } } } diff --git a/src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java b/src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java deleted file mode 100644 index 9c0d7153af..0000000000 --- a/src/main/java/janggi/domain/board/initializer/dto/ElephantSetUpDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package janggi.domain.board.initializer.dto; - -import janggi.domain.Position; -import janggi.domain.board.initializer.ElephantSetUp; -import janggi.domain.piece.Camp; -import janggi.domain.piece.Piece; -import java.util.Map; - -public record ElephantSetUpDto(Camp camp, ElephantSetUp elephantSetUp) { - - public Map settingUp() { - return elephantSetUp.settingUp(camp); - } -} diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java index 3b3277d0d0..4927ea0c24 100644 --- a/src/main/java/janggi/domain/piece/Camp.java +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -1,26 +1,17 @@ package janggi.domain.piece; +import java.util.Arrays; import java.util.List; public enum Camp { - HAN(-1, 9) { - @Override - public List convertElephantColumns(List columns) { - return columns; - } - + HAN(-1, 9, 1.5) { @Override public Camp next() { return CHO; } }, - CHO(1, 0) { - @Override - public List convertElephantColumns(List columns) { - return columns.reversed(); - } - + CHO(1, 0, 0) { @Override public Camp next() { return HAN; @@ -30,14 +21,14 @@ public Camp next() { private final int forwardDirection; private final int startRowPosition; + private final double bonusScoreForSecondPlayer; - Camp(int forwardDirection, int startRowPosition) { + Camp(int forwardDirection, int startRowPosition, double bonusScoreForSecondPlayer) { this.forwardDirection = forwardDirection; this.startRowPosition = startRowPosition; + this.bonusScoreForSecondPlayer = bonusScoreForSecondPlayer; } - public abstract List convertElephantColumns(List columns); - public abstract Camp next(); public void validateForwardDirection(int rowDirection) { @@ -49,4 +40,13 @@ public void validateForwardDirection(int rowDirection) { public int getStartRowPosition() { return startRowPosition; } + + public double getBonusScoreForSecondPlayer() { + return bonusScoreForSecondPlayer; + } + + public static List getAllCamp() { + return Arrays.stream(values()) + .toList(); + } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index 4c8dc9c941..bbbe5738a5 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -1,7 +1,7 @@ package janggi.domain.piece; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import java.util.List; public record Piece(PieceType pieceType, Camp camp) { @@ -11,11 +11,19 @@ public void validateMove(Position source, Position destination, BoardChecker boa pieceType.checkPath(path, camp, board); } - public boolean isSamePieceRule(PieceType pieceType) { + public boolean isSamePieceType(PieceType pieceType) { return this.pieceType == pieceType; } public boolean isSameCamp(Camp camp) { return this.camp == camp; } + + public boolean isGeneral() { + return pieceType == PieceType.GENERAL; + } + + public double score() { + return pieceType().score(); + } } diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java index 1d2924a0d7..8841244a00 100644 --- a/src/main/java/janggi/domain/piece/PieceType.java +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -1,34 +1,36 @@ package janggi.domain.piece; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.condition.EmptyCondition; import janggi.domain.piece.condition.MoveCondition; import janggi.domain.piece.condition.OnePieceExistsCondition; import janggi.domain.piece.strategy.ElephantStrategy; +import janggi.domain.piece.strategy.FriendlyPalaceSingleStepStrategy; import janggi.domain.piece.strategy.HorseStrategy; import janggi.domain.piece.strategy.MoveStrategy; import janggi.domain.piece.strategy.MultiStepStraightStrategy; -import janggi.domain.piece.strategy.SingleStepStraightStrategy; import janggi.domain.piece.strategy.SoldierStrategy; import java.util.List; public enum PieceType { - GENERAL(new SingleStepStraightStrategy(), new EmptyCondition()), - CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition()), - HORSE(new HorseStrategy(), new EmptyCondition()), - CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition()), - GUARD(new SingleStepStraightStrategy(), new EmptyCondition()), - ELEPHANT(new ElephantStrategy(), new EmptyCondition()), - SOLDIER(new SoldierStrategy(), new EmptyCondition()); + GENERAL(new FriendlyPalaceSingleStepStrategy(), new EmptyCondition(), 0.0), + CHARIOT(new MultiStepStraightStrategy(), new EmptyCondition(), 13.0), + CANNON(new MultiStepStraightStrategy(), new OnePieceExistsCondition(), 7.0), + HORSE(new HorseStrategy(), new EmptyCondition(), 5.0), + ELEPHANT(new ElephantStrategy(), new EmptyCondition(), 3.0), + GUARD(new FriendlyPalaceSingleStepStrategy(), new EmptyCondition(), 3.0), + SOLDIER(new SoldierStrategy(), new EmptyCondition(), 2.0); private final MoveStrategy moveStrategy; private final MoveCondition moveCondition; + private final double score; - PieceType(MoveStrategy moveStrategy, MoveCondition moveCondition) { + PieceType(MoveStrategy moveStrategy, MoveCondition moveCondition, double score) { this.moveStrategy = moveStrategy; this.moveCondition = moveCondition; + this.score = score; } public List findPath(Position source, Position destination, Camp camp) { @@ -38,4 +40,8 @@ public List findPath(Position source, Position destination, Camp camp) public void checkPath(List path, Camp camp, BoardChecker board) { moveCondition.checkPath(path, camp, board, this); } + + public double score() { + return score; + } } diff --git a/src/main/java/janggi/domain/piece/condition/EmptyCondition.java b/src/main/java/janggi/domain/piece/condition/EmptyCondition.java index f793e30669..0ab07e21bd 100644 --- a/src/main/java/janggi/domain/piece/condition/EmptyCondition.java +++ b/src/main/java/janggi/domain/piece/condition/EmptyCondition.java @@ -1,7 +1,7 @@ package janggi.domain.piece.condition; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; import java.util.List; @@ -9,7 +9,7 @@ public class EmptyCondition implements MoveCondition { private static final String PATH_NOT_EMPTY = "[ERROR] 경로 상에 기물이 존재합니다."; - public static final String SAME_CAMP_PIECE_AT_DESTINATION = "[ERROR] 목적지에 같은 진영의 기물이 존재합니다."; + private static final String SAME_CAMP_PIECE_AT_DESTINATION = "[ERROR] 목적지에 같은 진영의 기물이 존재합니다."; @Override public void checkPath(List path, Camp camp, BoardChecker board, PieceType pieceType) { diff --git a/src/main/java/janggi/domain/piece/condition/MoveCondition.java b/src/main/java/janggi/domain/piece/condition/MoveCondition.java index e73fc05e54..7992e08973 100644 --- a/src/main/java/janggi/domain/piece/condition/MoveCondition.java +++ b/src/main/java/janggi/domain/piece/condition/MoveCondition.java @@ -1,7 +1,7 @@ package janggi.domain.piece.condition; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java index 12b010112a..3176c67675 100644 --- a/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java +++ b/src/main/java/janggi/domain/piece/condition/OnePieceExistsCondition.java @@ -1,14 +1,14 @@ package janggi.domain.piece.condition; -import janggi.domain.Position; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; import java.util.List; public class OnePieceExistsCondition implements MoveCondition { - public static final int PASS_PIECE_COUNT = 1; + private static final int PASS_PIECE_COUNT = 1; private static final String SAME_PIECE_TYPE_IN_PATH = "[ERROR] 경로상에 같은 종류의 기물이 존재합니다."; private static final String INVALID_JUMPED_PIECE_COUNT = String.format( "[ERROR] 해당 기물은 정확히 %d개의 기물만 뛰어넘을 수 있습니다.", @@ -38,7 +38,7 @@ private int countPieceAt(BoardChecker board, PieceType pieceType, Position posit } private void validateSamePieceRule(BoardChecker board, PieceType pieceType, Position position) { - if (board.hasSamePieceRuleAt(position, pieceType)) { + if (board.hasSamePieceTypeAt(position, pieceType)) { throw new IllegalArgumentException(SAME_PIECE_TYPE_IN_PATH); } } @@ -54,7 +54,7 @@ private void validateDestination(Position destination, Camp camp, BoardChecker b throw new IllegalArgumentException(SAME_CAMP_PIECE_AT_DESTINATION); } - if (board.hasSamePieceRuleAt(destination, pieceType)) { + if (board.hasSamePieceTypeAt(destination, pieceType)) { throw new IllegalArgumentException(SAME_PIECE_TYPE_AT_DESTINATION); } } diff --git a/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java b/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java index d6678151fa..9d65cd2fc8 100644 --- a/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java +++ b/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; public record DirectionInformation(int rowDifference, int columnDifference) { diff --git a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java index 375cb8ab26..4bd934afd8 100644 --- a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; @@ -10,8 +10,8 @@ public class ElephantStrategy implements MoveStrategy { private static final int DIAGONAL_COUNT = 2; private static final int MIN_ABS_DELTA = 2; private static final int MAX_ABS_DELTA = 3; - public static final int ELEPHANT_STRAIGHT_MOVE_DISTANCE = 1; - public static final int ELEPHANT_DIAGONAL_MOVE_DISTANCE = 2; + private static final int ELEPHANT_STRAIGHT_MOVE_DISTANCE = 1; + private static final int ELEPHANT_DIAGONAL_MOVE_DISTANCE = 2; private static final String INVALID_ELEPHANT_MOVE = String.format( "[ERROR] 해당 기물은 직선 %d칸 이동 후 대각선 %d칸 이동만 가능합니다.", ELEPHANT_STRAIGHT_MOVE_DISTANCE, diff --git a/src/main/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategy.java b/src/main/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategy.java new file mode 100644 index 0000000000..6a0a3b93d6 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategy.java @@ -0,0 +1,16 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.board.Palace; +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; +import java.util.List; + +public class FriendlyPalaceSingleStepStrategy extends SingleStepStraightStrategy { + + @Override + public List findPath(Position source, Position destination, Camp camp) { + Palace.validateFriendlyPalace(camp, destination); + + return super.findPath(source, destination, camp); + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java index 291ed56ddc..bdfb867f9c 100644 --- a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; @@ -9,8 +9,8 @@ public class HorseStrategy implements MoveStrategy { private static final int MIN_ABS_DELTA = 1; private static final int MAX_ABS_DELTA = 2; - public static final int HORSE_STRAIGHT_MOVE_DISTANCE = 1; - public static final int HORSE_DIAGONAL_MOVE_DISTANCE = 1; + private static final int HORSE_STRAIGHT_MOVE_DISTANCE = 1; + private static final int HORSE_DIAGONAL_MOVE_DISTANCE = 1; private static final String INVALID_HORSE_MOVE = String.format( "[ERROR] 해당 기물은 직선 %d칸 이동 후 대각선 %d칸 이동만 가능합니다.", HORSE_STRAIGHT_MOVE_DISTANCE, @@ -34,11 +34,11 @@ private List createRowFirstPath(Position source, DirectionInformation int rowDirection = directionInformation.calculateRowDirection(); int columnDirection = directionInformation.calculateColumnDirection(); - source = source.moveRow(rowDirection); - path.add(source); + Position current = source.moveRow(rowDirection); + path.add(current); - source = source.moveDiagonal(rowDirection, columnDirection); - path.add(source); + current = current.moveDiagonal(rowDirection, columnDirection); + path.add(current); return path; } @@ -47,11 +47,11 @@ private List createColumnFirstPath(Position source, DirectionInformati int rowDirection = directionInformation.calculateRowDirection(); int columnDirection = directionInformation.calculateColumnDirection(); - source = source.moveColumn(columnDirection); - path.add(source); + Position current = source.moveColumn(columnDirection); + path.add(current); - source = source.moveDiagonal(rowDirection, columnDirection); - path.add(source); + current = current.moveDiagonal(rowDirection, columnDirection); + path.add(current); return path; } diff --git a/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java b/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java index 17e88a8b69..95d81443d9 100644 --- a/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/MoveStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; diff --git a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java index ea69eb7ad4..d9ac5fcf74 100644 --- a/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/MultiStepStraightStrategy.java @@ -1,6 +1,7 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Palace; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.ArrayList; import java.util.List; @@ -9,17 +10,61 @@ public class MultiStepStraightStrategy implements MoveStrategy { private static final String ONLY_STRAIGHT_MOVE_ALLOWED = "[ERROR] 해당 기물은 직선 이동만 가능합니다."; private static final String PIECE_MUST_MOVE = "[ERROR] 기물은 반드시 이동해야 합니다."; + private static final String INVALID_PALACE_DIAGONAL_STEP_MOVE = + "[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."; @Override public List findPath(Position source, Position destination, Camp camp) { DirectionInformation directionInformation = new DirectionInformation(source, destination); + validatePieceMoved(directionInformation); + + if (isPalace(source, destination) && isDiagonalStep(directionInformation)) { + return createPalaceDiagonalPath(source, directionInformation); + } validateStraightMove(directionInformation); + return createStraightPath(source, directionInformation); + } - if (directionInformation.isRowBiggerThanColumn()) { - return createRowPath(source, directionInformation.rowDifference()); + private boolean isPalace(Position source, Position destination) { + return Palace.isPalace(source) && Palace.isPalace(destination); + } + + private boolean isDiagonalStep(DirectionInformation directionInformation) { + int absRowDifference = directionInformation.calculateAbsRowDifference(); + int absColumnDifference = directionInformation.calculateAbsColumnDifference(); + + return absRowDifference == absColumnDifference; + } + + private List createPalaceDiagonalPath(Position source, DirectionInformation directionInformation) { + List path = createDiagonalPath(source, directionInformation); + validatePalaceDiagonalPath(source, path); + return path; + } + + private List createDiagonalPath(Position source, DirectionInformation directionInformation) { + List path = new ArrayList<>(); + + Position current = source; + int rowDirection = directionInformation.calculateRowDirection(); + int columnDirection = directionInformation.calculateColumnDirection(); + int stepCount = directionInformation.calculateAbsRowDifference(); + + for (int i = 0; i < stepCount; i++) { + current = current.moveDiagonal(rowDirection, columnDirection); + path.add(current); + } + return path; + } + + private void validatePalaceDiagonalPath(Position source, List path) { + boolean passesPalaceCenter = + Palace.isPalaceCenter(source) || path.stream().anyMatch(Palace::isPalaceCenter); + + if (!passesPalaceCenter) { + throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); } - return createColumnPath(source, directionInformation.columnDifference()); } private void validateStraightMove(DirectionInformation directionInformation) { @@ -30,19 +75,31 @@ private void validateStraightMove(DirectionInformation directionInformation) { if (sum != rowDifference && sum != columnDifference) { throw new IllegalArgumentException(ONLY_STRAIGHT_MOVE_ALLOWED); } + } + private void validatePieceMoved(DirectionInformation directionInformation) { + int rowDifference = directionInformation.rowDifference(); + int columnDifference = directionInformation.columnDifference(); if (rowDifference == 0 && columnDifference == 0) { throw new IllegalArgumentException(PIECE_MUST_MOVE); } } + private List createStraightPath(Position source, DirectionInformation directionInformation) { + if (directionInformation.rowDifference() != 0) { + return createRowPath(source, directionInformation.rowDifference()); + } + return createColumnPath(source, directionInformation.columnDifference()); + } + private List createRowPath(Position source, int rowDifference) { List path = new ArrayList<>(); + Position current = source; int rowDirection = rowDifference / Math.abs(rowDifference); while (rowDifference != 0) { - source = source.moveRow(rowDirection); - path.add(source); + current = current.moveRow(rowDirection); + path.add(current); rowDifference -= rowDirection; } return path; @@ -51,10 +108,11 @@ private List createRowPath(Position source, int rowDifference) { private List createColumnPath(Position source, int columnDifference) { List path = new ArrayList<>(); + Position current = source; int columnDirection = columnDifference / Math.abs(columnDifference); while (columnDifference != 0) { - source = source.moveColumn(columnDirection); - path.add(source); + current = current.moveColumn(columnDirection); + path.add(current); columnDifference -= columnDirection; } return path; diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java index 73010f66fc..43b0bf8d85 100644 --- a/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepStraightStrategy.java @@ -1,28 +1,71 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Palace; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; -public class SingleStepStraightStrategy implements MoveStrategy { +public abstract class SingleStepStraightStrategy implements MoveStrategy { - public static final int SINGLE_STEP_DISTANCE = 1; + private static final int SINGLE_STEP_DISTANCE = 1; private static final String INVALID_SINGLE_STEP_STRAIGHT_MOVE = String.format( "[ERROR] 해당 기물은 직선으로 %d칸 이동해야 합니다.", SINGLE_STEP_DISTANCE ); + private static final String INVALID_PALACE_SINGLE_STEP_MOVE = String.format( + "[ERROR] 해당 기물은 궁성 내에서 연결된 %d칸만 이동할 수 있습니다.", + SINGLE_STEP_DISTANCE + ); + private static final String INVALID_PALACE_DIAGONAL_STEP_MOVE = + "[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."; @Override public List findPath(Position source, Position destination, Camp camp) { DirectionInformation directionInformation = new DirectionInformation(source, destination); - validateSingleStepMovement(directionInformation); + if (isPalace(source, destination)) { + validatePalaceSingleStepMovement(source, destination, directionInformation); + return List.of(destination); + } + + validateSingleStepMovement(directionInformation); return List.of(destination); } + private boolean isPalace(Position source, Position destination) { + return Palace.isPalace(source) && Palace.isPalace(destination); + } + + private void validatePalaceSingleStepMovement(Position source, Position destination, + DirectionInformation directionInformation) { + if (isSingleDiagonalStep(directionInformation)) { + validateDiagonalMove(source, destination); + } + if (!isSingleDiagonalStep(directionInformation) && !isSingleStep(directionInformation)) { + throw new IllegalArgumentException(INVALID_PALACE_SINGLE_STEP_MOVE); + } + } + + private boolean isSingleDiagonalStep(DirectionInformation directionInformation) { + int absRowDifference = directionInformation.calculateAbsRowDifference(); + int absColumnDifference = directionInformation.calculateAbsColumnDifference(); + + return absRowDifference == 1 && absColumnDifference == 1; + } + + private void validateDiagonalMove(Position source, Position destination) { + if (!Palace.isPalaceCenter(source) && !Palace.isPalaceCenter(destination)) { + throw new IllegalArgumentException(INVALID_PALACE_DIAGONAL_STEP_MOVE); + } + } + + private boolean isSingleStep(DirectionInformation directionInformation) { + return directionInformation.calculateAbsRowDifference() + + directionInformation.calculateAbsColumnDifference() == SINGLE_STEP_DISTANCE; + } + private void validateSingleStepMovement(DirectionInformation directionInformation) { - if (directionInformation.calculateAbsRowDifference() - + directionInformation.calculateAbsColumnDifference() != SINGLE_STEP_DISTANCE) { + if (!isSingleStep(directionInformation)) { throw new IllegalArgumentException(INVALID_SINGLE_STEP_STRAIGHT_MOVE); } } diff --git a/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java b/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java index f8f901eb7f..6e3a414f45 100644 --- a/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java @@ -1,6 +1,6 @@ package janggi.domain.piece.strategy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; diff --git a/src/main/java/janggi/repository/GamePieceRepository.java b/src/main/java/janggi/repository/GamePieceRepository.java new file mode 100644 index 0000000000..a96a8cea96 --- /dev/null +++ b/src/main/java/janggi/repository/GamePieceRepository.java @@ -0,0 +1,89 @@ +package janggi.repository; + +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +public final class GamePieceRepository { + + private static final String SELECT_GAME_PIECES = """ + select row_position, column_position, piece_type, camp + from game_piece + where game_id = ? + """; + private static final String DELETE_GAME_PIECES = """ + delete from game_piece + where game_id = ? + """; + private static final String INSERT_GAME_PIECE = """ + insert into game_piece (game_id, row_position, column_position, piece_type, camp) + values (?, ?, ?, ?, ?) + """; + + public Map findByGameId(Connection connection, long gameId) throws SQLException { + Map boardSnapshot = new HashMap<>(); + PreparedStatement statement = connection.prepareStatement(SELECT_GAME_PIECES); + statement.setLong(1, gameId); + + try (statement; ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + boardSnapshot.put(toPosition(resultSet), toPiece(resultSet)); + } + } + return boardSnapshot; + } + + public void saveGameByBoard(Connection connection, long gameId, Map boardSnapshot) + throws SQLException { + deleteByGameId(connection, gameId); + + if (boardSnapshot.isEmpty()) { + return; + } + + try (PreparedStatement statement = connection.prepareStatement(INSERT_GAME_PIECE)) { + for (Map.Entry entry : boardSnapshot.entrySet()) { + setPieceStatement(statement, gameId, entry.getKey(), entry.getValue()); + } + statement.executeBatch(); + } + } + + public void deleteByGameId(Connection connection, long gameId) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(DELETE_GAME_PIECES)) { + statement.setLong(1, gameId); + statement.executeUpdate(); + } + } + + private Position toPosition(ResultSet resultSet) throws SQLException { + return new Position( + resultSet.getInt("row_position"), + resultSet.getInt("column_position") + ); + } + + private Piece toPiece(ResultSet resultSet) throws SQLException { + return new Piece( + PieceType.valueOf(resultSet.getString("piece_type")), + Camp.valueOf(resultSet.getString("camp")) + ); + } + + private void setPieceStatement(PreparedStatement statement, long gameId, Position position, Piece piece) + throws SQLException { + statement.setLong(1, gameId); + statement.setInt(2, position.row()); + statement.setInt(3, position.column()); + statement.setString(4, piece.pieceType().name()); + statement.setString(5, piece.camp().name()); + statement.addBatch(); + } +} diff --git a/src/main/java/janggi/repository/GameStateRepository.java b/src/main/java/janggi/repository/GameStateRepository.java new file mode 100644 index 0000000000..0328db378a --- /dev/null +++ b/src/main/java/janggi/repository/GameStateRepository.java @@ -0,0 +1,131 @@ +package janggi.repository; + +import janggi.domain.piece.Camp; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public final class GameStateRepository { + + private static final String SELECT_CURRENT_TURN = """ + select current_turn + from game_state + where game_id = ? + """; + private static final String SELECT_ALL_GAME_ID = """ + select game_id + from game_state + order by game_id asc + """; + private static final String UPDATE_GAME_STATE = """ + update game_state + set current_turn = ? + where game_id = ? + """; + private static final String INSERT_GAME_STATE = """ + insert into game_state (game_id, current_turn) + values (?, ?) + """; + private static final String CREATE_NEW_GAME_STATE = """ + insert into game_state (current_turn) + values (?) + """; + private static final String DELETE_GAME_STATE = """ + delete from game_state + where game_id = ? + """; + private static final String CANNOT_FIND_GAME = "[ERROR] 생성된 게임방 번호를 가져올 수 없습니다."; + + public List findAllIds(Connection connection) throws SQLException { + PreparedStatement statement = connection.prepareStatement(SELECT_ALL_GAME_ID); + + try (statement; ResultSet resultSet = statement.executeQuery()) { + List gameIds = new ArrayList<>(); + findAllGameIds(resultSet, gameIds); + + return gameIds; + } + } + + private void findAllGameIds(ResultSet resultSet, List gameIds) throws SQLException { + while (resultSet.next()) { + gameIds.add(resultSet.getLong("game_id")); + } + } + + public Optional findCurrentTurnByGameId(Connection connection, long gameId) throws SQLException { + PreparedStatement statement = connection.prepareStatement(SELECT_CURRENT_TURN); + statement.setLong(1, gameId); + + try (statement; ResultSet resultSet = statement.executeQuery()) { + if (!resultSet.next()) { + return Optional.empty(); + } + + return Optional.of(Camp.valueOf(resultSet.getString("current_turn"))); + } + } + + public void save(Connection connection, long gameId, Camp currentTurn) throws SQLException { + if (update(connection, gameId, currentTurn) > 0) { + return; + } + insert(connection, gameId, currentTurn); + } + + public long createGame(Connection connection, Camp currentTurn) throws SQLException { + PreparedStatement statement = connection.prepareStatement(CREATE_NEW_GAME_STATE, + Statement.RETURN_GENERATED_KEYS); + statement.setString(1, currentTurn.name()); + + try (statement) { + statement.executeUpdate(); + return getGeneratedKey(statement); + } + } + + private long getGeneratedKey(PreparedStatement statement) throws SQLException { + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + validateKeys(generatedKeys); + return generatedKeys.getLong(1); + } + } + + private void validateKeys(ResultSet generatedKeys) throws SQLException { + if (!generatedKeys.next()) { + throw new IllegalStateException(CANNOT_FIND_GAME); + } + } + + public void deleteById(Connection connection, long gameId) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(DELETE_GAME_STATE)) { + statement.setLong(1, gameId); + statement.executeUpdate(); + } + } + + private int update(Connection connection, long gameId, Camp currentTurn) throws SQLException { + PreparedStatement statement = connection.prepareStatement(UPDATE_GAME_STATE); + statement.setString(1, currentTurn.name()); + statement.setLong(2, gameId); + + try (statement) { + return statement.executeUpdate(); + } + } + + private void insert(Connection connection, long gameId, Camp currentTurn) throws SQLException { + PreparedStatement statement = connection.prepareStatement(INSERT_GAME_STATE); + statement.setLong(1, gameId); + statement.setString(2, currentTurn.name()); + + try (statement) { + statement.executeUpdate(); + } + } +} diff --git a/src/main/java/janggi/service/GameService.java b/src/main/java/janggi/service/GameService.java new file mode 100644 index 0000000000..4ca43b7061 --- /dev/null +++ b/src/main/java/janggi/service/GameService.java @@ -0,0 +1,110 @@ +package janggi.service; + +import janggi.db.TransactionManager; +import janggi.db.TransactionManager.SqlConsumer; +import janggi.db.TransactionManager.SqlFunction; +import janggi.domain.Game; +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.board.initializer.SnapshotBoardInitializer; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.repository.GamePieceRepository; +import janggi.repository.GameStateRepository; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public final class GameService { + + private static final String GAME_ACCESS_FAILED = "[ERROR] 게임 상태를 DB에서 처리하는 중 문제가 발생했습니다."; + + private final TransactionManager transactionManager; + private final GameStateRepository gameStateRepository; + private final GamePieceRepository gamePieceRepository; + + public GameService( + TransactionManager transactionManager, + GameStateRepository gameStateRepository, + GamePieceRepository gamePieceRepository + ) { + this.transactionManager = transactionManager; + this.gameStateRepository = gameStateRepository; + this.gamePieceRepository = gamePieceRepository; + } + + public List findAllIds() { + return readOnly(gameStateRepository::findAllIds); + } + + public Optional findById(long gameId) { + return readOnly(connection -> { + Optional currentTurn = gameStateRepository.findCurrentTurnByGameId(connection, gameId); + if (currentTurn.isEmpty()) { + return Optional.empty(); + } + + Map boardSnapshot = gamePieceRepository.findByGameId(connection, gameId); + Board board = new Board(new SnapshotBoardInitializer(boardSnapshot)); + + return Optional.of(Game.restore(gameId, board, currentTurn.orElseThrow())); + }); + } + + public Game create(Board board) { + return inTransaction(connection -> { + long gameId = gameStateRepository.createGame(connection, Camp.CHO); + Game game = Game.start(gameId, board); + + gamePieceRepository.saveGameByBoard( + connection, + gameId, + game.boardSnapshot() + ); + return game; + }); + } + + public void save(Game game) { + inTransaction(connection -> { + gameStateRepository.save(connection, game.id(), game.currentTurn()); + gamePieceRepository.saveGameByBoard( + connection, + game.id(), + game.boardSnapshot() + ); + }); + } + + public void deleteById(long gameId) { + inTransaction(connection -> { + gamePieceRepository.deleteByGameId(connection, gameId); + gameStateRepository.deleteById(connection, gameId); + }); + } + + private T readOnly(SqlFunction action) { + try { + return transactionManager.readOnly(action); + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } + + private T inTransaction(SqlFunction action) { + try { + return transactionManager.inTransaction(action); + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } + + private void inTransaction(SqlConsumer action) { + try { + transactionManager.inTransaction(action); + } catch (SQLException e) { + throw new IllegalStateException(GAME_ACCESS_FAILED); + } + } +} diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index ac7c7bea9f..5b06633f79 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -1,8 +1,7 @@ package janggi.view; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.board.initializer.ElephantSetUp; -import janggi.util.Parser; import janggi.view.dto.CampDto; import janggi.view.format.ElephantSetUpFormat; import java.util.List; @@ -13,6 +12,7 @@ public final class InputView { private static final String INVALID_INPUT_FORMAT = "[ERROR] 잘못된 입력 형식입니다."; private static final String DELIMITER = ","; private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String RESET = "\u001B[0m"; private static final String ELEPHANT_SETTING = """ @@ -22,6 +22,7 @@ public final class InputView { private static final String TURN = LINE_SEPARATOR + "%s나라 차례 입니다."; private static final String SOURCE = "공격할 기물의 좌표를 행,열 순으로 입력해 주세요. (예: 9,8)"; private static final String DESTINATION = LINE_SEPARATOR + "이동 시킬 목적지의 좌표를 행,열 순으로 입력해 주세요. (예: 2,0)"; + private static final String SELECT_GAME = "입장할 게임방 번호를 입력하세요. (새 게임 생성: 0)"; private final Scanner scanner; @@ -30,11 +31,11 @@ public InputView(Scanner scanner) { } public ElephantSetUp readElephantSetting(CampDto campDto) { - System.out.println(String.format( - ELEPHANT_SETTING, - campDto.name(), - ElephantSetUpFormat.outputMessage()) - ); + String campName = campDto.color() + campDto.name() + RESET; + System.out.printf( + (ELEPHANT_SETTING) + "%n", + campName, + ElephantSetUpFormat.outputMessage()); return ElephantSetUpFormat.from(readLine()).toElephantSetting(); } @@ -51,7 +52,8 @@ private void validateInput(String input) { } public Position readSource(CampDto campDto) { - System.out.println(String.format(TURN, campDto.name())); + String campName = campDto.color() + campDto.name() + RESET; + System.out.printf((TURN) + "%n", campName); System.out.println(SOURCE); return toPosition(Parser.parseByDelimiter(DELIMITER, readLine())); } @@ -71,4 +73,9 @@ private void validatePositionSize(List rawPosition) { throw new IllegalArgumentException(INVALID_INPUT_FORMAT); } } + + public long readSelectedGameRoom() { + System.out.println(SELECT_GAME); + return Parser.parseToLong(readLine()); + } } diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index 3b1c28920a..59c6ad7a6a 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -2,7 +2,8 @@ import static java.util.stream.Collectors.joining; -import janggi.domain.Position; +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.view.dto.CampDto; import janggi.view.dto.PiecePositionDto; @@ -26,12 +27,22 @@ public final class OutputView { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + private static final String SCORE = "%s나라 점수: %.1f"; + private static final String WINNER = "%s나라가 승리하였습니다!! 축하드립니다!!"; + private static final String GAME_ROOM = "현재 게임방: "; + private static final String EMPTY_GAME = "현재 게임방이 존재하지 않습니다."; + public void printError(String errorMessage) { System.out.println(errorMessage); } public void printBoard(Map boardState) { - System.out.println(renderBoard(toPiecePositions(boardState))); + System.out.println(renderBoard(toPiecePositions(boardState)) + LINE_SEPARATOR); + } + + public void printWinner(Camp camp) { + String winnerName = toCampName(camp); + System.out.printf((WINNER) + "%n", winnerName); } private List toPiecePositions(Map boardState) { @@ -84,4 +95,24 @@ private String colorize(PiecePositionDto piecePosition) { private String fullWidthNumber(int number) { return FULL_WIDTH_NUMBERS[number]; } + + public void printScore(Map eachCampScore) { + for (Camp camp : eachCampScore.keySet()) { + String campName = toCampName(camp); + System.out.printf(SCORE + "%n", campName, eachCampScore.get(camp)); + } + } + + private String toCampName(Camp camp) { + CampDto campDto = CampDto.from(camp); + return campDto.color() + campDto.name() + RESET; + } + + public void printExistGameRoom(List gameIds) { + if (gameIds.isEmpty()) { + System.out.println(LINE_SEPARATOR + EMPTY_GAME); + return; + } + System.out.println(LINE_SEPARATOR + GAME_ROOM + gameIds); + } } diff --git a/src/main/java/janggi/util/Parser.java b/src/main/java/janggi/view/Parser.java similarity index 71% rename from src/main/java/janggi/util/Parser.java rename to src/main/java/janggi/view/Parser.java index 21a0d569c1..481d9dfbbe 100644 --- a/src/main/java/janggi/util/Parser.java +++ b/src/main/java/janggi/view/Parser.java @@ -1,4 +1,4 @@ -package janggi.util; +package janggi.view; import java.util.Arrays; import java.util.List; @@ -24,4 +24,12 @@ private static int parseToInt(String number) { throw new IllegalArgumentException(ONLY_NUMBERS_ALLOWED); } } + + public static long parseToLong(String number) { + try { + return Long.parseLong(number); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(ONLY_NUMBERS_ALLOWED); + } + } } diff --git a/src/main/java/janggi/view/dto/PiecePositionDto.java b/src/main/java/janggi/view/dto/PiecePositionDto.java index 22c351d12e..bd7a7a9e36 100644 --- a/src/main/java/janggi/view/dto/PiecePositionDto.java +++ b/src/main/java/janggi/view/dto/PiecePositionDto.java @@ -1,6 +1,6 @@ package janggi.view.dto; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Piece; import janggi.view.format.PieceFormat; diff --git a/src/main/java/janggi/view/format/PieceFormat.java b/src/main/java/janggi/view/format/PieceFormat.java index 23f331d882..30acbc92aa 100644 --- a/src/main/java/janggi/view/format/PieceFormat.java +++ b/src/main/java/janggi/view/format/PieceFormat.java @@ -1,61 +1,41 @@ package janggi.view.format; +import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; +import java.util.Arrays; public enum PieceFormat { - GENERAL_CHO("楚"), - GENERAL_HAN("漢"), - SOLDIER_CHO("卒"), - SOLDIER_HAN("兵"), - CHARIOT_CHO("車"), - CHARIOT_HAN("車"), - HORSE_CHO("馬"), - HORSE_HAN("馬"), - CANNON_CHO("包"), - CANNON_HAN("包"), - GUARD_CHO("士"), - GUARD_HAN("士"), - ELEPHANT_CHO("象"), - ELEPHANT_HAN("象"), + GENERAL_CHO("楚", PieceType.GENERAL, Camp.CHO), + GENERAL_HAN("漢", PieceType.GENERAL, Camp.HAN), + SOLDIER_CHO("卒", PieceType.SOLDIER, Camp.CHO), + SOLDIER_HAN("兵", PieceType.SOLDIER, Camp.HAN), + CHARIOT_CHO("車", PieceType.CHARIOT, Camp.CHO), + CHARIOT_HAN("車", PieceType.CHARIOT, Camp.HAN), + HORSE_CHO("馬", PieceType.HORSE, Camp.CHO), + HORSE_HAN("馬", PieceType.HORSE, Camp.HAN), + CANNON_CHO("包", PieceType.CANNON, Camp.CHO), + CANNON_HAN("包", PieceType.CANNON, Camp.HAN), + GUARD_CHO("士", PieceType.GUARD, Camp.CHO), + GUARD_HAN("士", PieceType.GUARD, Camp.HAN), + ELEPHANT_CHO("象", PieceType.ELEPHANT, Camp.CHO), + ELEPHANT_HAN("象", PieceType.ELEPHANT, Camp.HAN), ; private final String symbol; + private final Piece piece; - PieceFormat(String symbol) { + PieceFormat(String symbol, PieceType pieceType, Camp camp) { this.symbol = symbol; + this.piece = new Piece(pieceType, camp); } public static PieceFormat from(Piece piece) { - return switch (piece.camp()) { - case CHO -> choOf(piece.pieceType()); - case HAN -> hanOf(piece.pieceType()); - }; - } - - private static PieceFormat choOf(PieceType pieceType) { - return switch (pieceType) { - case GENERAL -> GENERAL_CHO; - case CHARIOT -> CHARIOT_CHO; - case HORSE -> HORSE_CHO; - case CANNON -> CANNON_CHO; - case GUARD -> GUARD_CHO; - case ELEPHANT -> ELEPHANT_CHO; - case SOLDIER -> SOLDIER_CHO; - }; - } - - private static PieceFormat hanOf(PieceType pieceType) { - return switch (pieceType) { - case GENERAL -> GENERAL_HAN; - case CHARIOT -> CHARIOT_HAN; - case HORSE -> HORSE_HAN; - case CANNON -> CANNON_HAN; - case GUARD -> GUARD_HAN; - case ELEPHANT -> ELEPHANT_HAN; - case SOLDIER -> SOLDIER_HAN; - }; + return Arrays.stream(values()) + .filter(pieceFormat -> pieceFormat.piece.equals(piece)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 기물입니다.")); } public String symbol() { diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000000..86d21bcc0d --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,14 @@ +create table if not exists game_state ( + game_id bigint generated by default as identity primary key, + current_turn varchar(20) not null +); + +create table if not exists game_piece ( + game_id bigint not null, + row_position int not null, + column_position int not null, + piece_type varchar(30) not null, + camp varchar(20) not null, + primary key (game_id, row_position, column_position), + foreign key (game_id) references game_state(game_id) +); diff --git a/src/test/java/janggi/db/TestConnectionManager.java b/src/test/java/janggi/db/TestConnectionManager.java new file mode 100644 index 0000000000..ab7193f8e4 --- /dev/null +++ b/src/test/java/janggi/db/TestConnectionManager.java @@ -0,0 +1,32 @@ +package janggi.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +public final class TestConnectionManager implements ConnectionManager { + + private static final String UNABLE_TO_ACCESS_DATABASE = "[ERROR] H2 데이터베이스에 연결할 수 없습니다."; + private static final String URL = "jdbc:h2:mem:test-db;DB_CLOSE_DELAY=-1"; + + @Override + public Connection createConnection() { + try { + return DriverManager.getConnection(URL, "sa", ""); + } catch (SQLException e) { + throw new IllegalStateException(UNABLE_TO_ACCESS_DATABASE); + } + } + + public void clear() { + try ( + Connection connection = createConnection(); + Statement statement = connection.createStatement() + ) { + statement.execute("DROP ALL OBJECTS"); + } catch (SQLException e) { + throw new IllegalStateException(UNABLE_TO_ACCESS_DATABASE); + } + } +} diff --git a/src/test/java/janggi/domain/TurnTest.java b/src/test/java/janggi/domain/TurnTest.java deleted file mode 100644 index 3032d0c6f5..0000000000 --- a/src/test/java/janggi/domain/TurnTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package janggi.domain; - -import janggi.domain.piece.Camp; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.Test; - -class TurnTest { - - @Test - void 첫번쨰_턴은_초나라가_시작한다() { - //given - Turn turn = new Turn(); - //when - //then - Assertions.assertThat(turn.currentTurn()).isEqualTo(Camp.CHO); - } - - @Test - void 자신의_턴_종료_후_다음_턴은_반드시_상대_진영이다() { - //given - Turn turn = new Turn(); - //when - Camp firstTurn = turn.currentTurn(); - turn.finishTurn(); - Camp secondTurn = turn.currentTurn(); - turn.finishTurn(); - Camp thirdTurn = turn.currentTurn(); - //then - SoftAssertions.assertSoftly(assertSoftly -> { - assertSoftly.assertThat(firstTurn).isEqualTo(Camp.CHO); - assertSoftly.assertThat(secondTurn).isEqualTo(Camp.HAN); - assertSoftly.assertThat(thirdTurn).isEqualTo(Camp.CHO); - }); - } - -} diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 9d0504c67d..5e9768a2ce 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -1,6 +1,7 @@ package janggi.domain.board; -import janggi.domain.Position; +import static org.assertj.core.api.Assertions.assertThat; + import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; @@ -23,9 +24,9 @@ destination, new Piece(PieceType.HORSE, Camp.CHO), source, new Piece(PieceType.CANNON, Camp.HAN) )); // when - board.movePiece(source, destination, Camp.HAN); + board.movePieceAndCheckGameEnd(source, destination, Camp.HAN); // then - boolean destinationExists = board.hasSamePieceRuleAt(destination, PieceType.CANNON); + boolean destinationExists = board.hasSamePieceTypeAt(destination, PieceType.CANNON); boolean sourceExists = board.hasPieceAt(source); SoftAssertions.assertSoftly(assertSoftly -> { @@ -46,7 +47,7 @@ destination, new Piece(PieceType.HORSE, Camp.CHO), source, new Piece(PieceType.CANNON, Camp.CHO) )); // then - Assertions.assertThatThrownBy(() -> board.movePiece(source, destination, Camp.HAN)) + Assertions.assertThatThrownBy(() -> board.movePieceAndCheckGameEnd(source, destination, Camp.HAN)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 상대 진영의 기물은 이동할 수 없습니다."); } @@ -59,8 +60,48 @@ source, new Piece(PieceType.CANNON, Camp.CHO) // when Board board = new Board(Map::of); // then - Assertions.assertThatThrownBy(() -> board.movePiece(source, destination, Camp.HAN)) + Assertions.assertThatThrownBy(() -> board.movePieceAndCheckGameEnd(source, destination, Camp.HAN)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 출발지에 기물이 존재하지 않습니다."); } + + @Test + void 궁을_잡으면_게임이_끝난다() { + // given + Position source = new Position(7, 4); + Position destination = new Position(1, 4); + + Board board = new Board(() -> Map.of( + new Position(6, 4), new Piece(PieceType.SOLDIER, Camp.HAN), + source, new Piece(PieceType.CANNON, Camp.HAN), + destination, new Piece(PieceType.GENERAL, Camp.CHO) + )); + + // when + boolean gameEnded = board.movePieceAndCheckGameEnd(source, destination, Camp.HAN); + + // then + assertThat(gameEnded).isTrue(); + } + + @Test + void 각_진영의_남아있는_기물로_점수를_계산한다() { + // given + Board board = new Board(() -> Map.of( + new Position(1, 4), new Piece(PieceType.GENERAL, Camp.CHO), + new Position(0, 0), new Piece(PieceType.CHARIOT, Camp.CHO), + new Position(0, 8), new Piece(PieceType.CHARIOT, Camp.CHO), + + new Position(4, 1), new Piece(PieceType.SOLDIER, Camp.HAN), + new Position(7, 1), new Piece(PieceType.CANNON, Camp.HAN), + new Position(7, 7), new Piece(PieceType.CANNON, Camp.HAN) + )); + + // when + Map eachCampScore = board.calculateScore(); + + // then + assertThat(eachCampScore.get(Camp.CHO)).isEqualTo(26); + assertThat(eachCampScore.get(Camp.HAN)).isEqualTo(17.5); + } } diff --git a/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java b/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java index ea0d4d8e83..b3b533770c 100644 --- a/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java +++ b/src/test/java/janggi/domain/board/EmptyConditionTestBoardInitializer.java @@ -1,6 +1,5 @@ package janggi.domain.board; -import janggi.domain.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; diff --git a/src/test/java/janggi/domain/board/PalaceTest.java b/src/test/java/janggi/domain/board/PalaceTest.java new file mode 100644 index 0000000000..703b8fecd8 --- /dev/null +++ b/src/test/java/janggi/domain/board/PalaceTest.java @@ -0,0 +1,86 @@ +package janggi.domain.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import janggi.domain.piece.Camp; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PalaceTest { + + private static Stream friendlyPalacePositions() { + return Stream.of( + Arguments.of(Camp.CHO, new Position(0, 3)), + Arguments.of(Camp.CHO, new Position(1, 4)), + Arguments.of(Camp.HAN, new Position(8, 4)), + Arguments.of(Camp.HAN, new Position(9, 5)) + ); + } + + @ParameterizedTest + @MethodSource("friendlyPalacePositions") + void 아군_궁성_내부를_판별한다(Camp camp, Position position) { + assertDoesNotThrow(() -> + Palace.validateFriendlyPalace(camp, position) + ); + } + + private static Stream palacePositions() { + return Stream.of( + Arguments.of(new Position(0, 3), true), + Arguments.of(new Position(1, 4), true), + Arguments.of(new Position(2, 5), true), + Arguments.of(new Position(7, 3), true), + Arguments.of(new Position(8, 4), true), + Arguments.of(new Position(9, 5), true), + Arguments.of(new Position(0, 2), false), + Arguments.of(new Position(3, 3), false), + Arguments.of(new Position(6, 4), false), + Arguments.of(new Position(9, 6), false) + ); + } + + @ParameterizedTest + @MethodSource("palacePositions") + void 궁성을_판별한다(Position position, boolean expected) { + assertThat(Palace.isPalace(position)).isEqualTo(expected); + } + + private static Stream palaceCenterPositions() { + return Stream.of( + Arguments.of(new Position(1, 4), true), + Arguments.of(new Position(8, 4), true), + Arguments.of(new Position(0, 4), false), + Arguments.of(new Position(1, 3), false), + Arguments.of(new Position(9, 4), false), + Arguments.of(new Position(8, 5), false) + ); + } + + @ParameterizedTest + @MethodSource("palaceCenterPositions") + void 궁성_중앙을_판별한다(Position position, boolean expected) { + assertThat(Palace.isPalaceCenter(position)).isEqualTo(expected); + } + + private static Stream invalidFriendlyPalacePositions() { + return Stream.of( + Arguments.of(Camp.CHO, new Position(8, 4)), + Arguments.of(Camp.CHO, new Position(3, 3)), + Arguments.of(Camp.HAN, new Position(1, 4)), + Arguments.of(Camp.HAN, new Position(6, 4)) + ); + } + + @ParameterizedTest + @MethodSource("invalidFriendlyPalacePositions") + void 상대_궁성이나_궁성_밖은_예외가_발생한다(Camp camp, Position position) { + assertThatThrownBy(() -> Palace.validateFriendlyPalace(camp, position)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } +} diff --git a/src/test/java/janggi/domain/PositionTest.java b/src/test/java/janggi/domain/board/PositionTest.java similarity index 98% rename from src/test/java/janggi/domain/PositionTest.java rename to src/test/java/janggi/domain/board/PositionTest.java index 93099e5a62..cc536a47ba 100644 --- a/src/test/java/janggi/domain/PositionTest.java +++ b/src/test/java/janggi/domain/board/PositionTest.java @@ -1,4 +1,4 @@ -package janggi.domain; +package janggi.domain.board; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java b/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java index c076b41695..42e35885d7 100644 --- a/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java +++ b/src/test/java/janggi/domain/board/StandardBoardInitializerTest.java @@ -2,11 +2,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.board.initializer.ElephantSetUp; import janggi.domain.board.initializer.StandardBoardInitializer; -import janggi.domain.board.initializer.dto.ElephantSetUpDto; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; import janggi.domain.piece.PieceType; @@ -18,10 +16,9 @@ public class StandardBoardInitializerTest { @Test - void 같은_진영의_상차림을_두_번_전달하면_예외가_발생한다() { + void 두_진영의_상차림이_모두_존재하지_않으면_예외가_발생한다() { assertThatThrownBy(() -> new StandardBoardInitializer( - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT), - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.RIGHT_ELEPHANT))) + Map.of(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT))) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 진영별 상차림은 각각 하나씩만 존재해야 합니다."); } @@ -29,9 +26,10 @@ public class StandardBoardInitializerTest { @Test void 한_상마상마_초_마상마상_으로_보드를_초기화한다() { // given - BoardInitializer initializer = new StandardBoardInitializer( - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT), - new ElephantSetUpDto(Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT)); + BoardInitializer initializer = new StandardBoardInitializer(Map.of( + Camp.HAN, ElephantSetUp.LEFT_ELEPHANT, + Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT + )); Map expectedBoard = createExpectedBoard(); expectedBoard.putAll(createChoLeftHanRightBoard()); // when @@ -46,9 +44,10 @@ public class StandardBoardInitializerTest { @Test void 한_상마마상_초_마상상마_으로_보드를_초기화한다() { // given - BoardInitializer initializer = new StandardBoardInitializer( - new ElephantSetUpDto(Camp.HAN, ElephantSetUp.OUTER_ELEPHANT), - new ElephantSetUpDto(Camp.CHO, ElephantSetUp.INNER_ELEPHANT)); + BoardInitializer initializer = new StandardBoardInitializer(Map.of( + Camp.HAN, ElephantSetUp.OUTER_ELEPHANT, + Camp.CHO, ElephantSetUp.INNER_ELEPHANT + )); Map expectedBoard = createExpectedBoard(); expectedBoard.putAll(createChoInnerHanOuterBoard()); // when diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java index d0f521e73b..e571cc7188 100644 --- a/src/test/java/janggi/domain/piece/PieceTest.java +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -3,9 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import janggi.domain.Position; import janggi.domain.board.Board; import janggi.domain.board.BoardChecker; +import janggi.domain.board.Position; import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; @@ -19,7 +19,7 @@ class PieceTest { // given Piece piece = new Piece(PieceType.CANNON, Camp.CHO); // when - boolean result = piece.isSamePieceRule(PieceType.CANNON); + boolean result = piece.isSamePieceType(PieceType.CANNON); // then assertThat(result).isTrue(); } @@ -55,9 +55,9 @@ class General { BoardChecker board = new Board(Map::of); // when & then Assertions.assertThatThrownBy( - () -> piece.validateMove(new Position(1, 4), new Position(3, 4), board)) + () -> piece.validateMove(new Position(0, 4), new Position(2, 4), board)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + .hasMessage("[ERROR] 해당 기물은 궁성 내에서 연결된 1칸만 이동할 수 있습니다."); } } @@ -84,7 +84,7 @@ class Guard { Assertions.assertThatThrownBy( () -> piece.validateMove(new Position(0, 3), new Position(0, 5), board)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + .hasMessage("[ERROR] 해당 기물은 궁성 내에서 연결된 1칸만 이동할 수 있습니다."); } } @@ -198,6 +198,19 @@ class Cannon { piece.validateMove(new Position(2, 1), new Position(8, 1), board) ); } + + @Test + void 포는_궁성_내에서_대각선으로_이동할_수_있다() { + // given + Piece piece = new Piece(PieceType.CANNON, Camp.CHO); + BoardChecker board = new Board(() -> Map.of( + new Position(1, 4), new Piece(PieceType.CHARIOT, Camp.HAN) + )); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 3), new Position(2, 5), board) + ); + } } @DisplayName("차 행마법 테스트") @@ -241,6 +254,17 @@ class Chariot { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); } + + @Test + void 차는_궁성_내에서_대각선으로_이동할_수_있다() { + // given + Piece piece = new Piece(PieceType.CHARIOT, Camp.CHO); + BoardChecker board = new Board(Map::of); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 3), new Position(2, 5), board) + ); + } } @DisplayName("졸 행마법 테스트") @@ -318,6 +342,6 @@ class SoldierHan { () -> piece.validateMove(new Position(6, 0), new Position(1, 0), board)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); + } } } -} diff --git a/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java b/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java index cb6dde24ee..b4492a8554 100644 --- a/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java +++ b/src/test/java/janggi/domain/piece/condition/EmptyConditionTest.java @@ -2,9 +2,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; import janggi.domain.board.Board; import janggi.domain.board.EmptyConditionTestBoardInitializer; +import janggi.domain.board.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.PieceType; diff --git a/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java b/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java index 05f00266ef..dc0bd9f15b 100644 --- a/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java +++ b/src/test/java/janggi/domain/piece/condition/OnePieceExistsConditionTest.java @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; import janggi.domain.board.Board; +import janggi.domain.board.Position; import janggi.domain.board.initializer.BoardInitializer; import janggi.domain.piece.Camp; import janggi.domain.piece.Piece; diff --git a/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java index 26c8ccde8d..02aea5115f 100644 --- a/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategyTest.java new file mode 100644 index 0000000000..b181134620 --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/FriendlyPalaceSingleStepStrategyTest.java @@ -0,0 +1,148 @@ +package janggi.domain.piece.strategy; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.board.Position; +import janggi.domain.piece.Camp; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class FriendlyPalaceSingleStepStrategyTest { + + private final MoveStrategy strategy = new FriendlyPalaceSingleStepStrategy(); + + @DisplayName("정상 경우") + @Nested + class success { + private static Stream successMovePositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(0, 4), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 4), Camp.CHO) + ); + } + + @ParameterizedTest + @MethodSource("successMovePositions") + void 궁과_사는_상하좌우_1칸_이동한다(Position source, Position destination, Camp camp) { + //when + List path = strategy.findPath(source, destination, camp); + //then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } + + private static Stream successDiagonalMovePositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(0, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(0, 5), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 3), Camp.CHO), + Arguments.of(new Position(1, 4), new Position(2, 5), Camp.CHO), + + Arguments.of(new Position(8, 4), new Position(7, 3), Camp.HAN), + Arguments.of(new Position(8, 4), new Position(7, 5), Camp.HAN), + Arguments.of(new Position(8, 4), new Position(9, 3), Camp.HAN), + Arguments.of(new Position(8, 4), new Position(9, 5), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("successDiagonalMovePositions") + void 궁과_사는_궁성_내부에서_대각선으로_1칸_이동한다(Position source, Position destination, Camp camp) { + //when + List path = strategy.findPath(source, destination, camp); + //then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } + } + + @DisplayName("예외 경우") + @Nested + class exception { + @Test + void 궁과_사는_1칸_이동이_아니면_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(0, 4), new Position(2, 4), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 궁성 내에서 연결된 1칸만 이동할 수 있습니다."); + } + + private static Stream exceptionPalaceMovePositions() { + return Stream.of( + Arguments.of(new Position(2, 3), new Position(2, 2), Camp.CHO), + Arguments.of(new Position(2, 3), new Position(3, 3), Camp.CHO), + Arguments.of(new Position(2, 5), new Position(3, 5), Camp.CHO), + Arguments.of(new Position(2, 5), new Position(2, 6), Camp.CHO), + + Arguments.of(new Position(7, 3), new Position(7, 2), Camp.HAN), + Arguments.of(new Position(7, 3), new Position(6, 3), Camp.HAN), + Arguments.of(new Position(7, 5), new Position(6, 5), Camp.HAN), + Arguments.of(new Position(7, 5), new Position(7, 6), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionPalaceMovePositions") + void 궁과_사는_아군_궁성_밖으로_벗어나면_예외가_발생한다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } + + private static Stream exceptionPalaceCampPositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(0, 3), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(0, 5), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(2, 3), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(2, 5), Camp.HAN), + + Arguments.of(new Position(8, 4), new Position(7, 3), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(7, 5), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 3), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 5), Camp.CHO) + ); + } + + @ParameterizedTest + @MethodSource("exceptionPalaceCampPositions") + void 궁과_사는_상대_궁성_영역에서_이동하면_예외가_발생한다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 아군 궁성 영역 밖으로 이동할 수 없습니다."); + } + + private static Stream exceptionInvalidDiagonalMovePositions() { + return Stream.of( + Arguments.of(new Position(0, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(0, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 3), new Position(2, 4), Camp.CHO), + Arguments.of(new Position(1, 5), new Position(2, 4), Camp.CHO), + + Arguments.of(new Position(7, 4), new Position(8, 3), Camp.HAN), + Arguments.of(new Position(7, 4), new Position(8, 5), Camp.HAN), + Arguments.of(new Position(8, 3), new Position(9, 4), Camp.HAN), + Arguments.of(new Position(8, 5), new Position(9, 4), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionInvalidDiagonalMovePositions") + void 궁과_사는_궁성_대각선이_연결되지_않은_칸으로는_대각선_이동할_수_없다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java index 9cc2995536..bd0c9fd0a2 100644 --- a/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java index 8d1c203e04..d2fd7f6603 100644 --- a/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/MultiStepStraightStrategyTest.java @@ -2,11 +2,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -16,82 +18,178 @@ public class MultiStepStraightStrategyTest { private final MoveStrategy strategy = new MultiStepStraightStrategy(); - private static Stream createPositionsAndPath() { - return Stream.of( - Arguments.of(new Position(0, 0), new Position(9, 0), 9, - List.of( - new Position(1, 0), - new Position(2, 0), - new Position(3, 0), - new Position(4, 0), - new Position(5, 0), - new Position(6, 0), - new Position(7, 0), - new Position(8, 0), - new Position(9, 0) - )), - Arguments.of(new Position(0, 0), new Position(0, 8), 8, - List.of( - new Position(0, 1), - new Position(0, 2), - new Position(0, 3), - new Position(0, 4), - new Position(0, 5), - new Position(0, 6), - new Position(0, 7), - new Position(0, 8) - ) - ), - Arguments.of(new Position(0, 8), new Position(0, 0), 8, - List.of( - new Position(0, 7), - new Position(0, 6), - new Position(0, 5), - new Position(0, 4), - new Position(0, 3), - new Position(0, 2), - new Position(0, 1), - new Position(0, 0) - ) - ), - Arguments.of(new Position(9, 0), new Position(0, 0), 9, - List.of( - new Position(8, 0), - new Position(7, 0), - new Position(6, 0), - new Position(5, 0), - new Position(4, 0), - new Position(3, 0), - new Position(2, 0), - new Position(1, 0), - new Position(0, 0) - ) - ) - ); - } + @DisplayName("정상 경우") + @Nested + class success { + private static Stream createPositionsAndPath() { + return Stream.of( + Arguments.of(new Position(0, 0), new Position(9, 0), 9, + List.of( + new Position(1, 0), + new Position(2, 0), + new Position(3, 0), + new Position(4, 0), + new Position(5, 0), + new Position(6, 0), + new Position(7, 0), + new Position(8, 0), + new Position(9, 0) + )), + Arguments.of(new Position(0, 0), new Position(0, 8), 8, + List.of( + new Position(0, 1), + new Position(0, 2), + new Position(0, 3), + new Position(0, 4), + new Position(0, 5), + new Position(0, 6), + new Position(0, 7), + new Position(0, 8) + ) + ), + Arguments.of(new Position(0, 8), new Position(0, 0), 8, + List.of( + new Position(0, 7), + new Position(0, 6), + new Position(0, 5), + new Position(0, 4), + new Position(0, 3), + new Position(0, 2), + new Position(0, 1), + new Position(0, 0) + ) + ), + Arguments.of(new Position(9, 0), new Position(0, 0), 9, + List.of( + new Position(8, 0), + new Position(7, 0), + new Position(6, 0), + new Position(5, 0), + new Position(4, 0), + new Position(3, 0), + new Position(2, 0), + new Position(1, 0), + new Position(0, 0) + ) + ), + Arguments.of(new Position(0, 5), new Position(4, 5), 4, + List.of( + new Position(1, 5), + new Position(2, 5), + new Position(3, 5), + new Position(4, 5) + ) + ) + ); + } - @ParameterizedTest - @MethodSource("createPositionsAndPath") - void 차와_포는_한_방향으로만_1칸_이상_이동_할_수_있다(Position source, Position destination, int size, List expectedPath) { - List path = strategy.findPath(source, destination, Camp.HAN); + @ParameterizedTest + @MethodSource("createPositionsAndPath") + void 차와_포는_한_방향으로만_1칸_이상_이동_할_수_있다(Position source, Position destination, int size, + List expectedPath) { + List path = strategy.findPath(source, destination, Camp.HAN); - SoftAssertions.assertSoftly(assertSoftly -> { - assertSoftly.assertThat(path).hasSize(size); - assertSoftly.assertThat(path).containsExactlyElementsOf(expectedPath); - }); - } + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(size); + assertSoftly.assertThat(path).containsExactlyElementsOf(expectedPath); + }); + } + + private static Stream createDiagonalPositionsAndPath() { + return Stream.of( + Arguments.of(new Position(0, 3), new Position(2, 5), 2, + List.of( + new Position(1, 4), + new Position(2, 5) + )), + Arguments.of(new Position(0, 5), new Position(2, 3), 2, + List.of( + new Position(1, 4), + new Position(2, 3) + )), + Arguments.of(new Position(7, 3), new Position(9, 5), 2, + List.of( + new Position(8, 4), + new Position(9, 5) + )), + Arguments.of(new Position(7, 5), new Position(9, 3), 2, + List.of( + new Position(8, 4), + new Position(9, 3) + )), + Arguments.of(new Position(1, 4), new Position(2, 5), 1, + List.of( + new Position(2, 5) + )) + ); + } - @Test - void 차와_포는_한_방향으로_이동하지_않으면_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(5, 5), Camp.CHO)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); + @ParameterizedTest + @MethodSource("createDiagonalPositionsAndPath") + void 차와_포는_궁성_내에서_대각선으로_이동_할_수_있다(Position source, Position destination, int size, + List expectedPath) { + List path = strategy.findPath(source, destination, Camp.HAN); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(size); + assertSoftly.assertThat(path).containsExactlyElementsOf(expectedPath); + }); + } } - @Test - void 차와_포는_제자리_이동_시_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(0, 0), Camp.CHO)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 기물은 반드시 이동해야 합니다."); + @DisplayName("예외 경우") + @Nested + class exception { + private static Stream exceptionInvalidPalaceDiagonalPath() { + return Stream.of( + Arguments.of(new Position(0, 4), new Position(1, 3), Camp.CHO), + Arguments.of(new Position(0, 4), new Position(1, 5), Camp.CHO), + Arguments.of(new Position(1, 3), new Position(2, 4), Camp.CHO), + Arguments.of(new Position(1, 5), new Position(2, 4), Camp.CHO), + Arguments.of(new Position(7, 4), new Position(8, 3), Camp.HAN), + Arguments.of(new Position(7, 4), new Position(8, 5), Camp.HAN), + Arguments.of(new Position(8, 3), new Position(9, 4), Camp.HAN), + Arguments.of(new Position(8, 5), new Position(9, 4), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionInvalidPalaceDiagonalPath") + void 차와_포는_궁성_대각선이_연결되지_않은_경로로_이동할_수_없다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } + + private static Stream exceptionDiagonalPathOutOfPalace() { + return Stream.of( + Arguments.of(new Position(0, 3), new Position(3, 6), Camp.CHO), + Arguments.of(new Position(3, 6), new Position(0, 3), Camp.CHO), + Arguments.of(new Position(7, 3), new Position(6, 2), Camp.HAN), + Arguments.of(new Position(6, 2), new Position(7, 3), Camp.HAN) + ); + } + + @ParameterizedTest + @MethodSource("exceptionDiagonalPathOutOfPalace") + void 차와_포는_궁성_영역을_벗어나는_대각선으로_이동할_수_없다(Position source, Position destination, Camp camp) { + assertThatThrownBy(() -> strategy.findPath(source, destination, camp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); + } + + @Test + void 차와_포는_한_방향으로_이동하지_않으면_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(5, 5), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 해당 기물은 직선 이동만 가능합니다."); + } + + @Test + void 차와_포는_제자리_이동_시_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(0, 0), new Position(0, 0), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 기물은 반드시 이동해야 합니다."); + } } } diff --git a/src/test/java/janggi/domain/piece/strategy/SingleStepStraightStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/SingleStepStraightStrategyTest.java deleted file mode 100644 index 23968920b2..0000000000 --- a/src/test/java/janggi/domain/piece/strategy/SingleStepStraightStrategyTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package janggi.domain.piece.strategy; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import janggi.domain.Position; -import janggi.domain.piece.Camp; -import java.util.List; -import java.util.stream.Stream; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class SingleStepStraightStrategyTest { - - private final MoveStrategy strategy = new SingleStepStraightStrategy(); - - private static Stream successMovePositions() { - return Stream.of( - Arguments.of(new Position(1, 4), new Position(1, 5)), - Arguments.of(new Position(1, 4), new Position(1, 3)), - Arguments.of(new Position(1, 4), new Position(0, 4)), - Arguments.of(new Position(1, 4), new Position(2, 4)) - ); - } - - @ParameterizedTest - @MethodSource("successMovePositions") - void 궁과_사는_상하좌우_1칸_이동한다(Position source, Position destination) { - //when - List path = strategy.findPath(source, destination, Camp.HAN); - //then - SoftAssertions.assertSoftly(assertSoftly -> { - assertSoftly.assertThat(path).hasSize(1); - assertSoftly.assertThat(path).containsExactly(destination); - }); - } - - @Test - void 궁과_사는_1칸_이동이_아니면_예외가_발생한다() { - assertThatThrownBy(() -> strategy.findPath(new Position(3, 0), new Position(5, 0), Camp.HAN)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("[ERROR] 해당 기물은 직선으로 1칸 이동해야 합니다."); - } -} diff --git a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java index 6d05937bef..1d73163bea 100644 --- a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java +++ b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import janggi.domain.Position; +import janggi.domain.board.Position; import janggi.domain.piece.Camp; import java.util.List; import java.util.stream.Stream; @@ -30,18 +30,30 @@ private static Stream successMovePositions() { ); } - private static Stream exceptionMovePositions() { + @ParameterizedTest + @MethodSource("successMovePositions") + void 병의_1칸_이동_여부를_확인한다(Position source, Position destination) { + List path = strategy.findPath(source, destination, Camp.HAN); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } + + private static Stream successDiagonalMovePositions() { return Stream.of( - Arguments.of(new Position(6, 0), new Position(5, 1)), - Arguments.of(new Position(6, 0), new Position(6, 2)), - Arguments.of(new Position(3, 2), new Position(2, 3)) + Arguments.of(new Position(2, 3), new Position(1, 4), Camp.HAN), + Arguments.of(new Position(2, 5), new Position(1, 4), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(0, 3), Camp.HAN), + Arguments.of(new Position(1, 4), new Position(0, 5), Camp.HAN) ); } @ParameterizedTest - @MethodSource("successMovePositions") - void 병의_1칸_이동_여부를_확인한다(Position source, Position destination) { - List path = strategy.findPath(source, destination, Camp.HAN); + @MethodSource("successDiagonalMovePositions") + void 병은_상대_궁성_내부에서_대각선_이동한다(Position source, Position destination, Camp camp) { + List path = strategy.findPath(source, destination, camp); SoftAssertions.assertSoftly(assertSoftly -> { assertSoftly.assertThat(path).hasSize(1); @@ -49,6 +61,14 @@ private static Stream exceptionMovePositions() { }); } + private static Stream exceptionMovePositions() { + return Stream.of( + Arguments.of(new Position(6, 0), new Position(5, 1)), + Arguments.of(new Position(6, 0), new Position(6, 2)), + Arguments.of(new Position(3, 2), new Position(2, 3)) + ); + } + @ParameterizedTest @MethodSource("exceptionMovePositions") void 병은_1칸_이동이_아니면_예외가_발생한다(Position source, Position destination) { @@ -63,6 +83,13 @@ private static Stream exceptionMovePositions() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 후진할 수 없습니다."); } + + @Test + void 병은_궁성에서_대각선이_없을때_대각_이동_시_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(1, 3), new Position(0, 4), Camp.HAN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } } @DisplayName("졸 행마법 테스트") @@ -81,6 +108,26 @@ class Zol { }); } + private static Stream successDiagonalMovePositions() { + return Stream.of( + Arguments.of(new Position(7, 3), new Position(8, 4), Camp.CHO), + Arguments.of(new Position(7, 5), new Position(8, 4), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 3), Camp.CHO), + Arguments.of(new Position(8, 4), new Position(9, 5), Camp.CHO) + ); + } + + @ParameterizedTest + @MethodSource("successDiagonalMovePositions") + void 졸은_상대_궁성_내부에서_대각선_이동한다(Position source, Position destination, Camp camp) { + List path = strategy.findPath(source, destination, camp); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(path).hasSize(1); + assertSoftly.assertThat(path).containsExactly(destination); + }); + } + @Test void 졸은_1칸_이동이_아니면_예외가_발생한다() { assertThatThrownBy(() -> strategy.findPath(new Position(3, 0), new Position(5, 0), Camp.CHO)) @@ -94,5 +141,12 @@ class Zol { .isInstanceOf(IllegalArgumentException.class) .hasMessage("[ERROR] 해당 기물은 후진할 수 없습니다."); } + + @Test + void 졸은_궁성에서_대각선이_없을때_대각_이동_시_예외가_발생한다() { + assertThatThrownBy(() -> strategy.findPath(new Position(8, 3), new Position(9, 4), Camp.CHO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 궁성 내에 대각선이 존재하지 않는 경로 입니다."); + } } } diff --git a/src/test/java/janggi/service/GameServiceTest.java b/src/test/java/janggi/service/GameServiceTest.java new file mode 100644 index 0000000000..5839a714f0 --- /dev/null +++ b/src/test/java/janggi/service/GameServiceTest.java @@ -0,0 +1,98 @@ +package janggi.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.db.DatabaseInitializer; +import janggi.db.TestConnectionManager; +import janggi.db.TransactionManager; +import janggi.domain.Game; +import janggi.domain.board.Board; +import janggi.domain.board.Position; +import janggi.domain.board.initializer.ElephantSetUp; +import janggi.domain.board.initializer.StandardBoardInitializer; +import janggi.domain.piece.Camp; +import janggi.repository.GamePieceRepository; +import janggi.repository.GameStateRepository; +import java.util.Map; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GameServiceTest { + + private GameService gameService; + private Board board; + private TestConnectionManager connectionManager; + + @BeforeEach + void setUp() { + connectionManager = new TestConnectionManager(); + new DatabaseInitializer(connectionManager).initialize(); + + gameService = new GameService( + new TransactionManager(connectionManager), + new GameStateRepository(), + new GamePieceRepository() + ); + board = createBoard(); + } + + @AfterEach + void tearDown() { + connectionManager.clear(); + } + + @Test + void 새_게임을_생성하고_다시_조회할_수_있다() { + // when + Game createdGame = gameService.create(board); + Game loadedGame = gameService.findById(createdGame.id()).orElseThrow(); + + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(createdGame.id()).isPositive(); + assertSoftly.assertThat(gameService.findAllIds()).containsExactly(createdGame.id()); + assertSoftly.assertThat(loadedGame.currentTurn()).isEqualTo(Camp.CHO); + assertSoftly.assertThat(loadedGame.boardSnapshot()).isEqualTo(createdGame.boardSnapshot()); + }); + } + + @Test + void 게임을_두_개_생성하면_전체_게임방_번호를_조회할_수_있다() { + // when + Game firstGame = gameService.create(createBoard()); + Game secondGame = gameService.create(createBoard()); + + // then + assertThat(gameService.findAllIds()).containsExactly(firstGame.id(), secondGame.id()); + } + + @Test + void 게임을_저장하면_변경된_턴과_보드_상태가_반영된다() { + // given + Game createdGame = gameService.create(board); + Position source = new Position(3, 0); + Position destination = new Position(4, 0); + createdGame.play(source, destination); + + // when + gameService.save(createdGame); + Game loadedGame = gameService.findById(createdGame.id()).orElseThrow(); + + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(loadedGame.currentTurn()).isEqualTo(Camp.HAN); + assertSoftly.assertThat(loadedGame.boardSnapshot()).isEqualTo(createdGame.boardSnapshot()); + assertSoftly.assertThat(loadedGame.boardSnapshot()).doesNotContainKey(source); + assertSoftly.assertThat(loadedGame.boardSnapshot()).containsKey(destination); + }); + } + + private Board createBoard() { + return new Board(new StandardBoardInitializer(Map.of( + Camp.HAN, ElephantSetUp.LEFT_ELEPHANT, + Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT + ))); + } +} diff --git a/src/test/java/janggi/util/ParserTest.java b/src/test/java/janggi/view/ParserTest.java similarity index 64% rename from src/test/java/janggi/util/ParserTest.java rename to src/test/java/janggi/view/ParserTest.java index 3a5769c301..230e59ee72 100644 --- a/src/test/java/janggi/util/ParserTest.java +++ b/src/test/java/janggi/view/ParserTest.java @@ -1,5 +1,6 @@ -package janggi.util; +package janggi.view; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import org.assertj.core.api.SoftAssertions; @@ -21,4 +22,11 @@ class ParserTest { assertSoftly.assertThat(parsedExpression.getLast()).isEqualTo(20); }); } + + @Test + void 숫자가_아닌_문자가_포함되면_예외가_발생한다() { + assertThatThrownBy(() -> Parser.parseByDelimiter(",", "10,a")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 숫자만 입력 가능합니다"); + } }